FlowPadCurveLauncher
0xca3e46e17e752e49173ee0371a426179a88cd335
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
16,089 bytes
ABI entries
75
20 read · 18 write
License
none
Contract information
- Address
- 0xca3e46e17e752e49173ee0371a426179a88cd335
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0xbFd1DFfd49…435ce0F502
- Creation tx
- 0xa6ee2f843d…5d9363307b
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (20)
MAX_FEE_RECIPIENTS() → uint256
MAX_LOCK() → uint128
allowedRouter(address) → bool
creatorOf(address) → address
curveFee() → uint256, uint256
feeSplitOf(address) → address[], uint16[]
hook() → address
lastTickerUse(bytes32) → uint40
launchAllowlist(address) → bool
launchAllowlistEnabled() → bool
launchFee() → uint256
launchesPaused() → bool
maxWallet() → uint256
maxWalletBps() → uint16
pendingFees() → uint16, uint16, uint96, uint64, bool
poolManager() → address
protocolRecipient() → address
streams() → address
tickerAvailableAt(string) → uint256
tickerCooldown() → uint32
Events (16)
CreatorshipTransferredFeeChangeAnnouncedFeeChangeAppliedFeeChangeCancelledFeeSplitSetHookSetLaunchAllowlistEnabledSetLaunchAllowlistSetLaunchedLaunchesPausedSetMaxFeeBpsSetMaxLaunchFeeSetMaxWalletBpsSetProtocolRecipientChangedRouterAllowedSetTickerCooldownSet
ABI
[
{
"inputs": [
{
"internalType": "contract IPoolManager",
"name": "poolManager_",
"type": "address"
},
{
"internalType": "address",
"name": "protocol_",
"type": "address"
},
{
"internalType": "contract IRobinFlowStreams",
"name": "streams_",
"type": "address"
},
{
"components": [
{
"internalType": "uint16",
"name": "feeBps",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "creatorFeeShareBps",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "decayStartExtraBps",
"type": "uint16"
},
{
"internalType": "uint32",
"name": "decayWindow",
"type": "uint32"
},
{
"internalType": "uint16",
"name": "snipeGuardBlocks",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "maxWalletBps",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "maxDevBuyBps",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "maxVestBps",
"type": "uint16"
},
{
"internalType": "uint32",
"name": "minVestDuration",
"type": "uint32"
},
{
"internalType": "uint96",
"name": "launchFee",
"type": "uint96"
},
{
"internalType": "uint128",
"name": "virtualEth",
"type": "uint128"
},
{
"internalType": "uint128",
"name": "virtualToken",
"type": "uint128"
},
{
"internalType": "uint128",
"name": "baseCurveSupply",
"type": "uint128"
},
{
"internalType": "uint128",
"name": "lpReserve",
"type": "uint128"
},
{
"internalType": "uint16",
"name": "hookFeeBps",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "hookCreatorShareBps",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "maxFeeBps",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "maxHookFeeBps",
"type": "uint16"
},
{
"internalType": "uint96",
"name": "maxLaunchFee",
"type": "uint96"
},
{
"internalType": "uint32",
"name": "feeTimelock",
"type": "uint32"
},
{
"internalType": "uint32",
"name": "tickerCooldown",
"type": "uint32"
},
{
"internalType": "uint96",
"name": "minGradSurplus",
"type": "uint96"
}
],
"internalType": "struct FlowPadConfig",
"name": "cfg",
"type": "tuple"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "DevBuySlippage",
"type": "error"
},
{
"inputs": [],
"name": "FeeTransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "HookAlreadySet",
"type": "error"
},
{
"inputs": [],
"name": "HookNotSet",
"type": "error"
},
{
"inputs": [],
"name": "InvalidConfig",
"type": "error"
},
{
"inputs": [],
"name": "InvalidFeeSplit",
"type": "error"
},
{
"inputs": [],
"name": "InvalidVestParams",
"type": "error"
},
{
"inputs": [],
"name": "LaunchFeeTooLow",
"type": "error"
},
{
"inputs": [],
"name": "LaunchesArePaused",
"type": "error"
},
{
"inputs": [],
"name": "NoPendingChange",
"type": "error"
},
{
"inputs": [],
"name": "NotAllowlisted",
"type": "error"
},
{
"inputs": [],
"name": "NotCreator",
"type": "error"
},
{
"inputs": [],
"name": "NotPoolManager",
"type": "error"
},
{
"inputs": [],
"name": "NotProtocol",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "SniperOptsExceedCap",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "availableAt",
"type": "uint256"
}
],
"name": "TickerOnCooldown",
"type": "error"
},
{
"inputs": [],
"name": "TimelockActive",
"type": "error"
},
{
"inputs": [],
"name": "UnknownToken",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "oldCreator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newCreator",
"type": "address"
}
],
"name": "CreatorshipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint16",
"name": "feeBps",
"type": "uint16"
},
{
"indexed": false,
"internalType": "uint16",
"name": "creatorFeeShareBps",
"type": "uint16"
},
{
"indexed": false,
"internalType": "uint96",
"name": "launchFee",
"type": "uint96"
},
{
"indexed": false,
"internalType": "uint64",
"name": "applyAfter",
"type": "uint64"
}
],
"name": "FeeChangeAnnounced",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint16",
"name": "feeBps",
"type": "uint16"
},
{
"indexed": false,
"internalType": "uint16",
"name": "creatorFeeShareBps",
"type": "uint16"
},
{
"indexed": false,
"internalType": "uint96",
"name": "launchFee",
"type": "uint96"
}
],
"name": "FeeChangeApplied",
"type": "event"
},
{
"anonymous": false,
"inputs": [],
"name": "FeeChangeCancelled",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "address[]",
"name": "recipients",
"type": "address[]"
},
{
"indexed": false,
"internalType": "uint16[]",
"name": "shareBps",
"type": "uint16[]"
},
{
"indexed": false,
"internalType": "bool",
"name": "rescue",
"type": "bool"
}
],
"name": "FeeSplitSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "hook",
"type": "address"
}
],
"name": "HookSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "bool",
"name": "enabled",
"type": "bool"
}
],
"name": "LaunchAllowlistEnabledSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "account",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "allowed",
"type": "bool"
}
],
"name": "LaunchAllowlistSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "uint256",
"name": "devBuyTokens",
"type": "uint256"
}
],
"name": "Launched",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "bool",
"name": "paused",
"type": "bool"
}
],
"name": "LaunchesPausedSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint16",
"name": "bps",
"type": "uint16"
}
],
"name": "MaxFeeBpsSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint96",
"name": "fee",
"type": "uint96"
}
],
"name": "MaxLaunchFeeSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint16",
"name": "bps",
"type": "uint16"
}
],
"name": "MaxWalletBpsSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "oldRecipient",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newRecipient",
"type": "address"
}
],
"name": "ProtocolRecipientChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "router",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "allowed",
"type": "bool"
}
],
"name": "RouterAllowedSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint32",
"name": "cooldownSeconds",
"type": "uint32"
}
],
"name": "TickerCooldownSet",
"type": "event"
},
{
"inputs": [],
"name": "MAX_FEE_RECIPIENTS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_LOCK",
"outputs": [
{
"internalType": "uint128",
"name": "",
"type": "uint128"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "allowedRouter",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint16",
"name": "feeBps_",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "creatorFeeShareBps_",
"type": "uint16"
},
{
"internalType": "uint96",
"name": "launchFee_",
"type": "uint96"
}
],
"name": "announceFeeChange",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "applyFeeChange",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "cancelFeeChange",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "creatorOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "curveFee",
"outputs": [
{
"internalType": "uint256",
"name": "feeBps",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "creatorShareBps",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "feeSplitOf",
"outputs": [
{
"internalType": "address[]",
"name": "recipients",
"type": "address[]"
},
{
"internalType": "uint16[]",
"name": "shareBps",
"type": "uint16[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "hook",
"outputs": [
{
"internalType": "contract FlowPadCurveHook",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"name": "lastTickerUse",
"outputs": [
{
"internalType": "uint40",
"name": "",
"type": "uint40"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "uint16",
"name": "lockBps",
"type": "uint16"
},
{
"internalType": "uint40",
"name": "lockDuration",
"type": "uint40"
},
{
"internalType": "uint40",
"name": "lockCliff",
"type": "uint40"
},
{
"internalType": "uint256",
"name": "minDevBuyTokensOut",
"type": "uint256"
},
{
"internalType": "uint16",
"name": "snipeGuardBlocks",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "decayExtraBps",
"type": "uint16"
},
{
"internalType": "address",
"name": "feeRecipient",
"type": "address"
}
],
"internalType": "struct FlowPadCurveLauncher.LaunchParams",
"name": "p",
"type": "tuple"
}
],
"name": "launch",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "launchAllowlist",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "launchAllowlistEnabled",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "launchFee",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "launchesPaused",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "maxWallet",
"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": "pendingFees",
"outputs": [
{
"internalType": "uint16",
"name": "feeBps",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "creatorFeeShareBps",
"type": "uint16"
},
{
"internalType": "uint96",
"name": "launchFee",
"type": "uint96"
},
{
"internalType": "uint64",
"name": "applyAfter",
"type": "uint64"
},
{
"internalType": "bool",
"name": "exists",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poolManager",
"outputs": [
{
"internalType": "contract IPoolManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "protocolRecipient",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address[]",
"name": "recipients",
"type": "address[]"
},
{
"internalType": "uint16[]",
"name": "shareBps",
"type": "uint16[]"
}
],
"name": "rescueFeeSplit",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address[]",
"name": "recipients",
"type": "address[]"
},
{
"internalType": "uint16[]",
"name": "shareBps",
"type": "uint16[]"
}
],
"name": "setFeeSplit",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract FlowPadCurveHook",
"name": "hook_",
"type": "address"
}
],
"name": "setHook",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address[]",
"name": "accounts",
"type": "address[]"
},
{
"internalType": "bool",
"name": "allowed",
"type": "bool"
}
],
"name": "setLaunchAllowlist",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bool",
"name": "enabled",
"type": "bool"
}
],
"name": "setLaunchAllowlistEnabled",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bool",
"name": "paused",
"type": "bool"
}
],
"name": "setLaunchesPaused",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint16",
"name": "bps",
"type": "uint16"
}
],
"name": "setMaxFeeBps",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint96",
"name": "fee",
"type": "uint96"
}
],
"name": "setMaxLaunchFee",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint16",
"name": "bps",
"type": "uint16"
}
],
"name": "setMaxWalletBps",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newRecipient",
"type": "address"
}
],
"name": "setProtocolRecipient",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "router",
"type": "address"
},
{
"internalType": "bool",
"name": "allowed",
"type": "bool"
}
],
"name": "setRouterAllowed",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint32",
"name": "cooldownSeconds",
"type": "uint32"
}
],
"name": "setTickerCooldown",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "streams",
"outputs": [
{
"internalType": "contract IRobinFlowStreams",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "symbol",
"type": "string"
}
],
"name": "tickerAvailableAt",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tickerCooldown",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "newCreator",
"type": "address"
}
],
"name": "transferCreatorship",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "unlockCallback",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol";
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {FlowPadConfig} from "../interfaces/FlowPadConfig.sol";
import {IFlowPadFactoryViews} from "../interfaces/IFlowPadFactoryViews.sol";
import {IRobinFlowStreams} from "../interfaces/IRobinFlowStreams.sol";
import {CurveMath} from "../libraries/CurveMath.sol";
import {FlowPadCurveToken} from "./FlowPadCurveToken.sol";
import {FlowPadCurveHook, ICurveFeeSource} from "./FlowPadCurveHook.sol";
/// @title FlowPadCurveLauncher (v4 single-sided model)
/// @notice EXPERIMENTAL, LOCAL-ONLY. The v4 counterpart to FlowPadFactory at
/// FULL feature parity: launch policy (pause / allowlist / ticker cooldown /
/// router allowlist), per-launch sniper options, creator vest carve-out, dev
/// buy, fee routing / splitting, the fee-governance TIMELOCK, creatorship
/// transfer and the protocol emergency rescue. A token launches as a REAL,
/// single-sided Uniswap v4 pool (see FlowPadCurveHook) — tradable by any v4
/// aggregator from block one. This contract is the hook's single source of
/// truth (IFlowPadFactoryViews + ICurveFeeSource).
contract FlowPadCurveLauncher is IFlowPadFactoryViews, ICurveFeeSource, IUnlockCallback {
using CurrencyLibrary for Currency;
using SafeERC20 for IERC20;
uint256 internal constant BPS = 10_000;
uint256 public constant MAX_FEE_RECIPIENTS = 5;
error HookAlreadySet();
error HookNotSet();
error NotPoolManager();
error NotProtocol();
error NotCreator();
error LaunchesArePaused();
error NotAllowlisted();
error LaunchFeeTooLow();
error InvalidVestParams();
error SniperOptsExceedCap();
error TickerOnCooldown(uint256 availableAt);
error InvalidFeeSplit();
error UnknownToken();
error FeeTransferFailed();
error ZeroAddress();
error InvalidConfig();
error NoPendingChange();
error TimelockActive();
error DevBuySlippage();
event HookSet(address indexed hook);
event Launched(address indexed token, address indexed creator, bytes32 poolId, uint256 devBuyTokens);
event FeeSplitSet(address indexed token, address[] recipients, uint16[] shareBps, bool rescue);
event CreatorshipTransferred(address indexed token, address indexed oldCreator, address indexed newCreator);
event ProtocolRecipientChanged(address indexed oldRecipient, address indexed newRecipient);
event RouterAllowedSet(address indexed router, bool allowed);
event TickerCooldownSet(uint32 cooldownSeconds);
event MaxWalletBpsSet(uint16 bps);
event MaxFeeBpsSet(uint16 bps);
event MaxLaunchFeeSet(uint96 fee);
event LaunchesPausedSet(bool paused);
event LaunchAllowlistEnabledSet(bool enabled);
event LaunchAllowlistSet(address indexed account, bool allowed);
event FeeChangeAnnounced(uint16 feeBps, uint16 creatorFeeShareBps, uint96 launchFee, uint64 applyAfter);
event FeeChangeApplied(uint16 feeBps, uint16 creatorFeeShareBps, uint96 launchFee);
event FeeChangeCancelled();
struct LaunchParams {
string name;
string symbol;
// Of the tokens the creator BUYS at launch (msg.value - launchFee, a real
// curve swap), this % is LOCKED and streamed to the creator via RobinFlow;
// the rest is delivered to the creator's wallet. 0 = buy, no lock. There
// is NO free supply carve-out — the creator pays the curve price for every
// locked token. The absolute locked amount is capped at MAX_LOCK (750M).
uint16 lockBps;
uint40 lockDuration;
uint40 lockCliff;
uint256 minDevBuyTokensOut;
uint16 snipeGuardBlocks;
uint16 decayExtraBps;
address feeRecipient;
}
struct Payee {
address recipient;
uint16 shareBps;
}
struct PendingFees {
uint16 feeBps;
uint16 creatorFeeShareBps;
uint96 launchFee;
uint64 applyAfter;
bool exists;
}
IPoolManager public immutable poolManager;
IRobinFlowStreams public immutable streams;
uint128 public immutable MAX_LOCK; // absolute cap on a locked dev buy (750M)
// immutable per-version params
uint256 internal immutable _feeTimelock;
uint16 internal immutable _maxVestBps;
uint40 internal immutable _minVestDuration;
uint16 internal immutable _snipeGuardBlocksCap;
uint16 internal immutable _decayStartExtraBpsCap;
uint128 internal immutable _baseCurveSupply;
FlowPadConfig internal _cfg;
FlowPadCurveHook public hook;
// governed economics (mutable via timelock)
uint16 internal _feeBps;
uint16 internal _creatorFeeShareBps;
uint96 internal _launchFee;
PendingFees public pendingFees;
// governable caps/policy (protocol-settable, no redeploy)
uint16 public maxWalletBps; // pre-graduation per-holder cap (post-grad: none)
uint16 internal _maxFeeBps;
uint96 internal _maxLaunchFee;
// admin
address public protocolRecipient;
bool public launchesPaused;
bool public launchAllowlistEnabled;
uint32 public tickerCooldown;
mapping(address => bool) public launchAllowlist;
mapping(address => bool) public allowedRouter;
mapping(bytes32 => uint40) public lastTickerUse;
mapping(address token => address) public creatorOf;
mapping(address token => Payee[]) internal _feeSplit;
modifier onlyProtocol() {
if (msg.sender != protocolRecipient) revert NotProtocol();
_;
}
constructor(IPoolManager poolManager_, address protocol_, IRobinFlowStreams streams_, FlowPadConfig memory cfg) {
if (protocol_ == address(0)) revert ZeroAddress();
// Definitional invariants (mirror FlowPadFactory's constructor) so a
// mis-tuned config fails LOUD at deploy instead of bricking trades:
if (cfg.feeBps == 0 || cfg.feeBps > cfg.maxFeeBps) revert InvalidConfig();
if (cfg.creatorFeeShareBps > BPS) revert InvalidConfig();
if (cfg.launchFee > cfg.maxLaunchFee) revert InvalidConfig();
// fee + full anti-sniper decay must stay strictly below 100% of a trade,
// for BOTH the curve fee and the post-grad hook fee, or swaps revert.
if (uint256(cfg.maxFeeBps) + cfg.decayStartExtraBps >= BPS) revert InvalidConfig();
if (uint256(cfg.maxHookFeeBps) + cfg.decayStartExtraBps >= BPS) revert InvalidConfig();
if (cfg.hookFeeBps == 0 || cfg.hookFeeBps > cfg.maxHookFeeBps || cfg.hookCreatorShareBps > BPS) {
revert InvalidConfig();
}
if (cfg.maxWalletBps == 0 || cfg.maxWalletBps > BPS || cfg.maxVestBps > BPS) revert InvalidConfig();
// full curve supply stays public (no carve-out); the lock is a locked buy.
if (cfg.baseCurveSupply == 0 || cfg.lpReserve == 0) revert InvalidConfig();
if (uint256(cfg.baseCurveSupply) + cfg.lpReserve > CurveMath.TOTAL_SUPPLY) revert InvalidConfig();
if (uint256(cfg.virtualToken) <= cfg.baseCurveSupply) revert InvalidConfig();
poolManager = poolManager_;
streams = streams_;
MAX_LOCK = uint128((CurveMath.TOTAL_SUPPLY * cfg.maxVestBps) / BPS); // 750M
protocolRecipient = protocol_;
_cfg = cfg;
maxWalletBps = cfg.maxWalletBps;
_feeBps = cfg.feeBps;
_creatorFeeShareBps = cfg.creatorFeeShareBps;
_launchFee = cfg.launchFee;
_maxFeeBps = cfg.maxFeeBps;
_maxLaunchFee = cfg.maxLaunchFee;
_feeTimelock = cfg.feeTimelock;
_maxVestBps = cfg.maxVestBps;
_minVestDuration = uint40(cfg.minVestDuration);
_snipeGuardBlocksCap = cfg.snipeGuardBlocks;
_decayStartExtraBpsCap = cfg.decayStartExtraBps;
_baseCurveSupply = cfg.baseCurveSupply;
tickerCooldown = cfg.tickerCooldown;
}
/// @notice The current pre-graduation per-holder cap in tokens, baked into
/// each token as an immutable at launch (the token enforces it with no
/// external call). After graduation the token lifts the cap entirely.
function maxWallet() public view returns (uint256) {
return (CurveMath.TOTAL_SUPPLY * maxWalletBps) / BPS;
}
function setHook(FlowPadCurveHook hook_) external onlyProtocol {
if (address(hook) != address(0)) revert HookAlreadySet();
hook = hook_;
emit HookSet(address(hook_));
}
// ---------------------------------------------------------------- views
function launchFee() external view returns (uint256) {
return _launchFee;
}
function curveFee() external view returns (uint256 feeBps, uint256 creatorShareBps) {
return (_feeBps, _creatorFeeShareBps);
}
// ---------------------------------------------------------------- launch
function launch(LaunchParams calldata p) external payable returns (address token) {
if (address(hook) == address(0)) revert HookNotSet();
if (launchesPaused) revert LaunchesArePaused();
if (launchAllowlistEnabled && !launchAllowlist[msg.sender]) revert NotAllowlisted();
if (msg.value < _launchFee) revert LaunchFeeTooLow();
if (p.lockBps > BPS) revert InvalidVestParams();
if (p.lockBps > 0 && (p.lockDuration < _minVestDuration || p.lockCliff > p.lockDuration)) {
revert InvalidVestParams();
}
if (p.lockBps == 0 && (p.lockDuration != 0 || p.lockCliff != 0)) revert InvalidVestParams();
if (p.snipeGuardBlocks > _snipeGuardBlocksCap || p.decayExtraBps > _decayStartExtraBpsCap) {
revert SniperOptsExceedCap();
}
bytes32 tickerKey = keccak256(bytes(_lower(p.symbol)));
uint256 last = lastTickerUse[tickerKey];
if (last != 0 && block.timestamp < last + tickerCooldown) revert TickerOnCooldown(last + tickerCooldown);
lastTickerUse[tickerKey] = uint40(block.timestamp);
// NO carve-out: the entire base curve supply stays public.
uint128 curveSupply = _baseCurveSupply;
// Bake the anti-whale cap + exempt creator into the token as IMMUTABLES
// so the token needs NO external call on transfer (self-contained =
// scanner-clean). maxWallet() is the current governed cap at launch time.
FlowPadCurveToken t =
new FlowPadCurveToken(p.name, p.symbol, address(hook), address(poolManager), msg.sender, maxWallet());
token = address(t);
creatorOf[token] = msg.sender;
PoolKey memory key = _key(token);
(,,, uint160 sqrtStartX96,) = hook.curveTicks(curveSupply);
poolManager.initialize(key, sqrtStartX96);
// Curve + LP inventory to the hook (no vest carve-out).
IERC20(token).transfer(address(hook), uint256(curveSupply) + _cfg.lpReserve);
hook.registerPool(
key,
FlowPadCurveHook.LaunchConfig({
creator: msg.sender,
snipeGuardBlocks: p.snipeGuardBlocks,
decayExtraBps: p.decayExtraBps,
curveSupply: curveSupply,
vestAmount: 0, // no supply carve-out; the lock is a locked DEV BUY
vestDuration: 0,
vestCliff: 0
})
);
if (p.feeRecipient != address(0) && p.feeRecipient != msg.sender) {
_feeSplit[token].push(Payee(p.feeRecipient, uint16(BPS)));
}
// Forward the launch fee, then run the dev buy (+ optional lock).
if (_launchFee > 0) {
(bool ok,) = protocolRecipient.call{value: _launchFee}("");
if (!ok) revert FeeTransferFailed();
}
uint256 devTokens;
uint256 devBuyEth = msg.value - _launchFee;
if (devBuyEth > 0) {
devTokens = _devBuyAndLock(key, token, devBuyEth, p);
if (hook.curveFullOf(key)) hook.graduate(key); // a curve-clearing dev buy graduates in-tx
}
emit Launched(token, msg.sender, PoolId.unwrap(key.toId()), devTokens);
}
/// @dev The dev buy is a REAL curve swap by the launcher (snipe-guard/decay
/// exempt). Of the tokens bought, `lockBps` (capped at MAX_LOCK) is streamed
/// to the creator via RobinFlow; the rest goes to the creator's wallet. The
/// creator PAYS the curve price for every locked token — no free carve-out.
function _devBuyAndLock(PoolKey memory key, address token, uint256 devBuyEth, LaunchParams calldata p)
private
returns (uint256 tokensOut)
{
uint256 flatFee = p.lockBps > 0 ? streams.flatFee() : 0;
if (devBuyEth <= flatFee) revert DevBuySlippage(); // need ETH left to actually buy
uint256 buyEth = devBuyEth - flatFee;
// swap buyEth -> tokens (to THIS launcher for splitting); refund any
// unspent ETH (crossing buy) to the creator.
bytes memory ret = poolManager.unlock(abi.encode(key, buyEth, msg.sender));
tokensOut = abi.decode(ret, (uint256));
if (tokensOut < p.minDevBuyTokensOut) revert DevBuySlippage();
uint256 lockAmount = (tokensOut * p.lockBps) / BPS;
if (lockAmount > MAX_LOCK) lockAmount = MAX_LOCK;
if (tokensOut > lockAmount) IERC20(token).safeTransfer(msg.sender, tokensOut - lockAmount);
if (lockAmount > 0) {
uint40 start = uint40(block.timestamp);
IERC20(token).forceApprove(address(streams), lockAmount);
streams.createStream{value: flatFee}(
msg.sender,
token,
uint128(lockAmount),
0,
start,
start + p.lockCliff,
start + p.lockDuration,
0,
false,
false,
"FlowPad creator lock"
);
} else if (flatFee > 0) {
// nothing to lock after rounding/cap — refund the reserved flat fee
(bool ok,) = msg.sender.call{value: flatFee}("");
if (!ok) revert FeeTransferFailed();
}
}
/// @dev v4 unlock for the dev buy: a real ETH->token swap as the launcher;
/// tokens are taken to the launcher (for the lock/deliver split) and any
/// unspent ETH (partial fill) is refunded to the creator.
function unlockCallback(bytes calldata data) external returns (bytes memory) {
if (msg.sender != address(poolManager)) revert NotPoolManager();
(PoolKey memory key, uint256 ethIn, address refundTo) = abi.decode(data, (PoolKey, uint256, address));
BalanceDelta d = poolManager.swap(
key,
SwapParams({zeroForOne: true, amountSpecified: -int256(ethIn), sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1}),
""
);
uint256 spent = d.amount0() < 0 ? uint256(uint128(-d.amount0())) : 0;
uint256 tokensOut = d.amount1() > 0 ? uint256(uint128(d.amount1())) : 0;
if (spent > 0) poolManager.settle{value: spent}();
if (tokensOut > 0) poolManager.take(key.currency1, address(this), tokensOut); // to launcher for split
uint256 refund = ethIn - spent;
if (refund > 0) {
(bool ok,) = refundTo.call{value: refund}("");
if (!ok) revert FeeTransferFailed();
}
return abi.encode(tokensOut);
}
// ------------------------------------------------------- fee governance
/// @notice Announce a curve-fee / creator-share / launch-fee change; applies
/// no earlier than this version's timelock, bounded by this version's caps.
function announceFeeChange(uint16 feeBps_, uint16 creatorFeeShareBps_, uint96 launchFee_) external onlyProtocol {
if (feeBps_ == 0 || feeBps_ > _maxFeeBps || creatorFeeShareBps_ > BPS || launchFee_ > _maxLaunchFee) {
revert InvalidConfig();
}
uint64 applyAfter = uint64(block.timestamp + _feeTimelock);
pendingFees = PendingFees(feeBps_, creatorFeeShareBps_, launchFee_, applyAfter, true);
emit FeeChangeAnnounced(feeBps_, creatorFeeShareBps_, launchFee_, applyAfter);
}
/// @notice Apply an announced change after the timelock. Anyone may call.
function applyFeeChange() external {
PendingFees memory pf = pendingFees;
if (!pf.exists) revert NoPendingChange();
if (block.timestamp < pf.applyAfter) revert TimelockActive();
_feeBps = pf.feeBps;
_creatorFeeShareBps = pf.creatorFeeShareBps;
_launchFee = pf.launchFee;
delete pendingFees;
emit FeeChangeApplied(pf.feeBps, pf.creatorFeeShareBps, pf.launchFee);
}
function cancelFeeChange() external onlyProtocol {
if (!pendingFees.exists) revert NoPendingChange();
delete pendingFees;
emit FeeChangeCancelled();
}
// ------------------------------------------------------------- fee split
function setFeeSplit(address token, address[] calldata recipients, uint16[] calldata shareBps) external {
if (msg.sender != creatorOf[token]) revert NotCreator();
_storeFeeSplit(token, recipients, shareBps, false);
}
/// @notice Protocol emergency re-route (e.g. a compromised creator wallet).
/// Transparent: emits FeeSplitSet with rescue=true.
function rescueFeeSplit(address token, address[] calldata recipients, uint16[] calldata shareBps)
external
onlyProtocol
{
if (creatorOf[token] == address(0)) revert UnknownToken();
_storeFeeSplit(token, recipients, shareBps, true);
}
function _storeFeeSplit(address token, address[] memory recipients, uint16[] memory shareBps, bool rescue) private {
if (recipients.length != shareBps.length || recipients.length > MAX_FEE_RECIPIENTS) revert InvalidFeeSplit();
delete _feeSplit[token];
if (recipients.length > 0) {
uint256 sum;
for (uint256 i; i < recipients.length; i++) {
if (recipients[i] == address(0) || shareBps[i] == 0) revert InvalidFeeSplit();
sum += shareBps[i];
_feeSplit[token].push(Payee(recipients[i], shareBps[i]));
}
if (sum != BPS) revert InvalidFeeSplit();
}
emit FeeSplitSet(token, recipients, shareBps, rescue);
}
/// @notice Hand the creator role (fee stream + vest recipient) to a new
/// address; clears any custom split so fees follow the new creator.
function transferCreatorship(address token, address newCreator) external {
if (msg.sender != creatorOf[token]) revert NotCreator();
if (newCreator == address(0)) revert ZeroAddress();
emit CreatorshipTransferred(token, creatorOf[token], newCreator);
creatorOf[token] = newCreator;
if (_feeSplit[token].length > 0) {
delete _feeSplit[token];
emit FeeSplitSet(token, new address[](0), new uint16[](0), false);
}
}
// ------------------------------------------------------------- policy
/// @notice Governable pre-graduation max-wallet cap (post-grad: no cap).
/// Each token bakes this value as an IMMUTABLE at its own launch (so the
/// token stays self-contained / scanner-clean), so a change here applies to
/// FUTURE launches only — it does not retroactively affect live tokens.
function setMaxWalletBps(uint16 bps) external onlyProtocol {
if (bps == 0 || bps > BPS) revert InvalidConfig();
maxWalletBps = bps;
emit MaxWalletBpsSet(bps);
}
/// @notice Governable ceiling on the timelock-governed curve fee. The hard
/// invariant (cap + full decay < 100%) is kept so trades can never be bricked.
function setMaxFeeBps(uint16 bps) external onlyProtocol {
if (bps == 0 || uint256(bps) + _decayStartExtraBpsCap >= BPS) revert InvalidConfig();
_maxFeeBps = bps;
emit MaxFeeBpsSet(bps);
}
/// @notice Governable ceiling on the timelock-governed launch fee.
function setMaxLaunchFee(uint96 fee) external onlyProtocol {
_maxLaunchFee = fee;
emit MaxLaunchFeeSet(fee);
}
function setProtocolRecipient(address newRecipient) external onlyProtocol {
if (newRecipient == address(0)) revert ZeroAddress();
emit ProtocolRecipientChanged(protocolRecipient, newRecipient);
protocolRecipient = newRecipient;
}
function setRouterAllowed(address router, bool allowed) external onlyProtocol {
allowedRouter[router] = allowed;
emit RouterAllowedSet(router, allowed);
}
function setTickerCooldown(uint32 cooldownSeconds) external onlyProtocol {
tickerCooldown = cooldownSeconds;
emit TickerCooldownSet(cooldownSeconds);
}
function tickerAvailableAt(string calldata symbol) external view returns (uint256) {
uint256 last = lastTickerUse[keccak256(bytes(_lower(symbol)))];
if (last == 0) return 0;
uint256 freeAt = last + tickerCooldown;
return block.timestamp < freeAt ? freeAt : 0;
}
function setLaunchesPaused(bool paused) external onlyProtocol {
launchesPaused = paused;
emit LaunchesPausedSet(paused);
}
function setLaunchAllowlistEnabled(bool enabled) external onlyProtocol {
launchAllowlistEnabled = enabled;
emit LaunchAllowlistEnabledSet(enabled);
}
function setLaunchAllowlist(address[] calldata accounts, bool allowed) external onlyProtocol {
for (uint256 i; i < accounts.length; i++) {
launchAllowlist[accounts[i]] = allowed;
emit LaunchAllowlistSet(accounts[i], allowed);
}
}
// ------------------------------------------------ IFlowPadFactoryViews
function feeSplitOf(address token) external view returns (address[] memory recipients, uint16[] memory shareBps) {
Payee[] storage split = _feeSplit[token];
if (split.length == 0) {
recipients = new address[](1);
shareBps = new uint16[](1);
recipients[0] = creatorOf[token];
shareBps[0] = uint16(BPS);
return (recipients, shareBps);
}
recipients = new address[](split.length);
shareBps = new uint16[](split.length);
for (uint256 i; i < split.length; i++) {
recipients[i] = split[i].recipient;
shareBps[i] = split[i].shareBps;
}
}
// ------------------------------------------------------------- internal
function _key(address token) internal view returns (PoolKey memory) {
return PoolKey({
currency0: Currency.wrap(address(0)),
currency1: Currency.wrap(token),
fee: 0,
tickSpacing: 60,
hooks: IHooks(address(hook))
});
}
function _lower(string memory s) internal pure returns (string memory) {
bytes memory b = bytes(s);
for (uint256 i; i < b.length; i++) {
if (b[i] >= 0x41 && b[i] <= 0x5A) b[i] = bytes1(uint8(b[i]) + 32);
}
return string(b);
}
}
Chain explorer4817msChain node93ms