RobinPadFactory

0xc25c1e209313856e3a66fdd3afd98abe90b047f6

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
14,030 bytes
ABI entries
65
29 read · 10 write
License
none

Contract information

Address
0xc25c1e209313856e3a66fdd3afd98abe90b047f6
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0xaea5720738…de061d812c

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (29)

DEFAULT_ADMIN_ROLE()bytes32
GRADUATION_TARGET()uint256
MAX_LAUNCH_FEE()uint256
PAUSER_ROLE()bytes32
POOL_FEE_TIER()uint24
STANDARD_SUPPLY()uint256
UPGRADER_ROLE()bytes32
allTokens(uint256)address
feeRouter()address
getAllTokens()address[]
getRoleAdmin(bytes32)bytes32
hasRole(bytes32, address)bool
isGraduated(address)bool
isLaunchedToken(address)bool
launchFee()uint256
lpLocker()address
paused()bool
platformFeeRecipient()address
positionManager()address
referralRegistry()address
supportsInterface(bytes4)bool
swapRouter()address
tokenCount()uint256
tokenCreator(address)address
tokenImplementation()address
tokenLpTokenId(address)uint256
tokenPool(address)address
v3Factory()address
weth()address

Events (6)

LaunchFeeUpdatedPlatformFeeRecipientUpdatedRoleAdminChangedRoleGrantedRoleRevokedTokenLaunched

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "v3Factory_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "positionManager_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "swapRouter_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "weth_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "lpLocker_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "referralRegistry_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "feeRouter_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "tokenImplementation_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "admin",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "platformFeeRecipient_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AccessControlBadConfirmation",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "neededRole",
        "type": "bytes32"
      }
    ],
    "name": "AccessControlUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "target",
        "type": "address"
      }
    ],
    "name": "AddressEmptyCode",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "AddressInsufficientBalance",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "DeadlineExpired",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ERC1167FailedCreateClone",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FailedInnerCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "requested",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "max",
        "type": "uint256"
      }
    ],
    "name": "FeeTooHigh",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "sent",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "required",
        "type": "uint256"
      }
    ],
    "name": "InsufficientPayment",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LPMintFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotPaused",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "Paused",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "PoolAlreadyExists",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PoolInitFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "RefundFailed",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "oldFee",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newFee",
        "type": "uint256"
      }
    ],
    "name": "LaunchFeeUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "oldRecipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "newRecipient",
        "type": "address"
      }
    ],
    "name": "PlatformFeeRecipientUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "previousAdminRole",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "newAdminRole",
        "type": "bytes32"
      }
    ],
    "name": "RoleAdminChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "sender",
        "type": "address"
      }
    ],
    "name": "RoleGranted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "sender",
        "type": "address"
      }
    ],
    "name": "RoleRevoked",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "lpTokenId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "launchFeePaid",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "timestamp",
        "type": "uint256"
      }
    ],
    "name": "TokenLaunched",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "DEFAULT_ADMIN_ROLE",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "GRADUATION_TARGET",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_LAUNCH_FEE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "PAUSER_ROLE",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "POOL_FEE_TIER",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "STANDARD_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "UPGRADER_ROLE",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "allTokens",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "uint16",
        "name": "royaltyBps",
        "type": "uint16"
      },
      {
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "buyAmount",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "referrer",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minAmountOut",
        "type": "uint256"
      }
    ],
    "name": "createToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeRouter",
    "outputs": [
      {
        "internalType": "contract FeeRouter",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "getAllTokens",
    "outputs": [
      {
        "internalType": "address[]",
        "name": "",
        "type": "address[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      }
    ],
    "name": "getRoleAdmin",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "grantRole",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "hasRole",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "isGraduated",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "isLaunchedToken",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchFee",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "lpLocker",
    "outputs": [
      {
        "internalType": "contract LPLocker",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pause",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "paused",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "platformFeeRecipient",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "positionManager",
    "outputs": [
      {
        "internalType": "contract INonfungiblePositionManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "referralRegistry",
    "outputs": [
      {
        "internalType": "contract ReferralRegistry",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "callerConfirmation",
        "type": "address"
      }
    ],
    "name": "renounceRole",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "rescueETH",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "revokeRole",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "newFee",
        "type": "uint256"
      }
    ],
    "name": "setLaunchFee",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newRecipient",
        "type": "address"
      }
    ],
    "name": "setPlatformFeeRecipient",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newImpl",
        "type": "address"
      }
    ],
    "name": "setTokenImplementation",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes4",
        "name": "interfaceId",
        "type": "bytes4"
      }
    ],
    "name": "supportsInterface",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "swapRouter",
    "outputs": [
      {
        "internalType": "contract ISwapRouter",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokenCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "tokenCreator",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokenImplementation",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "tokenLpTokenId",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "tokenPool",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "unpause",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "v3Factory",
    "outputs": [
      {
        "internalType": "contract IUniswapV3Factory",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "weth",
    "outputs": [
      {
        "internalType": "contract IWETH",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

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

import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";

import {TokenTemplate} from "./TokenTemplate.sol";
import {LPLocker} from "./LPLocker.sol";
import {ReferralRegistry} from "./ReferralRegistry.sol";
import {FeeRouter} from "./FeeRouter.sol";
import {IUniswapV3Factory} from "./interfaces/IUniswapV3Factory.sol";
import {INonfungiblePositionManager} from "./interfaces/INonfungiblePositionManager.sol";
import {ISwapRouter} from "./interfaces/ISwapRouter.sol";
import {IWETH} from "./interfaces/IWETH.sol";

/**
 * @title RobinPadFactory
 * @notice Direct-to-Uniswap-V3 token launchpad (Model B - Single-Sided LP).
 *
 * @dev Launch flow (single atomic transaction):
 *      1. Deploy ERC-20 token via ERC-1167 clone (1B supply, fixed)
 *      2. Create V3 pool (token/WETH, 1% fee tier)
 *      3. Deposit 1B tokens as single-sided LP (no WETH seed required)
 *      4. Lock LP NFT permanently in LPLocker
 *      5. Optional: creator initial buy via SwapRouter
 *      6. Collect platform fee (launchFee ETH)
 *
 *      Security model:
 *      - ReentrancyGuard on createToken
 *      - Deadline parameter prevents mempool stuck transactions
 *      - Salt prevents token address collision / duplicate pool
 *      - LP NFT goes directly to LPLocker — cannot be retrieved by anyone
 *      - Fee recipient is a hot-wallet address, not this contract
 *      - No ETH stored in this contract after createToken (forwarded immediately)
 *      - Admin cannot rug: launchFee capped at MAX_LAUNCH_FEE
 *      - Admin cannot change immutable infrastructure (v3Factory, positionManager, etc.)
 *      - Pauser can only pause — admin alone can unpause
 *      - tokenImplementation upgrade requires UPGRADER_ROLE (separate from admin)
 *
 *      Fee model:
 *      - 0.0011 ETH platform fee per launch (admin-configurable, max 0.01 ETH)
 *      - 1% V3 pool fee accrues to locked LP; split 60% creator / 30% platform / 10% referral
 *
 * @custom:security No private keys, no backdoors, no selfdestruct, no delegatecall.
 */
contract RobinPadFactory is ReentrancyGuard, AccessControl {
    using SafeERC20 for IERC20;
    using Clones for address;

    // ── Roles ──────────────────────────────────────────────────────────────
    bytes32 public constant PAUSER_ROLE   = keccak256("PAUSER_ROLE");
    bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");

    // ── Immutable infrastructure (set in constructor, never changeable) ────
    IUniswapV3Factory              public immutable v3Factory;
    INonfungiblePositionManager    public immutable positionManager;
    ISwapRouter                    public immutable swapRouter;
    IWETH                          public immutable weth;
    LPLocker                       public immutable lpLocker;
    ReferralRegistry               public immutable referralRegistry;
    FeeRouter                      public immutable feeRouter;

    // ── Mutable config (admin-controlled, bounded) ────────────────────────
    /// @notice ERC-1167 implementation for clone. Changeable only by UPGRADER_ROLE.
    address public tokenImplementation;
    /// @notice ETH fee paid per token launch. Capped at MAX_LAUNCH_FEE.
    uint256 public launchFee = 0.0011 ether;
    /// @notice Recipient of launchFee. Must be non-zero before any launch.
    address public platformFeeRecipient;

    // ── Constants ─────────────────────────────────────────────────────────
    uint24  public constant POOL_FEE_TIER       = 10_000;                        // 1%
    uint256 public constant STANDARD_SUPPLY     = 1_000_000_000 * 10 ** 18;     // 1B
    uint256 public constant GRADUATION_TARGET   = 4.2 ether;                    // UI milestone
    uint256 public constant MAX_LAUNCH_FEE      = 0.01 ether;                   // admin cap

    // ── Pause ─────────────────────────────────────────────────────────────
    bool public paused;

    // ── Bookkeeping ───────────────────────────────────────────────────────
    mapping(address => bool)    public isLaunchedToken;
    address[]                   public allTokens;
    mapping(address => address) public tokenCreator;
    mapping(address => address) public tokenPool;
    mapping(address => uint256) public tokenLpTokenId;

    // ── Events ────────────────────────────────────────────────────────────
    event TokenLaunched(
        address indexed token,
        address indexed creator,
        address indexed pool,
        uint256 lpTokenId,
        uint256 launchFeePaid,
        uint256 timestamp
    );
    event LaunchFeeUpdated(uint256 oldFee, uint256 newFee);
    event PlatformFeeRecipientUpdated(address oldRecipient, address newRecipient);

    // ── Errors ────────────────────────────────────────────────────────────
    error DeadlineExpired(uint256 deadline);
    error Paused();
    error NotPaused();
    error PoolAlreadyExists(address token);
    error LPMintFailed();
    error InsufficientPayment(uint256 sent, uint256 required);
    error FeeTooHigh(uint256 requested, uint256 max);
    error ZeroAddress();
    error PoolInitFailed();
    error RefundFailed();

    // ── Modifiers ─────────────────────────────────────────────────────────
    modifier whenNotPaused() {
        if (paused) revert Paused();
        _;
    }
    modifier whenPaused() {
        if (!paused) revert NotPaused();
        _;
    }

    // ── Constructor ───────────────────────────────────────────────────────
    constructor(
        address v3Factory_,
        address positionManager_,
        address swapRouter_,
        address weth_,
        address lpLocker_,
        address referralRegistry_,
        address feeRouter_,
        address tokenImplementation_,
        address admin,
        address platformFeeRecipient_
    ) {
        if (
            v3Factory_         == address(0) ||
            positionManager_   == address(0) ||
            swapRouter_        == address(0) ||
            weth_              == address(0) ||
            lpLocker_          == address(0) ||
            referralRegistry_  == address(0) ||
            feeRouter_         == address(0) ||
            tokenImplementation_ == address(0) ||
            admin              == address(0) ||
            platformFeeRecipient_ == address(0)
        ) revert ZeroAddress();

        v3Factory         = IUniswapV3Factory(v3Factory_);
        positionManager   = INonfungiblePositionManager(positionManager_);
        swapRouter        = ISwapRouter(swapRouter_);
        weth              = IWETH(weth_);
        lpLocker          = LPLocker(lpLocker_);
        referralRegistry  = ReferralRegistry(referralRegistry_);
        feeRouter         = FeeRouter(feeRouter_);
        tokenImplementation   = tokenImplementation_;
        platformFeeRecipient  = platformFeeRecipient_;

        _grantRole(DEFAULT_ADMIN_ROLE, admin);
        _grantRole(PAUSER_ROLE,        admin);
        // UPGRADER_ROLE NOT granted at deploy — admin must explicitly grant it
    }

    // ── Core: Launch ──────────────────────────────────────────────────────

    /**
     * @notice Launch a token directly into Uniswap V3 in one atomic tx.
     *
     * @param name          Token name (stored in ERC-20, emitted on-chain)
     * @param symbol        Token symbol
     * @param royaltyBps    Creator royalty hint 0-200 bps (informational for FeeRouter)
     * @param metadataURI   IPFS/HTTPS URI for logo + metadata JSON
     * @param buyAmount     Wei to spend on creator initial buy (0 = skip)
     * @param referrer      Referral address (address(0) = none)
     * @param deadline      Unix timestamp; tx reverts if included in a block after this
     * @param minAmountOut  Minimum tokens out for creator buy (slippage protection; 0 = no check)
     * @return token        Newly deployed ERC-20 address
     * @return pool         Uniswap V3 pool address
     */
    function createToken(
        string  calldata name,
        string  calldata symbol,
        uint16           royaltyBps,
        string  calldata metadataURI,
        uint256          buyAmount,
        address          referrer,
        uint256          deadline,
        uint256          minAmountOut
    ) external payable nonReentrant whenNotPaused returns (address token, address pool) {

        // ── Deadline guard ────────────────────────────────────────────────
        // Standard deadline pattern (same as Uniswap): validator-level timestamp
        // manipulation tolerance (seconds) is negligible against a caller-chosen
        // deadline meant to bound minutes/hours of mempool exposure.
        // forge-lint: disable-next-line(block-timestamp)
        if (block.timestamp > deadline) revert DeadlineExpired(deadline);

        // ── Payment: must cover platform fee; remainder is creator buy budget ──
        uint256 totalRequired = launchFee;
        if (msg.value < totalRequired) revert InsufficientPayment(msg.value, totalRequired);
        uint256 creatorBuyBudget = msg.value - totalRequired;

        // Enforce buyAmount matches actual ETH sent (prevent griefing)
        if (buyAmount > creatorBuyBudget) {
            buyAmount = creatorBuyBudget;
        }

        // ── Step 1: Deploy ERC-20 via ERC-1167 clone ─────────────────────
        // Salt includes msg.sender + block.number to prevent predictable address attacks
        bytes32 salt = keccak256(abi.encodePacked(name, symbol, msg.sender, block.number, block.timestamp));
        token = tokenImplementation.cloneDeterministic(salt);
        // royaltyBps capped at 200 inside TokenTemplate.initialize
        TokenTemplate(token).initialize(name, symbol, msg.sender, royaltyBps, metadataURI);

        // ── Step 2: Create V3 pool ────────────────────────────────────────
        (address token0, address token1) = token < address(weth)
            ? (token, address(weth))
            : (address(weth), token);

        pool = v3Factory.getPool(token0, token1, POOL_FEE_TIER);
        if (pool != address(0)) revert PoolAlreadyExists(token);
        pool = v3Factory.createPool(token0, token1, POOL_FEE_TIER);

        // ── Step 3: Initialize pool price ────────────────────────────────
        // 1 token ≈ 1 nanoETH (10^-9 ETH). Tick: ±207200 depending on ordering.
        uint160 sqrtPriceX96;
        int24   tickLower;
        int24   tickUpper;

        if (token0 == token) {
            sqrtPriceX96 = 2505777000000000000000000;       // token is token0: price ~1e-9
            tickLower    = -207200;
            tickUpper    =  887200;
        } else {
            sqrtPriceX96 = 2505160000000000000000000000000000; // token is token1
            tickLower    = -887200;
            tickUpper    =  207200;
        }

        (bool ok,) = pool.call(abi.encodeWithSignature("initialize(uint160)", sqrtPriceX96));
        if (!ok) revert PoolInitFailed();

        // ── Step 4: Single-sided LP mint (1B tokens, 0 WETH) ─────────────
        IERC20(token).forceApprove(address(positionManager), STANDARD_SUPPLY);

        INonfungiblePositionManager.MintParams memory mintP = INonfungiblePositionManager.MintParams({
            token0:          token0,
            token1:          token1,
            fee:             POOL_FEE_TIER,
            tickLower:       tickLower,
            tickUpper:       tickUpper,
            amount0Desired:  token0 == token ? STANDARD_SUPPLY : 0,
            amount1Desired:  token1 == token ? STANDARD_SUPPLY : 0,
            amount0Min:      0,
            amount1Min:      0,
            recipient:       address(this),
            deadline:        deadline
        });

        (uint256 lpTokenId, uint128 liquidity,,) = positionManager.mint(mintP);
        if (liquidity == 0) revert LPMintFailed();
        // Defensive: reset any dangling allowance if mint() didn't consume the full
        // approved amount (single-sided full-range mint should consume it exactly).
        if (IERC20(token).allowance(address(this), address(positionManager)) > 0) {
            IERC20(token).forceApprove(address(positionManager), 0);
        }

        // ── Step 5: Lock LP NFT permanently ──────────────────────────────
        positionManager.approve(address(lpLocker), lpTokenId);
        positionManager.safeTransferFrom(address(this), address(lpLocker), lpTokenId);
        // Verify NFT actually transferred — prevents any approval-hijack scenario
        require(positionManager.ownerOf(lpTokenId) == address(lpLocker), "Lock failed");
        lpLocker.recordLock(lpTokenId, token, msg.sender);

        // ── Step 6: Pay platform fee ──────────────────────────────────────
        // Forward immediately — do NOT accumulate ETH in this contract
        if (launchFee > 0) {
            (bool feeOk,) = payable(platformFeeRecipient).call{value: launchFee}("");
            require(feeOk, "Fee transfer failed");
        }

        // ── Step 7: Register in FeeRouter ────────────────────────────────
        feeRouter.registerToken(token, msg.sender);

        // ── Step 8: Creator initial buy (optional) ────────────────────────
        uint256 actualBuy = buyAmount > 0 ? buyAmount : creatorBuyBudget;
        if (actualBuy > 0) {
            weth.deposit{value: actualBuy}();
            weth.approve(address(swapRouter), actualBuy);
            ISwapRouter.ExactInputSingleParams memory swapP = ISwapRouter.ExactInputSingleParams({
                tokenIn:           address(weth),
                tokenOut:          token,
                fee:               POOL_FEE_TIER,
                recipient:         msg.sender,
                deadline:          deadline,
                amountIn:          actualBuy,
                amountOutMinimum:  minAmountOut,
                sqrtPriceLimitX96: 0
            });
            swapRouter.exactInputSingle(swapP);
        }

        // Refund any ETH sent beyond launchFee + actualBuy (e.g. buyAmount
        // param was less than the budget implied by msg.value) — otherwise it
        // would sit stranded in this contract, recoverable only by admin via
        // rescueETH() to platformFeeRecipient, never back to the caller.
        uint256 unusedBudget = creatorBuyBudget - actualBuy;
        if (unusedBudget > 0) {
            (bool refundOk,) = payable(msg.sender).call{value: unusedBudget}("");
            if (!refundOk) revert RefundFailed();
        }

        // ── Step 9: Register referral ─────────────────────────────────────
        if (referrer != address(0) && referrer != msg.sender) {
            if (!referralRegistry.hasReferrer(msg.sender)) {
                try referralRegistry.registerReferrer(msg.sender, referrer) {} catch {}
            }
        }

        // ── Step 10: Record launch ────────────────────────────────────────
        isLaunchedToken[token]  = true;
        allTokens.push(token);
        tokenCreator[token]     = msg.sender;
        tokenPool[token]        = pool;
        tokenLpTokenId[token]   = lpTokenId;

        emit TokenLaunched(token, msg.sender, pool, lpTokenId, launchFee, block.timestamp);
    }

    // ── View helpers ──────────────────────────────────────────────────────

    function getAllTokens() external view returns (address[] memory) {
        return allTokens;
    }

    function tokenCount() external view returns (uint256) {
        return allTokens.length;
    }

    /**
     * @notice Simple graduation check: has the pool received ≥ 4.2 ETH in buys?
     *         Off-chain check is more reliable; this is a helper hint only.
     */
    function isGraduated(address token) external view returns (bool) {
        address pool = tokenPool[token];
        if (pool == address(0)) return false;
        // ETH side = WETH balance of pool
        uint256 wethInPool = IERC20(address(weth)).balanceOf(pool);
        return wethInPool >= GRADUATION_TARGET;
    }

    // ── Admin setters (bounded / role-gated) ─────────────────────────────

    /// @notice Update launch fee. Capped at MAX_LAUNCH_FEE (0.01 ETH) — admin cannot rug via fee.
    function setLaunchFee(uint256 newFee) external onlyRole(DEFAULT_ADMIN_ROLE) {
        if (newFee > MAX_LAUNCH_FEE) revert FeeTooHigh(newFee, MAX_LAUNCH_FEE);
        emit LaunchFeeUpdated(launchFee, newFee);
        launchFee = newFee;
    }

    /// @notice Update platform fee recipient. Must be non-zero.
    function setPlatformFeeRecipient(address newRecipient) external onlyRole(DEFAULT_ADMIN_ROLE) {
        if (newRecipient == address(0)) revert ZeroAddress();
        emit PlatformFeeRecipientUpdated(platformFeeRecipient, newRecipient);
        platformFeeRecipient = newRecipient;
    }

    /// @notice Update token implementation (new features, never changes existing tokens).
    ///         Requires separate UPGRADER_ROLE — admin alone cannot do this.
    function setTokenImplementation(address newImpl) external onlyRole(UPGRADER_ROLE) {
        if (newImpl == address(0)) revert ZeroAddress();
        tokenImplementation = newImpl;
    }

    // ── Pausing ───────────────────────────────────────────────────────────

    function pause() external onlyRole(PAUSER_ROLE) {
        paused = true;
    }

    function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) whenPaused {
        paused = false;
    }

    // ── ETH safety ────────────────────────────────────────────────────────

    /**
     * @notice Rescue any ETH accidentally sent to this contract.
     *         Only admin, only goes to platformFeeRecipient (not arbitrary address).
     */
    function rescueETH() external onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant {
        uint256 bal = address(this).balance;
        require(bal > 0, "Nothing to rescue");
        (bool ok,) = payable(platformFeeRecipient).call{value: bal}("");
        require(ok, "Rescue failed");
    }

    receive() external payable {}
}

/// @dev Minimal interface for reading V3 pool slot0 (used off-chain via view calls).
interface IUniswapV3PoolState {
    function slot0() external view returns (
        uint160 sqrtPriceX96,
        int24   tick,
        uint16  observationIndex,
        uint16  observationCardinality,
        uint16  observationCardinalityNext,
        uint8   feeProtocol,
        bool    unlocked
    );
}
Chain explorer2541msChain node88ms