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
- Creator
- 0x9880Ce9a03…e2d8548ce9
- 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