MintFastLaunchpad
0xd61998ae9b29e1f19dfb70ba890bc85895c83f1b
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
23,375 bytes
ABI entries
97
36 read · 14 write
License
none
Contract information
- Address
- 0xd61998ae9b29e1f19dfb70ba890bc85895c83f1b
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0xe805CCdF6F…6cF459c681
- Creation tx
- 0x92fc26a4bc…b736031d82
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (36)
BPS() → uint256
DEAD() → address
KIND_GITHUB() → uint8
KIND_WALLET() → uint8
KIND_X() → uint8
MAX_BENEFICIARIES() → uint256
MAX_CREATION_FEE() → uint256
MIN_TICK() → int24
POOL_FEE() → uint24
TICK_SPACING() → int24
TOTAL_SUPPLY() → uint256
beneficiaries(address) → tuple[]
beneficiariesLocked(address) → bool
beneficiaryCount(address) → uint256
boundarySqrtPriceX96() → uint160
boundaryTick() → int24
createPaused() → bool
creationFee() → uint256
defaultLpSplitBps() → uint16
defaultSocialSplitBps() → uint16
feeVault() → address
hook() → address
isFeeManager(address) → bool
lpPosition(address) → bytes32, int24, int24, uint128
owner() → address
pendingEth(address) → uint256
pendingOwner() → address
poolIdOf(address) → bytes32
poolManager() → address
protocolFeesAccrued() → uint256
renounceOwnership()
state(address) → tuple, bytes32, uint256
tokenNonce() → uint256
tokens(address) → address, uint40, uint16, uint16, bool
totalPendingEth() → uint256
treasury() → address
Events (15)
BeneficiariesClearedBeneficiariesUpdatedBeneficiaryRegisteredCreatePausedSetCreationFeeSetDefaultFeesSetFeeManagerSetFeesDistributedHarvestedLaunchedOwnershipTransferStartedOwnershipTransferredProtocolFeesSweptTokenCreatedTreasurySet
ABI
[
{
"inputs": [
{
"internalType": "contract IPoolManager",
"name": "poolManager_",
"type": "address"
},
{
"internalType": "contract ISocialFeeVault",
"name": "feeVault_",
"type": "address"
},
{
"internalType": "address",
"name": "treasury_",
"type": "address"
},
{
"internalType": "bytes32",
"name": "hookSalt",
"type": "bytes32"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "BadBeneficiary",
"type": "error"
},
{
"inputs": [],
"name": "BadCallback",
"type": "error"
},
{
"inputs": [],
"name": "BadMetadata",
"type": "error"
},
{
"inputs": [],
"name": "BeneficiariesLocked",
"type": "error"
},
{
"inputs": [],
"name": "BpsSumMismatch",
"type": "error"
},
{
"inputs": [],
"name": "CreationPaused",
"type": "error"
},
{
"inputs": [],
"name": "DeadlineExpired",
"type": "error"
},
{
"inputs": [],
"name": "DirectEthRejected",
"type": "error"
},
{
"inputs": [],
"name": "DuplicateBeneficiary",
"type": "error"
},
{
"inputs": [],
"name": "FeeTooHigh",
"type": "error"
},
{
"inputs": [],
"name": "InsufficientCreationFee",
"type": "error"
},
{
"inputs": [],
"name": "InvalidHandle",
"type": "error"
},
{
"inputs": [],
"name": "InvalidPlatform",
"type": "error"
},
{
"inputs": [],
"name": "LaunchGriefed",
"type": "error"
},
{
"inputs": [],
"name": "NotAuthorized",
"type": "error"
},
{
"inputs": [],
"name": "NothingToHarvest",
"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": "OwnershipRenounced",
"type": "error"
},
{
"inputs": [],
"name": "PoolNotLive",
"type": "error"
},
{
"inputs": [],
"name": "R",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [],
"name": "Slippage",
"type": "error"
},
{
"inputs": [],
"name": "SplitTooHigh",
"type": "error"
},
{
"inputs": [],
"name": "T",
"type": "error"
},
{
"inputs": [],
"name": "TooManyBeneficiaries",
"type": "error"
},
{
"inputs": [],
"name": "TransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "UnknownToken",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAmount",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "BeneficiariesCleared",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "caller",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "count",
"type": "uint256"
}
],
"name": "BeneficiariesUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "bytes32",
"name": "key",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "uint8",
"name": "kind",
"type": "uint8"
},
{
"indexed": false,
"internalType": "string",
"name": "handle",
"type": "string"
},
{
"indexed": false,
"internalType": "address",
"name": "wallet",
"type": "address"
},
{
"indexed": false,
"internalType": "uint16",
"name": "bps",
"type": "uint16"
}
],
"name": "BeneficiaryRegistered",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "bool",
"name": "paused",
"type": "bool"
}
],
"name": "CreatePausedSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "fee",
"type": "uint256"
}
],
"name": "CreationFeeSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint16",
"name": "socialSplitBps",
"type": "uint16"
},
{
"indexed": false,
"internalType": "uint16",
"name": "lpSplitBps",
"type": "uint16"
}
],
"name": "DefaultFeesSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "account",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "allowed",
"type": "bool"
}
],
"name": "FeeManagerSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "bytes32",
"name": "key",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "FeesDistributed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "caller",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethCollected",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "devEth",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "protocolEth",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "liquidityMinted",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethReinvested",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "pendingEthAfter",
"type": "uint256"
}
],
"name": "Harvested",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "uint160",
"name": "sqrtPriceX96",
"type": "uint160"
},
{
"indexed": false,
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"indexed": false,
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"indexed": false,
"internalType": "uint128",
"name": "liquidity",
"type": "uint128"
}
],
"name": "Launched",
"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": true,
"internalType": "address",
"name": "treasury",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "ProtocolFeesSwept",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"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": "beneficiaryCount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "bool",
"name": "beneficiariesLocked",
"type": "bool"
},
{
"indexed": false,
"internalType": "uint16",
"name": "socialSplitBps",
"type": "uint16"
},
{
"indexed": false,
"internalType": "uint16",
"name": "lpSplitBps",
"type": "uint16"
}
],
"name": "TokenCreated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "treasury",
"type": "address"
}
],
"name": "TreasurySet",
"type": "event"
},
{
"inputs": [],
"name": "BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "DEAD",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "KIND_GITHUB",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "KIND_WALLET",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "KIND_X",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_BENEFICIARIES",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_CREATION_FEE",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MIN_TICK",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"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": "TOTAL_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "acceptOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "beneficiaries",
"outputs": [
{
"components": [
{
"internalType": "bytes32",
"name": "key",
"type": "bytes32"
},
{
"internalType": "uint16",
"name": "bps",
"type": "uint16"
}
],
"internalType": "struct MintFastLaunchpad.Beneficiary[]",
"name": "",
"type": "tuple[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "beneficiariesLocked",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "beneficiaryCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "boundarySqrtPriceX96",
"outputs": [
{
"internalType": "uint160",
"name": "",
"type": "uint160"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "boundaryTick",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "minTokensOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "buy",
"outputs": [
{
"internalType": "uint256",
"name": "tokensOut",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "createPaused",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "metadataURI",
"type": "string"
},
{
"components": [
{
"internalType": "uint8",
"name": "kind",
"type": "uint8"
},
{
"internalType": "string",
"name": "handle",
"type": "string"
},
{
"internalType": "address",
"name": "wallet",
"type": "address"
},
{
"internalType": "uint16",
"name": "bps",
"type": "uint16"
}
],
"internalType": "struct MintFastLaunchpad.BeneficiaryInput[]",
"name": "beneficiaries",
"type": "tuple[]"
},
{
"internalType": "uint256",
"name": "initialBuyMinTokensOut",
"type": "uint256"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
}
],
"name": "createToken",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "creationFee",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "defaultLpSplitBps",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "defaultSocialSplitBps",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "feeVault",
"outputs": [
{
"internalType": "contract ISocialFeeVault",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "harvest",
"outputs": [
{
"internalType": "uint256",
"name": "devEth",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "hook",
"outputs": [
{
"internalType": "contract MintFastHook",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "isFeeManager",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "lpPosition",
"outputs": [
{
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"internalType": "uint128",
"name": "liquidity",
"type": "uint128"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "pendingEth",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingOwner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "poolIdOf",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poolManager",
"outputs": [
{
"internalType": "contract IPoolManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "protocolFeesAccrued",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minEthOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "sell",
"outputs": [
{
"internalType": "uint256",
"name": "ethOut",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"components": [
{
"internalType": "uint8",
"name": "kind",
"type": "uint8"
},
{
"internalType": "string",
"name": "handle",
"type": "string"
},
{
"internalType": "address",
"name": "wallet",
"type": "address"
},
{
"internalType": "uint16",
"name": "bps",
"type": "uint16"
}
],
"internalType": "struct MintFastLaunchpad.BeneficiaryInput[]",
"name": "inputs",
"type": "tuple[]"
}
],
"name": "setBeneficiaries",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bool",
"name": "paused",
"type": "bool"
}
],
"name": "setCreatePaused",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "fee",
"type": "uint256"
}
],
"name": "setCreationFee",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint16",
"name": "socialSplitBps",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "lpSplitBps",
"type": "uint16"
}
],
"name": "setDefaultFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
},
{
"internalType": "bool",
"name": "allowed",
"type": "bool"
}
],
"name": "setFeeManager",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "treasury_",
"type": "address"
}
],
"name": "setTreasury",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "state",
"outputs": [
{
"components": [
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "uint40",
"name": "createdAt",
"type": "uint40"
},
{
"internalType": "uint16",
"name": "socialSplitBps",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "lpSplitBps",
"type": "uint16"
},
{
"internalType": "bool",
"name": "beneficiariesLocked",
"type": "bool"
}
],
"internalType": "struct MintFastLaunchpad.TokenInfo",
"name": "info",
"type": "tuple"
},
{
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "spotPriceWad",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "sweepProtocolFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "tokenNonce",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "tokens",
"outputs": [
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "uint40",
"name": "createdAt",
"type": "uint40"
},
{
"internalType": "uint16",
"name": "socialSplitBps",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "lpSplitBps",
"type": "uint16"
},
{
"internalType": "bool",
"name": "beneficiariesLocked",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalPendingEth",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"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": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "unlockCallback",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol";
import {MintFastToken} from "./MintFastToken.sol";
import {MintFastHook} from "./MintFastHook.sol";
import {HandleLib} from "./libraries/HandleLib.sol";
import {ISocialFeeVault} from "./interfaces/ISocialFeeVault.sol";
import {
BalanceDelta,
BalanceDeltaLib,
Currency,
CurrencyLib,
IPoolManager,
IUnlockCallback,
ModifyLiquidityParams,
PoolId,
PoolIdLib,
PoolKey,
SwapParams,
V4StateReader
} from "./interfaces/IUniswapV4.sol";
import {TickMath} from "./vendor/v3/TickMath.sol";
import {LiquidityAmounts} from "./vendor/v3/LiquidityAmounts.sol";
/// @title MintFastLaunchpad
/// @notice AMM-native launchpad for Robinhood Chain: every token launches DIRECTLY into
/// its own Uniswap v4 pool (0% LP fee + the MintFastHook) against NATIVE ETH.
/// The entire 1B supply is minted as a single-sided position owned by this
/// contract, so the pool itself plays the bonding curve: ETH-per-token starts
/// at ~1.5 ETH market cap and climbs as people buy (token-per-ETH descends
/// through the locked tick range). There is no separate curve phase, no
/// graduation mechanism, and no moment where this contract custodies raw ETH
/// reserves beyond its accounted earmarks.
///
/// ETH is currency0 of every pool (address(0) sorts first), which fixes the
/// token ordering once and for all: the token is ALWAYS currency1.
///
/// Fee model (split defaults snapshotted per token at creation):
/// - The pool's own LP fee is ZERO. The MintFastHook (deployed by this contract's
/// constructor, attached to every pool) charges 2% of the ETH side of every swap
/// on buys AND sells, in ETH only, and accrues it per token. Exact-in buys charge
/// 2% of the FULL ETH input in beforeSwap (including any unfilled remainder on a
/// partial fill); every other swap shape charges 2% of ETH actually moved.
/// - Permissionless harvest() pulls the accrued ETH and splits it: 50% escrows to
/// the token's Fletchers, 25% is added to the pool as ETH-side-only liquidity
/// (a spacing-aligned range between the current price and the launch boundary,
/// pure buy-side depth, locked forever), 25% to the protocol. Per swap that is
/// 1% dev / 0.5% liquidity / 0.5% protocol, in ETH, for the life of the token.
/// ETH that cannot yet be placed (price within one spacing of the launch boundary)
/// stays in pendingEth until the spot moves back into a reinvestable range. It is
/// never swept to the protocol.
///
/// The "listing cap" (a USD-pegged market-cap milestone that fluctuates with the ETH
/// price) is a UI/indexer concept, deliberately NOT on-chain, so it can track the
/// real ETH price without an oracle dependency.
///
/// Owner powers (the complete list; everything else is immutable by construction):
/// - pause/unpause NEW token creation (never affects existing tokens)
/// - set creationFee (hard-capped), future creations only
/// - set default socialSplitBps / lpSplitBps (sum-capped), snapshotted per token
/// - set treasury (receives protocol fees via pull-payment only)
/// - add/remove fee managers (can rewrite a token's Fletcher beneficiaries after lock)
///
/// Fletcher beneficiary locking:
/// - Empty beneficiary list at create → 100% to creator wallet, UNLOCKED. Creator may
/// call setBeneficiaries once; that call locks forever.
/// - Non-empty list at create → locked immediately.
/// - Fee managers may call setBeneficiaries on any live token (always leaves it locked).
/// - setBeneficiaries auto-harvests pending hook fees / LP earmarks to the OLD set
/// before swapping recipients; already-escrowed vault balances stay under old keys.
/// Only fees accrued AFTER the change use the new set. socialSplitBps / lpSplitBps
/// remain immutable per-token snapshots.
///
/// The owner can NOT: freeze trading (it lives on Uniswap), seize anything, mint,
/// alter splits on live tokens, touch vault escrow, or withdraw locked liquidity.
/// There is no code path that removes position liquidity (modifyLiquidity is only
/// ever called with a delta >= 0). No proxies exist.
contract MintFastLaunchpad is Ownable2Step, ReentrancyGuard, IUnlockCallback {
using FixedPointMathLib for uint256;
using BalanceDeltaLib for BalanceDelta;
using PoolIdLib for PoolKey;
// ─── Types ─────────────────────────────────────────────────────────────────
/// @dev slot 1: 160 + 40 + 16 + 16 + 8 = 240. Splits are immutable; beneficiariesLocked
/// flips false→true once (create with named list, creator one-shot, or fee manager).
struct TokenInfo {
address creator;
uint40 createdAt;
uint16 socialSplitBps; // share of harvested fees for the Fletchers (the "dev cut")
uint16 lpSplitBps; // share of harvested fees reinvested into the locked liquidity
bool beneficiariesLocked; // true => only fee managers may rewrite recipients
}
/// @notice A resolved fee beneficiary. `key` is a SocialFeeVault escrow key:
/// X/GitHub -> identityKey (claimed via attestation); WALLET -> walletKey
/// (claimed by the address with no signature).
struct Beneficiary {
bytes32 key;
uint16 bps; // share of the creator-allocatable cut; all beneficiaries sum to 10000
}
/// @notice Creation-time beneficiary description (calldata form).
struct BeneficiaryInput {
uint8 kind; // 1 = X, 2 = GitHub, 3 = Wallet
string handle; // normalized handle for X/GitHub; "" for Wallet
address wallet; // payout address for Wallet; address(0) for X/GitHub
uint16 bps; // share of the creator-allocatable cut
}
/// @dev unlockCallback dispatch.
enum Action {
LAUNCH,
BUY,
SELL,
REINVEST
}
// ─── Constants ─────────────────────────────────────────────────────────────
uint256 public constant BPS = 10_000;
uint256 public constant MAX_CREATION_FEE = 0.005 ether;
uint8 public constant KIND_X = 1;
uint8 public constant KIND_GITHUB = 2;
uint8 public constant KIND_WALLET = 3;
uint256 public constant MAX_BENEFICIARIES = 8;
address public constant DEAD = 0x000000000000000000000000000000000000dEaD;
uint256 public constant TOTAL_SUPPLY = 1_000_000_000e18;
/// @dev Uniswap v4: the pool's own LP fee is ZERO. The 2% ETH fee lives in the
/// MintFastHook attached to every pool instead.
uint24 public constant POOL_FEE = 0;
int24 public constant TICK_SPACING = 200;
/// @dev Widest spacing-aligned lower bound (full-range on the buy side).
int24 public constant MIN_TICK = -887_200;
/// @dev Launch price: 1.5e-9 ETH per token (3/2e9) => ~1.5 ETH initial market cap.
/// As a v4 price (currency1 per currency0 = token per ETH) that is DEN/NUM.
uint256 internal constant START_PRICE_NUM = 3;
uint256 internal constant START_PRICE_DEN = 2e9;
// ─── Immutables ────────────────────────────────────────────────────────────
IPoolManager public immutable poolManager;
ISocialFeeVault public immutable feeVault;
/// @notice The fee hook attached to every launchpad pool (deployed by this
/// contract's constructor via CREATE2. The salt is mined so the address
/// carries the v4 permission bits).
MintFastHook public immutable hook;
/// @dev Every pool starts at the same boundary: the launch price snapped to tick
/// spacing away from the token side, so the initial mint is guaranteed
/// single-sided. The locked range is [MIN_TICK, boundaryTick] and the price
/// descends through it (in token-per-ETH terms) as people buy.
int24 public immutable boundaryTick;
uint160 public immutable boundarySqrtPriceX96;
// ─── Storage ───────────────────────────────────────────────────────────────
address public treasury;
uint256 public creationFee = 0.0005 ether;
uint16 public defaultSocialSplitBps = 5000; // 50% of harvested fees → dev/Fletchers
uint16 public defaultLpSplitBps = 2500; // 25% → back into the coin's liquidity
bool public createPaused;
uint256 public protocolFeesAccrued; // pull-payment to treasury
uint256 public tokenNonce;
/// @notice Sum of every token's pendingEth, the slice of this contract's ETH
/// balance already earmarked for liquidity reinvestment. Together with
/// protocolFeesAccrued it accounts for the ENTIRE resting ETH balance:
/// address(this).balance == protocolFeesAccrued + totalPendingEth
/// whenever no flow is mid-flight.
uint256 public totalPendingEth;
mapping(address token => TokenInfo) public tokens;
/// @notice Per-token ETH earmarked for liquidity but not yet placeable (no
/// spacing-aligned room between the current price and the launch
/// boundary, or liquidity-math dust), carried into the next harvest.
mapping(address token => uint256) public pendingEth;
mapping(address token => Beneficiary[]) internal _beneficiaries;
/// @notice Protocol operators who may rewrite Fletcher beneficiaries on any token
/// (including after lock). Owner add/remove via setFeeManager.
mapping(address account => bool) public isFeeManager;
// ─── Events ────────────────────────────────────────────────────────────────
event TokenCreated(
address indexed token,
address indexed creator,
string name,
string symbol,
string metadataURI,
uint256 beneficiaryCount,
bool beneficiariesLocked,
uint16 socialSplitBps,
uint16 lpSplitBps
);
/// @dev Same tx as TokenCreated, after the full supply is locked into the pool.
/// `poolId` keys the v4 singleton's Swap events for this token's pool.
event Launched(
address indexed token,
bytes32 indexed poolId,
uint160 sqrtPriceX96,
int24 tickLower,
int24 tickUpper,
uint128 liquidity
);
/// @dev One per beneficiary, same tx as TokenCreated. For WALLET, `handle` is "" and
/// `wallet` is the payout address; for X/GitHub, `wallet` is address(0). The
/// default (no beneficiaries named) emits a single WALLET beneficiary = creator.
event BeneficiaryRegistered(
address indexed token, bytes32 indexed key, uint8 kind, string handle, address wallet, uint16 bps
);
/// @dev Lifetime-fee collection from the fee hook (ETH only). `devEth` also lands
/// in the vault as per-beneficiary Deposited events in the same tx.
/// `liquidityMinted` is Uniswap liquidity units; `ethReinvested` is the ETH
/// actually settled into the new position (0 when the earmark stays pending).
/// `pendingEthAfter` is the token's remaining LP earmark after this harvest.
event Harvested(
address indexed token,
address indexed caller,
uint256 ethCollected,
uint256 devEth,
uint256 protocolEth,
uint256 liquidityMinted,
uint256 ethReinvested,
uint256 pendingEthAfter
);
/// @dev One per Fletcher share when the social cut is escrowed. Indexers use this
/// for exact per-token attribution; vault Deposited stays key-only.
event FeesDistributed(address indexed token, bytes32 indexed key, uint256 amount);
event CreationFeeSet(uint256 fee);
event DefaultFeesSet(uint16 socialSplitBps, uint16 lpSplitBps);
event TreasurySet(address treasury);
event CreatePausedSet(bool paused);
event ProtocolFeesSwept(address indexed treasury, uint256 amount);
/// @dev Emitted before re-registering beneficiaries on setBeneficiaries. Indexers
/// should delete the token's prior beneficiary rows, then apply the following
/// BeneficiaryRegistered logs in the same tx.
event BeneficiariesCleared(address indexed token);
event BeneficiariesUpdated(address indexed token, address indexed caller, uint256 count);
event FeeManagerSet(address indexed account, bool allowed);
// ─── Errors ────────────────────────────────────────────────────────────────
error CreationPaused();
error InsufficientCreationFee();
error BadMetadata();
error UnknownToken();
error DeadlineExpired();
error ZeroAmount();
error Slippage();
error NothingToHarvest();
error BadCallback();
error DirectEthRejected();
error TransferFailed();
error ZeroAddress();
error FeeTooHigh();
error SplitTooHigh();
error TooManyBeneficiaries();
error BadBeneficiary();
error BpsSumMismatch();
error PoolNotLive();
error LaunchGriefed();
error NotAuthorized();
error BeneficiariesLocked();
error DuplicateBeneficiary();
error OwnershipRenounced();
/// @param hookSalt CREATE2 salt for the fee hook, mined off-chain so the hook
/// address carries the v4 permission bits (see HookMiner). The hook's
/// constructor self-checks the flags and reverts on a bad salt.
constructor(IPoolManager poolManager_, ISocialFeeVault feeVault_, address treasury_, bytes32 hookSalt)
Ownable(msg.sender)
{
if (address(poolManager_) == address(0) || address(feeVault_) == address(0) || treasury_ == address(0)) {
revert ZeroAddress();
}
poolManager = poolManager_;
feeVault = feeVault_;
treasury = treasury_;
hook = new MintFastHook{salt: hookSalt}(poolManager_, address(this));
// token-per-ETH launch price, snapped down to the tick spacing (away from the
// token side: a lower token-per-ETH tick means a token price >= launch price)
uint160 targetSqrtP =
uint160(FixedPointMathLib.sqrt(START_PRICE_DEN.fullMulDiv(1 << 192, START_PRICE_NUM)));
int24 boundary = _floorToSpacing(TickMath.getTickAtSqrtRatio(targetSqrtP));
boundaryTick = boundary;
boundarySqrtPriceX96 = TickMath.getSqrtRatioAtTick(boundary);
}
/// @dev The PoolManager pays out native ETH with plain transfers (take), both
/// for pool flows and when the fee hook redeems collected fees during
/// harvest; nothing else may send ETH outside the payable entrypoints.
receive() external payable {
if (msg.sender != address(poolManager)) revert DirectEthRejected();
}
// ─── Create ────────────────────────────────────────────────────────────────
/// @notice Launch a token straight into its own Uniswap v4 pool. `msg.value` =
/// creationFee + optional atomic first buy. The full supply is locked as
/// single-sided liquidity from ~1.5 ETH market cap upward, tradable by
/// anyone, on any interface, immediately.
/// @param beneficiaries how the Fletcher share of harvested fees is divided. Each is
/// an X handle, a GitHub handle, or a wallet address, with a bps share; the
/// shares must sum to 10000. Pass an EMPTY array to send 100% to the creator
/// (the "dev"), claimable via SocialFeeVault.claimWallet. That path leaves
/// beneficiaries unlocked so the creator may reassign once. Any non-empty
/// list locks immediately. Max 8 beneficiaries.
/// @param initialBuyMinTokensOut slippage floor for the atomic first buy (0 if none)
/// @param salt caller entropy mixed into the CREATE2 salt so same-block pool grief
/// cannot cheaply target a predictable token address (retry with a new salt)
function createToken(
string calldata name,
string calldata symbol,
string calldata metadataURI,
BeneficiaryInput[] calldata beneficiaries,
uint256 initialBuyMinTokensOut,
bytes32 salt
) external payable nonReentrant returns (address token) {
if (createPaused) revert CreationPaused();
if (msg.value < creationFee) revert InsufficientCreationFee();
if (
bytes(name).length == 0 || bytes(name).length > 32 || bytes(symbol).length == 0
|| bytes(symbol).length > 10 || bytes(metadataURI).length > 256
) revert BadMetadata();
uint256 nonce = ++tokenNonce;
// block.number + caller salt keep the address unpredictable enough that a
// pool-pre-initialization grief can't permanently pin one creator (see _launch),
// and a failed creation can simply be retried with a fresh salt / next block.
token = address(
new MintFastToken{
salt: keccak256(abi.encode(msg.sender, nonce, block.chainid, block.number, salt))
}(name, symbol, metadataURI)
);
TokenInfo storage info = tokens[token];
info.creator = msg.sender;
info.createdAt = uint40(block.timestamp);
info.socialSplitBps = defaultSocialSplitBps;
info.lpSplitBps = defaultLpSplitBps;
// Named list at create → locked immediately. Empty → unlocked (creator one-shot).
info.beneficiariesLocked = beneficiaries.length > 0;
protocolFeesAccrued += creationFee;
uint256 count = _registerBeneficiaries(token, beneficiaries, msg.sender);
emit TokenCreated(
token,
msg.sender,
name,
symbol,
metadataURI,
count,
info.beneficiariesLocked,
info.socialSplitBps,
info.lpSplitBps
);
_launch(token);
uint256 buyEth = msg.value - creationFee;
if (buyEth > 0) {
_buy(token, buyEth, initialBuyMinTokensOut, msg.sender);
}
}
/// @notice Replace a token's Fletcher beneficiaries. Creator may call once while
/// unlocked (empty-list creates). Fee managers may call anytime. Always
/// locks afterward. `inputs` must be non-empty (use an explicit wallet /
/// social row. Empty is only valid at createToken). Pending fees are
/// harvested to the current beneficiaries before the list is replaced.
function setBeneficiaries(address token, BeneficiaryInput[] calldata inputs) external nonReentrant {
TokenInfo storage info = tokens[token];
if (info.creator == address(0)) revert UnknownToken();
if (inputs.length == 0) revert BadBeneficiary();
if (isFeeManager[msg.sender]) {
// protocol operator, allowed even when locked
} else if (msg.sender == info.creator) {
if (info.beneficiariesLocked) revert BeneficiariesLocked();
} else {
revert NotAuthorized();
}
// Flush pending fees to the OLD set before swapping recipients.
if (hook.pendingFees(token) != 0 || pendingEth[token] != 0) _harvest(token);
_clearBeneficiaries(token);
uint256 count = _registerBeneficiaries(token, inputs, info.creator);
info.beneficiariesLocked = true;
emit BeneficiariesUpdated(token, msg.sender, count);
}
/// @dev Initializes the pool at the launch boundary and locks the entire supply as
/// a single-sided position. External pre-initialization of our PoolKey is
/// blocked by MintFastHook.beforeInitialize (launchpad-only). Keep the
/// try/catch + push-back / LaunchGriefed path as defense in depth if spot is
/// ever off-boundary after init (e.g. odd retry); do not remove without the
/// hook gate:
/// - pre-initialized on the ETH side of the boundary (token cheaper than
/// launch): harmless. Nobody can own the token yet, so no token-side
/// liquidity can exist; the first buy free-jumps the price to our range.
/// - pre-initialized on the token side (token pricier than launch): we push
/// the price back toward the boundary by selling supply into whatever
/// ETH-side liquidity was posted. Their ETH ends up locked in OUR
/// position, and then mint with both balances at the corrected price.
/// Only this contract may call PoolManager.initialize for our hook pools.
function _launch(address token) private {
PoolKey memory key = _poolKey(token);
try poolManager.initialize(key, boundarySqrtPriceX96) returns (int24) {}
catch {
// pool already initialized (defense in depth); push-back absorbs below
}
PoolId id = key.toId();
(uint160 sqrtP,) = V4StateReader.getSlot0(poolManager, id);
if (sqrtP == 0) revert PoolNotLive();
bytes memory res = poolManager.unlock(abi.encode(Action.LAUNCH, abi.encode(token, sqrtP)));
(uint128 liquidity, uint160 finalSqrtP) = abi.decode(res, (uint128, uint160));
emit Launched(token, PoolId.unwrap(id), finalSqrtP, MIN_TICK, boundaryTick, liquidity);
}
/// @dev Grief push-back + supply lock. Runs inside the PoolManager unlock.
function _launchCallback(bytes memory payload) private returns (bytes memory) {
(address token, uint160 sqrtP) = abi.decode(payload, (address, uint160));
PoolKey memory key = _poolKey(token);
uint256 arbEth;
if (sqrtP < boundarySqrtPriceX96) {
// price sits inside our range (token side): sell supply through the hostile
// liquidity to drag it back to the boundary
BalanceDelta d = poolManager.swap(
key,
SwapParams({
zeroForOne: false,
amountSpecified: -int256(TOTAL_SUPPLY),
sqrtPriceLimitX96: boundarySqrtPriceX96
}),
""
);
uint256 sold = _neg(d.amount1());
if (sold > 0) _settleToken(token, sold);
arbEth = _pos(d.amount0());
if (arbEth > 0) poolManager.take(CurrencyLib.NATIVE, address(this), arbEth);
(sqrtP,) = V4StateReader.getSlot0(poolManager, key.toId());
}
// Defense in depth (external pre-init is blocked by the hook). If spot is still
// below the boundary after push-back, or the entire supply was sold into depth
// (tokenBal == 0), fail cleanly instead of underflowing on `tokenBal - 1`.
uint256 tokenBal = MintFastToken(token).balanceOf(address(this));
if (sqrtP < boundarySqrtPriceX96 || tokenBal == 0) revert LaunchGriefed();
// Lock everything we hold (the supply, plus any ETH arbed off a griefer). One
// wei per side is held back from the liquidity math: v4 rounds principal
// charges up, so the charge can exceed the computed amount by a wei. The
// held-back wei guarantees settlement never overdraws this flow's budget.
// Both weis just join the pending dust and reinvest at the first harvest.
uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts(
sqrtP,
TickMath.getSqrtRatioAtTick(MIN_TICK),
TickMath.getSqrtRatioAtTick(boundaryTick),
arbEth == 0 ? 0 : arbEth - 1,
tokenBal - 1
);
if (liquidity == 0) revert ZeroAmount();
(BalanceDelta cd,) = poolManager.modifyLiquidity(
key,
ModifyLiquidityParams({
tickLower: MIN_TICK,
tickUpper: boundaryTick,
liquidityDelta: int256(uint256(liquidity)),
salt: bytes32(0)
}),
""
);
uint256 owedEth = _neg(cd.amount0());
uint256 owedToken = _neg(cd.amount1());
// Cap vs budget so ethDust / settle never assume a charge beyond what we hold.
// With the 1-wei holdback this is a no-op; if rounding ever overshoots, settling
// only the budgeted amount fails the unlock cleanly instead of bricking later.
if (owedEth > arbEth) owedEth = arbEth;
if (owedToken > tokenBal) owedToken = tokenBal;
if (owedEth > 0) poolManager.settle{value: owedEth}();
if (owedToken > 0) _settleToken(token, owedToken);
// token-side liquidity-math dust is unusable now that fees (and reinvests)
// are ETH-only. Burn it; ETH dust (arbed off a griefer) earmarks for the
// first harvest's ETH-side reinvest
uint256 tokenDust = MintFastToken(token).balanceOf(address(this));
if (tokenDust > 0) MintFastToken(token).transfer(DEAD, tokenDust);
uint256 ethDust = arbEth - owedEth;
pendingEth[token] = ethDust;
totalPendingEth += ethDust;
return abi.encode(liquidity, sqrtP);
}
function _floorToSpacing(int24 tick) private pure returns (int24) {
int24 d = tick / TICK_SPACING;
if (tick % TICK_SPACING < 0) d--;
return d * TICK_SPACING;
}
/// @dev The canonical PoolKey for a launched token: native ETH is currency0 of
/// every pool (address(0) sorts below any token), 0% LP fee, the fee hook.
/// Fully derivable; nothing about the pool needs storage.
function _poolKey(address token) private view returns (PoolKey memory) {
return PoolKey({
currency0: CurrencyLib.NATIVE,
currency1: Currency.wrap(token),
fee: POOL_FEE,
tickSpacing: TICK_SPACING,
hooks: address(hook)
});
}
/// @dev Wipe stored beneficiaries and signal indexers before a re-register.
function _clearBeneficiaries(address token) private {
delete _beneficiaries[token];
emit BeneficiariesCleared(token);
}
/// @dev Validates + stores the beneficiary set and emits one event per beneficiary.
/// Empty input => a single WALLET beneficiary = `defaultWallet` at 100%.
/// `defaultWallet` must be the token creator (not msg.sender) so fee-manager
/// updates never accidentally key fees to the operator.
function _registerBeneficiaries(
address token,
BeneficiaryInput[] calldata inputs,
address defaultWallet
) private returns (uint256) {
Beneficiary[] storage stored = _beneficiaries[token];
if (inputs.length == 0) {
bytes32 key = HandleLib.deriveWalletKey(defaultWallet);
stored.push(Beneficiary({key: key, bps: uint16(BPS)}));
emit BeneficiaryRegistered(token, key, KIND_WALLET, "", defaultWallet, uint16(BPS));
return 1;
}
if (inputs.length > MAX_BENEFICIARIES) revert TooManyBeneficiaries();
uint256 sumBps;
for (uint256 i; i < inputs.length; ++i) {
BeneficiaryInput calldata b = inputs[i];
if (b.bps == 0) revert BadBeneficiary();
sumBps += b.bps;
bytes32 key;
if (b.kind == KIND_X || b.kind == KIND_GITHUB) {
if (b.wallet != address(0)) revert BadBeneficiary();
HandleLib.validate(b.kind, b.handle);
key = HandleLib.deriveIdentityKey(b.kind, HandleLib.hashHandle(b.handle));
} else if (b.kind == KIND_WALLET) {
if (b.wallet == address(0) || bytes(b.handle).length != 0) revert BadBeneficiary();
key = HandleLib.deriveWalletKey(b.wallet);
} else {
revert BadBeneficiary();
}
for (uint256 j; j < i; ++j) {
if (stored[j].key == key) revert DuplicateBeneficiary();
}
stored.push(Beneficiary({key: key, bps: b.bps}));
emit BeneficiaryRegistered(token, key, b.kind, b.handle, b.wallet, b.bps);
}
if (sumBps != BPS) revert BpsSumMismatch();
return inputs.length;
}
/// @dev Splits the creator-allocatable fee cut across the token's beneficiaries.
/// The last beneficiary absorbs any rounding remainder so the sum is exact.
function _distributeCreatorCut(address token, uint256 creatorCut) private {
if (creatorCut == 0) return;
Beneficiary[] storage bens = _beneficiaries[token];
uint256 n = bens.length;
uint256 distributed;
for (uint256 i; i < n; ++i) {
uint256 share = (i + 1 == n) ? creatorCut - distributed : creatorCut.mulDiv(bens[i].bps, BPS);
distributed += share;
if (share != 0) {
emit FeesDistributed(token, bens[i].key, share);
feeVault.deposit{value: share}(bens[i].key);
}
}
}
// Trade: thin wrappers over the pool, no launchpad surcharge beyond MintFastHook, no router
/// @notice Buy `token` with ETH through its pool. Tokens go straight to the caller;
/// if the buy would exhaust the range (partial fill), the unspent ETH is
/// refunded. Identical to swapping on any Uniswap v4 interface.
function buy(address token, uint256 minTokensOut, uint256 deadline)
external
payable
nonReentrant
returns (uint256 tokensOut)
{
if (block.timestamp > deadline) revert DeadlineExpired();
if (msg.value == 0) revert ZeroAmount();
tokensOut = _buy(token, msg.value, minTokensOut, msg.sender);
}
function _buy(address token, uint256 ethIn, uint256 minTokensOut, address recipient)
private
returns (uint256 tokensOut)
{
if (tokens[token].creator == address(0)) revert UnknownToken();
bytes memory res =
poolManager.unlock(abi.encode(Action.BUY, abi.encode(token, ethIn, recipient)));
(uint256 ethUsed, uint256 out) = abi.decode(res, (uint256, uint256));
tokensOut = out;
if (tokensOut < minTokensOut) revert Slippage();
// partial fill (price ran past the locked range): refund the unspent ETH
if (ethUsed < ethIn) {
(bool ok,) = recipient.call{value: ethIn - ethUsed}("");
if (!ok) revert TransferFailed();
}
}
function _buyCallback(bytes memory payload) private returns (bytes memory) {
(address token, uint256 ethIn, address recipient) =
abi.decode(payload, (address, uint256, address));
BalanceDelta d = poolManager.swap(
_poolKey(token),
SwapParams({
zeroForOne: true, // ETH in
amountSpecified: -int256(ethIn), // negative = exact input
sqrtPriceLimitX96: TickMath.MIN_SQRT_RATIO + 1
}),
""
);
uint256 ethUsed = _neg(d.amount0());
uint256 tokensOut = _pos(d.amount1());
if (tokensOut == 0) revert ZeroAmount();
poolManager.settle{value: ethUsed}();
poolManager.take(Currency.wrap(token), recipient, tokensOut);
return abi.encode(ethUsed, tokensOut);
}
/// @notice Sell `token` for ETH through its pool. Requires prior ERC20 approval.
function sell(address token, uint256 tokenAmount, uint256 minEthOut, uint256 deadline)
external
nonReentrant
returns (uint256 ethOut)
{
if (block.timestamp > deadline) revert DeadlineExpired();
if (tokenAmount == 0) revert ZeroAmount();
if (tokens[token].creator == address(0)) revert UnknownToken();
MintFastToken(token).transferFrom(msg.sender, address(this), tokenAmount);
bytes memory res =
poolManager.unlock(abi.encode(Action.SELL, abi.encode(token, tokenAmount)));
(uint256 tokenUsed, uint256 out) = abi.decode(res, (uint256, uint256));
ethOut = out;
if (ethOut < minEthOut) revert Slippage();
// if the pool couldn't absorb everything (no bid-side liquidity left), return
// the unsold remainder rather than leaving it stranded here
if (tokenUsed < tokenAmount) {
MintFastToken(token).transfer(msg.sender, tokenAmount - tokenUsed);
}
(bool ok,) = msg.sender.call{value: ethOut}("");
if (!ok) revert TransferFailed();
}
function _sellCallback(bytes memory payload) private returns (bytes memory) {
(address token, uint256 tokenAmount) = abi.decode(payload, (address, uint256));
BalanceDelta d = poolManager.swap(
_poolKey(token),
SwapParams({
zeroForOne: false, // token in
amountSpecified: -int256(tokenAmount),
sqrtPriceLimitX96: TickMath.MAX_SQRT_RATIO - 1
}),
""
);
uint256 tokenUsed = _neg(d.amount1());
uint256 ethOut = _pos(d.amount0());
if (ethOut == 0) revert ZeroAmount();
_settleToken(token, tokenUsed);
poolManager.take(CurrencyLib.NATIVE, address(this), ethOut);
return abi.encode(tokenUsed, ethOut);
}
// ─── Lifetime fees ─────────────────────────────────────────────────────────
/// @notice Pull the token's accrued hook fees (ETH only) and split them:
/// socialSplitBps → Fletcher escrow, lpSplitBps → added to the pool as
/// ETH-side-only locked liquidity, remainder → protocol (pull-payment).
/// Permissionless; ETH that can't be placed yet (no spacing-aligned room
/// below the launch boundary, or liquidity-math dust) carries into the
/// next harvest as the token's pendingEth earmark.
function harvest(address token) external nonReentrant returns (uint256 devEth) {
if (tokens[token].creator == address(0)) revert UnknownToken();
return _harvest(token);
}
/// @dev Shared harvest body for `harvest` and `setBeneficiaries`. Caller must
/// already have verified the token exists and hold the reentrancy lock.
/// Reverts `NothingToHarvest` when there is neither fresh hook ETH nor an
/// LP earmark. `setBeneficiaries` pre-checks so it never hits that path.
function _harvest(address token) private returns (uint256 devEth) {
TokenInfo storage info = tokens[token];
uint256 fresh = hook.collect(token); // ETH lands in this contract (or 0)
devEth = fresh.mulDiv(info.socialSplitBps, BPS);
uint256 lpEthFresh = fresh.mulDiv(info.lpSplitBps, BPS);
uint256 protoEth = fresh - devEth - lpEthFresh;
uint256 lpEth = lpEthFresh + pendingEth[token];
if (fresh == 0 && lpEth == 0) revert NothingToHarvest();
// Reinvest the LP share as ETH-side-only liquidity (needs >1 wei: one wei is
// held back from the liquidity math so the rounded-up charge can never
// overdraw this token's earmark).
uint256 usedEth;
uint128 minted;
if (lpEth > 1) {
bytes memory res =
poolManager.unlock(abi.encode(Action.REINVEST, abi.encode(token, lpEth)));
(usedEth, minted) = abi.decode(res, (uint256, uint128));
}
// usedEth is already capped to lpEth inside _reinvestCallback; belt-and-suspenders
// so pendingEth / totalPendingEth accounting can never underflow.
if (usedEth > lpEth) usedEth = lpEth;
totalPendingEth = totalPendingEth + lpEthFresh - usedEth;
pendingEth[token] = lpEth - usedEth;
protocolFeesAccrued += protoEth;
emit Harvested(token, msg.sender, fresh, devEth, protoEth, minted, usedEth, pendingEth[token]);
_distributeCreatorCut(token, devEth);
}
/// @dev Mint the LP share as a single-sided ETH (currency0) position in the
/// spacing-aligned range strictly above the current tick, capped at the
/// launch boundary, pure buy-side depth between the current price and the
/// launch price. If the price sits within one spacing of the boundary (a
/// fresh or fully-exited pool) there is no room and the ETH stays pending
/// until the spot moves back into range (never routed to the protocol).
/// Runs inside the PoolManager unlock. Never removes liquidity.
function _reinvestCallback(bytes memory payload) private returns (bytes memory) {
(address token, uint256 lpEth) = abi.decode(payload, (address, uint256));
PoolKey memory key = _poolKey(token);
(uint160 sqrtP, int24 tick) = V4StateReader.getSlot0(poolManager, key.toId());
int24 lower = _floorToSpacing(tick) + TICK_SPACING;
if (lower >= boundaryTick) return abi.encode(uint256(0), uint128(0));
uint128 minted = LiquidityAmounts.getLiquidityForAmounts(
sqrtP,
TickMath.getSqrtRatioAtTick(lower),
TickMath.getSqrtRatioAtTick(boundaryTick),
lpEth - 1, // one-wei holdback: the round-up charge can exceed the floor by a wei
0
);
if (minted == 0) return abi.encode(uint256(0), uint128(0));
(BalanceDelta cd,) = poolManager.modifyLiquidity(
key,
ModifyLiquidityParams({
tickLower: lower,
tickUpper: boundaryTick,
liquidityDelta: int256(uint256(minted)),
salt: bytes32(0)
}),
""
);
// the range is strictly above the current tick, so the charge is ETH-only
uint256 usedEth = _neg(cd.amount0());
// Cap vs earmark so pendingEth = lpEth - usedEth never underflows. With the
// 1-wei holdback this is a no-op; if rounding ever overshoots, settling only
// the earmark fails the unlock cleanly instead of bricking future harvests.
if (usedEth > lpEth) usedEth = lpEth;
if (usedEth > 0) poolManager.settle{value: usedEth}();
return abi.encode(usedEth, minted);
}
// ─── Uniswap v4 unlock callback ────────────────────────────────────────────
/// @dev Only the PoolManager may call this, and it only ever runs inside an unlock
/// this contract itself initiated (v4 calls back the msg.sender of unlock()).
function unlockCallback(bytes calldata data) external returns (bytes memory) {
if (msg.sender != address(poolManager)) revert BadCallback();
(Action action, bytes memory payload) = abi.decode(data, (Action, bytes));
if (action == Action.LAUNCH) return _launchCallback(payload);
if (action == Action.BUY) return _buyCallback(payload);
if (action == Action.SELL) return _sellCallback(payload);
return _reinvestCallback(payload);
}
/// @dev ERC20 settlement against the PoolManager: sync, transfer in, settle.
function _settleToken(address token, uint256 amount) private {
poolManager.sync(Currency.wrap(token));
MintFastToken(token).transfer(address(poolManager), amount);
poolManager.settle();
}
function _pos(int128 x) private pure returns (uint256) {
return x > 0 ? uint256(uint128(x)) : 0;
}
function _neg(int128 x) private pure returns (uint256) {
return x < 0 ? uint256(uint128(-x)) : 0;
}
// ─── Protocol fees (pull-payment) ──────────────────────────────────────────
/// @notice Push accrued protocol fees to the treasury. Callable by anyone; a reverting
/// treasury can never brick anything because fees only accrue in a counter.
function sweepProtocolFees() external nonReentrant {
uint256 amount = protocolFeesAccrued;
if (amount == 0) revert ZeroAmount();
protocolFeesAccrued = 0;
address to = treasury;
(bool ok,) = to.call{value: amount}("");
if (!ok) revert TransferFailed();
emit ProtocolFeesSwept(to, amount);
}
// ─── Views ─────────────────────────────────────────────────────────────────
/// @notice The v4 PoolId of a token's canonical pool (keys the singleton's Swap
/// events); the zero id for unknown tokens.
function poolIdOf(address token) public view returns (bytes32) {
if (tokens[token].creator == address(0)) return bytes32(0);
return PoolId.unwrap(_poolKey(token).toId());
}
/// @notice Live launch state: creator/config plus the pool's spot price in ETH per
/// token (wad). Market cap = spotPriceWad * 1e9 (fixed supply).
function state(address token)
external
view
returns (TokenInfo memory info, bytes32 poolId, uint256 spotPriceWad)
{
info = tokens[token];
if (info.creator != address(0)) {
PoolId id = _poolKey(token).toId();
poolId = PoolId.unwrap(id);
(uint160 sqrtP,) = V4StateReader.getSlot0(poolManager, id);
spotPriceWad = _sqrtPToEthPerTokenWad(sqrtP);
}
}
/// @dev The pool price is token-per-ETH (the token is always currency1); invert to
/// ETH-per-token wad. Full-precision mulDivs keep this exact across the range.
function _sqrtPToEthPerTokenWad(uint160 sqrtP) private pure returns (uint256) {
// ratioX128 = sqrtP^2 / 2^64 (token per ETH, Q128)
uint256 ratioX128 = uint256(sqrtP).fullMulDiv(sqrtP, 1 << 64);
if (ratioX128 == 0) return 0;
return uint256(1 << 128).fullMulDiv(1e18, ratioX128);
}
/// @notice The primary locked v4 position (the launch supply lock): poolId, range,
/// and live liquidity. Harvest reinvests mint ADDITIONAL single-sided ETH
/// positions in sub-ranges of [MIN_TICK, boundaryTick], not included here.
/// Unharvested fees live in the hook: hook.pendingFees(token).
function lpPosition(address token)
external
view
returns (bytes32 poolId, int24 tickLower, int24 tickUpper, uint128 liquidity)
{
if (tokens[token].creator == address(0)) return (bytes32(0), 0, 0, 0);
PoolId id = _poolKey(token).toId();
poolId = PoolId.unwrap(id);
tickLower = MIN_TICK;
tickUpper = boundaryTick;
liquidity = V4StateReader.getPositionLiquidity(
poolManager, id, address(this), MIN_TICK, boundaryTick, bytes32(0)
);
}
/// @notice The token's fee beneficiaries (resolved escrow keys + bps shares).
function beneficiaries(address token) external view returns (Beneficiary[] memory) {
return _beneficiaries[token];
}
function beneficiaryCount(address token) external view returns (uint256) {
return _beneficiaries[token].length;
}
/// @notice Whether Fletcher recipients are locked (only fee managers may change).
function beneficiariesLocked(address token) external view returns (bool) {
return tokens[token].beneficiariesLocked;
}
// ─── Admin (bounded; see contract NatSpec for the complete power list) ─────
/// @dev Accidental renounce would permanently freeze fee-manager / treasury /
/// default-fee admin. Trading and claims would still work, but support ops
/// would brick, so disallow it.
function renounceOwnership() public pure override {
revert OwnershipRenounced();
}
function setFeeManager(address account, bool allowed) external onlyOwner {
if (account == address(0)) revert ZeroAddress();
isFeeManager[account] = allowed;
emit FeeManagerSet(account, allowed);
}
function setTreasury(address treasury_) external onlyOwner {
if (treasury_ == address(0)) revert ZeroAddress();
treasury = treasury_;
emit TreasurySet(treasury_);
}
function setCreationFee(uint256 fee) external onlyOwner {
if (fee > MAX_CREATION_FEE) revert FeeTooHigh();
creationFee = fee;
emit CreationFeeSet(fee);
}
function setDefaultFees(uint16 socialSplitBps, uint16 lpSplitBps) external onlyOwner {
if (uint256(socialSplitBps) + uint256(lpSplitBps) > BPS) revert SplitTooHigh();
defaultSocialSplitBps = socialSplitBps;
defaultLpSplitBps = lpSplitBps;
emit DefaultFeesSet(socialSplitBps, lpSplitBps);
}
function setCreatePaused(bool paused) external onlyOwner {
createPaused = paused;
emit CreatePausedSet(paused);
}
}
Chain explorer3806msChain node91ms