LaunchFactory

0x9fafa3b9ffcc7b3b9d118085797024be6eaf83ea

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
9,470 bytes
ABI entries
21
8 read · 6 write
License
none

Contract information

Address
0x9fafa3b9ffcc7b3b9d118085797024be6eaf83ea
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0x5a79a692c3…a961cf931a

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (8)

POOL_FEE()uint24
antiSnipeSeconds()uint256
creationFee()uint256
creatorFeeBps()uint16
locker()address
owner()address
paused()bool
treasury()address

Events (2)

OwnershipTransferredTokenLaunched

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "treasury_",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "antiSnipeSeconds_",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "FeeTooLow",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LaunchesPaused",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotOwner",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "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": "lpTokenId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      }
    ],
    "name": "TokenLaunched",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "POOL_FEE",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "antiSnipeSeconds",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "creationFee",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "creatorFeeBps",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "string",
            "name": "name",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "symbol",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "metadataURI",
            "type": "string"
          }
        ],
        "internalType": "struct LaunchFactory.TokenMeta",
        "name": "meta",
        "type": "tuple"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      }
    ],
    "name": "launchToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "lpTokenId",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "locker",
    "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": "paused",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "v",
        "type": "uint256"
      }
    ],
    "name": "setCreationFee",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint16",
        "name": "v",
        "type": "uint16"
      }
    ],
    "name": "setCreatorFeeBps",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bool",
        "name": "v",
        "type": "bool"
      }
    ],
    "name": "setPaused",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "v",
        "type": "address"
      }
    ],
    "name": "setTreasury",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "treasury",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

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

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {LaunchToken} from "./LaunchToken.sol";
import {LpLocker} from "./LpLocker.sol";
import {RobinhoodChain} from "./RobinhoodChain.sol";
import {
    IUniswapV3Factory,
    IUniswapV3Pool,
    INonfungiblePositionManager,
    IWETH9,
    ISwapRouter02
} from "./interfaces/IUniswapV3.sol";

/// @title LaunchFactory
/// @notice One-transaction token launch, reproducing Noxa Fun on Uniswap V3.
///
/// Observed Noxa entrypoint (decoded on-chain):
///   launchToken((name, symbol, metadataURI, ..., socials, address), configId, dexId, salt)
///   msg.value = creationFee (~0.0005 ETH) + optional dev-buy
///
/// The launch does, atomically:
///   1. take the flat creation fee
///   2. CREATE2-deploy the LaunchToken (full 1B supply minted to this factory)
///   3. create + initialize the 1% V3 pool at the opening price
///   4. mint the ENTIRE supply as a single-sided position (the "bonding curve")
///   5. transfer the LP NFT to the LpLocker (permanent, rug-proof)
///   6. optionally execute the creator's dev-buy in the same tx
///
/// @dev Steps 3–4 (pool init sqrtPrice + single-sided tick range) are the only
///      non-trivial mechanics; the exact tick math is finalized in Phase 3 against
///      a mainnet fork and by decoding one live Noxa launch. Everything else here
///      is the real control flow.
contract LaunchFactory {
    struct TokenMeta {
        string name;
        string symbol;
        string metadataURI; // ipfs://…
    }

    uint256 public creationFee = RobinhoodChain.CREATION_FEE;
    uint24 public constant POOL_FEE = RobinhoodChain.POOL_FEE; // 1%
    // Our split: 60% creator / 40% platform (Noxa gives creators only 35%, so this
    // is the lever to pull launches over). Change any time via setCreatorFeeBps.
    uint16 public creatorFeeBps = RobinhoodChain.CREATOR_FEE_BPS;
    uint256 public antiSnipeSeconds; // anti-snipe window (~1h) — set in constructor
    bool public paused; // owner kill switch for NEW launches only — never trading

    address public owner;
    address public treasury;
    LpLocker public immutable locker;

    IUniswapV3Factory constant V3_FACTORY = IUniswapV3Factory(RobinhoodChain.UNIV3_FACTORY);
    INonfungiblePositionManager constant NPM =
        INonfungiblePositionManager(RobinhoodChain.NONFUNGIBLE_POSITION_MANAGER);
    address constant WETH = RobinhoodChain.WETH;

    // ── Noxa launch template — read off every live Noxa LP position ──────────
    // fee 10000, tickLower -887200, tickUpper 204200, single-sided (all token).
    // Opening FDV ≈ 1.356 ETH (opening price ~1.36e-9 ETH/token × 1B supply).
    // The token side is single-sided, so the opening tick is the bound nearest
    // spot; ticks/price mirror depending on token↔WETH address ordering.
    int24 constant NOXA_TICK = 204200; // |opening tick|
    int24 constant FULL_TICK = 887200; // full-range bound (aligned to spacing 200)
    // sqrtPriceX96 at the opening tick (precomputed via TickMath):
    uint160 constant SQRT_OPEN_TOKEN1 = 2151813121295408910812139624586144; // tick  204200
    uint160 constant SQRT_OPEN_TOKEN0 = 2917122157712197017744680; //        tick -204200

    event TokenLaunched(
        address indexed token,
        address indexed creator,
        address pool,
        uint256 lpTokenId,
        string name,
        string symbol
    );
    event OwnershipTransferred(address indexed from, address indexed to);

    error FeeTooLow();
    error NotOwner();
    error ZeroAddress();
    error LaunchesPaused();

    modifier onlyOwner() {
        if (msg.sender != owner) revert NotOwner();
        _;
    }

    constructor(address treasury_, uint256 antiSnipeSeconds_) {
        owner = msg.sender;
        treasury = treasury_;
        antiSnipeSeconds = antiSnipeSeconds_;
        locker = new LpLocker(RobinhoodChain.NONFUNGIBLE_POSITION_MANAGER, treasury_, WETH);
    }

    /// @notice Launch a token. `msg.value` = creationFee + optional dev-buy.
    function launchToken(TokenMeta calldata meta, bytes32 salt)
        external
        payable
        returns (address token, address pool, uint256 lpTokenId)
    {
        if (paused) revert LaunchesPaused();
        if (msg.value < creationFee) revert FeeTooLow();
        uint256 devBuy = msg.value - creationFee;

        // 1. creation fee → treasury
        (bool ok,) = treasury.call{value: creationFee}("");
        require(ok, "fee xfer failed");

        // 2. deploy token — full supply minted to this factory. Pool/router are
        //    NOT exempt, so buyers stay capped to 2% during the window; the pool
        //    is registered below so sells (wallet → pool) clear the wallet cap.
        token = address(
            new LaunchToken{salt: salt}(
                meta.name, meta.symbol, msg.sender, _exemptList(), antiSnipeSeconds, 0
            )
        );
        uint256 supply = LaunchToken(token).SUPPLY();

        // Noxa mines the salt so the token address sorts ABOVE WETH, making the
        // token always token1 and the template deterministic. Enforce the same;
        // the deployer mines a qualifying salt off-chain (WETH is low, so most do).
        // 3. create + initialize the 1% pool at Noxa's exact opening price.
        //    Ticks, opening sqrtPrice and the seeded side all mirror on the
        //    token↔WETH address ordering — but the opening FDV (≈1.356 ETH) is
        //    identical either way, so no salt-mining is needed (any salt works).
        bool tokenIsToken0 = uint160(token) < uint160(WETH);
        (address t0, address t1) = tokenIsToken0 ? (token, WETH) : (WETH, token);
        int24 tickLower;
        int24 tickUpper;
        uint160 sqrtOpen;
        uint256 amt0;
        uint256 amt1;
        if (tokenIsToken0) {
            // token = token0: seed all token0, opened at the lower bound.
            tickLower = -NOXA_TICK; // -204200
            tickUpper = FULL_TICK; //   887200
            sqrtOpen = SQRT_OPEN_TOKEN0;
            amt0 = supply;
        } else {
            // token = token1 (Noxa's usual case): seed all token1, opened at 204200.
            tickLower = -FULL_TICK; // -887200
            tickUpper = NOXA_TICK; //   204200
            sqrtOpen = SQRT_OPEN_TOKEN1;
            amt1 = supply;
        }

        pool = V3_FACTORY.getPool(token, WETH, POOL_FEE);
        if (pool == address(0)) pool = V3_FACTORY.createPool(token, WETH, POOL_FEE);
        IUniswapV3Pool(pool).initialize(sqrtOpen);
        // Register the pool so it's excused from the max-wallet cap as recipient
        // (the sell path). It stays non-exempt otherwise, keeping buys capped.
        LaunchToken(token).setPool(pool);

        // 4. single-sided mint of the entire supply (the "bonding curve").
        IERC20(token).approve(address(NPM), supply);
        (lpTokenId,,,) = NPM.mint(
            INonfungiblePositionManager.MintParams({
                token0: t0,
                token1: t1,
                fee: POOL_FEE,
                tickLower: tickLower,
                tickUpper: tickUpper,
                amount0Desired: amt0,
                amount1Desired: amt1,
                amount0Min: 0,
                amount1Min: 0,
                recipient: address(this),
                deadline: block.timestamp
            })
        );

        // 5. lock the LP NFT permanently, carrying the creator/platform split.
        NPM.safeTransferFrom(
            address(this), address(locker), lpTokenId, abi.encode(msg.sender, creatorFeeBps)
        );

        // 6. optional dev-buy in the same tx: any ETH beyond the creation fee is
        //    swapped WETH→token via SwapRouter02 straight to the creator. The
        //    creator is anti-snipe-exempt, so the dev allocation isn't capped.
        if (devBuy > 0) {
            IWETH9(WETH).deposit{value: devBuy}();
            IWETH9(WETH).approve(RobinhoodChain.SWAP_ROUTER_02, devBuy);
            ISwapRouter02(RobinhoodChain.SWAP_ROUTER_02).exactInputSingle(
                ISwapRouter02.ExactInputSingleParams({
                    tokenIn: WETH,
                    tokenOut: token,
                    fee: POOL_FEE,
                    recipient: msg.sender,
                    amountIn: devBuy,
                    // Atomic with pool creation at the known opening price — nobody
                    // can sandwich between init and this buy, so 0 is safe here.
                    amountOutMinimum: 0,
                    sqrtPriceLimitX96: 0
                })
            );
        }

        emit TokenLaunched(token, msg.sender, pool, lpTokenId, meta.name, meta.symbol);
    }

    /// @dev Anti-snipe exemptions: the NPM (seeds the pool), the locker
    ///      (receives the token side of fees, which can exceed the cap), and
    ///      the dead address (fee burns must never revert). In a helper to
    ///      keep launchToken's stack depth in check.
    function _exemptList() internal view returns (address[] memory exempt) {
        exempt = new address[](3);
        exempt[0] = address(NPM);
        exempt[1] = address(locker);
        exempt[2] = 0x000000000000000000000000000000000000dEaD;
    }

    // ── admin ───────────────────────────────────────────────────────────────
    function setCreationFee(uint256 v) external onlyOwner {
        creationFee = v;
    }

    /// @notice Stop/resume NEW launches. Has no power over existing tokens,
    ///         pools, locked LP, or fee claims — those are unstoppable.
    function setPaused(bool v) external onlyOwner {
        paused = v;
    }

    function setCreatorFeeBps(uint16 v) external onlyOwner {
        require(v <= 10_000, "bad bps");
        creatorFeeBps = v;
    }

    function setTreasury(address v) external onlyOwner {
        if (v == address(0)) revert ZeroAddress();
        treasury = v;
    }

    /// @notice Hand admin (fee/treasury control) to a more secure wallet later —
    ///         e.g. move off the deployer key onto a hardware wallet or multisig.
    function transferOwnership(address newOwner) external onlyOwner {
        if (newOwner == address(0)) revert ZeroAddress();
        emit OwnershipTransferred(owner, newOwner);
        owner = newOwner;
    }
}
Chain explorer2428msChain node85ms