HoodLauncher
0x5e4121c262b846eb518ef3eadcd5566838aa841f
Verification
Verified
0.8.28+commit.7893614a
Type
Contract
22,741 bytes
ABI entries
86
34 read · 14 write
License
none
Contract information
- Address
- 0x5e4121c262b846eb518ef3eadcd5566838aa841f
- Chain
- Robinhood Chain (4663)
- Compiler
- 0.8.28+commit.7893614a
- Optimization
- Enabled
- Creator
- 0x26e17f2EA4…C92f268863
- Creation tx
- 0x5981b8171e…2257be0964
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (34)
DEFAULT_SUPPLY() → uint256
HARD_MAX_FDV_WEI() → uint256
MAX_BPS() → uint16
MAX_LAUNCH_FEE() → uint256
MAX_SUPPLY() → uint256
MAX_USABLE_TICK() → int24
MIN_CREATOR_BPS() → uint16
MIN_SUPPLY() → uint256
POOL_FEE() → uint24
TICK_SPACING() → int24
VENUE_SUSHI_V3() → uint8
VENUE_UNISWAP_V3() → uint8
allTokens(uint256) → address
computeTokenAddress(address, tuple) → address
creatorBps() → uint16
creatorFeeManager() → address
getLaunch(address) → tuple
getVenue(uint8) → tuple
launchFee() → uint256
launches(address) → address, address, uint256, uint64, uint8
maxFdvWei() → uint256
maxSupply() → uint256
maxWalletBps() → uint16
minFdvWei() → uint256
minSupply() → uint256
owner() → address
pendingOwner() → address
restrictionBlocks() → uint32
tokenCount() → uint256
tokenFeeManager() → address
tokenOwnerRegistry() → address
treasury() → address
venueExists(uint8) → bool
weth() → address
Events (13)
CreatorBpsUpdatedCreatorFeeManagerUpdatedFdvBoundsUpdatedLaunchFeeUpdatedOwnershipTransferStartedOwnershipTransferredRestrictionConfigUpdatedSupplyBoundsUpdatedTokenFeeManagerUpdatedTokenLaunchedTokenOwnerRegistryUpdatedTreasuryUpdatedVenueAdded
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "initialOwner",
"type": "address"
},
{
"internalType": "address",
"name": "weth_",
"type": "address"
},
{
"internalType": "address",
"name": "treasury_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "CreatorFeeManagerNotSet",
"type": "error"
},
{
"inputs": [],
"name": "EmptyString",
"type": "error"
},
{
"inputs": [],
"name": "EthTransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "IncompatibleVenue",
"type": "error"
},
{
"inputs": [],
"name": "InsufficientValue",
"type": "error"
},
{
"inputs": [],
"name": "InvalidBounds",
"type": "error"
},
{
"inputs": [],
"name": "InvalidBps",
"type": "error"
},
{
"inputs": [],
"name": "InvalidSupply",
"type": "error"
},
{
"inputs": [],
"name": "InvalidTick",
"type": "error"
},
{
"inputs": [],
"name": "LaunchFeeTooHigh",
"type": "error"
},
{
"inputs": [],
"name": "LockerMismatch",
"type": "error"
},
{
"inputs": [],
"name": "MarketCapOutOfRange",
"type": "error"
},
{
"inputs": [],
"name": "NotSingleSided",
"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": "PoolPriceMismatch",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "T",
"type": "error"
},
{
"inputs": [],
"name": "TokenFeeManagerNotSet",
"type": "error"
},
{
"inputs": [],
"name": "TokenOwnerRegistryNotSet",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint8",
"name": "venueId",
"type": "uint8"
}
],
"name": "UnknownVenue",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint8",
"name": "venueId",
"type": "uint8"
}
],
"name": "VenueExists",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint16",
"name": "previousBps",
"type": "uint16"
},
{
"indexed": false,
"internalType": "uint16",
"name": "newBps",
"type": "uint16"
}
],
"name": "CreatorBpsUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousManager",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newManager",
"type": "address"
}
],
"name": "CreatorFeeManagerUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "minFdvWei",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "maxFdvWei",
"type": "uint256"
}
],
"name": "FdvBoundsUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "previousFee",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "newFee",
"type": "uint256"
}
],
"name": "LaunchFeeUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferStarted",
"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": "uint16",
"name": "maxWalletBps",
"type": "uint16"
},
{
"indexed": false,
"internalType": "uint32",
"name": "restrictionBlocks",
"type": "uint32"
}
],
"name": "RestrictionConfigUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "minSupply",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "maxSupply",
"type": "uint256"
}
],
"name": "SupplyBoundsUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousManager",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newManager",
"type": "address"
}
],
"name": "TokenFeeManagerUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"indexed": false,
"internalType": "uint8",
"name": "venueId",
"type": "uint8"
},
{
"indexed": false,
"internalType": "uint256",
"name": "positionId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "supply",
"type": "uint256"
},
{
"indexed": false,
"internalType": "int24",
"name": "startTick",
"type": "int24"
},
{
"indexed": false,
"internalType": "uint16",
"name": "creatorBps",
"type": "uint16"
},
{
"indexed": false,
"internalType": "uint16",
"name": "maxWalletBps",
"type": "uint16"
},
{
"indexed": false,
"internalType": "uint32",
"name": "restrictionBlocks",
"type": "uint32"
},
{
"indexed": false,
"internalType": "string",
"name": "name",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "metadataURI",
"type": "string"
},
{
"indexed": false,
"internalType": "uint256",
"name": "devBuyEth",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "devBuyTokens",
"type": "uint256"
}
],
"name": "TokenLaunched",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousRegistry",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newRegistry",
"type": "address"
}
],
"name": "TokenOwnerRegistryUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousTreasury",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newTreasury",
"type": "address"
}
],
"name": "TreasuryUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint8",
"name": "venueId",
"type": "uint8"
},
{
"indexed": true,
"internalType": "address",
"name": "npm",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "factory",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "swapRouter",
"type": "address"
},
{
"indexed": false,
"internalType": "enum HoodLauncher.RouterKind",
"name": "routerKind",
"type": "uint8"
},
{
"indexed": false,
"internalType": "address",
"name": "feeLocker",
"type": "address"
}
],
"name": "VenueAdded",
"type": "event"
},
{
"inputs": [],
"name": "DEFAULT_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "HARD_MAX_FDV_WEI",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_BPS",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_LAUNCH_FEE",
"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_USABLE_TICK",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MIN_CREATOR_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": "POOL_FEE",
"outputs": [
{
"internalType": "uint24",
"name": "",
"type": "uint24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TICK_SPACING",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "VENUE_SUSHI_V3",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "VENUE_UNISWAP_V3",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "acceptOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint8",
"name": "venueId",
"type": "uint8"
},
{
"internalType": "address",
"name": "npm_",
"type": "address"
},
{
"internalType": "address",
"name": "swapRouter_",
"type": "address"
},
{
"internalType": "enum HoodLauncher.RouterKind",
"name": "routerKind_",
"type": "uint8"
},
{
"internalType": "address",
"name": "feeLocker_",
"type": "address"
}
],
"name": "addVenue",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "allTokens",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "deployer",
"type": "address"
},
{
"components": [
{
"internalType": "uint8",
"name": "venueId",
"type": "uint8"
},
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "image",
"type": "string"
},
{
"internalType": "string",
"name": "description",
"type": "string"
},
{
"internalType": "string",
"name": "socials",
"type": "string"
},
{
"internalType": "string",
"name": "metadataURI",
"type": "string"
},
{
"internalType": "bytes32",
"name": "userSalt",
"type": "bytes32"
},
{
"internalType": "int24",
"name": "tickIfToken0IsNewToken",
"type": "int24"
},
{
"internalType": "uint256",
"name": "supply",
"type": "uint256"
},
{
"internalType": "bool",
"name": "sniperGuard",
"type": "bool"
},
{
"internalType": "uint256",
"name": "devBuyMinOut",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "tokenFeeConfig",
"type": "bytes"
},
{
"internalType": "bytes",
"name": "creatorFeeConfig",
"type": "bytes"
}
],
"internalType": "struct HoodLauncher.LaunchParams",
"name": "p",
"type": "tuple"
}
],
"name": "computeTokenAddress",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "creatorBps",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "creatorFeeManager",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "getLaunch",
"outputs": [
{
"components": [
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"internalType": "uint256",
"name": "positionId",
"type": "uint256"
},
{
"internalType": "uint64",
"name": "createdAt",
"type": "uint64"
},
{
"internalType": "uint8",
"name": "venueId",
"type": "uint8"
}
],
"internalType": "struct HoodLauncher.LaunchInfo",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint8",
"name": "venueId",
"type": "uint8"
}
],
"name": "getVenue",
"outputs": [
{
"components": [
{
"internalType": "contract INonfungiblePositionManager",
"name": "npm",
"type": "address"
},
{
"internalType": "contract IUniswapV3Factory",
"name": "factory",
"type": "address"
},
{
"internalType": "address",
"name": "swapRouter",
"type": "address"
},
{
"internalType": "enum HoodLauncher.RouterKind",
"name": "routerKind",
"type": "uint8"
},
{
"internalType": "address",
"name": "feeLocker",
"type": "address"
}
],
"internalType": "struct HoodLauncher.Venue",
"name": "venue",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "uint8",
"name": "venueId",
"type": "uint8"
},
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "image",
"type": "string"
},
{
"internalType": "string",
"name": "description",
"type": "string"
},
{
"internalType": "string",
"name": "socials",
"type": "string"
},
{
"internalType": "string",
"name": "metadataURI",
"type": "string"
},
{
"internalType": "bytes32",
"name": "userSalt",
"type": "bytes32"
},
{
"internalType": "int24",
"name": "tickIfToken0IsNewToken",
"type": "int24"
},
{
"internalType": "uint256",
"name": "supply",
"type": "uint256"
},
{
"internalType": "bool",
"name": "sniperGuard",
"type": "bool"
},
{
"internalType": "uint256",
"name": "devBuyMinOut",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "tokenFeeConfig",
"type": "bytes"
},
{
"internalType": "bytes",
"name": "creatorFeeConfig",
"type": "bytes"
}
],
"internalType": "struct HoodLauncher.LaunchParams",
"name": "p",
"type": "tuple"
}
],
"name": "launch",
"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": "launchFee",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "launches",
"outputs": [
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"internalType": "uint256",
"name": "positionId",
"type": "uint256"
},
{
"internalType": "uint64",
"name": "createdAt",
"type": "uint64"
},
{
"internalType": "uint8",
"name": "venueId",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "maxFdvWei",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "maxSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "maxWalletBps",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "minFdvWei",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "minSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingOwner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "restrictionBlocks",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint16",
"name": "bps",
"type": "uint16"
}
],
"name": "setCreatorBps",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "manager",
"type": "address"
}
],
"name": "setCreatorFeeManager",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "minFdvWei_",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "maxFdvWei_",
"type": "uint256"
}
],
"name": "setFdvBounds",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "fee",
"type": "uint256"
}
],
"name": "setLaunchFee",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint16",
"name": "maxWalletBps_",
"type": "uint16"
},
{
"internalType": "uint32",
"name": "restrictionBlocks_",
"type": "uint32"
}
],
"name": "setRestrictionConfig",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "minSupply_",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "maxSupply_",
"type": "uint256"
}
],
"name": "setSupplyBounds",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "manager",
"type": "address"
}
],
"name": "setTokenFeeManager",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "registry",
"type": "address"
}
],
"name": "setTokenOwnerRegistry",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "treasury_",
"type": "address"
}
],
"name": "setTreasury",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "tokenCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tokenFeeManager",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tokenOwnerRegistry",
"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": "treasury",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint8",
"name": "venueId",
"type": "uint8"
}
],
"name": "venueExists",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"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 {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {HoodToken} from "./HoodToken.sol";
import {IFeePolicyManager} from "./interfaces/IFeePolicyManager.sol";
import {ITokenOwnerRegistry} from "./interfaces/ITokenOwnerRegistry.sol";
import {TickMath} from "./libraries/TickMath.sol";
import {FullMath} from "./libraries/FullMath.sol";
import {
INonfungiblePositionManager,
ISwapRouter02,
IUniswapV3Factory,
IUniswapV3Pool
} from "./interfaces/IUniswapV3.sol";
import {ISushiSwapRouter} from "./interfaces/ISushiV3.sol";
/// @dev The slice of FeeLocker we validate against when a venue is added.
interface IVenueFeeLocker {
function npm() external view returns (address);
function launcher() external view returns (address);
}
/**
* @title HoodLauncher
* @notice The hood.dev launchpad factory, v3: multi-venue. One transaction
* launches a token the Clanker/NOXA way — a concentrated-liquidity
* pool IS the bonding curve — on the creator's choice of V3-style
* venue (Uniswap V3, SushiSwap V3, future forks):
*
* 1. deploy a fixed-supply HoodToken (CREATE2, vanity-mineable salt —
* NOTE the address commits to the venue's infra addresses, so a
* salt is only valid for the venueId it was mined against);
* 2. create + initialize its WETH pool ON THE CHOSEN VENUE at the 1%
* tier at `startTick`;
* 3. mint the ENTIRE supply as one single-sided position
* [startTick → max usable tick] — zero WETH in, so early buyers
* walk the price up the range exactly like a bonding curve, with
* no graduation or migration ever needed;
* 4. lock the LP NFT permanently in the venue's FeeLocker (fees stay
* claimable, split creator/protocol, principal locked forever);
* 5. optionally dev-buy with the ETH sent above the launch fee, in
* the same transaction — the creator gets the first fill.
*
* Venues live in an APPEND-ONLY registry: the owner can add a venue
* (its position manager, router, router flavor, and dedicated
* FeeLocker) but never re-point or remove one, so a launched token's
* infra can never be swapped out from under it. Venue ids follow the
* platform-wide V4TradeManager dexId space (1 = Uniswap V3,
* 5 = SushiSwap V3).
*
* Launching is permissionless. The owner can only tune future
* launches (launch fee, the protocol-wide `creatorBps` LP-fee split,
* sniper-guard defaults) and append venues — it has no power over
* tokens already launched.
*/
contract HoodLauncher is Ownable2Step, ReentrancyGuard {
using SafeERC20 for IERC20;
// -------------------------------------------------------------- constants
uint24 public constant POOL_FEE = 10_000; // 1% tier
int24 public constant TICK_SPACING = 200;
int24 public constant MAX_USABLE_TICK = 887_200; // (887272 / 200) * 200
uint16 public constant MAX_BPS = 10_000;
/// @notice Hard floor on the creator's LP-fee share. Compiled in and
/// unreachable by any admin call: whatever the owner does with
/// `setCreatorBps`, a creator can never be given less than 10% of
/// their token's LP fees.
uint16 public constant MIN_CREATOR_BPS = 1_000; // 10%
uint256 public constant DEFAULT_SUPPLY = 1_000_000_000e18;
/// @dev Hard rail: `setSupplyBounds` can never go below this.
uint256 public constant MIN_SUPPLY = 1e18;
/// @dev Hard rail: `setSupplyBounds` can never go above this. Keeps
/// single-position liquidity comfortably under maxLiquidityPerTick.
uint256 public constant MAX_SUPPLY = 1e33;
uint256 public constant MAX_LAUNCH_FEE = 1 ether;
/// @dev Hard rail on `setFdvBounds` so a fat-finger can't set an absurd band.
uint256 public constant HARD_MAX_FDV_WEI = 1_000_000 ether;
/// @notice Canonical venue ids — the V4TradeManager dexId space, so one
/// id means the same venue everywhere on the platform.
uint8 public constant VENUE_UNISWAP_V3 = 1;
uint8 public constant VENUE_SUSHI_V3 = 5;
// ------------------------------------------------------------- immutables
address public immutable weth;
// ----------------------------------------------------------------- venues
/// @dev Which ABI flavor the venue's router speaks for the dev buy:
/// SwapRouter02 (deadline-less params — Uniswap V3 on this chain) or
/// the ORIGINAL SwapRouter (deadline inside the params struct,
/// checked on-chain — SushiSwap V3 on this chain).
enum RouterKind {
SwapRouter02,
LegacySwapRouter
}
struct Venue {
INonfungiblePositionManager npm;
IUniswapV3Factory factory; // derived from npm.factory() at addVenue
address swapRouter;
RouterKind routerKind;
/// @dev This venue's dedicated FeeLocker (a locker's `npm` is
/// immutable, so every venue needs its own).
address feeLocker;
}
mapping(uint8 venueId => Venue) internal _venues;
// ----------------------------------------------------------- owner config
/// @notice Canonical per-token ownership registry; every launch registers
/// its creator as the initial token owner. Must be set before
/// launches are possible.
address public tokenOwnerRegistry;
/// @notice Token-side fee-policy registry (burn / vest / stake the FULL
/// token side of LP fees). Swappable: re-pointing it changes the
/// policy system for FUTURE launches only — launched tokens stay
/// bound to the manager they registered with. Zero disables it.
address public tokenFeeManager;
/// @notice Creator-side fee-policy registry (buy-burn / stake the
/// creator's WETH fee share). Same swap semantics. Zero disables.
address public creatorFeeManager;
/// @notice Launch-fee destination.
address public treasury;
/// @notice Flat ETH fee per launch. Covers per-launch infra (image/metadata
/// uploads); raised from 0.0005 on the live deployment 2026-07-16.
uint256 public launchFee = 0.001 ether;
/// @notice Protocol-wide LP-fee split: the creator's share in bps, the
/// protocol keeps the rest. 5000 = 50/50. Creators do NOT choose
/// this — only the platform owner sets it, never below
/// MIN_CREATOR_BPS, and a change applies to FUTURE launches only:
/// the value in force at launch is stamped into the position and is
/// immutable for that token forever.
uint16 public creatorBps = 5000;
/// @notice Sniper-guard settings snapshotted into tokens that opt in.
uint16 public maxWalletBps = 200; // 2%
uint32 public restrictionBlocks = 366;
/// @notice Owner-tunable supply band for future launches, always within
/// the compiled-in [MIN_SUPPLY, MAX_SUPPLY] rails.
uint256 public minSupply = MIN_SUPPLY; // 1 token
uint256 public maxSupply = MAX_SUPPLY; // 1 quadrillion tokens
/// @notice Owner-tunable band on a launch's implied starting FDV, in wei
/// of WETH: `1.0001^tick × supply` must land inside it. Reject-only
/// sanity rail — it never moves value. ETH-denominated on purpose
/// (no oracle on this chain); re-tune via `setFdvBounds` if ETH
/// moves a lot. Defaults ≈ $2k–$10M at $1,800/ETH.
uint256 public minFdvWei = 1.11 ether;
uint256 public maxFdvWei = 5_556 ether;
// --------------------------------------------------------------- registry
struct LaunchInfo {
address creator;
address pool;
uint256 positionId;
uint64 createdAt;
uint8 venueId;
}
mapping(address token => LaunchInfo) public launches;
address[] public allTokens;
// -------------------------------------------------------------------- api
struct LaunchParams {
uint8 venueId; // which registered venue hosts the pool (1 = Uni V3, 5 = Sushi V3)
string name;
string symbol;
string image; // artwork URI (ipfs:// or https)
string description;
string socials; // single JSON string
string metadataURI; // ERC-7572 contractURI (ipfs:// JSON)
bytes32 userSalt; // vanity-mineable; final salt binds msg.sender. Venue-specific!
int24 tickIfToken0IsNewToken; // starting price tick, multiple of 200
uint256 supply; // 0 => DEFAULT_SUPPLY
bool sniperGuard; // creator's opt-in to launch protections
uint256 devBuyMinOut; // slippage floor for the dev buy
bytes tokenFeeConfig; // token side: abi.encode(uint16 policyId, bytes data); empty = in-kind split
bytes creatorFeeConfig; // creator WETH share: same encoding; empty = paid to creator (default)
}
// ----------------------------------------------------------------- events
event TokenLaunched(
address indexed token,
address indexed creator,
address indexed pool,
uint8 venueId,
uint256 positionId,
uint256 supply,
int24 startTick,
uint16 creatorBps,
uint16 maxWalletBps,
uint32 restrictionBlocks,
string name,
string symbol,
string metadataURI,
uint256 devBuyEth,
uint256 devBuyTokens
);
event VenueAdded(
uint8 indexed venueId,
address indexed npm,
address factory,
address swapRouter,
RouterKind routerKind,
address feeLocker
);
event TokenOwnerRegistryUpdated(address indexed previousRegistry, address indexed newRegistry);
event TokenFeeManagerUpdated(address indexed previousManager, address indexed newManager);
event CreatorFeeManagerUpdated(address indexed previousManager, address indexed newManager);
event TreasuryUpdated(address indexed previousTreasury, address indexed newTreasury);
event LaunchFeeUpdated(uint256 previousFee, uint256 newFee);
event CreatorBpsUpdated(uint16 previousBps, uint16 newBps);
event RestrictionConfigUpdated(uint16 maxWalletBps, uint32 restrictionBlocks);
event SupplyBoundsUpdated(uint256 minSupply, uint256 maxSupply);
event FdvBoundsUpdated(uint256 minFdvWei, uint256 maxFdvWei);
// ----------------------------------------------------------------- errors
error ZeroAddress();
error EmptyString();
error TokenOwnerRegistryNotSet();
error TokenFeeManagerNotSet();
error CreatorFeeManagerNotSet();
error UnknownVenue(uint8 venueId);
error VenueExists(uint8 venueId);
/// @notice The venue's factory doesn't support the 1% tier at our tick
/// spacing — it can't host launchpad pools.
error IncompatibleVenue();
/// @notice The FeeLocker handed to addVenue is wired to a different NPM
/// or a different launcher.
error LockerMismatch();
error InvalidTick();
error InvalidBps();
error InvalidSupply();
error InvalidBounds();
error MarketCapOutOfRange();
error InsufficientValue();
error LaunchFeeTooHigh();
error PoolPriceMismatch();
error NotSingleSided();
error EthTransferFailed();
// ------------------------------------------------------------ constructor
constructor(address initialOwner, address weth_, address treasury_) Ownable(initialOwner) {
if (weth_ == address(0) || treasury_ == address(0)) revert ZeroAddress();
weth = weth_;
treasury = treasury_;
}
// ----------------------------------------------------------------- venues
/**
* @notice Register a venue — APPEND-ONLY. A venueId can never be
* re-pointed or removed once set: tokens launched on it keep
* exactly the infra they launched with, forever. The locker must
* be this venue's own (its immutable `npm` must match) and must
* name this launcher as its depositor.
*/
function addVenue(uint8 venueId, address npm_, address swapRouter_, RouterKind routerKind_, address feeLocker_)
external
onlyOwner
{
if (npm_ == address(0) || swapRouter_ == address(0) || feeLocker_ == address(0)) revert ZeroAddress();
if (address(_venues[venueId].npm) != address(0)) revert VenueExists(venueId);
IUniswapV3Factory factory = IUniswapV3Factory(INonfungiblePositionManager(npm_).factory());
if (factory.feeAmountTickSpacing(POOL_FEE) != TICK_SPACING) revert IncompatibleVenue();
if (IVenueFeeLocker(feeLocker_).npm() != npm_ || IVenueFeeLocker(feeLocker_).launcher() != address(this)) {
revert LockerMismatch();
}
_venues[venueId] = Venue({
npm: INonfungiblePositionManager(npm_),
factory: factory,
swapRouter: swapRouter_,
routerKind: routerKind_,
feeLocker: feeLocker_
});
emit VenueAdded(venueId, npm_, address(factory), swapRouter_, routerKind_, feeLocker_);
}
function getVenue(uint8 venueId) public view returns (Venue memory venue) {
venue = _venues[venueId];
if (address(venue.npm) == address(0)) revert UnknownVenue(venueId);
}
function venueExists(uint8 venueId) external view returns (bool) {
return address(_venues[venueId].npm) != address(0);
}
// ----------------------------------------------------------------- launch
/// @dev Working set for launch(); a memory struct keeps the frame flat.
struct LaunchVars {
uint256 supply;
uint16 creatorBps;
uint16 walletBps;
uint32 restrBlocks;
uint256 fee;
address token;
address pool;
uint256 positionId;
uint256 devBuyEth;
uint256 devBuyTokens;
}
/**
* @notice Launch a token on the venue named by `p.venueId`. Send
* `launchFee` plus any ETH you want spent on the atomic dev buy
* (`msg.value - launchFee`, may be 0).
*/
function launch(LaunchParams calldata p)
external
payable
nonReentrant
returns (address token, address pool, uint256 positionId)
{
Venue memory venue = getVenue(p.venueId);
LaunchVars memory v;
v.fee = launchFee;
v.supply = _validate(p, v.fee);
// Snapshot the protocol-wide split once; it is stamped into the
// position below and never changes for this token again.
v.creatorBps = creatorBps;
(v.walletBps, v.restrBlocks) = _guardConfig(p.sniperGuard);
v.devBuyEth = msg.value - v.fee;
v.token = address(
new HoodToken{salt: keccak256(abi.encode(msg.sender, p.userSalt))}(
p.name,
p.symbol,
v.supply,
msg.sender,
HoodToken.Metadata(p.image, p.description, p.socials, p.metadataURI),
v.walletBps,
v.restrBlocks,
address(venue.npm),
venue.feeLocker,
venue.swapRouter,
tokenOwnerRegistry
)
);
ITokenOwnerRegistry(tokenOwnerRegistry).registerToken(v.token, msg.sender);
_registerFeePolicies(v.token, p.tokenFeeConfig, p.creatorFeeConfig);
(v.pool, v.positionId) = _createPoolAndLock(venue, v.token, v.supply, p.tickIfToken0IsNewToken, v.creatorBps);
if (v.devBuyEth > 0) {
v.devBuyTokens = _devBuy(venue, v.token, v.devBuyEth, p.devBuyMinOut);
}
if (v.fee > 0) {
(bool ok,) = treasury.call{value: v.fee}("");
if (!ok) revert EthTransferFailed();
}
launches[v.token] = LaunchInfo(msg.sender, v.pool, v.positionId, uint64(block.timestamp), p.venueId);
allTokens.push(v.token);
emit TokenLaunched(
v.token,
msg.sender,
v.pool,
p.venueId,
v.positionId,
v.supply,
p.tickIfToken0IsNewToken,
v.creatorBps,
v.walletBps,
v.restrBlocks,
p.name,
p.symbol,
p.metadataURI,
v.devBuyEth,
v.devBuyTokens
);
return (v.token, v.pool, v.positionId);
}
// ------------------------------------------------------------------ views
/// @notice CREATE2 address predictor, for frontend salt mining. Mirrors
/// launch() exactly — note the address commits to name/symbol/
/// supply/metadata, the launcher's current sniper-guard config,
/// AND the venue's infra addresses: a mined salt is only valid
/// for the venueId it was mined against.
function computeTokenAddress(address deployer, LaunchParams calldata p) external view returns (address) {
bytes32 initCodeHash = keccak256(abi.encodePacked(type(HoodToken).creationCode, _tokenCtorArgs(deployer, p)));
return address(
uint160(
uint256(
keccak256(
abi.encodePacked(
hex"ff", address(this), keccak256(abi.encode(deployer, p.userSalt)), initCodeHash
)
)
)
)
);
}
/// @dev ABI-encoded HoodToken constructor args, exactly as launch() passes them.
function _tokenCtorArgs(address deployer, LaunchParams calldata p) private view returns (bytes memory) {
Venue memory venue = getVenue(p.venueId);
uint256 supply = p.supply == 0 ? DEFAULT_SUPPLY : p.supply;
(uint16 walletBps, uint32 restrBlocks) = _guardConfig(p.sniperGuard);
HoodToken.Metadata memory metadata = HoodToken.Metadata(p.image, p.description, p.socials, p.metadataURI);
return abi.encode(
p.name, p.symbol, supply, deployer, metadata, walletBps, restrBlocks,
address(venue.npm), venue.feeLocker, venue.swapRouter, tokenOwnerRegistry
);
}
function tokenCount() external view returns (uint256) {
return allTokens.length;
}
function getLaunch(address token) external view returns (LaunchInfo memory) {
return launches[token];
}
// ------------------------------------------------------------------ admin
function setTreasury(address treasury_) external onlyOwner {
if (treasury_ == address(0)) revert ZeroAddress();
emit TreasuryUpdated(treasury, treasury_);
treasury = treasury_;
}
/// @notice Point at the canonical ownership registry (this launcher must
/// be its `launcher` so registrations succeed).
function setTokenOwnerRegistry(address registry) external onlyOwner {
if (registry == address(0)) revert ZeroAddress();
emit TokenOwnerRegistryUpdated(tokenOwnerRegistry, registry);
tokenOwnerRegistry = registry;
}
/// @notice Swap the token-side fee-policy system for future launches.
/// Zero disables policy launches (empty-config launches still work).
function setTokenFeeManager(address manager) external onlyOwner {
emit TokenFeeManagerUpdated(tokenFeeManager, manager);
tokenFeeManager = manager;
}
/// @notice Swap the creator-side (WETH) fee-policy system for future
/// launches. Zero disables it (empty-config launches still work).
function setCreatorFeeManager(address manager) external onlyOwner {
emit CreatorFeeManagerUpdated(creatorFeeManager, manager);
creatorFeeManager = manager;
}
function setLaunchFee(uint256 fee) external onlyOwner {
if (fee > MAX_LAUNCH_FEE) revert LaunchFeeTooHigh();
emit LaunchFeeUpdated(launchFee, fee);
launchFee = fee;
}
/// @notice Set the protocol-wide creator share of LP fees. Applies to
/// launches AFTER this call; tokens already launched keep the split
/// stamped into their locked position. Cannot go below
/// MIN_CREATOR_BPS — the creator's floor is not an admin knob.
function setCreatorBps(uint16 bps) external onlyOwner {
if (bps < MIN_CREATOR_BPS || bps > MAX_BPS) revert InvalidBps();
emit CreatorBpsUpdated(creatorBps, bps);
creatorBps = bps;
}
function setRestrictionConfig(uint16 maxWalletBps_, uint32 restrictionBlocks_) external onlyOwner {
if (maxWalletBps_ > MAX_BPS) revert InvalidBps();
maxWalletBps = maxWalletBps_;
restrictionBlocks = restrictionBlocks_;
emit RestrictionConfigUpdated(maxWalletBps_, restrictionBlocks_);
}
/// @notice Tune the supply band for future launches, within the
/// compiled-in [MIN_SUPPLY, MAX_SUPPLY] rails.
function setSupplyBounds(uint256 minSupply_, uint256 maxSupply_) external onlyOwner {
if (minSupply_ < MIN_SUPPLY || minSupply_ > maxSupply_ || maxSupply_ > MAX_SUPPLY) revert InvalidBounds();
minSupply = minSupply_;
maxSupply = maxSupply_;
emit SupplyBoundsUpdated(minSupply_, maxSupply_);
}
/// @notice Tune the implied-FDV band (wei of WETH) for future launches.
/// Reject-only rail; capped so a typo can't brick all launches.
function setFdvBounds(uint256 minFdvWei_, uint256 maxFdvWei_) external onlyOwner {
if (minFdvWei_ > maxFdvWei_ || maxFdvWei_ > HARD_MAX_FDV_WEI) revert InvalidBounds();
minFdvWei = minFdvWei_;
maxFdvWei = maxFdvWei_;
emit FdvBoundsUpdated(minFdvWei_, maxFdvWei_);
}
// -------------------------------------------------------------- internals
function _validate(LaunchParams calldata p, uint256 fee) private view returns (uint256 supply) {
if (tokenOwnerRegistry == address(0)) revert TokenOwnerRegistryNotSet();
if (bytes(p.name).length == 0 || bytes(p.symbol).length == 0) revert EmptyString();
if (msg.value < fee) revert InsufficientValue();
int24 tick = p.tickIfToken0IsNewToken;
if (tick % TICK_SPACING != 0 || tick < -MAX_USABLE_TICK || tick >= MAX_USABLE_TICK) revert InvalidTick();
supply = p.supply == 0 ? DEFAULT_SUPPLY : p.supply;
if (supply < minSupply || supply > maxSupply) revert InvalidSupply();
uint256 fdv = _impliedFdvWei(tick, supply);
if (fdv < minFdvWei || fdv > maxFdvWei) revert MarketCapOutOfRange();
}
/// @dev Implied starting FDV in wei of WETH: price(tick) × supply, where
/// price = 1.0001^tick is WETH-per-token off the normalized
/// "as-if the new token is token0" tick — so no dependence on the
/// not-yet-known CREATE2 address ordering. Same uint128-branch shape
/// as Uniswap's OracleLibrary.getQuoteAtTick.
function _impliedFdvWei(int24 tick, uint256 supply) internal pure returns (uint256) {
uint160 sqrtP = TickMath.getSqrtRatioAtTick(tick); // sqrt(1.0001^tick) * 2^96
if (sqrtP <= type(uint128).max) {
uint256 ratioX192 = uint256(sqrtP) * sqrtP;
return FullMath.mulDiv(ratioX192, supply, 1 << 192);
} else {
uint256 ratioX128 = FullMath.mulDiv(sqrtP, sqrtP, 1 << 64);
return FullMath.mulDiv(ratioX128, supply, 1 << 128);
}
}
/// @dev Register the creator's fee policies — token side (burn / vest /
/// stake the full token side of LP fees) and creator side (buy-burn
/// / stake the creator's WETH share) — and exempt each venue from
/// the token's transfer restrictions. Empty config = that side's
/// default (in-kind split / pay the creator), no registration.
function _registerFeePolicies(address token, bytes calldata tokenConfig, bytes calldata creatorConfig) private {
if (tokenConfig.length != 0) {
address manager = tokenFeeManager;
if (manager == address(0)) revert TokenFeeManagerNotSet();
HoodToken(token).addFeeVenue(IFeePolicyManager(manager).onTokenLaunched(token, msg.sender, tokenConfig));
}
if (creatorConfig.length != 0) {
address manager = creatorFeeManager;
if (manager == address(0)) revert CreatorFeeManagerNotSet();
HoodToken(token).addFeeVenue(
IFeePolicyManager(manager).onTokenLaunched(token, msg.sender, creatorConfig)
);
}
}
function _guardConfig(bool sniperGuard) private view returns (uint16 walletBps, uint32 restrBlocks) {
if (sniperGuard) {
walletBps = maxWalletBps;
restrBlocks = restrictionBlocks;
}
// opted out => (0, 0): no wallet cap, restrictions end at the launch block
}
/// @dev The atomic dev buy, through whichever router ABI the venue
/// speaks. Both flavors wrap attached ETH when tokenIn is WETH9;
/// the legacy flavor additionally checks the deadline on-chain.
function _devBuy(Venue memory venue, address token, uint256 ethIn, uint256 minOut)
private
returns (uint256 tokensOut)
{
if (venue.routerKind == RouterKind.SwapRouter02) {
tokensOut = ISwapRouter02(venue.swapRouter).exactInputSingle{value: ethIn}(
ISwapRouter02.ExactInputSingleParams({
tokenIn: weth,
tokenOut: token,
fee: POOL_FEE,
recipient: msg.sender,
amountIn: ethIn,
amountOutMinimum: minOut,
sqrtPriceLimitX96: 0
})
);
} else {
tokensOut = ISushiSwapRouter(venue.swapRouter).exactInputSingle{value: ethIn}(
ISushiSwapRouter.ExactInputSingleParams({
tokenIn: weth,
tokenOut: token,
fee: POOL_FEE,
recipient: msg.sender,
deadline: block.timestamp,
amountIn: ethIn,
amountOutMinimum: minOut,
sqrtPriceLimitX96: 0
})
);
}
}
/// @dev Create + initialize the pool at the position's boundary tick, mint
/// the whole supply single-sided, and lock the NFT in the venue's
/// FeeLocker.
///
/// The pool is initialized at EXACTLY getSqrtRatioAtTick(boundary) —
/// LiquidityAmounts' inclusive branches then derive liquidity purely
/// from the token side, and the pool's amount delta at a bit-equal
/// sqrt price is zero, so the mint consumes zero WETH by construction.
/// (Do not offset the init price by a spacing; fork tests pin this.)
function _createPoolAndLock(Venue memory venue, address token, uint256 supply, int24 startTick, uint16 creatorBps_)
private
returns (address pool, uint256 positionId)
{
bool tokenIsToken0 = token < weth;
(address token0, address token1) = tokenIsToken0 ? (token, weth) : (weth, token);
(int24 tickLower, int24 tickUpper) =
tokenIsToken0 ? (startTick, MAX_USABLE_TICK) : (-MAX_USABLE_TICK, -startTick);
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(tokenIsToken0 ? startTick : -startTick);
(uint256 amount0Desired, uint256 amount1Desired) =
tokenIsToken0 ? (supply, uint256(0)) : (uint256(0), supply);
pool = venue.npm.createAndInitializePoolIfNecessary(token0, token1, POOL_FEE, sqrtPriceX96);
(uint160 actualPrice,,,,,,) = IUniswapV3Pool(pool).slot0();
if (actualPrice != sqrtPriceX96) revert PoolPriceMismatch();
HoodToken(token).setPool(pool);
IERC20(token).forceApprove(address(venue.npm), supply);
(uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) = venue.npm.mint(
INonfungiblePositionManager.MintParams({
token0: token0,
token1: token1,
fee: POOL_FEE,
tickLower: tickLower,
tickUpper: tickUpper,
amount0Desired: amount0Desired,
amount1Desired: amount1Desired,
amount0Min: 0,
amount1Min: 0,
recipient: address(this),
deadline: block.timestamp
})
);
positionId = tokenId;
if (liquidity == 0 || (tokenIsToken0 ? amount1 : amount0) != 0) revert NotSingleSided();
IERC20(token).forceApprove(address(venue.npm), 0);
// Liquidity math rounds the consumed amount down a hair — burn the
// remainder so the launcher's balance invariant is exactly zero.
uint256 dust = IERC20(token).balanceOf(address(this));
if (dust > 0) HoodToken(token).burn(dust);
venue.npm.safeTransferFrom(address(this), venue.feeLocker, positionId, abi.encode(msg.sender, creatorBps_));
}
}
Chain explorer1173msChain node129ms