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
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