TokenLauncher

0x9e2d2581fc04cae50c21b4ba7b44b9a099c4fe6f

Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
7,386 bytes
ABI entries
36
13 read · 9 write
License
none

Contract information

Address
0x9e2d2581fc04cae50c21b4ba7b44b9a099c4fe6f
Chain
Robinhood Chain (4663)
Compiler
v0.8.28+commit.7893614a
Optimization
Enabled
Creation tx
0xe07776495c…a9b332a57a

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (13)

TOTAL_SUPPLY()uint256
complianceGate()address
configRegistry()address
dexAdapters(uint256)address
feeRouter()address
lastLaunchAt(address)uint256
launchCooldown()uint256
lpLocker()address
owner()address
paused()bool
swapRouter()address
tokenImplementation()address
weth()address

Events (6)

DexAdapterSetLaunchCooldownSetOwnershipTransferredPausedTokenLaunchedUnpaused

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "weth_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "swapRouter_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "treasury_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "development_",
        "type": "address"
      },
      {
        "internalType": "contract INonfungiblePositionManager",
        "name": "positionManager_",
        "type": "address"
      },
      {
        "internalType": "contract ConfigRegistry",
        "name": "configRegistry_",
        "type": "address"
      },
      {
        "internalType": "contract IComplianceGate",
        "name": "complianceGate_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "EnforcedPause",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ExpectedPause",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FailedDeployment",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "balance",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "needed",
        "type": "uint256"
      }
    ],
    "name": "InsufficientBalance",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "dexId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "adapter",
        "type": "address"
      }
    ],
    "name": "DexAdapterSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "cooldown",
        "type": "uint256"
      }
    ],
    "name": "LaunchCooldownSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "Paused",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "positionId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "configId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "dexId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "ipfsMetadata",
        "type": "string"
      }
    ],
    "name": "TokenLaunched",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "Unpaused",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "TOTAL_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "complianceGate",
    "outputs": [
      {
        "internalType": "contract IComplianceGate",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "configRegistry",
    "outputs": [
      {
        "internalType": "contract ConfigRegistry",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "dexAdapters",
    "outputs": [
      {
        "internalType": "contract IDexAdapter",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeRouter",
    "outputs": [
      {
        "internalType": "contract FeeRouter",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "lastLaunchAt",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchCooldown",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "ipfsMetadata",
        "type": "string"
      },
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "configId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "dexId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "initialBuyWeth",
        "type": "uint256"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      }
    ],
    "name": "launchToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "positionId",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "lpLocker",
    "outputs": [
      {
        "internalType": "contract LPLocker",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "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": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "contract IComplianceGate",
        "name": "gate",
        "type": "address"
      }
    ],
    "name": "setComplianceGate",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "contract ConfigRegistry",
        "name": "registry",
        "type": "address"
      }
    ],
    "name": "setConfigRegistry",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "dexId",
        "type": "uint256"
      },
      {
        "internalType": "contract IDexAdapter",
        "name": "adapter",
        "type": "address"
      }
    ],
    "name": "setDexAdapter",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "cooldown",
        "type": "uint256"
      }
    ],
    "name": "setLaunchCooldown",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "swapRouter",
    "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": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "unpause",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "weth",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

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

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { Pausable } from "@openzeppelin/contracts/utils/Pausable.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { Clones } from "@openzeppelin/contracts/proxy/Clones.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { INonfungiblePositionManager, ISwapRouter, IWETH9 } from "./interfaces/external/IUniswapV3.sol";
import { IComplianceGate } from "./interfaces/IComplianceGate.sol";
import { IDexAdapter } from "./interfaces/IDexAdapter.sol";
import { ConfigRegistry } from "./ConfigRegistry.sol";
import { FeeRouter } from "./FeeRouter.sol";
import { LPLocker } from "./LPLocker.sol";
import { ChadToken } from "./ChadToken.sol";

/// @notice The launchpad entrypoint. A single `launchToken` call deploys a fixed-supply token,
/// creates a Uniswap V3 pool with single-sided liquidity, and permanently locks the LP —
/// mirroring NOXA's `launchToken(name, symbol, ipfsMetadata, creator, configId, dexId)` shape.
///
/// Anti-abuse: launches pass through a pluggable compliance gate (blocklist by default), a
/// per-sender cooldown, a non-refundable creation fee, and launch-metadata validation. The whole
/// entrypoint is pausable (incident kill-switch) and reentrancy-guarded.
contract TokenLauncher is Ownable, Pausable, ReentrancyGuard {
    uint256 public constant TOTAL_SUPPLY = 69_000_000_000 ether; // 69B, 18 decimals

    address public immutable weth;
    address public immutable swapRouter; // Uniswap V3 SwapRouter, for the optional atomic launch buy
    address public immutable tokenImplementation; // cloned per launch (EIP-1167)
    ConfigRegistry public configRegistry;
    IComplianceGate public complianceGate;
    FeeRouter public immutable feeRouter;
    LPLocker public immutable lpLocker;

    // Anti-spam: minimum seconds between launches from the same sender (0 = disabled).
    uint256 public launchCooldown;
    mapping(address => uint256) public lastLaunchAt;

    mapping(uint256 => IDexAdapter) public dexAdapters; // dexId => adapter

    event DexAdapterSet(uint256 indexed dexId, address adapter);
    event LaunchCooldownSet(uint256 cooldown);
    event TokenLaunched(
        address indexed token,
        address indexed creator,
        address pool,
        uint256 positionId,
        uint256 configId,
        uint256 dexId,
        string ipfsMetadata
    );

    constructor(
        address owner_,
        address weth_,
        address swapRouter_,
        address treasury_,
        address development_,
        INonfungiblePositionManager positionManager_,
        ConfigRegistry configRegistry_,
        IComplianceGate complianceGate_
    ) Ownable(owner_) {
        weth = weth_;
        swapRouter = swapRouter_;
        configRegistry = configRegistry_;
        complianceGate = complianceGate_;
        tokenImplementation = address(new ChadToken());
        feeRouter = new FeeRouter(address(this), owner_, treasury_, development_);
        lpLocker = new LPLocker(address(this), positionManager_, feeRouter);
        feeRouter.setLocker(address(lpLocker));
    }

    function setDexAdapter(uint256 dexId, IDexAdapter adapter) external onlyOwner {
        dexAdapters[dexId] = adapter;
        emit DexAdapterSet(dexId, address(adapter));
    }

    function setComplianceGate(IComplianceGate gate) external onlyOwner {
        complianceGate = gate;
    }

    function setConfigRegistry(ConfigRegistry registry) external onlyOwner {
        configRegistry = registry;
    }

    function setLaunchCooldown(uint256 cooldown) external onlyOwner {
        launchCooldown = cooldown;
        emit LaunchCooldownSet(cooldown);
    }

    /// @notice Owner kill-switch: halts all new launches during an incident. Fee collection on
    /// already-launched tokens (LPLocker/FeeRouter) is unaffected.
    function pause() external onlyOwner {
        _pause();
    }

    function unpause() external onlyOwner {
        _unpause();
    }

    /// @param salt A caller-mined salt. Every launched token must sort BEFORE WETH by address
    /// (become Uniswap's token0) so a single fixed tick range yields the same market-cap curve for
    /// every launch — see the "smooth/unbounded" liquidity model. The frontend mines a salt whose
    /// deterministic clone address is < WETH; the salt is namespaced per sender to prevent griefing.
    /// @param initialBuyWeth Optional ETH (on top of the creation fee) the creator spends on an
    /// atomic first buy of their own token in this same transaction — guaranteed, front-run-proof,
    /// and bounded by the config's `maxInitialBuyBps` (0 = unlimited).
    function launchToken(
        string calldata name,
        string calldata symbol,
        string calldata ipfsMetadata,
        address creator,
        uint256 configId,
        uint256 dexId,
        uint256 initialBuyWeth,
        bytes32 salt
    ) external payable whenNotPaused nonReentrant returns (address token, address pool, uint256 positionId) {
        require(complianceGate.isAllowed(msg.sender), "not allowed");
        require(creator != address(0), "zero creator");
        _validateMetadata(name, symbol, ipfsMetadata);

        if (launchCooldown != 0) {
            require(block.timestamp >= lastLaunchAt[msg.sender] + launchCooldown, "cooldown");
        }
        lastLaunchAt[msg.sender] = block.timestamp;

        IDexAdapter adapter = dexAdapters[dexId];
        require(address(adapter) != address(0), "unknown dex");

        ConfigRegistry.LaunchConfig memory cfg = configRegistry.getConfig(configId);
        require(msg.value >= cfg.creationFee + initialBuyWeth, "insufficient value");

        token = Clones.cloneDeterministic(tokenImplementation, keccak256(abi.encodePacked(msg.sender, salt)));
        require(token < weth, "token must sort first"); // enforce token0 ordering for a uniform curve
        ChadToken(token).initialize(
            name,
            symbol,
            TOTAL_SUPPLY,
            address(this),
            cfg.maxWalletBps,
            cfg.maxWalletBlocks,
            cfg.antiSnipeBlocks,
            cfg.antiSnipeMaxTxBps
        );

        // Exempt the adapter up front so pulling the full supply for seeding doesn't trip the
        // wallet cap (the cap itself stays disabled until finalizeLaunch, below).
        ChadToken(token).setCapExempt(address(adapter), true);

        IERC20(token).approve(address(adapter), TOTAL_SUPPLY);
        (pool, positionId) = adapter.createPoolAndMintSingleSided(
            IDexAdapter.SingleSidedParams({
                token: token,
                pairedToken: weth,
                tokenAmount: TOTAL_SUPPLY,
                amountTokenMin: TOTAL_SUPPLY - (TOTAL_SUPPLY * cfg.slippageBps) / 10_000,
                tickLower: cfg.tickLower,
                tickUpper: cfg.tickUpper,
                feeTier: cfg.feeTier,
                positionRecipient: address(lpLocker)
            })
        );

        // Pool must stay exempt permanently so sells back into it never trip the cap.
        ChadToken(token).setCapExempt(pool, true);
        ChadToken(token).setCapExempt(address(lpLocker), true);

        // Optional atomic creator buy — done while the token is still pre-launch (caps off) so it
        // can't be front-run and isn't subject to the anti-snipe/wallet caps. Bounded separately by
        // maxInitialBuyBps below.
        if (initialBuyWeth != 0) {
            _initialBuy(token, creator, initialBuyWeth, cfg.feeTier, cfg.maxInitialBuyBps);
        }

        ChadToken(token).finalizeLaunch();

        lpLocker.registerPosition(positionId, token, weth);
        feeRouter.registerPosition(
            positionId, creator, token, weth, pool, cfg.treasuryFeeBps, cfg.devFeeBps, cfg.graduationTick
        );

        _settleCreationFee(cfg.creationFee, cfg.treasuryFeeBps, cfg.devFeeBps, initialBuyWeth);

        emit TokenLaunched(token, creator, pool, positionId, configId, dexId, ipfsMetadata);
    }

    /// @dev Wraps the creator's ETH and swaps it for the freshly-launched token via the V3 router,
    /// sending the tokens straight to the creator. Runs inside launchToken (pre-finalize), so it's
    /// atomic with the launch — no MEV/sandwich is possible, hence amountOutMinimum can be 0. The
    /// resulting balance is bounded by maxInitialBuyBps (0 = unlimited).
    function _initialBuy(address token, address creator, uint256 amountWeth, uint24 feeTier, uint16 maxInitialBuyBps)
        internal
    {
        IWETH9(weth).deposit{ value: amountWeth }();
        IWETH9(weth).approve(swapRouter, amountWeth);
        uint256 out = ISwapRouter(swapRouter).exactInputSingle(
            ISwapRouter.ExactInputSingleParams({
                tokenIn: weth,
                tokenOut: token,
                fee: feeTier,
                recipient: creator,
                deadline: block.timestamp,
                amountIn: amountWeth,
                amountOutMinimum: 0,
                sqrtPriceLimitX96: 0
            })
        );
        IWETH9(weth).approve(swapRouter, 0); // clear any residual allowance (defense-in-depth)
        if (maxInitialBuyBps != 0) {
            require(out <= (TOTAL_SUPPLY * maxInitialBuyBps) / 10_000, "initial buy exceeds max");
        }
    }

    function _validateMetadata(string calldata name, string calldata symbol, string calldata ipfsMetadata)
        internal
        pure
    {
        uint256 nameLen = bytes(name).length;
        uint256 symLen = bytes(symbol).length;
        require(nameLen != 0 && nameLen <= 64, "bad name");
        require(symLen != 0 && symLen <= 16, "bad symbol");
        require(bytes(ipfsMetadata).length != 0, "no metadata");
    }

    /// @notice The creation fee is an anti-spam charge paid by the launcher, so no creator share:
    /// it is split between treasury and development by the config's treasury:dev weight (all to
    /// treasury if both weights are zero). Any overpayment is refunded to the sender.
    function _settleCreationFee(uint256 fee, uint16 treasuryFeeBps, uint16 devFeeBps, uint256 initialBuyWeth)
        internal
    {
        if (fee != 0) {
            uint256 denom = uint256(treasuryFeeBps) + devFeeBps;
            uint256 toDev = denom == 0 ? 0 : (fee * devFeeBps) / denom;
            uint256 toTreasury = fee - toDev;
            if (toTreasury != 0) {
                (bool okT,) = feeRouter.treasury().call{ value: toTreasury }("");
                require(okT, "treasury xfer failed");
            }
            if (toDev != 0) {
                (bool okD,) = feeRouter.development().call{ value: toDev }("");
                require(okD, "dev xfer failed");
            }
        }
        // initialBuyWeth was already spent on the atomic buy; refund only the true overpayment.
        uint256 refund = msg.value - fee - initialBuyWeth;
        if (refund != 0) {
            (bool okR,) = msg.sender.call{ value: refund }("");
            require(okR, "refund failed");
        }
    }
}
Chain explorer6940msChain node89ms