Launchpad

0xa92989b5b2ca27b4bc7ea4d2a3e6801f3f4fdc33

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
16,623 bytes
ABI entries
61
18 read · 10 write
License
none

Contract information

Address
0xa92989b5b2ca27b4bc7ea4d2a3e6801f3f4fdc33
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0xb91b960152…3815d2496e

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (18)

DEFAULT_CREATOR_FEE_BPS()uint256
MAX_CREATOR_FEE_BPS()uint256
MAX_WALLET_BPS()uint256
MAX_WALLET_WINDOW()uint256
MIN_CLAIM()uint256
PLATFORM_FEE_BPS()uint256
creatorAccrued(address, address)uint256
curves(address)address, uint256, uint256, uint256, uint256, uint256, uint256, uint256, uint256, uint256, address, address, uint8, uint256, bool, bool, uint256, uint256, uint8, uint256, uint256
graduatorOf(uint8)address
predictTokenAddress(address, bytes32)address
previewBuy(address, uint256)uint256, uint256, uint256
previewDevBuy(uint256, uint256, uint256, uint256, uint256, uint256)uint256, uint256, uint256
previewSell(address, uint256)uint256, uint256, uint256
protocolFees(address)uint256
spotPriceX18(address)uint256
taxTokenImplementation()address
tokenImplementation()address
treasury()address

Events (9)

BoughtCreatorFeePaidCreatorFeesClaimedGraduatedGraduatorSetLaunchedPlatformFeePaidProtocolFeesWithdrawnSold

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "treasury_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AlreadyExists",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "AlreadyGraduated",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "DeadlinePassed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FeeTooHigh",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "GraduatorAlreadySet",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "GraduatorNotSet",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientReserve",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidParams",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "MaxWalletExceeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NativeValueMismatch",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NonNativeDevBuy",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotGraduatable",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotTreasury",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NothingToClaim",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "Reentrancy",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SlippageExceeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "UnexpectedNativeValue",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "UnexpectedValue",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "UnknownToken",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAmount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAmount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroReserve",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "buyer",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "quoteIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "platformFee",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "creatorFee",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newVToken",
        "type": "uint256"
      }
    ],
    "name": "Bought",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "quote",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "CreatorFeePaid",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "quote",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "CreatorFeesClaimed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "lpSupply",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "quoteForLp",
        "type": "uint256"
      }
    ],
    "name": "Graduated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "enum GradTarget",
        "name": "target",
        "type": "uint8"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "graduator",
        "type": "address"
      }
    ],
    "name": "GraduatorSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "quote",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "feeRecipient",
        "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": "x0",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "y0",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "supply",
        "type": "uint256"
      }
    ],
    "name": "Launched",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "quote",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "PlatformFeePaid",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "quote",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "ProtocolFeesWithdrawn",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "seller",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "quoteOut",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "platformFee",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "creatorFee",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newVToken",
        "type": "uint256"
      }
    ],
    "name": "Sold",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "DEFAULT_CREATOR_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_CREATOR_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_WALLET_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_WALLET_WINDOW",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_CLAIM",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "PLATFORM_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "quoteInTotal",
        "type": "uint256"
      },
      {
        "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": [
      {
        "internalType": "address",
        "name": "quote",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      }
    ],
    "name": "claim",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "creatorAccrued",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "curves",
    "outputs": [
      {
        "internalType": "address",
        "name": "quote",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "x0",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "y0",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "curveSupply",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "lpSupply",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "raiseTarget",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "vQuote",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "vToken",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "realTokenReserves",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "quoteCollected",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "feeRecipient",
        "type": "address"
      },
      {
        "internalType": "enum GradTarget",
        "name": "graduationTarget",
        "type": "uint8"
      },
      {
        "internalType": "uint256",
        "name": "creatorFeeBps",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "graduated",
        "type": "bool"
      },
      {
        "internalType": "bool",
        "name": "exists",
        "type": "bool"
      },
      {
        "internalType": "uint256",
        "name": "cashbackBps",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "buybackBps",
        "type": "uint256"
      },
      {
        "internalType": "uint8",
        "name": "curveBuybackDest",
        "type": "uint8"
      },
      {
        "internalType": "uint256",
        "name": "buybackAccrued",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "cashbackAccrued",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "graduate",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "enum GradTarget",
        "name": "",
        "type": "uint8"
      }
    ],
    "name": "graduatorOf",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "address",
        "name": "feeRecipient",
        "type": "address"
      },
      {
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      },
      {
        "internalType": "address",
        "name": "quote",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "m0",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "raiseTarget",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "curveSupply",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "totalSupply",
        "type": "uint256"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      }
    ],
    "name": "launch",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "address",
        "name": "feeRecipient",
        "type": "address"
      },
      {
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      },
      {
        "internalType": "address",
        "name": "quote",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "m0",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "raiseTarget",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "curveSupply",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "totalSupply",
        "type": "uint256"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      },
      {
        "internalType": "uint256",
        "name": "devBuyQuote",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "launchAndBuy",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "devTokensOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "address",
        "name": "feeRecipient",
        "type": "address"
      },
      {
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      },
      {
        "internalType": "address",
        "name": "quote",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "m0",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "raiseTarget",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "curveSupply",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "totalSupply",
        "type": "uint256"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      },
      {
        "internalType": "enum GradTarget",
        "name": "target",
        "type": "uint8"
      },
      {
        "internalType": "uint256",
        "name": "creatorFeeBps",
        "type": "uint256"
      },
      {
        "components": [
          {
            "internalType": "uint256",
            "name": "cashbackBps",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "buybackBps",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "maxRewardShare",
            "type": "uint256"
          },
          {
            "internalType": "uint8",
            "name": "curveBuybackDest",
            "type": "uint8"
          }
        ],
        "internalType": "struct Launchpad.ModeArgs",
        "name": "modes",
        "type": "tuple"
      }
    ],
    "name": "launchTo",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "address",
        "name": "feeRecipient",
        "type": "address"
      },
      {
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      },
      {
        "internalType": "address",
        "name": "quote",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "m0",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "raiseTarget",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "curveSupply",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "totalSupply",
        "type": "uint256"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      },
      {
        "internalType": "enum GradTarget",
        "name": "target",
        "type": "uint8"
      },
      {
        "internalType": "uint256",
        "name": "creatorFeeBps",
        "type": "uint256"
      },
      {
        "components": [
          {
            "internalType": "uint256",
            "name": "cashbackBps",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "buybackBps",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "maxRewardShare",
            "type": "uint256"
          },
          {
            "internalType": "uint8",
            "name": "curveBuybackDest",
            "type": "uint8"
          }
        ],
        "internalType": "struct Launchpad.ModeArgs",
        "name": "modes",
        "type": "tuple"
      },
      {
        "internalType": "uint256",
        "name": "devBuyQuote",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "launchToAndBuy",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "devTokensOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      }
    ],
    "name": "predictTokenAddress",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "quoteInTotal",
        "type": "uint256"
      }
    ],
    "name": "previewBuy",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "platformFee",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "creatorFee",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "m0",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "raiseTarget",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "curveSupply",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "totalSupply",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "creatorFeeBps",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "devBuyQuote",
        "type": "uint256"
      }
    ],
    "name": "previewDevBuy",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "platformFee",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "creatorFee",
        "type": "uint256"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokensIn",
        "type": "uint256"
      }
    ],
    "name": "previewSell",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "netQuoteOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "platformFee",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "creatorFee",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "protocolFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokensIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minQuoteOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "sell",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "quoteOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "enum GradTarget",
        "name": "target",
        "type": "uint8"
      },
      {
        "internalType": "address",
        "name": "graduator_",
        "type": "address"
      }
    ],
    "name": "setGraduator",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "spotPriceX18",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "taxTokenImplementation",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokenImplementation",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "treasury",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "quote",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      }
    ],
    "name": "withdrawProtocolFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol";
import {LibClone} from "solady/utils/LibClone.sol";
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol";
import {Token} from "./Token.sol";
import {TaxToken} from "./TaxToken.sol";
import {CurveMath} from "./libraries/CurveMath.sol";
import {CurveParams} from "./libraries/CurveParams.sol";
import {QuoteAsset} from "./libraries/QuoteAsset.sol";
import {IGraduator} from "./interfaces/IGraduator.sol";
import {IUniswapV2Router02} from "./interfaces/IUniV2.sol";
import {UniV2Graduator} from "./UniV2Graduator.sol";

/// @dev Where a curve graduates. Chosen per-launch; picks BOTH the graduator adapter and the token type:
///   - V4  → clean `Token`, seeds a hooked Uniswap v4 pool, post-grad fee lives in `Hook`.
///   - V2  → fee-on-transfer `TaxToken`, seeds a Uniswap v2 pair, post-grad fee lives in the token's
///           transfer tax (v2 has no hooks). The tax is dormant during the curve (pair unset) and switches
///           on at graduation via `setPair`.
enum GradTarget {
    V4,
    V2
}

/// @title Launchpad
/// @notice Singleton, permissionless bonding-curve launchpad — one instance per chain holds
///         EVERY pre-graduation curve in a `mapping(token => CurveState)`, with no per-token
///         curve contract. Anyone can `launch()`; trades run on an oracle-free constant-product
///         virtual-reserve curve denominated in that launch's quote asset (native or ERC-20),
///         with a 1% tax taken in the quote asset on every buy and sell. When the curve's real
///         token reserve hits zero the launch `graduate()`s: a clean state flip that reserves the
///         LP allocation + collected quote for the (pluggable) v4 graduator and permanently
///         blocks further curve trading.
///
/// @dev    Design invariants (build-strategy §2-3, checklist §3, §7):
///           - Pricing uses VIRTUAL reserves only, never balanceOf (donation/inflation defense).
///           - Round AGAINST the user everywhere (delegated to CurveMath).
///           - Strict CEI + nonReentrant on every value-moving fn (Cork/Bunni lessons).
///           - Mandatory slippage (`minOut`) + `deadline` on every trade.
///           - Quote asset is per-launch config (address(0)=native) → chain-agnostic spine.
///           - Graduation is permissionless and a hard state flip (no curve sells after).
contract Launchpad is ReentrancyGuard {
    using QuoteAsset for address;
    using SafeTransferLib for address;

    // --- Fee config ----------------------------------------------------------------------
    /// @dev Two-leg fee, both taken in the quote asset on every buy and sell:
    ///        - PLATFORM (fixed 1%): protocol revenue, withdrawable by the treasury.
    ///        - CREATOR: accrues to the launch creator, claimed via `claim()`.
    ///      FULL-LIFECYCLE TAX ALIGNMENT: the creator fee is the SAME rate on the curve as the post-graduation
    ///      transfer tax. For a V2-graduating launch the creator picks `creatorFeeBps` (e.g. 3%) and it is
    ///      charged on every curve buy/sell AND on every v2 swap after graduation — one consistent tax
    ///      through the whole lifecycle. For a V4-graduating launch (no user-chosen tax) the creator leg is
    ///      the flat `DEFAULT_CREATOR_FEE_BPS` (1%). Platform is always 1%. Each leg rounds DOWN.
    uint256 public constant PLATFORM_FEE_BPS = 100; // 1.00% — fixed, both phases
    uint256 public constant DEFAULT_CREATOR_FEE_BPS = 100; // 1.00% — the creator leg for V4 (clean-token) launches
    /// @dev Hard ceiling on the CREATOR leg so curve+platform can't exceed the token's honeypot cap
    ///      (mirrors TaxToken.MAX_TOTAL_BPS = 10% total → creator ≤ 9%).
    uint256 public constant MAX_CREATOR_FEE_BPS = 900; // 9.00% (creator) → 10% with the 1% platform
    uint256 internal constant BPS = 10_000;

    /// @notice FAIR-LAUNCH CURVE CAP. For the first `MAX_WALLET_WINDOW` blocks of a token's LIFE, no third-party
    ///         curve buy may push a wallet past this share of total supply (mirrors TaxToken.MAX_WALLET_BPS,
    ///         which enforces the SAME 2% on the v2 pool). The window is anchored at the token's launch block
    ///         (read back from the token), so it is ONE continuous ~1h window: it starts on the curve and runs
    ///         straight through graduation into the pool — if a token bonds 30 min in, the pool sees the
    ///         remaining ~30 min, then the cap lifts everywhere at once. Anti-snipe / anti-whale, NOT anti-rug.
    ///         After the window, the block check short-circuits (a lone buyer can then bond a token solo).
    ///         The creator's atomic dev buy is EXEMPT (they seed their own position at launch) — see _executeBuy.
    uint256 public constant MAX_WALLET_BPS = 200; // 2% of total supply
    /// @dev Cap window in blocks — MUST match TaxToken.MAX_WALLET_WINDOW so curve + pool lift in lockstep.
    ///      Measured on `block.number`, which on this OP-Stack L2 is the ETHEREUM L1 height (~12s/block), so
    ///      300 blocks ≈ 1 hour of real time. See TaxToken.MAX_WALLET_WINDOW for the full note.
    uint256 public constant MAX_WALLET_WINDOW = 300; // ~1h at 12s L1 blocks

    /// @dev The effective per-launch creator fee bps: the chosen `creatorFeeBps` for a V2 launch, else the
    ///      flat 1% default. Used by buy/sell and the graduation cap-split so the curve tax == the token tax.
    function _creatorFeeBpsOf(CurveState storage c) internal view returns (uint256) {
        return c.graduationTarget == GradTarget.V2 ? c.creatorFeeBps : DEFAULT_CREATOR_FEE_BPS;
    }

    /// @dev Auto-push the platform fee straight to the treasury; on failure (a griefing/reverting
    ///      treasury — shouldn't happen, but never brick a trade) fall back to the pull mapping so the
    ///      treasury can still `withdrawProtocolFees`. Curve-phase fees are now AUTO (no manual claim),
    ///      with the pull path retained only as a safety net.
    function _payPlatformFee(CurveState storage c, uint256 platformFee) internal {
        if (platformFee == 0) return;
        if (c.quote.tryPushOut(treasury, platformFee)) {
            emit PlatformFeePaid(c.quote, treasury, platformFee);
        } else {
            protocolFees[c.quote] += platformFee; // fallback: pull-based, as before
        }
    }

    /// @dev Auto-push the creator's remainder straight to `feeRecipient`; on failure fall back to the
    ///      `creatorAccrued` pull mapping (they can still `claim`). `token` is passed for the event only.
    function _payCreatorRemainder(CurveState storage c, address token, uint256 amount) internal {
        if (amount == 0) return;
        if (c.quote.tryPushOut(c.feeRecipient, amount)) {
            emit CreatorFeePaid(token, c.feeRecipient, c.quote, amount);
        } else {
            creatorAccrued[c.feeRecipient][c.quote] += amount; // fallback: pull-based, as before
        }
    }

    /// @dev Route the curve-phase cash-back slice. FULLY AUTOMATED, LIVE ON THE CURVE for a native (ETH)
    ///      quote: the ETH is pushed straight into the token's reward index via `seedRewards()` on every
    ///      trade, so curve holders earn cash-back in real time exactly as they do post-graduation — no
    ///      wait for graduation. The launchpad is the token's configurator, so it's authorized to seed.
    ///
    ///      FAIRNESS/ORDERING: this runs in `buy` BEFORE the buyer's tokens are delivered (and in `sell`
    ///      AFTER the seller's tokens are pulled in), so a trader never earns cash-back from their own
    ///      trade's fee — the distribution is split across the holders who held at that moment. The token's
    ///      reward index handles the churning early-curve holder set exactly (per-share, correction-booked).
    ///
    ///      FIRST-BUYER EDGE: on the very first buy no eligible holders exist yet (the buyer isn't credited
    ///      until after fee routing), so `seedRewards → _distributeRewards` forwards that slice to the
    ///      creator rather than trapping it — the token's designed no-holders fallback. Acceptable + rare.
    ///
    ///      ERC-20-QUOTED CURVES: the reward index is ETH-denominated and `seedRewards` is payable, so a
    ///      non-native quote can't distribute live. Those (RWA/stablecoin-quoted) curves ACCRUE the slice to
    ///      `cashbackAccrued` and hand it over at graduation, as before. Native (the v1 curve→v2 case) is live.
    function _routeCurveCashback(CurveState storage c, address token, uint256 toCashback) internal {
        if (toCashback == 0) return;
        if (c.quote.isNative()) {
            // LIVE: distribute into the token's reward index now (curve holders earn immediately).
            TaxToken(payable(token)).seedRewards{value: toCashback}();
        } else {
            // ERC-20 quote: no ETH to seed — accrue and hand over at graduation.
            c.cashbackAccrued += toCashback;
        }
    }

    /// @dev Route the curve-phase creator fee per the launch's token modes. The cash-back / buyback slices
    ///      are carved from the CREATOR fee (never the platform leg). Pre-graduation there is no AMM to
    ///      market-buy on, so:
    ///        - cash-back slice → distributed LIVE to holders via the token's reward index (native quote),
    ///                            else accrued to `cashbackAccrued` (ERC-20 quote) — see `_routeCurveCashback`.
    ///        - buyback slice   → per `curveBuybackDest`: 0 accrue (real buyback at graduation) /
    ///                            1 pay the creator now / 2 fold into the cash-back (also distributed live).
    ///        - remainder       → the creator (auto-pushed to `feeRecipient`, pull fallback).
    ///      For V4 launches (no modes) this is just the whole leg → creator.
    function _routeCurveCreatorFee(CurveState storage c, address token, uint256 creatorFee) internal {
        if (creatorFee == 0) return;
        // V4 or modeless V2 → the whole creator leg is the remainder → auto-push to the creator.
        if (c.graduationTarget != GradTarget.V2 || (c.cashbackBps == 0 && c.buybackBps == 0)) {
            _payCreatorRemainder(c, token, creatorFee);
            return;
        }

        uint256 cf = c.creatorFeeBps; // > 0 here (modes require it)
        uint256 toCashback = c.cashbackBps == 0 ? 0 : (creatorFee * c.cashbackBps) / cf;
        uint256 toBuyback = c.buybackBps == 0 ? 0 : (creatorFee * c.buybackBps) / cf;

        // buyback destination during the curve
        if (toBuyback != 0) {
            if (c.curveBuybackDest == 1) {
                // 1: pay the creator now (treated like the remainder)
                // (folded into `toCreator` below by not diverting it)
            } else if (c.curveBuybackDest == 2) {
                // 2: fold into cash-back (guaranteed cashbackBps > 0 by the launch guard) → also live
                toCashback += toBuyback;
                toBuyback = 0;
            } else {
                // 0: accrue for a real buyback-and-burn executed at graduation
                c.buybackAccrued += toBuyback;
            }
        }
        // Cash-back is distributed LIVE to curve holders (native quote) or accrued (ERC-20 quote).
        uint256 diverted = toCashback + (c.curveBuybackDest == 0 ? toBuyback : 0);
        _routeCurveCashback(c, token, toCashback);

        uint256 toCreator = creatorFee - diverted;
        _payCreatorRemainder(c, token, toCreator);
    }

    /// @dev Minimum claimable creator balance — bounds dust-griefing on `claim()` (Flaunch uses a
    ///      similar small floor). Denominated in the quote asset's smallest units.
    uint256 public constant MIN_CLAIM = 0.0001 ether;

    // --- Per-launch state ----------------------------------------------------------------
    struct CurveState {
        // immutable curve params (set at launch)
        address quote; //            quote asset (address(0) = native)
        uint256 x0; //               initial virtual quote reserves
        uint256 y0; //               initial virtual token reserves
        uint256 curveSupply; //      tokens sold along the curve (Sc)
        uint256 lpSupply; //         tokens reserved to seed the v4 pool at graduation
        uint256 raiseTarget; //      quote collected when curve fully fills (R)
        // live state
        uint256 vQuote; //           current virtual quote reserves (x)
        uint256 vToken; //           current virtual token reserves (y)
        uint256 realTokenReserves; //tokens still buyable on the curve (solvency; graduation = 0)
        uint256 quoteCollected; //   net quote held for this launch (excludes fees)
        address creator; //          who launched it (provenance; immutable)
        address feeRecipient; //     who the 1% creator fee accrues to + who claims it (set at launch)
        GradTarget graduationTarget; // where it graduates (V4 clean-token / V2 tax-token). Immutable.
        uint256 creatorFeeBps; //    v2-only: the tax token's creator tax (bps). 0 for V4 launches.
        bool graduated;
        bool exists;
        // ── token modes (v2 launches only; appended to keep existing field indices stable) ──────────
        uint256 cashbackBps; //      slice of creatorFeeBps → holder cash-back (curve + post-grad)
        uint256 buybackBps; //       slice of creatorFeeBps → buyback-and-burn
        uint8 curveBuybackDest; //   curve-phase buyback destination: 0=accrue-to-pool, 1=creator, 2=cashback
        uint256 buybackAccrued; //   curve-phase buyback ETH held for the graduation buyback (dest 0)
        uint256 cashbackAccrued; //  curve-phase cash-back ETH held to seed the token's reward index at grad
    }

    /// @notice token address => its curve state.
    mapping(address => CurveState) public curves;

    /// @notice protocol fees accrued per quote asset (pull-based withdrawal by the owner/treasury).
    mapping(address => uint256) public protocolFees;

    /// @notice creator fees accrued, keyed by FEE RECIPIENT then quote asset (pull-based; the recipient
    ///         claims). Keying by quote supports a recipient receiving fees across multiple quote assets.
    ///         The recipient is `feeRecipient` from launch (defaults to the launcher) — not necessarily
    ///         the launcher, so a creator can route their fees to a separate/treasury wallet.
    mapping(address => mapping(address => uint256)) public creatorAccrued;

    /// @notice The CLEAN token implementation cloned for V4-graduating launches.
    address public immutable tokenImplementation;

    /// @notice The FEE-ON-TRANSFER tax-token implementation cloned for V2-graduating launches.
    address public immutable taxTokenImplementation;

    /// @notice The treasury that may withdraw accrued protocol fees.
    address public immutable treasury;

    /// @notice Graduation-adapter registry, keyed by target (V4/V2). Each is set exactly once by the
    ///         treasury post-deploy (they can't be constructor args — a graduator needs THIS launchpad's
    ///         address, a deploy-order cycle). Once set a slot is effectively immutable (no re-set path).
    ///         A launch may only pick a target whose graduator is already registered.
    mapping(GradTarget => address) public graduatorOf;

    // --- Events --------------------------------------------------------------------------
    event Launched(
        address indexed token,
        address indexed creator,
        address indexed quote,
        address feeRecipient,
        string name,
        string symbol,
        string metadataURI,
        uint256 x0,
        uint256 y0,
        uint256 supply
    );
    event Bought(
        address indexed token,
        address indexed buyer,
        uint256 quoteIn,
        uint256 platformFee,
        uint256 creatorFee,
        uint256 tokensOut,
        uint256 newVToken
    );
    event Sold(
        address indexed token,
        address indexed seller,
        uint256 tokensIn,
        uint256 quoteOut,
        uint256 platformFee,
        uint256 creatorFee,
        uint256 newVToken
    );
    event Graduated(address indexed token, uint256 lpSupply, uint256 quoteForLp);
    event ProtocolFeesWithdrawn(address indexed quote, address indexed to, uint256 amount);
    event CreatorFeesClaimed(address indexed creator, address indexed quote, address indexed to, uint256 amount);
    event GraduatorSet(GradTarget indexed target, address indexed graduator);
    /// @dev A curve-phase fee leg was auto-pushed straight to its recipient (no claim needed).
    event PlatformFeePaid(address indexed quote, address indexed to, uint256 amount);
    event CreatorFeePaid(address indexed token, address indexed to, address indexed quote, uint256 amount);

    // --- Errors --------------------------------------------------------------------------
    error AlreadyExists();
    error UnknownToken();
    error MaxWalletExceeded(); // a curve buy would push the wallet over MAX_WALLET_BPS of supply (fair-launch cap)
    error AlreadyGraduated();
    error NotGraduatable();
    error DeadlinePassed();
    error SlippageExceeded();
    error ZeroAmount();
    error NotTreasury();
    error ZeroAddress();
    error GraduatorAlreadySet();
    error GraduatorNotSet();
    error NothingToClaim();
    error FeeTooHigh();
    error UnexpectedValue();
    error NonNativeDevBuy();

    constructor(address treasury_) {
        if (treasury_ == address(0)) revert ZeroAddress();
        treasury = treasury_;
        tokenImplementation = address(new Token());
        taxTokenImplementation = address(new TaxToken());
    }

    // =====================================================================================
    // Launch
    // =====================================================================================

    /// @notice Permissionlessly launch a new token + curve.
    /// @param name token name
    /// @param symbol token symbol
    /// @param feeRecipient address the 1% creator fee accrues to (and that claims it). Pass
    ///        `address(0)` to default it to the launcher (`msg.sender`). Lets a creator route fees
    ///        to a separate/treasury wallet without giving up launch provenance.
    /// @param metadataURI off-chain metadata pointer (e.g. `ipfs://…` JSON with image/socials/desc).
    ///        Emitted in the `Launched` event for indexers; not stored on-chain (gas) and not trusted
    ///        by the contract. May be empty.
    /// @param quote quote asset (address(0) = native chain coin)
    /// @param m0 starting FDV in quote units
    /// @param raiseTarget quote to be collected when the curve fully fills (R)
    /// @param curveSupply tokens sold along the curve (Sc)
    /// @param totalSupply total token supply (T); the remainder (T - Sc) seeds the v4 LP
    /// @param salt clone salt (creator-controlled → pre-computable token address)
    /// @return token the freshly deployed token address
    function launch(
        string calldata name,
        string calldata symbol,
        address feeRecipient,
        string calldata metadataURI,
        address quote,
        uint256 m0,
        uint256 raiseTarget,
        uint256 curveSupply,
        uint256 totalSupply,
        bytes32 salt
    ) external returns (address token) {
        // Back-compat entrypoint: defaults to a V4 graduation (clean token, no transfer tax).
        return _launch(
            LaunchArgs({
                name: name,
                symbol: symbol,
                feeRecipient: feeRecipient,
                metadataURI: metadataURI,
                quote: quote,
                m0: m0,
                raiseTarget: raiseTarget,
                curveSupply: curveSupply,
                totalSupply: totalSupply,
                salt: salt,
                target: GradTarget.V4,
                creatorFeeBps: 0,
                modes: ModeArgs({cashbackBps: 0, buybackBps: 0, maxRewardShare: 0, curveBuybackDest: 0})
            })
        );
    }

    /// @notice Launch choosing the graduation target (V4 clean-token or V2 tax-token). For V2, `creatorFeeBps`
    ///         is the token's fee-on-transfer creator tax (bps); `+ TaxToken.PLATFORM_FEE_BPS` must be
    ///         `<= TaxToken.MAX_TOTAL_BPS`. `creatorFeeBps` is ignored for V4.
    function launchTo(
        string calldata name,
        string calldata symbol,
        address feeRecipient,
        string calldata metadataURI,
        address quote,
        uint256 m0,
        uint256 raiseTarget,
        uint256 curveSupply,
        uint256 totalSupply,
        bytes32 salt,
        GradTarget target,
        uint256 creatorFeeBps,
        ModeArgs calldata modes
    ) external returns (address token) {
        return _launch(
            LaunchArgs({
                name: name,
                symbol: symbol,
                feeRecipient: feeRecipient,
                metadataURI: metadataURI,
                quote: quote,
                m0: m0,
                raiseTarget: raiseTarget,
                curveSupply: curveSupply,
                totalSupply: totalSupply,
                salt: salt,
                target: target,
                creatorFeeBps: creatorFeeBps,
                modes: modes
            })
        );
    }

    /// @dev Token-mode config for a V2 launch (ignored for V4). `cashbackBps + buybackBps <= creatorFeeBps`.
    ///      `curveBuybackDest` governs where the buyback slice goes DURING the curve (post-grad it always
    ///      does real buyback-and-burn): 0 = accrue-to-pool (execute at graduation), 1 = to-creator,
    ///      2 = to-cashback (requires cashbackBps > 0). See docs/TOKEN_MODES_IMPLEMENTATION_PLAN.md §3.
    struct ModeArgs {
        uint256 cashbackBps;
        uint256 buybackBps;
        uint256 maxRewardShare;
        uint8 curveBuybackDest;
    }

    /// @dev Launch params bundled into a struct to dodge stack-too-deep in the shared `_launch`.
    struct LaunchArgs {
        string name;
        string symbol;
        address feeRecipient;
        string metadataURI;
        address quote;
        uint256 m0;
        uint256 raiseTarget;
        uint256 curveSupply;
        uint256 totalSupply;
        bytes32 salt;
        GradTarget target;
        uint256 creatorFeeBps;
        ModeArgs modes;
    }

    function _launch(LaunchArgs memory a) internal returns (address token) {
        // The chosen target's graduator must be registered (also gates V2 on a router being available).
        address grad = graduatorOf[a.target];
        if (grad == address(0)) revert GraduatorNotSet();

        // derive virtual reserves from the target FDV/raise (reverts on infeasible params)
        (uint256 x0, uint256 y0) = CurveParams.virtualReserves(a.m0, a.raiseTarget, a.curveSupply, a.totalSupply);
        (uint256 lpSupply,) = CurveParams.continuitySeed(x0, y0, a.raiseTarget, a.curveSupply);

        // default the fee recipient to the launcher when unspecified.
        address recipient = a.feeRecipient == address(0) ? msg.sender : a.feeRecipient;

        // domain-separate the clone salt by creator so two creators can't collide and a creator
        // gets deterministic, pre-computable addresses.
        bytes32 cloneSalt = keccak256(abi.encodePacked(msg.sender, a.salt));

        uint256 creatorFeeBps = 0;
        if (a.target == GradTarget.V4) {
            // V4: clean Token, full supply minted to the launchpad (it custodies curve + LP allocs).
            token = LibClone.cloneDeterministic(tokenImplementation, cloneSalt);
            if (curves[token].exists) revert AlreadyExists();
            Token(token).initialize(a.name, a.symbol, a.totalSupply, address(this));
        } else {
            // V2: fee-on-transfer TaxToken. Fee cap is enforced in the token's initializer, but check
            // here too so an over-cap launch reverts with a launchpad error before cloning.
            if (
                a.creatorFeeBps + TaxToken(payable(taxTokenImplementation)).PLATFORM_FEE_BPS()
                    > TaxToken(payable(taxTokenImplementation)).MAX_TOTAL_BPS()
            ) revert FeeTooHigh();
            // Token modes are funded FROM the creator tax — their split can't exceed it. `curveBuybackDest`
            // "to-cashback" (2) is only coherent when cash-back is actually on.
            if (a.modes.cashbackBps + a.modes.buybackBps > a.creatorFeeBps) revert FeeTooHigh();
            if (a.modes.curveBuybackDest > 2) revert FeeTooHigh();
            if (a.modes.curveBuybackDest == 2 && a.modes.cashbackBps == 0) revert FeeTooHigh();
            creatorFeeBps = a.creatorFeeBps;
            token = LibClone.cloneDeterministic(taxTokenImplementation, cloneSalt);
            if (curves[token].exists) revert AlreadyExists();
            // The tax token needs the v2 router (for post-grad swapback) — read it from the registered v2
            // graduator. The launchpad is the configurator + fee-exempt; the graduator is fee-exempt + may
            // setPair at graduation. The pair is unset until graduation, so the tax is dormant on the curve.
            address v2Router = address(UniV2Graduator(payable(grad)).router());
            TaxToken(payable(token))
                .initialize(
                    a.name,
                    a.symbol,
                    a.totalSupply,
                    address(this),
                    recipient,
                    treasury,
                    v2Router,
                    creatorFeeBps,
                    grad,
                    a.modes.cashbackBps,
                    a.modes.buybackBps,
                    a.modes.maxRewardShare
                );
        }

        curves[token] = CurveState({
            quote: a.quote,
            x0: x0,
            y0: y0,
            curveSupply: a.curveSupply,
            lpSupply: lpSupply,
            raiseTarget: a.raiseTarget,
            vQuote: x0,
            vToken: y0,
            realTokenReserves: a.curveSupply,
            quoteCollected: 0,
            creator: msg.sender,
            feeRecipient: recipient,
            graduationTarget: a.target,
            creatorFeeBps: creatorFeeBps,
            graduated: false,
            exists: true,
            cashbackBps: a.modes.cashbackBps,
            buybackBps: a.modes.buybackBps,
            curveBuybackDest: a.modes.curveBuybackDest,
            buybackAccrued: 0,
            cashbackAccrued: 0
        });

        emit Launched(token, msg.sender, a.quote, recipient, a.name, a.symbol, a.metadataURI, x0, y0, a.totalSupply);
    }

    // =====================================================================================
    // Trade — buy
    // =====================================================================================

    /// @notice Buy tokens off the curve with the quote asset. 1% fee taken off the quote input.
    /// @param token the launched token
    /// @param quoteInTotal total quote the buyer is spending (fee inclusive)
    /// @param minTokensOut slippage floor — revert if fewer tokens would be received
    /// @param deadline unix ts after which the trade reverts (stale-mempool protection)
    /// @return tokensOut tokens delivered to the buyer
    function buy(address token, uint256 quoteInTotal, uint256 minTokensOut, uint256 deadline)
        external
        payable
        nonReentrant
        returns (uint256 tokensOut)
    {
        if (block.timestamp > deadline) revert DeadlinePassed();
        if (quoteInTotal == 0) revert ZeroAmount();

        CurveState storage c = curves[token];
        if (!c.exists) revert UnknownToken();
        if (c.graduated) revert AlreadyGraduated();

        // CHECKS: pull the quote in first so FoT tokens are measured exactly, then run the shared buy body.
        // capExempt = false: a public curve buy is subject to the 2% fair-launch wallet cap.
        uint256 received = c.quote.pullIn(msg.sender, quoteInTotal, msg.value);
        tokensOut = _executeBuy(token, c, msg.sender, received, minTokensOut, false);
    }

    /// @dev Shared buy body used by both `buy` and `launchAndBuy` — the quote has ALREADY been pulled in
    ///      (so `received` is exact, FoT-measured). Splits both fee legs, quotes the curve on the net amount,
    ///      caps at remaining curve supply (with a fee-recompute + refund), enforces slippage, advances curve
    ///      state (CEI), pays fees + tokens + refund, and auto-graduates if the curve just sold out. Returns
    ///      the tokens delivered to `buyer`. Callers hold `nonReentrant` and have validated exists/!graduated.
    function _executeBuy(
        address token,
        CurveState storage c,
        address buyer,
        uint256 received,
        uint256 minTokensOut,
        bool capExempt
    )
        internal
        returns (uint256 tokensOut)
    {
        // Per-launch fee rates: platform is a fixed 1%; the creator leg is the launch-chosen rate for a V2
        // launch (== the post-graduation transfer tax) or the flat 1% default for V4. totalFeeBps = both.
        uint256 creatorFeeBps = _creatorFeeBpsOf(c);
        uint256 totalFeeBps = PLATFORM_FEE_BPS + creatorFeeBps;

        // Both legs round DOWN (in the pool's favor); the curve sees `received - platformFee - creatorFee`.
        uint256 platformFee = (received * PLATFORM_FEE_BPS) / BPS;
        uint256 creatorFee = (received * creatorFeeBps) / BPS;
        uint256 netIn = received - platformFee - creatorFee;
        if (netIn == 0) revert ZeroAmount();

        // quote the curve on the NET amount (post-fee), rounding tokens-out down.
        tokensOut = CurveMath.buyTokensForQuote(c.vQuote, c.vToken, netIn);

        // cap at the remaining real curve supply; refund the unused quote portion.
        uint256 refund;
        if (tokensOut > c.realTokenReserves) {
            // recompute the exact net quote needed for exactly realTokenReserves tokens (round up).
            uint256 cappedTokens = c.realTokenReserves;
            uint256 netNeeded = CurveMath.quoteInForExactTokens(c.vQuote, c.vToken, cappedTokens);
            // gross input s.t. gross - totalFee >= netNeeded, where totalFee = gross·totalFeeBps/BPS.
            // grossUsed = ceil(netNeeded·BPS / (BPS - totalFeeBps)).
            uint256 grossUsed = (netNeeded * BPS + (BPS - totalFeeBps) - 1) / (BPS - totalFeeBps);
            if (grossUsed > received) grossUsed = received;
            refund = received - grossUsed;
            // Split the total fee on the USED portion: platform floor, creator takes the remainder
            // (incl. any rounding dust) so platformFee + creatorFee == grossUsed - netNeeded exactly.
            uint256 totalFee = grossUsed - netNeeded;
            platformFee = (grossUsed * PLATFORM_FEE_BPS) / BPS;
            if (platformFee > totalFee) platformFee = totalFee; // defensive: never exceed the total
            creatorFee = totalFee - platformFee;
            netIn = netNeeded;
            tokensOut = cappedTokens;
        }

        if (tokensOut < minTokensOut) revert SlippageExceeded();
        if (tokensOut == 0) revert ZeroAmount();

        // FAIR-LAUNCH CURVE CAP: during the token's launch window, a third-party curve buy may not push the
        // buyer's RESULTING balance past 2% of total supply — the same guarantee TaxToken enforces on the v2
        // pool, anchored to the SAME launch block (it stamps `launchBlock` at init, in this launch tx), so the
        // cap is ONE continuous window across curve + pool. After the window it short-circuits (a lone buyer
        // can then bond solo). `capExempt` is the creator's atomic dev buy (they seed their own launch
        // position) — the only party allowed over the line, always. balanceOf(buyer) is the buyer's REAL
        // balance (curve tokens are transferred to the wallet, not escrowed), so this is the exact
        // resulting-holding check. The window/launchBlock live on the TaxToken (V2 path — the only live path);
        // the legacy V4 clean-Token has no such field, so the cap is a V2-token concept only.
        if (!capExempt && c.graduationTarget == GradTarget.V2) {
            if (block.number < TaxToken(payable(token)).launchBlock() + MAX_WALLET_WINDOW) {
                uint256 cap = (Token(token).totalSupply() * MAX_WALLET_BPS) / BPS;
                if (Token(token).balanceOf(buyer) + tokensOut > cap) revert MaxWalletExceeded();
            }
        }

        // EFFECTS: advance curve state before any outward transfer (CEI).
        c.vQuote += netIn;
        c.vToken -= tokensOut;
        c.realTokenReserves -= tokensOut;
        c.quoteCollected += netIn;

        // INTERACTIONS: auto-push both fee legs to their recipients (pull-fallback if a recipient
        // reverts), then pay out tokens + any refund. Fees are AUTO now — no manual claim in the curve.
        _payPlatformFee(c, platformFee);
        _routeCurveCreatorFee(c, token, creatorFee);
        token.safeTransfer(buyer, tokensOut);
        if (refund != 0) c.quote.pushOut(buyer, refund);

        emit Bought(token, buyer, received, platformFee, creatorFee, tokensOut, c.vToken);

        // graduation is a clean state flip on realTokenReserves == 0, atomic with this buy.
        if (c.realTokenReserves == 0) _graduate(token, c);
    }

    // =====================================================================================
    // Launch + atomic dev buy (unsnipeable first buy)
    // =====================================================================================

    /// @notice Launch a token AND execute the creator's ("dev") first buy in the SAME transaction, so the
    ///         curve never exists in a tradeable state before the dev's buy lands — the buy is provably first
    ///         and cannot be sniped/front-run (there is no block or mempool moment where another buyer can
    ///         act before it). The dev buy funds the fresh curve exactly like any buy: it pays the fixed 1%
    ///         platform fee + the creator fee, quotes the curve, and (if huge) can even fill+graduate in one
    ///         tx. This is the same atomicity guarantee we already rely on for graduation-inside-buy.
    ///
    /// @dev    NATIVE-QUOTE ONLY for the dev buy: `msg.value` is the dev's gross spend (fee-inclusive). For an
    ///         ERC-20-quoted curve, dev-buy atomically here isn't supported (no allowance flow inside launch)
    ///         — those launch with `launch`/`launchTo` and the dev buys separately. `devBuyQuote` must equal
    ///         `msg.value` for a native quote (guards a client mistake). Pass `devBuyQuote == 0` to just launch.
    /// @param minTokensOut slippage floor on the dev's received tokens (revert if fewer) — protects the dev's
    ///        own launch tx from executing at a surprise price if params are stale.
    /// @param deadline unix ts after which the whole launch+buy reverts (stale-mempool protection).
    /// @return token the freshly deployed token address
    /// @return devTokensOut tokens delivered to the dev from the first buy (0 if `devBuyQuote == 0`)
    function launchAndBuy(
        string calldata name,
        string calldata symbol,
        address feeRecipient,
        string calldata metadataURI,
        address quote,
        uint256 m0,
        uint256 raiseTarget,
        uint256 curveSupply,
        uint256 totalSupply,
        bytes32 salt,
        uint256 devBuyQuote,
        uint256 minTokensOut,
        uint256 deadline
    ) external payable nonReentrant returns (address token, uint256 devTokensOut) {
        return _launchAndBuy(
            LaunchArgs({
                name: name,
                symbol: symbol,
                feeRecipient: feeRecipient,
                metadataURI: metadataURI,
                quote: quote,
                m0: m0,
                raiseTarget: raiseTarget,
                curveSupply: curveSupply,
                totalSupply: totalSupply,
                salt: salt,
                target: GradTarget.V4,
                creatorFeeBps: 0,
                modes: ModeArgs({cashbackBps: 0, buybackBps: 0, maxRewardShare: 0, curveBuybackDest: 0})
            }),
            devBuyQuote,
            minTokensOut,
            deadline
        );
    }

    /// @notice `launchTo` (graduation target + v2 modes) with an atomic dev first buy. See `launchAndBuy`.
    function launchToAndBuy(
        string calldata name,
        string calldata symbol,
        address feeRecipient,
        string calldata metadataURI,
        address quote,
        uint256 m0,
        uint256 raiseTarget,
        uint256 curveSupply,
        uint256 totalSupply,
        bytes32 salt,
        GradTarget target,
        uint256 creatorFeeBps,
        ModeArgs calldata modes,
        uint256 devBuyQuote,
        uint256 minTokensOut,
        uint256 deadline
    ) external payable nonReentrant returns (address token, uint256 devTokensOut) {
        return _launchAndBuy(
            LaunchArgs({
                name: name,
                symbol: symbol,
                feeRecipient: feeRecipient,
                metadataURI: metadataURI,
                quote: quote,
                m0: m0,
                raiseTarget: raiseTarget,
                curveSupply: curveSupply,
                totalSupply: totalSupply,
                salt: salt,
                target: target,
                creatorFeeBps: creatorFeeBps,
                modes: modes
            }),
            devBuyQuote,
            minTokensOut,
            deadline
        );
    }

    /// @dev Shared launch+dev-buy body. Launches, then (if `devBuyQuote > 0`) runs the dev's first buy against
    ///      the fresh curve via `_executeBuy` — all inside this one `nonReentrant` transaction.
    function _launchAndBuy(LaunchArgs memory a, uint256 devBuyQuote, uint256 minTokensOut, uint256 deadline)
        internal
        returns (address token, uint256 devTokensOut)
    {
        if (block.timestamp > deadline) revert DeadlinePassed();

        token = _launch(a);

        if (devBuyQuote == 0) {
            // No dev buy — behave exactly like a plain launch. Reject stray ETH so funds can't be stranded.
            if (msg.value != 0) revert UnexpectedValue();
            return (token, 0);
        }

        // Dev buy is NATIVE-only (no ERC-20 allowance flow inside launch). Enforce a native quote + that the
        // sent value matches the declared dev-buy amount.
        CurveState storage c = curves[token];
        if (!c.quote.isNative()) revert NonNativeDevBuy();
        // pullIn(native) requires msg.value == amount; this also rejects a mismatched declared amount.
        uint256 received = c.quote.pullIn(msg.sender, devBuyQuote, msg.value);
        // capExempt = true: the creator's atomic dev buy seeds their own position at launch, so it's the one
        // buy allowed past the 2% fair-launch wallet cap. Every subsequent public buy() IS capped.
        devTokensOut = _executeBuy(token, c, msg.sender, received, minTokensOut, true);
    }

    // =====================================================================================
    // Trade — sell
    // =====================================================================================

    /// @notice Sell tokens back to the curve for the quote asset. 1% fee taken off the quote out.
    /// @param token the launched token
    /// @param tokensIn tokens to sell (must be approved to the launchpad)
    /// @param minQuoteOut slippage floor on the NET quote returned (after fee)
    /// @param deadline unix ts after which the trade reverts
    /// @return quoteOut net quote delivered to the seller (after fee)
    function sell(address token, uint256 tokensIn, uint256 minQuoteOut, uint256 deadline)
        external
        nonReentrant
        returns (uint256 quoteOut)
    {
        if (block.timestamp > deadline) revert DeadlinePassed();
        if (tokensIn == 0) revert ZeroAmount();

        CurveState storage c = curves[token];
        if (!c.exists) revert UnknownToken();
        if (c.graduated) revert AlreadyGraduated(); // hard state flip: no curve sells post-graduation

        // gross quote out from the curve (rounded down), then both fee legs off the top. Platform 1% fixed;
        // creator = the launch-chosen rate (== the post-grad transfer tax) for V2, else the flat 1% default.
        uint256 grossOut = CurveMath.sellQuoteForTokens(c.vQuote, c.vToken, tokensIn);
        uint256 platformFee = (grossOut * PLATFORM_FEE_BPS) / BPS;
        uint256 creatorFee = (grossOut * _creatorFeeBpsOf(c)) / BPS;
        quoteOut = grossOut - platformFee - creatorFee;
        if (quoteOut < minQuoteOut) revert SlippageExceeded();
        if (grossOut == 0) revert ZeroAmount();

        // EFFECTS (CEI): pull tokens in + update curve before sending quote out.
        token.safeTransferFrom(msg.sender, address(this), tokensIn);
        c.vQuote -= grossOut;
        c.vToken += tokensIn;
        c.realTokenReserves += tokensIn;
        c.quoteCollected -= grossOut;

        // INTERACTIONS: pay the seller, then auto-push both fee legs (pull-fallback on failure).
        c.quote.pushOut(msg.sender, quoteOut);
        _payPlatformFee(c, platformFee);
        _routeCurveCreatorFee(c, token, creatorFee);

        emit Sold(token, msg.sender, tokensIn, quoteOut, platformFee, creatorFee, c.vToken);
    }

    // =====================================================================================
    // Graduation
    // =====================================================================================

    /// @notice Permissionless graduation trigger (also fired automatically inside `buy`). Only
    ///         valid once the curve's real token reserve is exhausted.
    function graduate(address token) external nonReentrant {
        CurveState storage c = curves[token];
        if (!c.exists) revert UnknownToken();
        if (c.graduated) revert AlreadyGraduated();
        if (c.realTokenReserves != 0) revert NotGraduatable();
        _graduate(token, c);
    }

    /// @dev Internal state flip + atomic migration. Flips `graduated` FIRST (CEI: blocks any
    ///      re-entrant curve trade), then transfers the reserved LP token allocation + the collected
    ///      quote to the configured graduator and invokes it in the SAME transaction as the
    ///      triggering buy — no front-runnable keeper gap (checklist §2). The graduator seeds the v4
    ///      pool at the continuity price and permanently locks the liquidity. Funds are zeroed out
    ///      of curve accounting before the external call.
    function _graduate(address token, CurveState storage c) internal {
        // Route to the graduator for THIS launch's chosen target (set at launch; the target's graduator was
        // required to be registered then, but re-check defensively).
        address grad = graduatorOf[c.graduationTarget];
        if (grad == address(0)) revert GraduatorNotSet();

        c.graduated = true;

        uint256 lpTokens = c.lpSupply;
        uint256 quoteForLp = c.quoteCollected;
        address quote = c.quote;
        // Hand the graduator the FEE RECIPIENT (not the launcher) so any post-graduation creator-fee share
        // routes to the same wallet as the pre-graduation creator fee.
        address creator = c.feeRecipient;

        // CONTINUITY-SEED SAFETY: `lpSupply` = R/P_grad (continuity math) is NOT guaranteed to be ≤ the
        // physical residual (`totalSupply - curveSupply`) the launchpad actually holds after the curve fully
        // sells. For a steep run (e.g. Pump's 1:3 start:raise at 80% curve supply) the ideal LP token count
        // (~213M) EXCEEDS the residual (200M) — so a naive `safeTransfer(grad, lpSupply)` would revert and
        // BRICK graduation. Fix: cap the LP tokens to what we hold, and scale the seeded quote DOWN by the
        // same factor so the pool still opens at the exact continuity PRICE (price = quote/token is
        // preserved; only the depth shrinks). Any un-seeded quote remainder is refunded to the launchpad's
        // protocol-fee bucket rather than stranded.
        // launchpad's token holdings == residual after full sell (both token types expose balanceOf).
        uint256 residual = Token(token).balanceOf(address(this));
        uint256 quoteRemainder;
        if (lpTokens > residual && residual > 0) {
            // scale quote to hold price: newQuote = quoteForLp * residual / lpTokens (floor, pool-favorable).
            uint256 scaledQuote = FixedPointMathLib.fullMulDiv(quoteForLp, residual, lpTokens);
            quoteRemainder = quoteForLp - scaledQuote;
            quoteForLp = scaledQuote;
            lpTokens = residual;
        }

        // EFFECTS: zero the migrated quote out of curve accounting before transferring it. Any un-seedable
        // quote remainder is credited to the protocol-fee bucket (withdrawable by treasury) — never stranded.
        c.quoteCollected = 0;
        if (quoteRemainder != 0) protocolFees[quote] += quoteRemainder;

        emit Graduated(token, lpTokens, quoteForLp);

        // INTERACTIONS: fund the graduator with the LP tokens, then trigger atomic migration.
        // Native quote is forwarded as msg.value on the call; ERC-20 quote is transferred first.
        //
        // SECURITY NOTE (poisoned-pair grief): the v2 graduator requires the TOKEN/WETH pair be EMPTY before
        // seeding (UniV2Graduator's PairAlreadySeeded guard) — this fully PREVENTS the LP-capture/raise-theft
        // an attacker could otherwise cause by pre-seeding the pair, and a mere empty pre-create is harmless.
        // The residual is a COSTLY grief: an attacker who buys tokens on the curve (paying tax+fees) and then
        // pre-seeds the pair can make this call revert → the triggering buy reverts. That is bounded (not
        // free, attacker locks their own capital in the poisoned pool) and recoverable: the launch stays on
        // the curve, and treasury can `forceGraduateTo` a re-registered graduator variant if ever needed.
        token.safeTransfer(grad, lpTokens);
        if (quote.isNative()) {
            IGraduator(grad).graduate{value: quoteForLp}(token, quote, lpTokens, quoteForLp, creator);
        } else {
            quote.pushOut(grad, quoteForLp);
            IGraduator(grad).graduate(token, quote, lpTokens, quoteForLp, creator);
        }

        // TOKEN MODES handoff: the graduator has now created + set the pair, so the token can accept the
        // curve-phase mode ETH. Cash-back accrual seeds the reward index; buyback accrual funds the first
        // real buyback-and-burn. Both are only set for a V2 launch with modes + native (ETH) quote — which
        // is the only configuration that reaches here for tax tokens. Zero the buckets first (CEI).
        uint256 cashbackEth = c.cashbackAccrued;
        uint256 buybackEth = c.buybackAccrued;
        if (cashbackEth != 0 || buybackEth != 0) {
            c.cashbackAccrued = 0;
            c.buybackAccrued = 0;
            if (cashbackEth != 0) TaxToken(payable(token)).seedRewards{value: cashbackEth}();
            if (buybackEth != 0) TaxToken(payable(token)).seedBuyback{value: buybackEth}();
        }
    }

    // =====================================================================================
    // Treasury
    // =====================================================================================

    /// @notice Register the graduation adapter for a target (V4/V2), exactly once per target (treasury
    ///         only). See `graduatorOf`. A launch may only pick a target whose slot is set.
    function setGraduator(GradTarget target, address graduator_) external {
        if (msg.sender != treasury) revert NotTreasury();
        if (graduator_ == address(0)) revert ZeroAddress();
        if (graduatorOf[target] != address(0)) revert GraduatorAlreadySet();
        graduatorOf[target] = graduator_;
        emit GraduatorSet(target, graduator_);
    }

    /// @notice Withdraw accrued protocol fees for a given quote asset to an address.
    function withdrawProtocolFees(address quote, address to) external nonReentrant {
        if (msg.sender != treasury) revert NotTreasury();
        uint256 amount = protocolFees[quote];
        protocolFees[quote] = 0;
        quote.pushOut(to, amount);
        emit ProtocolFeesWithdrawn(quote, to, amount);
    }

    /// @notice Claim accrued creator fees for a given quote asset. Pull-based (the creator initiates),
    ///         CEI-safe (`nonReentrant` + zero-before-transfer). The caller (`msg.sender`) is the
    ///         creator whose balance is paid out; `to` is the recipient (lets a creator route fees to a
    ///         different wallet). Reverts below `MIN_CLAIM` to bound dust-griefing.
    /// @param quote the quote asset to claim (the asset the creator's curves were denominated in)
    /// @param to    recipient of the claimed fees
    /// @return amount the amount paid out
    function claim(address quote, address to) external nonReentrant returns (uint256 amount) {
        if (to == address(0)) revert ZeroAddress();
        amount = creatorAccrued[msg.sender][quote];
        if (amount < MIN_CLAIM) revert NothingToClaim();
        creatorAccrued[msg.sender][quote] = 0; // EFFECTS before INTERACTIONS (CEI)
        quote.pushOut(to, amount);
        emit CreatorFeesClaimed(msg.sender, quote, to, amount);
    }

    // =====================================================================================
    // Views
    // =====================================================================================

    /// @notice Preview a DEV BUY at launch time — BEFORE the token exists — from the raw launch params. Lets
    ///         the UI show "you'll receive ~N tokens (X% of supply) for this ETH" while filling the form. Uses
    ///         the exact same virtual-reserve derivation + fee math the launch+buy will use, so it matches the
    ///         on-chain result (modulo the graduating-cap refund, which only bites if the dev buys the whole
    ///         curve). `creatorFeeBps` = the launch's creator leg (the chosen v2 rate, or pass 100 for v4/1%).
    /// @return tokensOut   tokens the dev would receive (capped at the curve supply)
    /// @return platformFee the 1% platform fee on the input
    /// @return creatorFee  the creator fee on the input
    function previewDevBuy(
        uint256 m0,
        uint256 raiseTarget,
        uint256 curveSupply,
        uint256 totalSupply,
        uint256 creatorFeeBps,
        uint256 devBuyQuote
    ) external pure returns (uint256 tokensOut, uint256 platformFee, uint256 creatorFee) {
        if (devBuyQuote == 0) return (0, 0, 0);
        (uint256 x0, uint256 y0) = CurveParams.virtualReserves(m0, raiseTarget, curveSupply, totalSupply);
        platformFee = (devBuyQuote * PLATFORM_FEE_BPS) / BPS;
        creatorFee = (devBuyQuote * creatorFeeBps) / BPS;
        uint256 netIn = devBuyQuote - platformFee - creatorFee;
        tokensOut = CurveMath.buyTokensForQuote(x0, y0, netIn);
        if (tokensOut > curveSupply) tokensOut = curveSupply; // capped at the curve's real supply
    }

    /// @notice Quote a buy (tokens out + both fee legs for a gross quote input) without trading.
    /// @dev    Reports the headline fees on the full input and caps `tokensOut` at the remaining curve
    ///         supply. It does NOT replicate `buy`'s cap-and-refund fee recompute, so on a graduating
    ///         (capped) buy the returned fees are an upper bound — the executed trade charges fees only
    ///         on the used portion and refunds the rest. Use for display; the on-chain trade is exact.
    /// @return tokensOut   tokens the buyer would receive (capped at remaining curve supply)
    /// @return platformFee the platform (protocol) fee on the full input
    /// @return creatorFee  the creator fee on the full input
    function previewBuy(address token, uint256 quoteInTotal)
        external
        view
        returns (uint256 tokensOut, uint256 platformFee, uint256 creatorFee)
    {
        CurveState storage c = curves[token];
        if (!c.exists) revert UnknownToken();
        platformFee = (quoteInTotal * PLATFORM_FEE_BPS) / BPS;
        creatorFee = (quoteInTotal * _creatorFeeBpsOf(c)) / BPS;
        uint256 netIn = quoteInTotal - platformFee - creatorFee;
        tokensOut = CurveMath.buyTokensForQuote(c.vQuote, c.vToken, netIn);
        if (tokensOut > c.realTokenReserves) tokensOut = c.realTokenReserves;
    }

    /// @notice Quote a sell (net quote out + both fee legs for a token input) without trading.
    /// @dev    EXACT mirror of `sell`'s math: gross quote from the curve (rounded down, against the
    ///         seller), then both fee legs off the top. Unlike `previewBuy` there is no cap/refund
    ///         branch on a sell, so this preview is exact — `netQuoteOut` equals what `sell` would
    ///         return for the same `tokensIn` at the current reserves. Use it to derive a slippage
    ///         floor (`minQuoteOut`) for the off-chain execute path. Reverts if the token is unknown;
    ///         returns zeros for a graduated curve (sells revert there — see `sell`).
    /// @return netQuoteOut net quote the seller would receive (after both fees)
    /// @return platformFee the platform (protocol) fee on the gross quote out
    /// @return creatorFee  the creator fee on the gross quote out
    function previewSell(address token, uint256 tokensIn)
        external
        view
        returns (uint256 netQuoteOut, uint256 platformFee, uint256 creatorFee)
    {
        CurveState storage c = curves[token];
        if (!c.exists) revert UnknownToken();
        if (c.graduated) return (0, 0, 0); // no curve sells post-graduation; surface zero, don't revert
        uint256 grossOut = CurveMath.sellQuoteForTokens(c.vQuote, c.vToken, tokensIn);
        platformFee = (grossOut * PLATFORM_FEE_BPS) / BPS;
        creatorFee = (grossOut * _creatorFeeBpsOf(c)) / BPS;
        netQuoteOut = grossOut - platformFee - creatorFee;
    }

    /// @notice Pre-compute the deterministic token address for a creator + salt.
    function predictTokenAddress(address creator, bytes32 salt) external view returns (address) {
        bytes32 cloneSalt = keccak256(abi.encodePacked(creator, salt));
        return LibClone.predictDeterministicAddress(tokenImplementation, cloneSalt, address(this));
    }

    /// @notice Current spot price (quote per token, ×1e18) for display.
    function spotPriceX18(address token) external view returns (uint256) {
        CurveState storage c = curves[token];
        if (!c.exists) revert UnknownToken();
        return CurveMath.spotPriceX18(c.vQuote, c.vToken);
    }
}
Chain explorer1953msChain node92ms