LaunchFactory

0x965cac0abd623a2527da01269d7b9bc8aa12f39e

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
20,136 bytes
ABI entries
74
25 read · 4 write
License
none

Contract information

Address
0x965cac0abd623a2527da01269d7b9bc8aa12f39e
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0x3ebee5fa2a…baf4279e50

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (25)

BPS()uint256
CREATOR_BPS()uint16
FACTORY_VERSION()uint32
FEE()uint24
MAX_NAME_BYTES()uint256
MAX_SUPPLY()uint256
MAX_SYMBOL_BYTES()uint256
MAX_TX_CEIL()uint16
MAX_WALLET_CEIL()uint16
MAX_WINDOW()uint32
MIN_BPS()uint16
MIN_SUPPLY()uint256
MIN_WINDOW()uint32
TICK_SPACING()int24
feeManager()address
isLaunchToken(address)bool
launchCount()uint256
npm()address
owner()address
platformWallet()address
poolInitCodeHash()bytes32
previewToken(tuple, address)address
swapRouter02()address
v3Factory()address
weth()address

Events (3)

LaunchedOwnershipTransferredPlatformWalletUpdated

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "npm_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "v3Factory_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "swapRouter02_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "weth_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "feeManager_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "owner_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "platformWallet_",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "poolInitCodeHash_",
        "type": "bytes32"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "BadDevBuy",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "BadMcRange",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FeeManagerCodeMissing",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FeeManagerFactoryMismatch",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FeeManagerNpmMismatch",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FeeTierMismatch",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ForbiddenCreator",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ForbiddenPlatform",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LeftoverEth",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LeftoverWeth",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LiquidityMismatch",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LiquidityTooHigh",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LockFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "MaxTxAmountZero",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "MaxTxBpsOOB",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "MaxWalletAmountZero",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "MaxWalletBpsOOB",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NameLenOOB",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoLiquidity",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NpmFactoryMismatch",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NpmWethMismatch",
    "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": "PlatformChanged",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PoolMispriced",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PriceOutOfRange",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "RangeTooNarrow",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "RouterFactoryMismatch",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "RouterWethMismatch",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SupplyOOB",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SupplyZero",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SymbolLenOOB",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TickOutOfRange",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TokenAddressMismatch",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TokenNotZero",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "WalletBelowTx",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "WethConsumed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "WindowOOB",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "launchId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "components": [
          {
            "internalType": "address",
            "name": "platform",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "feeManager",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "pool",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "token0",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "token1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "feeTier",
            "type": "uint24"
          },
          {
            "internalType": "uint256",
            "name": "tokenId",
            "type": "uint256"
          },
          {
            "internalType": "uint128",
            "name": "liquidity",
            "type": "uint128"
          },
          {
            "internalType": "uint256",
            "name": "amount0Deposited",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "amount1Deposited",
            "type": "uint256"
          },
          {
            "internalType": "int24",
            "name": "tickLower",
            "type": "int24"
          },
          {
            "internalType": "int24",
            "name": "tickUpper",
            "type": "int24"
          },
          {
            "internalType": "uint160",
            "name": "initSqrtPriceX96",
            "type": "uint160"
          },
          {
            "internalType": "uint160",
            "name": "firstBuySqrtPriceX96",
            "type": "uint160"
          },
          {
            "internalType": "uint16",
            "name": "creatorBps",
            "type": "uint16"
          },
          {
            "internalType": "uint32",
            "name": "factoryVersion",
            "type": "uint32"
          },
          {
            "internalType": "bytes32",
            "name": "configHash",
            "type": "bytes32"
          },
          {
            "internalType": "uint256",
            "name": "devBuyAmountIn",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "devBuyAmountOut",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "restrictionsEndTime",
            "type": "uint256"
          },
          {
            "internalType": "uint16",
            "name": "maxTxBps",
            "type": "uint16"
          },
          {
            "internalType": "uint16",
            "name": "maxWalletBps",
            "type": "uint16"
          },
          {
            "internalType": "uint32",
            "name": "restrictionSeconds",
            "type": "uint32"
          }
        ],
        "indexed": false,
        "internalType": "struct LaunchFactory.LaunchInfo",
        "name": "info",
        "type": "tuple"
      }
    ],
    "name": "Launched",
    "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": true,
        "internalType": "address",
        "name": "oldWallet",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newWallet",
        "type": "address"
      }
    ],
    "name": "PlatformWalletUpdated",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "CREATOR_BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "FACTORY_VERSION",
    "outputs": [
      {
        "internalType": "uint32",
        "name": "",
        "type": "uint32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "FEE",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_NAME_BYTES",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_SYMBOL_BYTES",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_TX_CEIL",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_WALLET_CEIL",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_WINDOW",
    "outputs": [
      {
        "internalType": "uint32",
        "name": "",
        "type": "uint32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_WINDOW",
    "outputs": [
      {
        "internalType": "uint32",
        "name": "",
        "type": "uint32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TICK_SPACING",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeManager",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "isLaunchToken",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "string",
            "name": "name",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "symbol",
            "type": "string"
          },
          {
            "internalType": "uint256",
            "name": "totalSupply",
            "type": "uint256"
          },
          {
            "internalType": "uint16",
            "name": "maxTxBps",
            "type": "uint16"
          },
          {
            "internalType": "uint16",
            "name": "maxWalletBps",
            "type": "uint16"
          },
          {
            "internalType": "uint32",
            "name": "restrictionSeconds",
            "type": "uint32"
          },
          {
            "internalType": "uint256",
            "name": "startMcWeth",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "topMcWeth",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "devBuyMinOut",
            "type": "uint256"
          },
          {
            "internalType": "address",
            "name": "expectedPlatform",
            "type": "address"
          },
          {
            "internalType": "bytes32",
            "name": "userSalt",
            "type": "bytes32"
          }
        ],
        "internalType": "struct LaunchFactory.LaunchParams",
        "name": "p",
        "type": "tuple"
      }
    ],
    "name": "launch",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "npm",
    "outputs": [
      {
        "internalType": "contract INonfungiblePositionManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "platformWallet",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolInitCodeHash",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "string",
            "name": "name",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "symbol",
            "type": "string"
          },
          {
            "internalType": "uint256",
            "name": "totalSupply",
            "type": "uint256"
          },
          {
            "internalType": "uint16",
            "name": "maxTxBps",
            "type": "uint16"
          },
          {
            "internalType": "uint16",
            "name": "maxWalletBps",
            "type": "uint16"
          },
          {
            "internalType": "uint32",
            "name": "restrictionSeconds",
            "type": "uint32"
          },
          {
            "internalType": "uint256",
            "name": "startMcWeth",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "topMcWeth",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "devBuyMinOut",
            "type": "uint256"
          },
          {
            "internalType": "address",
            "name": "expectedPlatform",
            "type": "address"
          },
          {
            "internalType": "bytes32",
            "name": "userSalt",
            "type": "bytes32"
          }
        ],
        "internalType": "struct LaunchFactory.LaunchParams",
        "name": "p",
        "type": "tuple"
      },
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      }
    ],
    "name": "previewToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newWallet",
        "type": "address"
      }
    ],
    "name": "setPlatformWallet",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "swapRouter02",
    "outputs": [
      {
        "internalType": "contract ISwapRouter02",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "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 IWETH9",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

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

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {CREATE3} from "solady/utils/CREATE3.sol";

import {LaunchMath} from "./lib/LaunchMath.sol";
import {LaunchToken} from "./LaunchToken.sol";
import {INonfungiblePositionManager as INPM} from "./interfaces/INonfungiblePositionManager.sol";
import {IUniswapV3Factory} from "./interfaces/IUniswapV3Factory.sol";
import {IUniswapV3Pool} from "./interfaces/IUniswapV3Pool.sol";
import {ISwapRouter02} from "./interfaces/ISwapRouter02.sol";
import {IWETH9} from "./interfaces/IWETH9.sol";

/// @dev Minimal view surface of the FeeManager needed to hard-verify the bootstrap in the constructor.
interface IFeeManagerView {
    function factory() external view returns (address);
    function npm() external view returns (address);
}

/// @notice Versioned, permissionless orchestrator: in one atomic tx it deploys a LaunchToken (vanity CREATE3), opens
///         its single-sided Uniswap V3 market, locks the LP NFT into the FeeManager, does an optional fair-launch-
///         capped dev-buy, and self-registers. Replaceable control plane; the immutable artifacts are the tokens and
///         the locked LP. Revenue = swap fees only (the WETH raised is locked principal). See the 2026-07-14 spec.
/// @dev No receive/fallback/sweep/rescue: the factory is permanently `to`-exempt in every token it launches, so it
///      must never be a token-out surface. It holds no launched token / WETH / ETH after a launch (asserted).
contract LaunchFactory is Ownable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    // ---- policy constants (versioned; a v2 factory can retune) ----
    uint24 public constant FEE = 10000; // 1% tier; the only tier v1 supports
    int24 public constant TICK_SPACING = 200; // spacing of the 1% tier
    uint256 public constant BPS = 10_000;
    uint16 public constant CREATOR_BPS = 8000; // 80/20 split policy, never caller input (design D6)
    uint32 public constant FACTORY_VERSION = 1;

    // fair-launch bounds (design D8/§7). All in RAW base units where applicable.
    uint256 public constant MIN_SUPPLY = 1e18;
    uint256 public constant MAX_SUPPLY = 1e36; // <= uint128 fee-headroom (~3.4e38) and < type(uint256).max / BPS
    uint16 public constant MIN_BPS = 50; // 0.5% floor (matches LaunchToken)
    uint16 public constant MAX_TX_CEIL = 100; // 1% ceiling on per-buy size
    uint16 public constant MAX_WALLET_CEIL = 300; // 3% ceiling on accumulation
    uint32 public constant MIN_WINDOW = 300; // 5 min anti-sniper floor
    uint32 public constant MAX_WINDOW = 3600; // 1h ceiling (matches LaunchToken)
    uint256 public constant MAX_NAME_BYTES = 64;
    uint256 public constant MAX_SYMBOL_BYTES = 16;

    // ---- immutables (hard-verified at deploy) ----
    INPM public immutable npm;
    IUniswapV3Factory public immutable v3Factory;
    ISwapRouter02 public immutable swapRouter02;
    IWETH9 public immutable weth;
    address public immutable feeManager;
    bytes32 public immutable poolInitCodeHash; // §5 deploy-time cross-check only (design D9); unused in launch()

    // ---- state ----
    address public platformWallet; // the one admin knob; affects only future launches
    uint256 public launchCount;
    mapping(address => bool) public isLaunchToken;

    /// @param name/symbol the token metadata; totalSupply RAW base units; maxTxBps/maxWalletBps/restrictionSeconds
    ///        the anti-sniper params (floored/capped by factory policy); startMcWeth/topMcWeth the market-cap band the
    ///        single-sided range spans; devBuyMinOut the dev-buy slippage floor (0 iff msg.value == 0); expectedPlatform
    ///        binds the launch to the platformWallet the caller saw; userSalt the offline-ground vanity nonce.
    struct LaunchParams {
        string name;
        string symbol;
        uint256 totalSupply;
        uint16 maxTxBps;
        uint16 maxWalletBps;
        uint32 restrictionSeconds;
        uint256 startMcWeth;
        uint256 topMcWeth;
        uint256 devBuyMinOut;
        address expectedPlatform;
        bytes32 userSalt;
    }

    /// @dev Rich launch record for indexers (design §6.10 / inherited §4.4). Carries the ACTUAL pool + token0/token1
    ///      (no ordering assumption) and the REALIZED aligned ticks. Emits BOTH the init sqrtPrice (one tick below the
    ///      near boundary for token0) and the effective first-buy boundary price (what a trader actually gets).
    struct LaunchInfo {
        address platform;
        address feeManager;
        address pool;
        address token0;
        address token1;
        uint24 feeTier;
        uint256 tokenId;
        uint128 liquidity;
        uint256 amount0Deposited;
        uint256 amount1Deposited;
        int24 tickLower;
        int24 tickUpper;
        uint160 initSqrtPriceX96;
        uint160 firstBuySqrtPriceX96;
        uint16 creatorBps;
        uint32 factoryVersion;
        bytes32 configHash;
        uint256 devBuyAmountIn;
        uint256 devBuyAmountOut;
        uint256 restrictionsEndTime;
        uint16 maxTxBps;
        uint16 maxWalletBps;
        uint32 restrictionSeconds;
    }

    event Launched(uint256 indexed launchId, address indexed token, address indexed creator, LaunchInfo info);
    event PlatformWalletUpdated(address indexed oldWallet, address indexed newWallet);

    // ---- errors ----
    error ZeroAddress();
    error NpmFactoryMismatch();
    error NpmWethMismatch();
    error RouterFactoryMismatch();
    error RouterWethMismatch();
    error FeeTierMismatch();
    error FeeManagerCodeMissing();
    error FeeManagerFactoryMismatch();
    error FeeManagerNpmMismatch();
    error ForbiddenPlatform();
    error ForbiddenCreator();
    error PlatformChanged();
    error SupplyOOB();
    error MaxTxBpsOOB();
    error MaxWalletBpsOOB();
    error WalletBelowTx();
    error WindowOOB();
    error NameLenOOB();
    error SymbolLenOOB();
    error MaxTxAmountZero();
    error MaxWalletAmountZero();
    error BadDevBuy();
    error PoolMispriced();
    error TokenAddressMismatch();
    error WethConsumed();
    error NoLiquidity();
    error TokenNotZero();
    error LockFailed();
    error LiquidityMismatch();
    error LeftoverWeth();
    error LeftoverEth();

    constructor(
        address npm_,
        address v3Factory_,
        address swapRouter02_,
        address weth_,
        address feeManager_,
        address owner_,
        address platformWallet_,
        bytes32 poolInitCodeHash_
    ) Ownable(owner_) {
        if (
            npm_ == address(0) || v3Factory_ == address(0) || swapRouter02_ == address(0) || weth_ == address(0)
                || feeManager_ == address(0)
        ) revert ZeroAddress();

        // Cross-link the external Uniswap deployment (RH hosts multiple V3 deployments): the NPM and router must both
        // point at THIS v3Factory + weth, and the 1% tier must have spacing 200. Codehash-vs-canonical is a deploy-
        // script concern (§5); these on-chain links are the in-constructor half.
        if (INPM(npm_).factory() != v3Factory_) revert NpmFactoryMismatch();
        if (INPM(npm_).WETH9() != weth_) revert NpmWethMismatch();
        if (ISwapRouter02(swapRouter02_).factory() != v3Factory_) revert RouterFactoryMismatch();
        if (ISwapRouter02(swapRouter02_).WETH9() != weth_) revert RouterWethMismatch();
        if (IUniswapV3Factory(v3Factory_).feeAmountTickSpacing(FEE) != TICK_SPACING) revert FeeTierMismatch();

        // The FeeManager must already exist and trust THIS factory (address(this) is the final CREATE3 address, per the
        // §5 bootstrap) with the same NPM — closes the bootstrap-integrity gap: a FeeManager can't end up trusting an
        // empty or wrong factory.
        if (feeManager_.code.length == 0) revert FeeManagerCodeMissing();
        if (IFeeManagerView(feeManager_).factory() != address(this)) revert FeeManagerFactoryMismatch();
        if (IFeeManagerView(feeManager_).npm() != npm_) revert FeeManagerNpmMismatch();

        npm = INPM(npm_);
        v3Factory = IUniswapV3Factory(v3Factory_);
        swapRouter02 = ISwapRouter02(swapRouter02_);
        weth = IWETH9(weth_);
        feeManager = feeManager_;
        poolInitCodeHash = poolInitCodeHash_;

        if (_forbidden(platformWallet_)) revert ForbiddenPlatform();
        platformWallet = platformWallet_;
    }

    // ---- admin ----

    /// @notice Update the platform fee recipient for FUTURE launches (each launch snapshots it into the FeeManager).
    function setPlatformWallet(address newWallet) external onlyOwner {
        if (_forbidden(newWallet)) revert ForbiddenPlatform();
        emit PlatformWalletUpdated(platformWallet, newWallet);
        platformWallet = newWallet;
    }

    // ---- launch ----

    /// @notice Atomically deploy + market-make + lock + optionally dev-buy + register a fair launch.
    /// @dev creator = msg.sender; platform = platformWallet; split = CREATOR_BPS; msg.value = dev-buy WETH (0 = none).
    function launch(LaunchParams calldata p)
        external
        payable
        nonReentrant
        returns (address token, uint256 tokenId)
    {
        _checkBounds(p);
        address creator = msg.sender;
        address platform = platformWallet;
        if (p.expectedPlatform != platform) revert PlatformChanged();
        if (_forbidden(creator)) revert ForbiddenCreator();

        LaunchInfo memory info;
        info.platform = platform;
        info.feeManager = feeManager;
        info.creatorBps = CREATOR_BPS;
        info.factoryVersion = FACTORY_VERSION;
        info.feeTier = FEE;
        info.maxTxBps = p.maxTxBps;
        info.maxWalletBps = p.maxWalletBps;
        info.restrictionSeconds = p.restrictionSeconds;
        info.devBuyAmountIn = msg.value;

        bytes32 salt;
        (salt, info.configHash) = _saltFor(p, creator);
        token = CREATE3.predictDeterministicAddress(salt, address(this));

        bool isToken0 = token < address(weth);
        // Execution order (design sequencing note): LaunchMath BEFORE pool init.
        {
            (uint160 sqrtPriceX96, int24 tickLower, int24 tickUpper) =
                LaunchMath.computeRange(p.startMcWeth, p.topMcWeth, p.totalSupply, isToken0);
            info.initSqrtPriceX96 = sqrtPriceX96;
            // token0 inits one raw sqrtPrice unit below the near boundary (LaunchMath: getSqrtPriceAtTick(tickLower)-1),
            // so init+1 IS exactly the boundary price the first buy reaches. token1 trades immediately at the init
            // boundary. (Not a full tick gap — the empty span is sub-tick; the first buy still lands at tickLower.)
            info.firstBuySqrtPriceX96 = isToken0 ? sqrtPriceX96 + 1 : sqrtPriceX96;
            info.tickLower = tickLower;
            info.tickUpper = tickUpper;
            info.pool = _openPool(token, sqrtPriceX96);
        }
        (info.token0, info.token1) = isToken0 ? (token, address(weth)) : (address(weth), token);

        if (_deployToken(p, salt, info.pool) != token) revert TokenAddressMismatch();
        // forge-lint: disable-next-line(block-timestamp)
        info.restrictionsEndTime = LaunchToken(token).restrictionsEndTime();

        {
            uint256 a0;
            uint256 a1;
            (tokenId, info.liquidity, a0, a1) =
                _mintSingleSided(token, isToken0, info.tickLower, info.tickUpper, p.totalSupply);
            // The WETH-side *Desired was 0; the WETH-side realized amount MUST be exactly 0 (no principal consumed).
            if ((isToken0 ? a1 : a0) != 0) revert WethConsumed();
            info.amount0Deposited = a0;
            info.amount1Deposited = a1;
        }
        if (info.liquidity == 0) revert NoLiquidity();
        info.tokenId = tokenId;
        _dustAndLock(token, info.pool, tokenId, info.liquidity, creator, platform);

        info.devBuyAmountOut = _devBuy(token, creator, p.devBuyMinOut);

        isLaunchToken[token] = true;
        emit Launched(++launchCount, token, creator, info);
    }

    /// @notice The token address `launch(p)` will produce for `creator` — for offline vanity grinding + pre-checks.
    function previewToken(LaunchParams calldata p, address creator) public view returns (address) {
        (bytes32 salt,) = _saltFor(p, creator);
        return CREATE3.predictDeterministicAddress(salt, address(this));
    }

    // ---- internals ----

    /// @dev Caller-bound salt: keccak(creator, chainid, configHash, userSalt). Not stealable by a front-runner.
    function _saltFor(LaunchParams calldata p, address creator)
        internal
        view
        returns (bytes32 salt, bytes32 configHash)
    {
        configHash = keccak256(abi.encode(p, creator));
        salt = keccak256(abi.encode(creator, block.chainid, configHash, p.userSalt));
    }

    /// @dev getPool-first (both work with `token` having no code yet): reuse an existing pool at the expected price,
    ///      initialize an uninitialized one, reject a pre-initialized wrong-price pool (threat #1). A rejected vanity
    ///      salt is burned — re-grind a new userSalt (V3 pools cannot be re-initialized).
    function _openPool(address token, uint160 sqrtPriceX96) internal returns (address pool) {
        pool = v3Factory.getPool(token, address(weth), FEE);
        if (pool == address(0)) pool = v3Factory.createPool(token, address(weth), FEE);
        (uint160 existing,,,,,,) = IUniswapV3Pool(pool).slot0();
        if (existing == 0) {
            IUniswapV3Pool(pool).initialize(sqrtPriceX96);
        } else if (existing != sqrtPriceX96) {
            revert PoolMispriced();
        }
    }

    /// @dev Deploy the token at the predicted address with pool_ = the ACTUAL pool, recipient_ = this factory (full
    ///      supply minted here; factory auto-exempt as recipient), exempt_ = [feeManager] (only it; pool never exempt).
    function _deployToken(LaunchParams calldata p, bytes32 salt, address pool) internal returns (address) {
        address[] memory exempt = new address[](1);
        exempt[0] = feeManager;
        bytes memory initcode = abi.encodePacked(
            type(LaunchToken).creationCode,
            abi.encode(
                p.name,
                p.symbol,
                p.totalSupply,
                uint256(p.maxTxBps),
                uint256(p.maxWalletBps),
                uint256(p.restrictionSeconds),
                pool,
                address(this),
                exempt
            )
        );
        return CREATE3.deployDeterministic(initcode, salt);
    }

    /// @dev Mint the single-sided position (WETH-side *Desired == 0) and immediately reset the NPM allowance. Returns
    ///      the realized deposited amounts (amount0/amount1); the caller asserts the WETH-side one is exactly 0.
    function _mintSingleSided(
        address token,
        bool isToken0,
        int24 tickLower,
        int24 tickUpper,
        uint256 totalSupply
    ) internal returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) {
        IERC20(token).forceApprove(address(npm), totalSupply);
        (tokenId, liquidity, amount0, amount1) = npm.mint(
            INPM.MintParams({
                token0: isToken0 ? token : address(weth),
                token1: isToken0 ? address(weth) : token,
                fee: FEE,
                tickLower: tickLower,
                tickUpper: tickUpper,
                amount0Desired: isToken0 ? totalSupply : 0,
                amount1Desired: isToken0 ? 0 : totalSupply,
                amount0Min: 0,
                amount1Min: 0,
                recipient: address(this),
                // forge-lint: disable-next-line(block-timestamp)
                deadline: block.timestamp
            })
        );
        IERC20(token).forceApprove(address(npm), 0);
    }

    /// @dev Donate NPM rounding dust to the pool (keeping factory token balance a hard 0), then lock the NFT into the
    ///      FeeManager and assert ownership transferred with the mint-returned liquidity intact. No decreaseLiquidity/
    ///      collect/approve on the NFT between mint and lock.
    function _dustAndLock(
        address token,
        address pool,
        uint256 tokenId,
        uint128 liquidity,
        address creator,
        address platform
    ) internal {
        uint256 dust = IERC20(token).balanceOf(address(this));
        if (dust > 0) IERC20(token).safeTransfer(pool, dust); // to == pool is never restricted
        if (IERC20(token).balanceOf(address(this)) != 0) revert TokenNotZero();

        npm.safeTransferFrom(address(this), feeManager, tokenId, abi.encode(creator, platform, CREATOR_BPS));
        if (npm.ownerOf(tokenId) != feeManager) revert LockFailed();
        (,,,,,,, uint128 lockedLiq,,,,) = npm.positions(tokenId);
        if (lockedLiq != liquidity) revert LiquidityMismatch();
    }

    /// @dev Optional dev-buy: wrap msg.value, buy `token` to the NON-EXEMPT creator during the enforced window (so
    ///      LaunchToken caps it at maxTx and creator <= maxWallet — the whole launch reverts if the caller oversized
    ///      msg.value). Balance asserts use DELTAS vs pre-call balances so a griefing 1-wei force-send of WETH/ETH
    ///      cannot brick launches (only THIS launch's WETH/ETH flow must net to zero).
    function _devBuy(address token, address creator, uint256 minOut) internal returns (uint256 amountOut) {
        if (msg.value == 0) return 0;
        uint256 wethBefore = weth.balanceOf(address(this));
        uint256 ethResidual = address(this).balance - msg.value; // pre-existing forced ETH, tolerated

        weth.deposit{value: msg.value}();
        IERC20(address(weth)).forceApprove(address(swapRouter02), msg.value);
        amountOut = swapRouter02.exactInputSingle(
            ISwapRouter02.ExactInputSingleParams({
                tokenIn: address(weth),
                tokenOut: token,
                fee: FEE,
                recipient: creator,
                amountIn: msg.value,
                amountOutMinimum: minOut,
                sqrtPriceLimitX96: 0
            })
        );
        IERC20(address(weth)).forceApprove(address(swapRouter02), 0);

        // The full msg.value must have been consumed (a dev-buy that leaves WETH cleared the whole curve = misconfig).
        if (weth.balanceOf(address(this)) != wethBefore) revert LeftoverWeth();
        if (address(this).balance != ethResidual) revert LeftoverEth();
        // No launched token may be stranded here: the buy goes to the creator, so a router that instead routed any
        // token to the factory (hostile/misbehaving) is caught (§6.9 no-token-out). The token is born this tx and
        // was asserted 0 pre-dev-buy, so absolute ==0 (not a delta) is correct and un-griefable.
        if (IERC20(token).balanceOf(address(this)) != 0) revert TokenNotZero();
    }

    /// @dev Fair-launch + sanity bounds enforced on every launch (custom errors; §7). Delegates the MC/tick/liquidity
    ///      validity to LaunchMath (single source of truth). Also asserts the derived anti-sniper amounts are nonzero.
    function _checkBounds(LaunchParams calldata p) internal view {
        if (p.totalSupply < MIN_SUPPLY || p.totalSupply > MAX_SUPPLY) revert SupplyOOB();
        if (p.maxTxBps < MIN_BPS || p.maxTxBps > MAX_TX_CEIL) revert MaxTxBpsOOB();
        if (p.maxWalletBps < MIN_BPS || p.maxWalletBps > MAX_WALLET_CEIL) revert MaxWalletBpsOOB();
        if (p.maxWalletBps < p.maxTxBps) revert WalletBelowTx();
        if (p.restrictionSeconds < MIN_WINDOW || p.restrictionSeconds > MAX_WINDOW) revert WindowOOB();

        uint256 nameLen = bytes(p.name).length;
        if (nameLen == 0 || nameLen > MAX_NAME_BYTES) revert NameLenOOB();
        uint256 symLen = bytes(p.symbol).length;
        if (symLen == 0 || symLen > MAX_SYMBOL_BYTES) revert SymbolLenOOB();

        // Defense in depth (LaunchToken re-checks): the derived per-buy / per-wallet caps must be nonzero.
        if ((p.totalSupply * p.maxTxBps) / BPS == 0) revert MaxTxAmountZero();
        if ((p.totalSupply * p.maxWalletBps) / BPS == 0) revert MaxWalletAmountZero();

        // Dev-buy consistency in both directions: (no value <=> no minOut).
        if (msg.value == 0) {
            if (p.devBuyMinOut != 0) revert BadDevBuy();
        } else if (p.devBuyMinOut == 0) {
            revert BadDevBuy();
        }
    }

    /// @dev The forbidden recipient/creator set: none of these may be a fee recipient (would strand fees) nor creator.
    function _forbidden(address a) internal view returns (bool) {
        return a == address(0) || a == address(npm) || a == address(v3Factory) || a == address(swapRouter02)
            || a == address(weth) || a == feeManager || a == address(this);
    }
}
Chain explorer7963msChain node101ms