Token

0x69a183fcd59625771453aa32db01221915d3a958

Verification
Unverified
v0.8.35+commit.47b9dedd
Type
Contract
3,797 bytes
ABI entries
28
10 read · 7 write
License
none

Contract information

Address
0x69a183fcd59625771453aa32db01221915d3a958
Chain
Robinhood Chain (4663)
Compiler
v0.8.35+commit.47b9dedd
Optimization
Enabled
Creation tx
0xfbaf7b57f3…1b35d10b5a

Token

Name
Klik Dev Cat
Symbol
YUMI
Decimals
18
Holders
2

Read contract (10)

allowance(address, address)uint256
balanceOf(address)uint256
creator()address
decimals()uint8
isLaunchPeriodActive()bool
name()string
owner()address
platform()address
symbol()string
totalSupply()uint256

Events (3)

ApprovalFeesReceivedTransfer

ABI

[
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "_name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "_symbol",
        "type": "string"
      },
      {
        "internalType": "address",
        "name": "_creator",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_platform",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "_launchPeriod",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "allowance",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "needed",
        "type": "uint256"
      }
    ],
    "name": "ERC20InsufficientAllowance",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "balance",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "needed",
        "type": "uint256"
      }
    ],
    "name": "ERC20InsufficientBalance",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "approver",
        "type": "address"
      }
    ],
    "name": "ERC20InvalidApprover",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      }
    ],
    "name": "ERC20InvalidReceiver",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      }
    ],
    "name": "ERC20InvalidSender",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      }
    ],
    "name": "ERC20InvalidSpender",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "Approval",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "FeesReceived",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "Transfer",
    "type": "event"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      }
    ],
    "name": "allowance",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "approve",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "balanceOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "burn",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "burnFrom",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newCreator",
        "type": "address"
      }
    ],
    "name": "changeCreator",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "creator",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "decimals",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "isLaunchPeriodActive",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "name",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "platform",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "symbol",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalSupply",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "transfer",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "transferFrom",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "withdrawFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "balance",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

pragma solidity ^0.8.20;
/*

 __  _  _      ____  __  _      _____  ____  ____    ____  ____     __    ___
|  |/ ]| |    |    ||  |/ ]    |     ||    ||    \  /    ||    \   /  ]  /  _]
|  ' / | |     |  | |  ' /     |   __| |  | |  _  ||  o  ||  _  | /  /  /  [_
|    \ | |___  |  | |    \     |  |_   |  | |  |  ||     ||  |  |/  /  |    _]
|     \|     | |  | |     \ __ |   _]  |  | |  |  ||  _  ||  |  /   \_ |   [_
|  .  ||     | |  | |  .  ||  ||  |    |  | |  |  ||  |  ||  |  \     ||     |
|__|\_||_____||____||__|\_||__||__|   |____||__|__||__|__||__|__|\____||_____|
V4 Edition

https://klik.finance
https://x.com/klik_evm

*/

pragma solidity >=0.8.9;

import {SwapParams} from "v4-core/src/types/PoolOperation.sol";
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol";
import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol";
import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
import {Hooks} from "v4-core/src/libraries/Hooks.sol";
import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
import {TickMath} from "v4-core/src/libraries/TickMath.sol";
import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Actions} from "v4-periphery/src/libraries/Actions.sol";
import {LiquidityAmounts} from "v4-core/test/utils/LiquidityAmounts.sol";
import {StateLibrary} from "v4-core/src/libraries/StateLibrary.sol";

interface IStateView {
    function getSlot0(PoolId poolId) external view returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee);
}

import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol";
import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol";
import {IUniversalRouter} from "@uniswap/universal-router/contracts/interfaces/IUniversalRouter.sol";
import {IV4Router} from "@uniswap/v4-periphery/src/interfaces/IV4Router.sol";
import {IPermit2} from "permit2/src/interfaces/IPermit2.sol";
import {Commands} from "@uniswap/universal-router/contracts/libraries/Commands.sol";

import "v4-core/test/utils/LiquidityAmounts.sol";

interface IToken {
    function creator() external view returns (address);
    function withdrawFees() external returns (uint256);
}

interface IWETH {
    function withdraw(uint256 amount) external;
}

contract Factory is ReentrancyGuard {
    event ERC20TokenCreated(address tokenAddress);

    struct TokenInfo {
        address tokenAddress;
        string name;
        string symbol;
        address deployer;
        uint256 time;
        string metadata;
        uint256 marketCapInETH;
        uint256 totalFeesGenerated;
    }

    mapping(uint256 => TokenInfo) public deployedTokens;
    mapping(address => TokenInfo) public tokenInfoByAddress;
    uint256 public tokenCount = 0;
    address public platformController;
    address public klikHook; // Universal hook for all tokens (can be updated)
    uint256 private itemsPerPage = 250; // Configurable items per page for pagination
    
    // Mapping to store token addresses by creator/deployer
    mapping(address => address[]) public creatorTokens;
    
    // Mapping to track total fees generated per token
    mapping(address => uint256) public tokenFeesGenerated;

    // Mapping to store the hook address used at deploy time per token
    mapping(address => address) public tokenHook;

    // Uniswap v4 deployment addresses — Robinhood Chain (chain ID 4663)
    IAllowanceTransfer constant PERMIT2 = IAllowanceTransfer(address(0x000000000022D473030F116dDEE9F6B43aC78BA3));
    address public constant POSITION_MANAGER = 0x58daec3116aae6D93017bAAea7749052E8a04fA7;
    IPositionManager positionManager = IPositionManager(POSITION_MANAGER);
    address public constant POOL_MANAGER = 0x8366a39CC670B4001A1121B8F6A443A643e40951;
    IPoolManager poolManager = IPoolManager(POOL_MANAGER);
    address public constant STATE_VIEW = 0xF3334192D15450CdD385c8B70e03f9A6bD9E673b;
    IStateView stateView = IStateView(STATE_VIEW);
    uint256 constant Q96 = 2 ** 96;
    address public constant WETH = 0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73; // Canonical WETH on Robinhood Chain
    address public constant UNIVERSAL_ROUTER = 0x8876789976dEcBfCbBbe364623C63652db8C0904;
    IUniversalRouter router = IUniversalRouter(UNIVERSAL_ROUTER);

    uint24 private constant FEE_TIER = 0;

    bool public deployCoinEnabled = true;
    
    // Liquidity configuration struct
    struct LiquidityConfig {
        uint160 sqrtPriceX96;       // sqrtPriceX96 for the pool (address(0) is always currency0)
        int24 tickLower;            // Lower tick for the position
        int24 tickUpper;            // Upper tick for the position
        uint256 amount0Desired;     // Desired amount of currency0 (ETH)
        uint256 amount1Desired;     // Desired amount of currency1 (Token)
        uint256 virtualAmount;      // Virtual ETH amount for market cap calculation
        uint256 penaltyMultiplier;  // Penalty multiplier (50 = 50%, 100 = 100%, 200 = 200%)
    }
    
    // Multiple liquidity configurations
    mapping(uint256 => LiquidityConfig) public liquidityConfigs;
    uint256 public liquidityConfigCount = 0;
    uint256 public launchPeriod = 300; // blocks; set to 0 to disable max-wallet restrictions

    event TokenPurchased(address buyer, address tokenOut, uint256 ethSpent, uint256 tokensReceived);
    event KlikHookUpdated(address oldHook, address newHook);

    constructor(address _klikHook) {
        platformController = msg.sender;
        klikHook = _klikHook;

        // Pre-seeded liquidity configurations (IDs 0-4). All single-sided:
        // full 1B token supply, no ETH. virtualAmount = starting market cap
        // in ETH; sqrtPriceX96 prices the full supply at exactly that amount.
        // penaltyMultiplier 0 = no deploy-buy tax (100 = standard penalty).

        // ID 0: 0.69 ETH virtual liquidity (default)
        liquidityConfigs[0] = LiquidityConfig({
            sqrtPriceX96: 3014831488601586337191090825970934,
            tickLower: -887200,
            tickUpper: 210800,
            amount0Desired: 0,
            amount1Desired: 1000000000000000000000000000,
            virtualAmount: 690000000000000000,
            penaltyMultiplier: 0
        });

        // ID 1: 1 ETH virtual liquidity
        liquidityConfigs[1] = LiquidityConfig({
            sqrtPriceX96: 2505411999795360582221170761428213,
            tickLower: -887200,
            tickUpper: 207200,
            amount0Desired: 0,
            amount1Desired: 1000000000000000000000000000,
            virtualAmount: 1000000000000000000,
            penaltyMultiplier: 0
        });

        // ID 2: 2 ETH virtual liquidity
        liquidityConfigs[2] = LiquidityConfig({
            sqrtPriceX96: 1771577727172025373304338615273325,
            tickLower: -887200,
            tickUpper: 200200,
            amount0Desired: 0,
            amount1Desired: 1000000000000000000000000000,
            virtualAmount: 2000000000000000000,
            penaltyMultiplier: 0
        });

        // ID 3: 5 ETH virtual liquidity
        liquidityConfigs[3] = LiquidityConfig({
            sqrtPriceX96: 1120408587790087695236213992013890,
            tickLower: -887200,
            tickUpper: 191000,
            amount0Desired: 0,
            amount1Desired: 1000000000000000000000000000,
            virtualAmount: 5000000000000000000,
            penaltyMultiplier: 0
        });

        // ID 4: 10 ETH virtual liquidity
        liquidityConfigs[4] = LiquidityConfig({
            sqrtPriceX96: 792280926924313289846529216293289,
            tickLower: -887200,
            tickUpper: 184200,
            amount0Desired: 0,
            amount1Desired: 1000000000000000000000000000,
            virtualAmount: 10000000000000000000,
            penaltyMultiplier: 0
        });

        liquidityConfigCount = 5;
    }

    function setKlikHook(address _newHook) external {
        require(msg.sender == platformController, "Caller is not controller");
        address oldHook = klikHook;
        klikHook = _newHook;
        emit KlikHookUpdated(oldHook, _newHook);
    }

    function setLaunchPeriod(uint256 _blocks) external {
        require(msg.sender == platformController, "Caller is not controller");
        launchPeriod = _blocks;
    }

    receive() external payable {}

    function deployCoin(string memory _name, string memory _symbol, string memory _metadata, bytes32 salt, uint256 configId) public payable returns (uint256 tokensReceived) {
        require(deployCoinEnabled, "Token deployment is currently disabled");
        require(configId < liquidityConfigCount, "Invalid liquidity config ID");
        
        Token t = new Token{salt: salt}(
            _name,
            _symbol,
            msg.sender,
            address(this),
            launchPeriod
        );

        address coin_address = address(t);

        emit ERC20TokenCreated(coin_address);

        provideLiquidityV4(coin_address, configId);

        tokensReceived = 0; // Initialize return value

        if (msg.value > 0) {
            LiquidityConfig memory config = liquidityConfigs[configId];
            uint256 basePenalty = getPenalty(msg.value); // in basis points (e.g., 2500 = 25%)
            uint256 taxBps = (basePenalty * config.penaltyMultiplier) / 100; // Apply config multiplier
            uint256 tax;
            uint256 amountAfterTax;

            // Assembly optimized math operations
            unchecked {
                assembly {
                    tax := div(mul(callvalue(), taxBps), 10000)
                    amountAfterTax := sub(callvalue(), tax)
                }
            }

            uint256 tokensBefore = IERC20(coin_address).balanceOf(address(this));
            _buyToken(coin_address, amountAfterTax);
            uint256 tokensAfter = IERC20(coin_address).balanceOf(address(this));
            tokensReceived = tokensAfter - tokensBefore;
            
            // Transfer tokens to buyer
            IERC20(coin_address).transfer(msg.sender, tokensReceived);
            
            emit TokenPurchased(msg.sender, coin_address, amountAfterTax, tokensReceived);
        }

        // Cache tokenCount to avoid multiple SLOAD operations
        uint256 currentTokenCount = tokenCount;

        TokenInfo memory newTokenInfo = TokenInfo({
            tokenAddress: coin_address,
            name: _name,
            symbol: _symbol,
            deployer: msg.sender,
            time: block.timestamp,
            metadata: _metadata,
            marketCapInETH: 0,
            totalFeesGenerated: 0
        });

        deployedTokens[currentTokenCount] = newTokenInfo;
        tokenInfoByAddress[coin_address] = newTokenInfo;
        tokenHook[coin_address] = klikHook;

        // Add token to creator's array
        creatorTokens[msg.sender].push(coin_address);

        // Assembly optimized increment (unchecked)
        assembly {
            sstore(tokenCount.slot, add(currentTokenCount, 1))
        }

        return tokensReceived;
    }

    // Returns the exact init_code that deployCoin will use, so frontends can
    // compute the CREATE2 address for a given salt before submitting the tx.
    // launchPeriod is read from current state — callers must re-read this if
    // the controller changes launchPeriod between salt-mining and deploy, or
    // the predicted address will diverge from the actual one.
    function getTokenBytecode(
        string memory _name,
        string memory _symbol,
        address creator
    ) public view returns (bytes memory bytecode) {
        bytecode = abi.encodePacked(
            type(Token).creationCode,
            abi.encode(_name, _symbol, creator, address(this), launchPeriod)
        );
    }

    function getPenalty(uint256 ethAmount) public pure returns (uint256) {
        if (ethAmount < 0.05 ether) return 0;
        if (ethAmount >= 0.30 ether) return 5000; // max 50%

        uint256 slope = 18000;
        uint256 delta = ethAmount - 0.05 ether;
        uint256 penalty = 500 + (delta * slope) / 1 ether;

        return penalty;
    }

    function toggleDeployCoin() external {
        require(msg.sender == platformController, "Caller is not controller");
        deployCoinEnabled = !deployCoinEnabled;
    }

    // Create new liquidity configuration
    function createLiquidityConfig(
        uint160 _sqrtPriceX96,
        int24 _tickLower,
        int24 _tickUpper,
        uint256 _amount0Desired,
        uint256 _amount1Desired,
        uint256 _virtualAmount,
        uint256 _penaltyMultiplier
    ) external returns (uint256 configId) {
        require(msg.sender == platformController, "Only platform controller can create liquidity config");
        require(_penaltyMultiplier <= 500, "Penalty multiplier must be at most 500%");

        configId = liquidityConfigCount;
        liquidityConfigs[configId] = LiquidityConfig({
            sqrtPriceX96: _sqrtPriceX96,
            tickLower: _tickLower,
            tickUpper: _tickUpper,
            amount0Desired: _amount0Desired,
            amount1Desired: _amount1Desired,
            virtualAmount: _virtualAmount,
            penaltyMultiplier: _penaltyMultiplier
        });
        liquidityConfigCount++;

        return configId;
    }
    
    // Update existing liquidity configuration
    function updateLiquidityConfig(
        uint256 _configId,
        uint160 _sqrtPriceX96,
        int24 _tickLower,
        int24 _tickUpper,
        uint256 _amount0Desired,
        uint256 _amount1Desired,
        uint256 _virtualAmount,
        uint256 _penaltyMultiplier
    ) external {
        require(msg.sender == platformController, "Only platform controller can update liquidity config");
        require(_configId < liquidityConfigCount, "Invalid config ID");
        require(_penaltyMultiplier <= 500, "Penalty multiplier must be at most 500%");

        liquidityConfigs[_configId] = LiquidityConfig({
            sqrtPriceX96: _sqrtPriceX96,
            tickLower: _tickLower,
            tickUpper: _tickUpper,
            amount0Desired: _amount0Desired,
            amount1Desired: _amount1Desired,
            virtualAmount: _virtualAmount,
            penaltyMultiplier: _penaltyMultiplier
        });
    }
    
    // Delete liquidity configuration (sets to zero values)
    function deleteLiquidityConfig(uint256 _configId) external {
        require(msg.sender == platformController, "Only platform controller can delete liquidity config");
        require(_configId < liquidityConfigCount, "Invalid config ID");
        require(_configId != 0, "Cannot delete default config");
        
        delete liquidityConfigs[_configId];
    }
    
    // Get liquidity configuration
    function getLiquidityConfig(uint256 _configId) external view returns (LiquidityConfig memory) {
        require(_configId < liquidityConfigCount, "Invalid config ID");
        return liquidityConfigs[_configId];
    }

    // Change the number of items per page for pagination
    function setItemsPerPage(uint256 _itemsPerPage) external {
        require(msg.sender == platformController, "Only platform controller can change items per page");
        require(_itemsPerPage > 0 && _itemsPerPage <= 1000, "Items per page must be between 1 and 1000");
        itemsPerPage = _itemsPerPage;
    }
    
    // Get all tokens for a creator in one call  
    function getAllTokensByCreator(address _creator) public view returns (address[] memory) {
        return creatorTokens[_creator];
    }

    function getDeploysByPage(uint256 page, uint256 order) public view returns (TokenInfo[] memory) {
        require(tokenCount > 0, "No tokens deployed");

        uint256 totalPages = (tokenCount + itemsPerPage - 1) / itemsPerPage;
        require(page < totalPages, "Page out of range");

        uint256 start;
        uint256 end;
        uint256 j = 0;

        if (order == 0) {
            // Newest first
            start = tokenCount > (page + 1) * itemsPerPage ? tokenCount - (page + 1) * itemsPerPage : 0;
            end = tokenCount - page * itemsPerPage;
            if (end > tokenCount) end = tokenCount;
        } else {
            // Oldest first
            start = page * itemsPerPage;
            end = start + itemsPerPage;
            if (end > tokenCount) end = tokenCount;
        }

        TokenInfo[] memory tokens = new TokenInfo[](end - start);

        for (uint256 i = start; i < end; i++) {
            uint256 index = order == 0 ? end - 1 - (i - start) : i;
            TokenInfo memory info = deployedTokens[index];

            // Calculate market cap using the correct formula
            uint256 marketCap = getMarketCap(info.tokenAddress);

            tokens[j++] = TokenInfo({
                tokenAddress: info.tokenAddress,
                name: info.name,
                symbol: info.symbol,
                deployer: info.deployer,
                time: info.time,
                metadata: info.metadata,
                marketCapInETH: marketCap,
                totalFeesGenerated: tokenFeesGenerated[info.tokenAddress]
            });
        }

        return tokens;
    }

    function withdrawFeesWETH() external {
        require(msg.sender == platformController, "Caller is not controller");
        uint256 wethBalance = IERC20(WETH).balanceOf(address(this));
        require(wethBalance > 0, "No WETH to withdraw");

        IWETH(WETH).withdraw(wethBalance);

        (bool success, ) = msg.sender.call{value: wethBalance}("");
        require(success, "ETH transfer failed");
    }

    function withdrawFeesETH() external {
        require(msg.sender == platformController, "Caller is not controller");

        uint256 ethBalance = address(this).balance;
        require(ethBalance > 0, "No ETH to withdraw");

        (bool success, ) = msg.sender.call{value: ethBalance}("");
        require(success, "ETH transfer failed");
    }

    function provideLiquidityV4(address tokenA, uint256 configId) internal {
        LiquidityConfig memory config = liquidityConfigs[configId];
        
        // Since address(0) is always < any token address, it's always currency0
        // No need for complex comparison logic
        IERC20 token = IERC20(tokenA);

        token.approve(address(PERMIT2), type(uint256).max);
        PERMIT2.approve(tokenA, address(POSITION_MANAGER), type(uint160).max, type(uint48).max);
        PERMIT2.approve(tokenA, address(POOL_MANAGER), type(uint160).max, type(uint48).max);

        PoolKey memory pool = PoolKey({
            currency0: Currency.wrap(address(0)),  // ETH is always currency0
            currency1: Currency.wrap(address(tokenA)), // Token is always currency1
            fee: 0,
            tickSpacing: 200,
            hooks: IHooks(klikHook) // Use universal hook
        });

        poolManager.initialize(pool, config.sqrtPriceX96);

        uint128 liquidity = _calculateLiquidity(
            config.sqrtPriceX96,
            config.tickLower,
            config.tickUpper,
            config.amount0Desired,
            config.amount1Desired
        );

        bytes memory actions = abi.encodePacked(uint8(Actions.MINT_POSITION), uint8(Actions.SETTLE_PAIR));

        bytes memory hookData = new bytes(0);

        bytes[] memory params = new bytes[](2);
        params[0] = abi.encode(
            pool,
            config.tickLower,
            config.tickUpper,
            liquidity,
            config.amount0Desired,
            config.amount1Desired,
            address(0x000000000000000000000000000000000000dEaD), // LP burned — no removal functions exist in this factory anyway; burning makes the lock verifiable on-chain
            hookData
        );

        params[1] = abi.encode(pool.currency0, pool.currency1);

        try positionManager.modifyLiquidities(
            abi.encode(actions, params),
            block.timestamp + 120
        ) {
            // Successfully added liquidity
        } catch (bytes memory reason) {
            assembly {
                revert(add(reason, 0x20), mload(reason))
            }
        }
    }

    function _calculateLiquidity(
        uint160 sqrtPriceX96,
        int24 tickLower,
        int24 tickUpper,
        uint256 amount0Desired,
        uint256 amount1Desired
    ) private pure returns (uint128) {
        return LiquidityAmounts.getLiquidityForAmounts(
            sqrtPriceX96,
            TickMath.getSqrtPriceAtTick(tickLower),
            TickMath.getSqrtPriceAtTick(tickUpper),
            amount0Desired,
            amount1Desired
        );
    }

    // Mirror of IV4Router.ExactInputSingleParams, declared locally so its
    // poolKey field uses THIS file's PoolKey type. Referencing IV4Router's
    // struct directly fails to compile: its PoolKey comes from a different
    // import path (@uniswap/v4-core) than the factory's (v4-core), and Solidity
    // treats the two as distinct types. The ABI layout is identical, which is
    // all the Universal Router's CalldataDecoder reads.
    struct ExactInputSingleParams {
        PoolKey poolKey;
        bool zeroForOne;
        uint128 amountIn;
        uint128 amountOutMinimum;
        bytes hookData;
    }

    function _buyToken(address tokenAddress, uint256 ethAmount) internal {
        // Create PoolKey for ETH -> Token swap
        PoolKey memory pool = PoolKey({
            currency0: Currency.wrap(address(0)), // ETH
            currency1: Currency.wrap(tokenAddress), // New token
            fee: 0,
            tickSpacing: 200,
            hooks: IHooks(klikHook) // Use universal hook
        });

        // Encode the Universal Router command
        bytes memory commands = abi.encodePacked(uint8(Commands.V4_SWAP));
        bytes[] memory inputs = new bytes[](1);

        // Encode V4Router actions
        bytes memory actions = abi.encodePacked(
            uint8(Actions.SWAP_EXACT_IN_SINGLE),
            uint8(Actions.SETTLE_ALL),
            uint8(Actions.TAKE_ALL)
        );

        // Prepare parameters for each action.
        // params[0] MUST be the ExactInputSingleParams struct (not its fields
        // spread out) — the struct encoding carries a leading offset word that
        // the Universal Router's CalldataDecoder requires. Spreading the fields
        // omits that word and reverts SliceOutOfBounds() on current UR builds.
        bytes[] memory params = new bytes[](3);
        params[0] = abi.encode(
            ExactInputSingleParams({
                poolKey: pool,
                zeroForOne: true, // ETH (currency0) -> Token (currency1)
                amountIn: uint128(ethAmount),
                amountOutMinimum: uint128(0), // 0 = no minimum requirement
                hookData: bytes("")
            })
        );
        params[1] = abi.encode(pool.currency0, ethAmount); // SETTLE_ALL params
        params[2] = abi.encode(pool.currency1, uint128(0)); // TAKE_ALL params

        // Combine actions and params into inputs
        inputs[0] = abi.encode(actions, params);

        // Execute the swap
        uint256 deadline = block.timestamp + 120;
        router.execute{value: ethAmount}(commands, inputs, deadline);

        // Tokens remain in contract - caller handles transfer and event emission
    }

    function collectFees(address tokenAddress) external nonReentrant returns (uint256 ethCollected) {
        address creator = IToken(tokenAddress).creator();
        require(msg.sender == creator || msg.sender == platformController, "Not authorized");

        uint256 balanceBefore = address(this).balance;
        IToken(tokenAddress).withdrawFees();
        uint256 balanceAfter = address(this).balance;
        
        ethCollected = balanceAfter - balanceBefore;

        if (ethCollected > 0) {
            tokenFeesGenerated[tokenAddress] += ethCollected;

            // Platform share is taken atomically by the hook at swap time.
            // Everything in the token contract is the creator's portion.
            (bool success, ) = payable(creator).call{value: ethCollected}("");
            require(success, "ETH transfer to creator failed");
        }

        return ethCollected;
    }

    function changeTokenFeeReceiver(address tokenAddress, address newCreator) external {
        address currentCreator = IToken(tokenAddress).creator();
        require(msg.sender == platformController || msg.sender == currentCreator, "Not authorized");
        require(newCreator != address(0), "New creator cannot be zero address");

        Token(payable(tokenAddress)).changeCreator(newCreator);
    }

    function getTokenPrice(address tokenAddress) public view returns (bytes32 poolIdBytes, uint160 sqrtPrice, uint256 calculatedPrice, uint256 marketCapETH) {
        // Create PoolKey for ETH/Token pool
        address hook = tokenHook[tokenAddress] != address(0) ? tokenHook[tokenAddress] : klikHook;
        PoolKey memory poolKey = PoolKey({
            currency0: Currency.wrap(address(0)), // ETH
            currency1: Currency.wrap(tokenAddress), // Token
            fee: 0,
            tickSpacing: 200,
            hooks: IHooks(hook)
        });

        // V4 approach: Get actual pool state using StateView
        PoolId poolId = PoolId.wrap(keccak256(abi.encode(poolKey)));
        poolIdBytes = PoolId.unwrap(poolId);

        try stateView.getSlot0(poolId) returns (uint160 sqrtPriceX96, int24, uint24, uint24) {
            sqrtPrice = sqrtPriceX96;

            if (sqrtPriceX96 > 0) {
                uint256 sqrtPriceX96_uint = uint256(sqrtPriceX96);
                uint256 q96 = 2**96;

                uint256 scaledDivisor = (q96 * 1e18) / sqrtPriceX96_uint;
                calculatedPrice = (scaledDivisor * q96) / sqrtPriceX96_uint; // finalPrice

                // Market Cap = price * totalSupply
                uint256 totalSupply = IERC20(tokenAddress).totalSupply();
                marketCapETH = calculatedPrice * totalSupply/1e18;

                return (poolIdBytes, sqrtPrice, calculatedPrice, marketCapETH);
            }
        } catch {
            // Pool doesn't exist - return 0
            return (poolIdBytes, 0, 0, 0);
        }

        return (poolIdBytes, 0, 0, 0);
    }

    function getMarketCap(address tokenAddress) public view returns (uint256 marketCapETH) {
        (, , , marketCapETH) = getTokenPrice(tokenAddress);
        return marketCapETH;
    }
    
    // Get total fees generated by a specific token
    function getTokenFeesGenerated(address tokenAddress) public view returns (uint256) {
        return tokenFeesGenerated[tokenAddress];
    }
}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract Token is ERC20, ERC20Burnable {
    address public platform;
    address public creator;
    address private _owner;
    uint256 private launchBlock;
    uint256 private maxTxAmount;
    uint256 private immutable LAUNCH_PERIOD;
    uint256 private constant MAX_WALLET_PERCENTAGE = 2; // 2% of total supply

    address immutable pool = 0x8366a39CC670B4001A1121B8F6A443A643e40951; // Robinhood Chain PoolManager

    // Track transfers per tx.origin per block to detect multi-swaps
    mapping(address => uint256) private tokensFromPoolPerOrigin;

    event FeesReceived(uint256 amount);

    constructor(
        string memory _name,
        string memory _symbol,
        address _creator,
        address _platform,
        uint256 _launchPeriod
    ) ERC20(_name, _symbol) {
        platform = _platform;
        creator = _creator;
        _owner = address(0);
        launchBlock = block.number;
        LAUNCH_PERIOD = _launchPeriod;

        uint256 totalTokens = 1000000000 * 10 ** decimals();
        maxTxAmount = (totalTokens * MAX_WALLET_PERCENTAGE) / 100;

        _mint(_platform, totalTokens);
    }

    function _update(address from, address to, uint256 value) internal override {
        if (block.number > launchBlock && block.number <= launchBlock + LAUNCH_PERIOD) {
            // Get pool address for exemption

            if (from == pool && to != platform && to != creator) {
                tokensFromPoolPerOrigin[tx.origin] += value;
                require(
                    tokensFromPoolPerOrigin[tx.origin] <= maxTxAmount*110/100,
                    "Keeping 2% pool Limits In Kontrol"
                );
            }

            if (to != creator && to != platform && to != pool && from != address(0)) {
                require(
                    balanceOf(to) + value <= maxTxAmount,
                    "Max wallet limit exceeded during launch period"
                );
            }
        }

        // Block all buys at launch block except exempted transfers
        if (block.number == launchBlock &&
            from != address(0) &&
            to != platform &&
            from != platform &&
            !(from == platform && to == creator)) { // Only platform can send to creator
            revert("No buys allowed during launch block!");
        }

        super._update(from, to, value);
    }

    function isLaunchPeriodActive() public view returns (bool) {
        return block.number <= launchBlock + LAUNCH_PERIOD;
    }

    // Returns address(0) so on-chain tools (Etherscan, token scanners) report ownership
    // as renounced. _owner is hardcoded to zero in the constructor and is never reassignable.
    function owner() public view returns (address) {
        return _owner;
    }

    // Receive ETH from hook fees
    receive() external payable {
        emit FeesReceived(msg.value);
    }

    // Allow only Factory to withdraw accumulated fees
    function withdrawFees() external returns (uint256 balance) {
        require(msg.sender == platform, "Only factory can withdraw");
        balance = address(this).balance;
        require(balance > 0, "No fees to withdraw");

        (bool success, ) = payable(platform).call{value: balance}("");
        require(success, "Fee withdrawal failed");
    }

    function changeCreator(address newCreator) external {
        require(msg.sender == platform, "Only platform can change creator"); // Updating Fee Receiver
        creator = newCreator;
    }
}
Chain explorer3067msChain node72ms