HoodLauncher
0x3a1be08e3ae8b0396066f9c0ea97268cc910ab85
Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
21,620 bytes
ABI entries
84
34 read · 14 write
License
none
Contract information
- Address
- 0x3a1be08e3ae8b0396066f9c0ea97268cc910ab85
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.28+commit.7893614a
- Optimization
- Enabled
- Creator
- 0x26e17f2EA4…C92f268863
- Creation tx
- 0xe7a79adbf7…7878c05bcd
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
allTokens(uint256) → address
computeTokenAddress(address, tuple) → address
creatorBps() → uint16
creatorFeeManager() → address
factory() → address
feeLocker() → address
getLaunch(address) → tuple
launchFee() → uint256
launches(address) → address, address, uint256, uint64
maxFdvWei() → uint256
maxSupply() → uint256
maxWalletBps() → uint16
minFdvWei() → uint256
minSupply() → uint256
npm() → address
owner() → address
pendingOwner() → address
restrictionBlocks() → uint32
swapRouter() → address
tokenCount() → uint256
tokenFeeManager() → address
tokenOwnerRegistry() → address
treasury() → address
weth() → address
Events (13)
CreatorBpsUpdatedCreatorFeeManagerUpdatedFdvBoundsUpdatedFeeLockerSetLaunchFeeUpdatedOwnershipTransferStartedOwnershipTransferredRestrictionConfigUpdatedSupplyBoundsUpdatedTokenFeeManagerUpdatedTokenLaunchedTokenOwnerRegistryUpdatedTreasuryUpdated
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "initialOwner",
"type": "address"
},
{
"internalType": "address",
"name": "npm_",
"type": "address"
},
{
"internalType": "address",
"name": "weth_",
"type": "address"
},
{
"internalType": "address",
"name": "swapRouter_",
"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": "FeeLockerAlreadySet",
"type": "error"
},
{
"inputs": [],
"name": "FeeLockerNotSet",
"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": "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": [],
"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": true,
"internalType": "address",
"name": "feeLocker",
"type": "address"
}
],
"name": "FeeLockerSet",
"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": "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"
},
{
"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": "acceptOwnership",
"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": "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": [],
"name": "factory",
"outputs": [
{
"internalType": "contract IUniswapV3Factory",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "feeLocker",
"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": "struct HoodLauncher.LaunchInfo",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"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"
}
],
"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": "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": "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": "address",
"name": "locker",
"type": "address"
}
],
"name": "setFeeLocker",
"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": "swapRouter",
"outputs": [
{
"internalType": "contract ISwapRouter02",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"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": [],
"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";
/**
* @title HoodLauncher
* @notice The hood.dev launchpad factory. One transaction launches a token the
* Clanker/NOXA way — the Uniswap V3 pool IS the bonding curve:
*
* 1. deploy a fixed-supply HoodToken (CREATE2, vanity-mineable salt);
* 2. create + initialize its WETH pool 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 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.
*
* Launching is permissionless. The owner can only tune future
* launches (launch fee, the protocol-wide `creatorBps` LP-fee split,
* sniper-guard defaults) and set the FeeLocker once — 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;
// ------------------------------------------------------------- immutables
INonfungiblePositionManager public immutable npm;
IUniswapV3Factory public immutable factory;
address public immutable weth;
ISwapRouter02 public immutable swapRouter;
// ----------------------------------------------------------- owner config
/// @notice Destination of every locked LP NFT. One-time set.
address public feeLocker;
/// @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.
uint256 public launchFee = 0.0005 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;
}
mapping(address token => LaunchInfo) public launches;
address[] public allTokens;
// -------------------------------------------------------------------- api
struct LaunchParams {
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
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,
uint256 positionId,
uint256 supply,
int24 startTick,
uint16 creatorBps,
uint16 maxWalletBps,
uint32 restrictionBlocks,
string name,
string symbol,
string metadataURI,
uint256 devBuyEth,
uint256 devBuyTokens
);
event FeeLockerSet(address indexed 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 FeeLockerNotSet();
error FeeLockerAlreadySet();
error TokenOwnerRegistryNotSet();
error TokenFeeManagerNotSet();
error CreatorFeeManagerNotSet();
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 npm_, address weth_, address swapRouter_, address treasury_)
Ownable(initialOwner)
{
if (npm_ == address(0) || weth_ == address(0) || swapRouter_ == address(0) || treasury_ == address(0)) {
revert ZeroAddress();
}
npm = INonfungiblePositionManager(npm_);
factory = IUniswapV3Factory(INonfungiblePositionManager(npm_).factory());
weth = weth_;
swapRouter = ISwapRouter02(swapRouter_);
treasury = treasury_;
}
// ----------------------------------------------------------------- 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. 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)
{
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(npm),
feeLocker,
address(swapRouter),
tokenOwnerRegistry
)
);
ITokenOwnerRegistry(tokenOwnerRegistry).registerToken(v.token, msg.sender);
_registerFeePolicies(v.token, p.tokenFeeConfig, p.creatorFeeConfig);
(v.pool, v.positionId) = _createPoolAndLock(v.token, v.supply, p.tickIfToken0IsNewToken, v.creatorBps);
if (v.devBuyEth > 0) {
v.devBuyTokens = swapRouter.exactInputSingle{value: v.devBuyEth}(
ISwapRouter02.ExactInputSingleParams({
tokenIn: weth,
tokenOut: v.token,
fee: POOL_FEE,
recipient: msg.sender,
amountIn: v.devBuyEth,
amountOutMinimum: p.devBuyMinOut,
sqrtPriceLimitX96: 0
})
);
}
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));
allTokens.push(v.token);
emit TokenLaunched(
v.token,
msg.sender,
v.pool,
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 AND the launcher's current sniper-guard config.
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) {
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(npm), feeLocker, address(swapRouter), tokenOwnerRegistry
);
}
function tokenCount() external view returns (uint256) {
return allTokens.length;
}
function getLaunch(address token) external view returns (LaunchInfo memory) {
return launches[token];
}
// ------------------------------------------------------------------ admin
/// @notice One-time wiring of the FeeLocker (whose immutable `launcher` is us).
function setFeeLocker(address locker) external onlyOwner {
if (locker == address(0)) revert ZeroAddress();
if (feeLocker != address(0)) revert FeeLockerAlreadySet();
feeLocker = locker;
emit FeeLockerSet(locker);
}
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 (feeLocker == address(0)) revert FeeLockerNotSet();
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 Create + initialize the pool at the position's boundary tick, mint
/// the whole supply single-sided, and lock the NFT in the 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(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 = 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(npm), supply);
(uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) = 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(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);
npm.safeTransferFrom(address(this), feeLocker, positionId, abi.encode(msg.sender, creatorBps_));
}
}
Chain explorer4582msChain node75ms