HoodCustomLaunchpad

0x8c529f0a77c07ce0e6796f153d292501ee6f66f6

Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
23,208 bytes
ABI entries
125
53 read · 26 write
License
none

Contract information

Address
0x8c529f0a77c07ce0e6796f153d292501ee6f66f6
Chain
Robinhood Chain (4663)
Compiler
v0.8.28+commit.7893614a
Optimization
Enabled
Creation tx
0x84bf91a3f8…aca4de9edd

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (53)

ANTI_SNIPE_MAX_WALLET_BPS()uint256
ANTI_SNIPE_WINDOW()uint256
CTO_TIMELOCK()uint256
CURVE_BPS()uint256
DEFAULT_TOTAL_SUPPLY()uint256
DEFAULT_VIRTUAL_TOKEN_SEED()uint256
MAX_MIGRATION_FEE_BPS()uint256
MAX_PROTOCOL_MIGRATION_FEE()uint256
MAX_TOTAL_MIGRATION_FEE_BPS()uint256
MAX_TOTAL_SUPPLY()uint256
MAX_TRADE_FEE_BPS()uint256
MAX_WALLET_BPS()uint256
MIGRATOR_TIMELOCK()uint256
MIN_TOTAL_SUPPLY()uint256
MIN_TRADE_FEE_BPS()uint256
allTokens(uint256)address
antiSnipeEnabled(address)bool
config()uint128, uint64, uint16, uint64, uint16, uint16, uint16, uint16, bool
creatorFees(address)uint256
ctoPending(address)address
ctoUnlockAt(address)uint256
currentPrice(address)uint256
curves(address)uint128, uint128, uint128, uint128, address, uint48, bool, bool, uint16
getCurve(address)tuple
guardBought(address, address)uint256
isHoodToken(address)bool
launchTime(address)uint48
maxWalletEnabled(address)bool
migrationTerms(address)uint64, uint16, uint128, bool
migrator()address
migratorUnlockAt()uint256
owner()address
pendingCreatorClaim(address)uint256
pendingMigrator()address
pendingOwner()address
platformFees(address)uint256
platforms(address)bool, uint16
predictTokenAddress(address, bytes32, string, string, uint256)address
progressBps(address)uint256
protocolFeesAccrued()uint256
protocolMigrationFee()uint256
quoteBuy(address, uint256)uint256, uint256, uint256
quoteSell(address, uint256)uint256, uint256
tokenCount()uint256
tokenCurveSupply(address)uint256
tokenInitCodeHash(string, string, uint256)bytes32
tokenLpSupply(address)uint256
tokenPlatform(address)address
tokens(uint256, uint256)address[]
totalSupplyOf(address)uint256
uniswapFactory()address
walletBought(address, address)uint256
weth()address

Events (21)

ConfigUpdatedCreatorFeesClaimedCreatorTransferredCtoCancelledCtoExecutedCtoProposedFeesWithdrawnGraduatedMigratedMigratorChangeCancelledMigratorProposedMigratorSetOwnershipTransferStartedOwnershipTransferredPendingCreatorFeesClaimedPlatformFeesClaimedPlatformLaunchPlatformSetProtocolMigrationFeeSetTokenCreatedTrade

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "uniswapFactory_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "weth_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "owner_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AlreadyGraduated",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "AlreadyMigrated",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "BadCtoRecipient",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "BadSupply",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "BadVanitySalt",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "CtoNotReady",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EthTransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FeeTooHigh",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientPayment",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "MaxWalletExceeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "MigratorAlreadySet",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "MigratorNotReady",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotCreator",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotGraduated",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotPlatform",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NothingToWithdraw",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SlippageExceeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SnipeGuardExceeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "UnknownToken",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAmount",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "components": [
          {
            "internalType": "uint128",
            "name": "virtualEthSeed",
            "type": "uint128"
          },
          {
            "internalType": "uint64",
            "name": "creationFee",
            "type": "uint64"
          },
          {
            "internalType": "uint16",
            "name": "defaultTradeFeeBps",
            "type": "uint16"
          },
          {
            "internalType": "uint64",
            "name": "migrationFee",
            "type": "uint64"
          },
          {
            "internalType": "uint16",
            "name": "migrationFeeBps",
            "type": "uint16"
          },
          {
            "internalType": "uint16",
            "name": "guardBlocks",
            "type": "uint16"
          },
          {
            "internalType": "uint16",
            "name": "guardMaxWalletBps",
            "type": "uint16"
          },
          {
            "internalType": "uint16",
            "name": "creatorFeeShareBps",
            "type": "uint16"
          },
          {
            "internalType": "bool",
            "name": "vanityEnforced",
            "type": "bool"
          }
        ],
        "indexed": false,
        "internalType": "struct HoodCustomLaunchpad.Config",
        "name": "config",
        "type": "tuple"
      }
    ],
    "name": "ConfigUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "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": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      }
    ],
    "name": "CreatorTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "CtoCancelled",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newCreator",
        "type": "address"
      }
    ],
    "name": "CtoExecuted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newCreator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "unlockAt",
        "type": "uint256"
      }
    ],
    "name": "CtoProposed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "FeesWithdrawn",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "raisedEth",
        "type": "uint256"
      }
    ],
    "name": "Graduated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "pair",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethLiquidity",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenLiquidity",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "lpBurned",
        "type": "uint256"
      }
    ],
    "name": "Migrated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "migrator",
        "type": "address"
      }
    ],
    "name": "MigratorChangeCancelled",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "migrator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "unlockAt",
        "type": "uint256"
      }
    ],
    "name": "MigratorProposed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "migrator",
        "type": "address"
      }
    ],
    "name": "MigratorSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferStarted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "PendingCreatorFeesClaimed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "platform",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "PlatformFeesClaimed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "platform",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      }
    ],
    "name": "PlatformLaunch",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "platform",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "approved",
        "type": "bool"
      },
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "feeShareBps",
        "type": "uint16"
      }
    ],
    "name": "PlatformSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      }
    ],
    "name": "ProtocolMigrationFeeSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "virtualEth",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "virtualTokens",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "curveSupply",
        "type": "uint256"
      }
    ],
    "name": "TokenCreated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "trader",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "isBuy",
        "type": "bool"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "virtualEthAfter",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "virtualTokensAfter",
        "type": "uint256"
      }
    ],
    "name": "Trade",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "ANTI_SNIPE_MAX_WALLET_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "ANTI_SNIPE_WINDOW",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "CTO_TIMELOCK",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "CURVE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "DEFAULT_TOTAL_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "DEFAULT_VIRTUAL_TOKEN_SEED",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_MIGRATION_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_PROTOCOL_MIGRATION_FEE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_TOTAL_MIGRATION_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_TOTAL_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_TRADE_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": "MIGRATOR_TIMELOCK",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_TOTAL_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_TRADE_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "allTokens",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "antiSnipeEnabled",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      }
    ],
    "name": "buy",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      }
    ],
    "name": "buyFor",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "cancelCto",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "cancelMigrator",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "claimCreatorFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "claimPendingCreatorFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "claimPlatformFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "config",
    "outputs": [
      {
        "internalType": "uint128",
        "name": "virtualEthSeed",
        "type": "uint128"
      },
      {
        "internalType": "uint64",
        "name": "creationFee",
        "type": "uint64"
      },
      {
        "internalType": "uint16",
        "name": "defaultTradeFeeBps",
        "type": "uint16"
      },
      {
        "internalType": "uint64",
        "name": "migrationFee",
        "type": "uint64"
      },
      {
        "internalType": "uint16",
        "name": "migrationFeeBps",
        "type": "uint16"
      },
      {
        "internalType": "uint16",
        "name": "guardBlocks",
        "type": "uint16"
      },
      {
        "internalType": "uint16",
        "name": "guardMaxWalletBps",
        "type": "uint16"
      },
      {
        "internalType": "uint16",
        "name": "creatorFeeShareBps",
        "type": "uint16"
      },
      {
        "internalType": "bool",
        "name": "vanityEnforced",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      },
      {
        "internalType": "uint16",
        "name": "tradeFeeBps",
        "type": "uint16"
      },
      {
        "internalType": "uint256",
        "name": "totalSupply_",
        "type": "uint256"
      }
    ],
    "name": "createToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      },
      {
        "internalType": "uint16",
        "name": "tradeFeeBps",
        "type": "uint16"
      },
      {
        "internalType": "uint256",
        "name": "totalSupply_",
        "type": "uint256"
      }
    ],
    "name": "createTokenFor",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      },
      {
        "internalType": "uint16",
        "name": "tradeFeeBps",
        "type": "uint16"
      },
      {
        "internalType": "uint256",
        "name": "totalSupply_",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "antiSnipe",
        "type": "bool"
      },
      {
        "internalType": "bool",
        "name": "maxWallet",
        "type": "bool"
      }
    ],
    "name": "createTokenForGuarded",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      },
      {
        "internalType": "uint16",
        "name": "tradeFeeBps",
        "type": "uint16"
      },
      {
        "internalType": "uint256",
        "name": "totalSupply_",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "antiSnipe",
        "type": "bool"
      },
      {
        "internalType": "bool",
        "name": "maxWallet",
        "type": "bool"
      }
    ],
    "name": "createTokenGuarded",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "creatorFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "ctoPending",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "ctoUnlockAt",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "currentPrice",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "curves",
    "outputs": [
      {
        "internalType": "uint128",
        "name": "virtualEth",
        "type": "uint128"
      },
      {
        "internalType": "uint128",
        "name": "virtualTokens",
        "type": "uint128"
      },
      {
        "internalType": "uint128",
        "name": "realEth",
        "type": "uint128"
      },
      {
        "internalType": "uint128",
        "name": "realTokens",
        "type": "uint128"
      },
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "uint48",
        "name": "createdAtBlock",
        "type": "uint48"
      },
      {
        "internalType": "bool",
        "name": "graduated",
        "type": "bool"
      },
      {
        "internalType": "bool",
        "name": "migrated",
        "type": "bool"
      },
      {
        "internalType": "uint16",
        "name": "tradeFeeBps",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "executeCto",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "executeMigrator",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "getCurve",
    "outputs": [
      {
        "components": [
          {
            "internalType": "uint128",
            "name": "virtualEth",
            "type": "uint128"
          },
          {
            "internalType": "uint128",
            "name": "virtualTokens",
            "type": "uint128"
          },
          {
            "internalType": "uint128",
            "name": "realEth",
            "type": "uint128"
          },
          {
            "internalType": "uint128",
            "name": "realTokens",
            "type": "uint128"
          },
          {
            "internalType": "address",
            "name": "creator",
            "type": "address"
          },
          {
            "internalType": "uint48",
            "name": "createdAtBlock",
            "type": "uint48"
          },
          {
            "internalType": "bool",
            "name": "graduated",
            "type": "bool"
          },
          {
            "internalType": "bool",
            "name": "migrated",
            "type": "bool"
          },
          {
            "internalType": "uint16",
            "name": "tradeFeeBps",
            "type": "uint16"
          }
        ],
        "internalType": "struct HoodCustomLaunchpad.Curve",
        "name": "",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "wallet",
        "type": "address"
      }
    ],
    "name": "guardBought",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "isHoodToken",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "launchTime",
    "outputs": [
      {
        "internalType": "uint48",
        "name": "",
        "type": "uint48"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "maxWalletEnabled",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "migrate",
    "outputs": [
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "migrationTerms",
    "outputs": [
      {
        "internalType": "uint64",
        "name": "migrationFee",
        "type": "uint64"
      },
      {
        "internalType": "uint16",
        "name": "migrationFeeBps",
        "type": "uint16"
      },
      {
        "internalType": "uint128",
        "name": "protocolMigrationFee",
        "type": "uint128"
      },
      {
        "internalType": "bool",
        "name": "set",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "migrator",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "migratorUnlockAt",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      }
    ],
    "name": "pendingCreatorClaim",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingMigrator",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingOwner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "platform",
        "type": "address"
      }
    ],
    "name": "platformFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "platform",
        "type": "address"
      }
    ],
    "name": "platforms",
    "outputs": [
      {
        "internalType": "bool",
        "name": "approved",
        "type": "bool"
      },
      {
        "internalType": "uint16",
        "name": "feeShareBps",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "caller",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      },
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "totalSupply_",
        "type": "uint256"
      }
    ],
    "name": "predictTokenAddress",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "progressBps",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "newCreator",
        "type": "address"
      }
    ],
    "name": "proposeCto",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "migrator_",
        "type": "address"
      }
    ],
    "name": "proposeMigrator",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "protocolFeesAccrued",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "protocolMigrationFee",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "ethIn",
        "type": "uint256"
      }
    ],
    "name": "quoteBuy",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "ethUsed",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      }
    ],
    "name": "quoteSell",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "ethOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minEthOut",
        "type": "uint256"
      }
    ],
    "name": "sell",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "uint128",
            "name": "virtualEthSeed",
            "type": "uint128"
          },
          {
            "internalType": "uint64",
            "name": "creationFee",
            "type": "uint64"
          },
          {
            "internalType": "uint16",
            "name": "defaultTradeFeeBps",
            "type": "uint16"
          },
          {
            "internalType": "uint64",
            "name": "migrationFee",
            "type": "uint64"
          },
          {
            "internalType": "uint16",
            "name": "migrationFeeBps",
            "type": "uint16"
          },
          {
            "internalType": "uint16",
            "name": "guardBlocks",
            "type": "uint16"
          },
          {
            "internalType": "uint16",
            "name": "guardMaxWalletBps",
            "type": "uint16"
          },
          {
            "internalType": "uint16",
            "name": "creatorFeeShareBps",
            "type": "uint16"
          },
          {
            "internalType": "bool",
            "name": "vanityEnforced",
            "type": "bool"
          }
        ],
        "internalType": "struct HoodCustomLaunchpad.Config",
        "name": "newConfig",
        "type": "tuple"
      }
    ],
    "name": "setConfig",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "migrator_",
        "type": "address"
      }
    ],
    "name": "setMigrator",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "platform",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "approved",
        "type": "bool"
      },
      {
        "internalType": "uint16",
        "name": "feeShareBps",
        "type": "uint16"
      }
    ],
    "name": "setPlatform",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      }
    ],
    "name": "setProtocolMigrationFee",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokenCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "tokenCurveSupply",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "totalSupply_",
        "type": "uint256"
      }
    ],
    "name": "tokenInitCodeHash",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "tokenLpSupply",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "tokenPlatform",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "offset",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "limit",
        "type": "uint256"
      }
    ],
    "name": "tokens",
    "outputs": [
      {
        "internalType": "address[]",
        "name": "page",
        "type": "address[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "totalSupplyOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      }
    ],
    "name": "transferCreator",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "uniswapFactory",
    "outputs": [
      {
        "internalType": "contract IUniswapV2Factory",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "wallet",
        "type": "address"
      }
    ],
    "name": "walletBought",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "weth",
    "outputs": [
      {
        "internalType": "contract IWETH",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      }
    ],
    "name": "withdrawFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {HoodToken} from "./HoodToken.sol";
import {IUniswapV2Factory, IUniswapV2Pair, IWETH} from "./interfaces/IUniswapV2.sol";
import {IHoodMigrator} from "./interfaces/IV3Migration.sol";

/// @title HoodCustomLaunchpad
/// @notice hood.fun's public bonding-curve launchpad. A coin trades on an x*y=k
///         virtual-reserve curve, then graduates and MIGRATES into a locked
///         Uniswap v3 1% pool. Differs from the classic launchpad in:
///           1. Per-token custom TOTAL SUPPLY (1M … 1 quadrillion), chosen at
///              launch. Curve seeds scale with supply so the ETH economics stay
///              identical (same seed MC, same raise, same graduation MC) — only
///              the token count / price-per-token changes.
///           2. Fee split: 80% creator / 20% protocol-platform (vs 50/50).
///           3. No 0x600d vanity enforcement.
///         Anyone may createToken(); the createTokenFor() partner/bundle path
///         stays whitelisted (setPlatform) for Proxima + the hood.tools bundler.
///
/// @dev getCurve() returns the SAME 9-field tuple + same TokenCreated / Trade /
///      Graduated events as the classic launchpad, so the indexer/UI need only the
///      new address added. Per-token supply is kept in separate mappings so the
///      Curve tuple stays byte-identical.
contract HoodCustomLaunchpad is Ownable2Step, ReentrancyGuard {
    // ---------------------------------------------------------------- types

    struct Curve {
        uint128 virtualEth;
        uint128 virtualTokens;
        uint128 realEth;
        uint128 realTokens;
        address creator;
        uint48 createdAtBlock;
        bool graduated;
        bool migrated;
        uint16 tradeFeeBps;
    }

    struct Config {
        uint128 virtualEthSeed;
        uint64 creationFee;
        uint16 defaultTradeFeeBps;
        uint64 migrationFee;
        uint16 migrationFeeBps;
        uint16 guardBlocks;
        uint16 guardMaxWalletBps;
        uint16 creatorFeeShareBps;
        bool vanityEnforced;
    }

    // ------------------------------------------------------------ constants

    /// @dev The baseline the curve seeds are tuned for (same as the main
    ///      launchpad). Custom supplies scale the token-side seeds off these so
    ///      the ETH-denominated economics stay identical.
    uint256 public constant DEFAULT_TOTAL_SUPPLY = 1_000_000_000e18;
    uint256 public constant DEFAULT_VIRTUAL_TOKEN_SEED = 1_145_000_000e18;
    uint256 public constant CURVE_BPS = 8000; // 80% of supply sells on the curve, 20% → LP

    /// @dev Custom-supply bounds: 1 … 1 quadrillion whole tokens. The upper bound
    ///      keeps the scaled seeds inside uint128 and away from precision loss.
    uint256 public constant MIN_TOTAL_SUPPLY = 1e18; // 1 whole token
    uint256 public constant MAX_TOTAL_SUPPLY = 1_000_000_000_000_000e18;

    uint256 internal constant BPS = 10_000;
    uint256 public constant MIN_TRADE_FEE_BPS = 100;
    uint256 public constant MAX_TRADE_FEE_BPS = 500;
    uint256 public constant MAX_MIGRATION_FEE_BPS = 1000;
    address internal constant LP_BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD;
    uint256 internal constant VANITY_SUFFIX = 0x600d;

    // ---- per-coin anti-whale toggles (set at launch, immutable) ----------
    /// @dev Anti-snipe window: for this many seconds after launch, buys are
    ///      capped tighter (ANTI_SNIPE_MAX_WALLET_BPS). Own platform + creator
    ///      dev-buy are exempt so block-0 bundles still work.
    uint256 public constant ANTI_SNIPE_WINDOW = 120; // 2 minutes
    uint256 public constant ANTI_SNIPE_MAX_WALLET_BPS = 50; // 0.5% of curve supply
    /// @dev CURVE-PHASE per-wallet buy ceiling when the 2% toggle is on. NOTE:
    ///      this is only enforceable while the coin trades on the bonding curve —
    ///      after graduation the token is unlocked and trades on Uniswap, where no
    ///      per-wallet cap is possible (the pool itself holds >2%). It is an
    ///      anti-whale guard for the LAUNCH phase, not a lifetime cap.
    uint256 public constant MAX_WALLET_BPS = 500; // 5% of curve supply (drain cap; default-on, creator can disable)
    /// @dev CTO handoff public timelock, and a HARD cap on the protocol migration
    ///      fee so even a compromised owner can't crank it to gut a raise.
    uint256 public constant CTO_TIMELOCK = 7 days;
    uint256 public constant MAX_PROTOCOL_MIGRATION_FEE = 1 ether;

    // ---- V3 hardening ------------------------------------------------------
    /// @dev V3: migrator changes go through a PUBLIC timelock (no instant swap),
    ///      so a graduated coin can never be silently routed through a malicious
    ///      migrator. The FIRST migrator is bootstrapped once (migrator unset);
    ///      every change after that waits MIGRATOR_TIMELOCK behind a loud event,
    ///      giving holders time to exit if they distrust a proposed migrator.
    uint256 public constant MIGRATOR_TIMELOCK = 7 days;
    /// @dev V3: hard ceiling on the TOTAL migration take (flat + bps + protocol)
    ///      as a share of the raise. The take is CLAMPED to this — migration never
    ///      reverts on fees, and can never gut a graduation. 30% worst-case.
    uint256 public constant MAX_TOTAL_MIGRATION_FEE_BPS = 3000;

    // -------------------------------------------------------------- storage

    IUniswapV2Factory public immutable uniswapFactory;
    IWETH public immutable weth;

    Config public config;
    uint256 public protocolFeesAccrued;
    address public migrator;

    mapping(address token => Curve) public curves;
    mapping(address token => mapping(address wallet => uint256)) public guardBought;
    mapping(address token => uint256) public creatorFees;
    address[] public allTokens;

    /// @dev Per-token supply split — stored OUTSIDE the Curve struct so getCurve
    ///      stays tuple-compatible with the main launchpad's indexer.
    mapping(address token => uint256) public tokenCurveSupply;
    mapping(address token => uint256) public tokenLpSupply;

    struct Platform {
        bool approved;
        uint16 feeShareBps;
    }

    /// @dev Launch-time options threaded into `_create` so they take effect
    ///      BEFORE the atomic dev-buy (packed into one struct to stay under the
    ///      via-IR stack limit).
    struct LaunchOpts {
        bool antiSnipe;
        bool maxWallet;
        address platform;
    }

    mapping(address platform => Platform) public platforms;
    mapping(address token => address) public tokenPlatform;
    mapping(address platform => uint256) public platformFees;

    // ---- per-coin anti-whale toggles + accounting (stored OUTSIDE Curve so the
    //      getCurve tuple stays byte-identical for the indexer) ----------------
    mapping(address token => bool) public antiSnipeEnabled;
    mapping(address token => bool) public maxWalletEnabled;
    mapping(address token => uint48) public launchTime; // timestamp for the anti-snipe window
    mapping(address token => mapping(address wallet => uint256)) public walletBought; // cumulative buys, for caps

    /// @dev Flat protocol fee taken at each migration, 100% to protocol, ON TOP of
    ///      the existing 80/20 trade-fee split. Owner-settable, hard-capped. The
    ///      LIVE value is only the default for NEW launches — each coin SNAPSHOTS
    ///      it at creation (see migrationTerms) so it can never change post-launch.
    uint256 public protocolMigrationFee;

    /// @dev V3 per-token SNAPSHOT of the migration economics, frozen at launch.
    ///      `_migrate` reads these — NOT the live config — so the owner can never
    ///      alter the fees (incl. the 0.5 ETH protocol fee) on an already-launched
    ///      coin. Snapshotting the ECONOMICS (not the migrator address) is what
    ///      preserves the guarantee while the migrator stays globally patchable
    ///      behind a timelock. `set` guards against a zero-snapshot for pre-V3 rows.
    struct MigrationTerms {
        uint64 migrationFee;
        uint16 migrationFeeBps;
        uint128 protocolMigrationFee;
        bool set;
    }
    mapping(address token => MigrationTerms) public migrationTerms;

    /// @dev V3 migrator-timelock state. `migrator` is the live one; a change is
    ///      staged in `pendingMigrator` and only promotable after `migratorUnlockAt`.
    address public pendingMigrator;
    uint256 public migratorUnlockAt;

    // ---- CTO handoff state ----
    mapping(address token => address) public ctoPending;
    mapping(address token => uint256) public ctoUnlockAt;
    /// @dev PULL-pattern ledger: on a handoff, the old creator's accrued fees are
    ///      credited here (claimable) instead of pushed — so a non-receiving
    ///      contract-creator can never DoS a handoff/CTO.
    mapping(address creator => uint256) public pendingCreatorClaim;

    // --------------------------------------------------------------- events

    event TokenCreated(
        address indexed token,
        address indexed creator,
        string name,
        string symbol,
        string metadataURI,
        uint256 virtualEth,
        uint256 virtualTokens,
        uint256 curveSupply
    );
    event Trade(
        address indexed token,
        address indexed trader,
        bool isBuy,
        uint256 ethAmount,
        uint256 tokenAmount,
        uint256 fee,
        uint256 virtualEthAfter,
        uint256 virtualTokensAfter
    );
    event Graduated(address indexed token, uint256 raisedEth);
    event Migrated(
        address indexed token, address indexed pair, uint256 ethLiquidity, uint256 tokenLiquidity, uint256 lpBurned
    );
    event ConfigUpdated(Config config);
    event MigratorSet(address indexed migrator);
    event MigratorProposed(address indexed migrator, uint256 unlockAt);
    event MigratorChangeCancelled(address indexed migrator);
    event FeesWithdrawn(address indexed to, uint256 amount);
    event CreatorFeesClaimed(address indexed token, address indexed creator, uint256 amount);
    event PlatformSet(address indexed platform, bool approved, uint16 feeShareBps);
    event PlatformLaunch(address indexed token, address indexed platform, address indexed creator);
    event PlatformFeesClaimed(address indexed platform, uint256 amount);
    event CreatorTransferred(address indexed token, address indexed from, address indexed to);
    /// @dev A DELAYED, PUBLIC fee-redirection notice — NOT a trustless action. The
    ///      7-day timelock + this loudly-indexed event are the real protection; the
    ///      not-to-self check is only a minor extra barrier (a determined owner
    ///      could still point at a controlled address — the defense is visibility).
    event CtoProposed(address indexed token, address indexed from, address indexed newCreator, uint256 unlockAt);
    event CtoExecuted(address indexed token, address indexed newCreator);
    event CtoCancelled(address indexed token);
    event PendingCreatorFeesClaimed(address indexed creator, uint256 amount);
    event ProtocolMigrationFeeSet(uint256 fee);

    // --------------------------------------------------------------- errors

    error UnknownToken();
    error AlreadyGraduated();
    error NotGraduated();
    error AlreadyMigrated();
    error ZeroAmount();
    error SlippageExceeded();
    error InsufficientPayment();
    error SnipeGuardExceeded();
    error FeeTooHigh();
    error EthTransferFailed();
    error NothingToWithdraw();
    error BadVanitySalt();
    error NotCreator();
    error NotPlatform();
    error ZeroAddress();
    error BadSupply();
    error MaxWalletExceeded();
    error CtoNotReady();
    error BadCtoRecipient();
    error MigratorAlreadySet();
    error MigratorNotReady();

    // ---------------------------------------------------------- constructor

    constructor(address uniswapFactory_, address weth_, address owner_) Ownable(owner_) {
        uniswapFactory = IUniswapV2Factory(uniswapFactory_);
        weth = IWETH(weth_);
        config = Config({
            virtualEthSeed: 2.81 ether,
            creationFee: 0,
            defaultTradeFeeBps: 100,
            migrationFee: 0.05 ether,
            migrationFeeBps: 300,
            guardBlocks: 100,
            guardMaxWalletBps: 1000,
            creatorFeeShareBps: 8000, // 80% of every fee to the creator, 20% protocol/platform
            vanityEnforced: true // every launched token CA must end in 0x600d
        });
        protocolMigrationFee = 0.5 ether; // flat protocol cut per migration, on top of 80/20
    }

    // ---------------------------------------------------------------- admin

    function setConfig(Config calldata newConfig) external onlyOwner {
        if (newConfig.defaultTradeFeeBps > MAX_TRADE_FEE_BPS) revert FeeTooHigh();
        if (newConfig.migrationFeeBps > MAX_MIGRATION_FEE_BPS) revert FeeTooHigh();
        if (newConfig.creatorFeeShareBps > BPS) revert FeeTooHigh();
        if (newConfig.virtualEthSeed == 0) revert ZeroAmount();
        config = newConfig;
        emit ConfigUpdated(newConfig);
    }

    /// @notice Bootstrap the FIRST migrator (one-time, while unset). After this,
    ///         changes must go through proposeMigrator + the public timelock.
    function setMigrator(address migrator_) external onlyOwner {
        if (migrator != address(0)) revert MigratorAlreadySet();
        if (migrator_ == address(0)) revert ZeroAddress();
        migrator = migrator_;
        emit MigratorSet(migrator_);
    }

    /// @notice Stage a migrator change. It cannot take effect until MIGRATOR_TIMELOCK
    ///         has passed and executeMigrator() is called — a loud, public delay so
    ///         a graduated coin can never be silently routed through a bad migrator,
    ///         and holders can exit if they distrust the proposed one.
    function proposeMigrator(address migrator_) external onlyOwner {
        if (migrator_ == address(0)) revert ZeroAddress();
        pendingMigrator = migrator_;
        migratorUnlockAt = block.timestamp + MIGRATOR_TIMELOCK;
        emit MigratorProposed(migrator_, migratorUnlockAt);
    }

    function executeMigrator() external onlyOwner {
        uint256 unlockAt = migratorUnlockAt;
        if (unlockAt == 0 || block.timestamp < unlockAt) revert MigratorNotReady();
        address m = pendingMigrator;
        if (m == address(0)) revert ZeroAddress();
        pendingMigrator = address(0);
        migratorUnlockAt = 0;
        migrator = m;
        emit MigratorSet(m);
    }

    function cancelMigrator() external onlyOwner {
        address m = pendingMigrator;
        pendingMigrator = address(0);
        migratorUnlockAt = 0;
        emit MigratorChangeCancelled(m);
    }

    /// @notice Set the flat protocol fee taken at each migration (100% protocol,
    ///         on top of the 80/20 split). Hard-capped so it can never be abusive.
    function setProtocolMigrationFee(uint256 fee) external onlyOwner {
        if (fee > MAX_PROTOCOL_MIGRATION_FEE) revert FeeTooHigh();
        protocolMigrationFee = fee;
        emit ProtocolMigrationFeeSet(fee);
    }

    function setPlatform(address platform, bool approved, uint16 feeShareBps) external onlyOwner {
        if (platform == address(0)) revert ZeroAddress();
        if (feeShareBps > BPS) revert FeeTooHigh();
        platforms[platform] = Platform({approved: approved, feeShareBps: feeShareBps});
        emit PlatformSet(platform, approved, feeShareBps);
    }

    function withdrawFees(address to) external onlyOwner nonReentrant {
        uint256 amount = protocolFeesAccrued;
        if (amount == 0) revert NothingToWithdraw();
        protocolFeesAccrued = 0;
        (bool ok,) = to.call{value: amount}("");
        if (!ok) revert EthTransferFailed();
        emit FeesWithdrawn(to, amount);
    }

    function claimCreatorFees(address token) external nonReentrant {
        if (curves[token].creator != msg.sender) revert NotCreator();
        uint256 amount = creatorFees[token];
        if (amount == 0) revert NothingToWithdraw();
        creatorFees[token] = 0;
        (bool ok,) = msg.sender.call{value: amount}("");
        if (!ok) revert EthTransferFailed();
        emit CreatorFeesClaimed(token, msg.sender, amount);
    }

    function claimPlatformFees() external nonReentrant {
        uint256 amount = platformFees[msg.sender];
        if (amount == 0) revert NothingToWithdraw();
        platformFees[msg.sender] = 0;
        (bool ok,) = msg.sender.call{value: amount}("");
        if (!ok) revert EthTransferFailed();
        emit PlatformFeesClaimed(msg.sender, amount);
    }

    // ------------------------------------------------------------- CTO handoff

    /// @notice A creator hands off THEIR OWN coin's future fees (friendly/paid
    ///         handoff). Already-accrued fees are paid to the OLD creator first,
    ///         so only FUTURE fees transfer. No timelock — it's their own coin.
    function transferCreator(address token, address to) external nonReentrant {
        if (curves[token].creator != msg.sender) revert NotCreator();
        if (to == address(0)) revert ZeroAddress();
        _handoff(token, to);
        emit CreatorTransferred(token, msg.sender, to);
    }

    /// @notice Owner-only CTO for an abandoned coin. Fenced against abuse:
    ///         Safe-only · public 7-day timelock · future-fees-only (accrued is
    ///         paid to the old creator) · recipient can NEVER be owner/protocol.
    function proposeCto(address token, address newCreator) external onlyOwner {
        if (curves[token].creator == address(0)) revert UnknownToken();
        if (newCreator == address(0) || newCreator == owner() || newCreator == address(this)) revert BadCtoRecipient();
        ctoPending[token] = newCreator;
        ctoUnlockAt[token] = block.timestamp + CTO_TIMELOCK;
        emit CtoProposed(token, curves[token].creator, newCreator, ctoUnlockAt[token]);
    }

    function executeCto(address token) external onlyOwner nonReentrant {
        uint256 unlockAt = ctoUnlockAt[token];
        if (unlockAt == 0 || block.timestamp < unlockAt) revert CtoNotReady();
        address newCreator = ctoPending[token];
        if (newCreator == address(0) || newCreator == owner() || newCreator == address(this)) revert BadCtoRecipient();
        ctoPending[token] = address(0);
        ctoUnlockAt[token] = 0;
        _handoff(token, newCreator);
        emit CtoExecuted(token, newCreator);
    }

    function cancelCto(address token) external onlyOwner {
        ctoPending[token] = address(0);
        ctoUnlockAt[token] = 0;
        emit CtoCancelled(token);
    }

    /// @dev Switch the creator to `to`, crediting any accrued fees to the OLD
    ///      creator as a CLAIMABLE balance (pull-pattern — no external call). This
    ///      keeps earned fees with whoever earned them (future-only) AND makes the
    ///      handoff impossible to DoS: a contract-creator that can't receive ETH
    ///      can never block a CTO — it just claims later via claimPendingCreatorFees.
    function _handoff(address token, address to) internal {
        address old = curves[token].creator;
        uint256 accrued = creatorFees[token];
        if (accrued != 0) {
            creatorFees[token] = 0;
            pendingCreatorClaim[old] += accrued;
        }
        curves[token].creator = to;
    }

    /// @notice Withdraw fees credited to you from a past handoff/CTO (pull-pattern).
    function claimPendingCreatorFees() external nonReentrant {
        uint256 amount = pendingCreatorClaim[msg.sender];
        if (amount == 0) revert NothingToWithdraw();
        pendingCreatorClaim[msg.sender] = 0;
        (bool ok,) = msg.sender.call{value: amount}("");
        if (!ok) revert EthTransferFailed();
        emit PendingCreatorFeesClaimed(msg.sender, amount);
    }

    function _accrueFee(address token, uint256 fee) internal {
        uint256 creatorCut = (fee * config.creatorFeeShareBps) / BPS;
        if (creatorCut != 0) creatorFees[token] += creatorCut;
        uint256 protocolCut = fee - creatorCut;
        address platform = tokenPlatform[token];
        if (platform != address(0)) {
            Platform memory p = platforms[platform];
            if (p.approved && p.feeShareBps != 0) {
                uint256 platformCut = (protocolCut * p.feeShareBps) / BPS;
                if (platformCut != 0) {
                    platformFees[platform] += platformCut;
                    protocolCut -= platformCut;
                }
            }
        }
        protocolFeesAccrued += protocolCut;
    }

    // --------------------------------------------------------------- create

    /// @notice Deploy a custom-supply token + curve. `totalSupply_` sets the mint
    ///         (bounded MIN…MAX); the curve seeds scale so the ETH economics match
    ///         a standard launch. Any ETH beyond the creation fee is the creator's
    ///         atomic first buy. Caller must be an approved creator.
    function createToken(
        string calldata name,
        string calldata symbol,
        string calldata metadataURI,
        uint256 minTokensOut,
        bytes32 salt,
        uint16 tradeFeeBps,
        uint256 totalSupply_
    ) external payable nonReentrant returns (address token) {
        // V3: 2% per-wallet drain cap defaults ON (maxWallet=true). Creators who
        // want it off use createTokenGuarded(..., maxWallet=false).
        return _create(msg.sender, name, symbol, metadataURI, minTokensOut, salt, tradeFeeBps, totalSupply_, LaunchOpts(false, true, address(0)));
    }

    /// @notice Same as createToken, plus the two per-coin anti-whale toggles.
    ///         `antiSnipe` = first 2 min tighter 0.5%/wallet (own platform + dev
    ///         exempt). `maxWallet` = 2%/wallet cumulative-buy cap WHILE ON THE
    ///         BONDING CURVE (not a post-graduation / lifetime cap — see
    ///         MAX_WALLET_BPS). BOTH toggles are set here at launch and can NEVER
    ///         change afterward.
    function createTokenGuarded(
        string calldata name,
        string calldata symbol,
        string calldata metadataURI,
        uint256 minTokensOut,
        bytes32 salt,
        uint16 tradeFeeBps,
        uint256 totalSupply_,
        bool antiSnipe,
        bool maxWallet
    ) external payable nonReentrant returns (address token) {
        token = _create(msg.sender, name, symbol, metadataURI, minTokensOut, salt, tradeFeeBps, totalSupply_, LaunchOpts(antiSnipe, maxWallet, address(0)));
    }

    /// @notice Partner-platform entrypoint (Proxima, hood.tools bundler, …). An
    ///         approved platform launches on behalf of `creator`. The salt binds
    ///         to the CALLING PLATFORM (grind against your own address). `creator`
    ///         must be an approved creator; the dev-buy credits `creator`.
    function createTokenFor(
        address creator,
        string calldata name,
        string calldata symbol,
        string calldata metadataURI,
        uint256 minTokensOut,
        bytes32 salt,
        uint16 tradeFeeBps,
        uint256 totalSupply_
    ) external payable nonReentrant returns (address token) {
        if (!platforms[msg.sender].approved) revert NotPlatform();
        if (creator == address(0)) revert ZeroAddress();
        // V3: drain cap defaults ON here too; partners use createTokenForGuarded
        // with maxWallet=false to opt out.
        token = _create(creator, name, symbol, metadataURI, minTokensOut, salt, tradeFeeBps, totalSupply_, LaunchOpts(false, true, msg.sender));
        emit PlatformLaunch(token, msg.sender, creator);
    }

    /// @notice Partner entrypoint with the two per-coin anti-whale toggles.
    function createTokenForGuarded(
        address creator,
        string calldata name,
        string calldata symbol,
        string calldata metadataURI,
        uint256 minTokensOut,
        bytes32 salt,
        uint16 tradeFeeBps,
        uint256 totalSupply_,
        bool antiSnipe,
        bool maxWallet
    ) external payable nonReentrant returns (address token) {
        if (!platforms[msg.sender].approved) revert NotPlatform();
        if (creator == address(0)) revert ZeroAddress();
        token = _create(creator, name, symbol, metadataURI, minTokensOut, salt, tradeFeeBps, totalSupply_, LaunchOpts(antiSnipe, maxWallet, msg.sender));
        emit PlatformLaunch(token, msg.sender, creator);
    }

    function _create(
        address creator,
        string calldata name,
        string calldata symbol,
        string calldata metadataURI,
        uint256 minTokensOut,
        bytes32 salt,
        uint16 tradeFeeBps,
        uint256 totalSupply_,
        LaunchOpts memory opts
    ) internal returns (address token) {
        // Public launchpad — anyone may create with any supply in range. The
        // partner/bundle path (createTokenFor) stays whitelisted separately.
        if (totalSupply_ < MIN_TOTAL_SUPPLY || totalSupply_ > MAX_TOTAL_SUPPLY) revert BadSupply();

        Config memory cfg = config;
        if (msg.value < cfg.creationFee) revert InsufficientPayment();
        if (tradeFeeBps < MIN_TRADE_FEE_BPS || tradeFeeBps > MAX_TRADE_FEE_BPS) revert FeeTooHigh();
        protocolFeesAccrued += cfg.creationFee;

        // Scale the token-side seeds with supply → identical ETH economics.
        uint256 curveSupply_ = (totalSupply_ * CURVE_BPS) / BPS;
        uint256 lpSupply_ = totalSupply_ - curveSupply_;
        uint256 vTokSeed = (DEFAULT_VIRTUAL_TOKEN_SEED * totalSupply_) / DEFAULT_TOTAL_SUPPLY;

        token = address(new HoodToken{salt: keccak256(abi.encodePacked(msg.sender, salt))}(name, symbol, totalSupply_));
        if (cfg.vanityEnforced && uint160(token) & 0xffff != VANITY_SUFFIX) revert BadVanitySalt();

        curves[token] = Curve({
            virtualEth: cfg.virtualEthSeed,
            virtualTokens: uint128(vTokSeed),
            realEth: 0,
            realTokens: uint128(curveSupply_),
            creator: creator,
            createdAtBlock: uint48(block.number),
            graduated: false,
            migrated: false,
            tradeFeeBps: tradeFeeBps
        });
        tokenCurveSupply[token] = curveSupply_;
        tokenLpSupply[token] = lpSupply_;
        allTokens.push(token);
        launchTime[token] = uint48(block.timestamp);

        // V3: snapshot the migration economics at launch. `_migrate` reads THESE,
        // never the live config — so this coin's migration fees (incl. the 0.5 ETH
        // protocol fee) are frozen at creation and the owner can never change them
        // on an already-launched coin. This is the GMGN "per-token snapshot".
        migrationTerms[token] = MigrationTerms({
            migrationFee: cfg.migrationFee,
            migrationFeeBps: cfg.migrationFeeBps,
            protocolMigrationFee: uint128(protocolMigrationFee),
            set: true
        });

        // Set the anti-whale toggles and platform binding BEFORE the atomic
        // dev-buy so the creator's own first buy is subject to the 2% max-wallet
        // cap and the platform's fee share is routed on that buy too. (Previously
        // these were set by the wrappers AFTER _create returned, so the dev-buy
        // bypassed the cap and mis-routed the platform fee.)
        if (opts.antiSnipe) antiSnipeEnabled[token] = true;
        if (opts.maxWallet) maxWalletEnabled[token] = true;
        if (opts.platform != address(0)) tokenPlatform[token] = opts.platform;

        emit TokenCreated(token, creator, name, symbol, metadataURI, cfg.virtualEthSeed, vTokSeed, curveSupply_);

        uint256 buyEth = msg.value - cfg.creationFee;
        if (buyEth > 0) {
            _buy(token, creator, buyEth, minTokensOut, cfg, true);
        } else if (minTokensOut != 0) {
            revert SlippageExceeded();
        }
    }

    // ---------------------------------------------------------------- trade

    function buy(address token, uint256 minTokensOut) external payable nonReentrant {
        if (msg.value == 0) revert ZeroAmount();
        _requireLiveCurve(token);
        _buy(token, msg.sender, msg.value, minTokensOut, config, false);
    }

    /// @notice Pay-and-credit-recipient buy. Whitelisted platforms (e.g. Proxima's
    ///         block-0 bundler) are snipe-guard EXEMPT here: a trusted, approved
    ///         platform can bundle across many wallets in the launch block/window
    ///         without the 10%-per-wallet cap. Everyone else's buyFor is guarded
    ///         exactly like a normal buy — the exemption is the whitelist privilege.
    function buyFor(address token, address recipient, uint256 minTokensOut) external payable nonReentrant {
        if (msg.value == 0) revert ZeroAmount();
        if (recipient == address(0)) revert ZeroAddress();
        _requireLiveCurve(token);
        // A whitelisted platform is guard-exempt ONLY on coins IT launched — so a
        // partner can block-0 bundle its own launches but can never snipe others'.
        bool exemptGuard = platforms[msg.sender].approved && tokenPlatform[token] == msg.sender;
        _buy(token, recipient, msg.value, minTokensOut, config, exemptGuard);
    }

    function sell(address token, uint256 tokenAmount, uint256 minEthOut) external nonReentrant {
        if (tokenAmount == 0) revert ZeroAmount();
        Curve storage c = _requireLiveCurve(token);

        uint256 vEth = c.virtualEth;
        uint256 vTok = c.virtualTokens;
        uint256 ethOut = vEth - Math.ceilDiv(vEth * vTok, vTok + tokenAmount);
        uint256 fee = (ethOut * c.tradeFeeBps) / BPS;
        uint256 ethToSeller = ethOut - fee;
        if (ethToSeller < minEthOut || ethToSeller == 0) revert SlippageExceeded();

        c.virtualEth = uint128(vEth - ethOut);
        c.virtualTokens = uint128(vTok + tokenAmount);
        c.realEth = uint128(c.realEth - ethOut);
        c.realTokens = uint128(c.realTokens + tokenAmount);
        _accrueFee(token, fee);

        emit Trade(token, msg.sender, false, ethToSeller, tokenAmount, fee, c.virtualEth, c.virtualTokens);

        require(HoodToken(token).transferFrom(msg.sender, address(this), tokenAmount), "TRANSFER_FAILED");
        (bool ok,) = msg.sender.call{value: ethToSeller}("");
        if (!ok) revert EthTransferFailed();
    }

    function _buy(address token, address recipient, uint256 ethIn, uint256 minTokensOut, Config memory cfg, bool exemptGuard)
        internal
    {
        Curve storage c = curves[token];
        uint256 feeBps = c.tradeFeeBps;

        uint256 vEth = c.virtualEth;
        uint256 vTok = c.virtualTokens;
        uint256 realTokens = c.realTokens;

        uint256 fee = (ethIn * feeBps) / BPS;
        uint256 ethForCurve = ethIn - fee;
        uint256 tokensOut = vTok - (vEth * vTok) / (vEth + ethForCurve);
        uint256 refund = 0;

        if (tokensOut >= realTokens) {
            tokensOut = realTokens;
            uint256 newVTok = vTok - realTokens;
            uint256 exactEth = Math.ceilDiv(vEth * vTok, newVTok) - vEth;
            if (exactEth > ethForCurve) exactEth = ethForCurve;
            uint256 grossEth = feeBps == 0 ? exactEth : Math.ceilDiv(exactEth * BPS, BPS - feeBps);
            if (grossEth > ethIn) grossEth = ethIn;
            refund = ethIn - grossEth;
            fee = grossEth - exactEth;
            ethForCurve = exactEth;
        }

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

        if (!exemptGuard && block.number < c.createdAtBlock + cfg.guardBlocks && cfg.guardMaxWalletBps != 0) {
            uint256 bought = guardBought[token][recipient] + tokensOut;
            if (bought > (tokenCurveSupply[token] * cfg.guardMaxWalletBps) / BPS) revert SnipeGuardExceeded();
            guardBought[token][recipient] = bought;
        }

        // ---- per-coin anti-whale toggles (chosen at launch, immutable) --------
        if (antiSnipeEnabled[token] || maxWalletEnabled[token]) {
            uint256 curveSupply_ = tokenCurveSupply[token];
            uint256 wb = walletBought[token][recipient] + tokensOut;
            // 2%/wallet cumulative-BUY cap on the bonding curve — applies to
            // everyone (curve phase only; a real cap can't exist post-graduation)
            if (maxWalletEnabled[token] && wb > (curveSupply_ * MAX_WALLET_BPS) / BPS) {
                revert MaxWalletExceeded();
            }
            // anti-snipe: first ANTI_SNIPE_WINDOW seconds, tighter 0.5%/wallet;
            // the coin's own platform + the creator's dev-buy (exemptGuard) skip it
            if (
                antiSnipeEnabled[token] && !exemptGuard
                    && block.timestamp < launchTime[token] + ANTI_SNIPE_WINDOW
                    && wb > (curveSupply_ * ANTI_SNIPE_MAX_WALLET_BPS) / BPS
            ) {
                revert SnipeGuardExceeded();
            }
            walletBought[token][recipient] = wb;
        }

        c.virtualEth = uint128(vEth + ethForCurve);
        c.virtualTokens = uint128(vTok - tokensOut);
        c.realEth = uint128(c.realEth + ethForCurve);
        c.realTokens = uint128(realTokens - tokensOut);
        _accrueFee(token, fee);

        emit Trade(token, recipient, true, ethForCurve + fee, tokensOut, fee, c.virtualEth, c.virtualTokens);

        bool graduating = (c.realTokens == 0);
        if (graduating) {
            c.graduated = true;
            emit Graduated(token, c.realEth);
        }

        require(HoodToken(token).transfer(recipient, tokensOut), "TRANSFER_FAILED");

        // ATOMIC AUTO-MIGRATION: the very tx that fills the curve also seeds and
        // permanently locks the Uniswap v3 pool. No keeper, no separate call, no
        // frozen window — every launch is on Uniswap v3 the instant it graduates.
        if (graduating) {
            _migrate(token, c);
        }

        if (refund > 0) {
            (bool ok,) = msg.sender.call{value: refund}("");
            if (!ok) revert EthTransferFailed();
        }
    }

    // -------------------------------------------------------------- migrate

    /// @notice Defensive, permissionless fallback. In normal operation migration
    ///         is ATOMIC — it runs inside the buy that fills the curve (see
    ///         `_buy`), so a coin is on Uniswap v3 in the same tx it graduates.
    ///         This entrypoint only fires in the (unexpected) case that a curve
    ///         is graduated but not yet migrated; otherwise it reverts
    ///         AlreadyMigrated.
    function migrate(address token) external nonReentrant returns (address pool) {
        Curve storage c = curves[token];
        if (c.creator == address(0)) revert UnknownToken();
        if (!c.graduated) revert NotGraduated();
        if (c.migrated) revert AlreadyMigrated();
        return _migrate(token, c);
    }

    /// @dev Seeds + locks the Uniswap v3 pool. Caller MUST have already verified
    ///      the curve is graduated and not migrated. Reused by the atomic path in
    ///      `_buy` and the public fallback above.
    function _migrate(address token, Curve storage c) internal returns (address pool) {
        c.migrated = true;

        uint256 lpSupply_ = tokenLpSupply[token];
        uint256 rawRaise = c.realEth;
        uint256 ethLiq = rawRaise;

        // V3: read the SNAPSHOT taken at this coin's launch — never the live config
        // — so the fees are exactly what it was created with and cannot be changed
        // post-launch. (Pre-V3 rows have set==false and fall back to live config;
        // none exist on a fresh deploy.)
        MigrationTerms memory t = migrationTerms[token];
        uint256 migrationFee =
            uint256(t.set ? t.migrationFee : config.migrationFee) + (rawRaise * (t.set ? t.migrationFeeBps : config.migrationFeeBps)) / BPS;
        uint256 protoFee = t.set ? uint256(t.protocolMigrationFee) : protocolMigrationFee;

        // V3: CLAMP the TOTAL take (flat + bps + protocol) to a hard share of the
        // raise. It CLAMPS, never reverts — a graduation always seeds its pool.
        // With the standard curve (~6.5 ETH raise) this never bites; it's a ceiling.
        uint256 maxTotal = (rawRaise * MAX_TOTAL_MIGRATION_FEE_BPS) / BPS;
        uint256 totalFee = migrationFee + protoFee;
        if (totalFee > maxTotal && totalFee != 0) {
            migrationFee = (migrationFee * maxTotal) / totalFee;
            protoFee = maxTotal - migrationFee;
        }

        if (migrationFee != 0 && migrationFee < ethLiq) {
            ethLiq -= migrationFee;
            _accrueFee(token, migrationFee);
        }
        // flat protocol migration fee — 100% to protocol, ON TOP of the 80/20
        // above. Guarded so it can never exceed the raise and brick migration.
        if (protoFee != 0 && ethLiq > protoFee) {
            ethLiq -= protoFee;
            protocolFeesAccrued += protoFee;
        }
        c.realEth = 0;

        HoodToken(token).unlock();

        address _migrator = migrator;
        if (_migrator != address(0)) {
            require(HoodToken(token).transfer(_migrator, lpSupply_), "TRANSFER_FAILED");
            pool = IHoodMigrator(_migrator).migrate{value: ethLiq}(token, c.creator);
            emit Migrated(token, pool, ethLiq, lpSupply_, 0);
        } else {
            pool = uniswapFactory.getPair(token, address(weth));
            if (pool == address(0)) {
                pool = uniswapFactory.createPair(token, address(weth));
            }
            weth.deposit{value: ethLiq}();
            require(weth.transfer(pool, ethLiq), "TRANSFER_FAILED");
            require(HoodToken(token).transfer(pool, lpSupply_), "TRANSFER_FAILED");
            uint256 liquidity = IUniswapV2Pair(pool).mint(address(this));
            require(IUniswapV2Pair(pool).transfer(LP_BURN_ADDRESS, liquidity), "TRANSFER_FAILED");
            emit Migrated(token, pool, ethLiq, lpSupply_, liquidity);
        }
    }

    // ---------------------------------------------------------------- views

    function quoteBuy(address token, uint256 ethIn)
        external
        view
        returns (uint256 tokensOut, uint256 fee, uint256 ethUsed)
    {
        Curve memory c = curves[token];
        if (c.creator == address(0)) revert UnknownToken();
        if (c.graduated || ethIn == 0) return (0, 0, 0);
        uint256 feeBps = c.tradeFeeBps;

        fee = (ethIn * feeBps) / BPS;
        uint256 ethForCurve = ethIn - fee;
        tokensOut = c.virtualTokens - (uint256(c.virtualEth) * c.virtualTokens) / (c.virtualEth + ethForCurve);
        ethUsed = ethIn;

        if (tokensOut >= c.realTokens) {
            tokensOut = c.realTokens;
            uint256 newVTok = c.virtualTokens - c.realTokens;
            uint256 exactEth = Math.ceilDiv(uint256(c.virtualEth) * c.virtualTokens, newVTok) - c.virtualEth;
            if (exactEth > ethForCurve) exactEth = ethForCurve;
            ethUsed = feeBps == 0 ? exactEth : Math.ceilDiv(exactEth * BPS, BPS - feeBps);
            if (ethUsed > ethIn) ethUsed = ethIn;
            fee = ethUsed - exactEth;
        }
    }

    function quoteSell(address token, uint256 tokenAmount) external view returns (uint256 ethOut, uint256 fee) {
        Curve memory c = curves[token];
        if (c.creator == address(0)) revert UnknownToken();
        if (c.graduated || tokenAmount == 0) return (0, 0);
        uint256 gross =
            c.virtualEth - Math.ceilDiv(uint256(c.virtualEth) * c.virtualTokens, c.virtualTokens + tokenAmount);
        fee = (gross * c.tradeFeeBps) / BPS;
        ethOut = gross - fee;
    }

    /// @notice Spot price in ETH wei per whole token (1e18 base units).
    function currentPrice(address token) external view returns (uint256) {
        Curve memory c = curves[token];
        if (c.creator == address(0)) revert UnknownToken();
        return (uint256(c.virtualEth) * 1e18) / c.virtualTokens;
    }

    /// @notice Curve progress toward graduation in basis points.
    function progressBps(address token) external view returns (uint256) {
        Curve memory c = curves[token];
        if (c.creator == address(0)) revert UnknownToken();
        uint256 curveSupply_ = tokenCurveSupply[token];
        return ((curveSupply_ - c.realTokens) * BPS) / curveSupply_;
    }

    function tokenCount() external view returns (uint256) {
        return allTokens.length;
    }

    function getCurve(address token) external view returns (Curve memory) {
        return curves[token];
    }

    /// @notice The token's chosen total supply (curve + LP).
    function totalSupplyOf(address token) external view returns (uint256) {
        return tokenCurveSupply[token] + tokenLpSupply[token];
    }

    function isHoodToken(address token) external view returns (bool) {
        return curves[token].creator != address(0);
    }

    /// @notice Predicted CREATE2 address for a launch of a given supply. Grind
    ///         `salt` until the result ends in 0x600d (when vanity is enforced).
    function predictTokenAddress(
        address caller,
        bytes32 salt,
        string calldata name,
        string calldata symbol,
        uint256 totalSupply_
    ) external view returns (address) {
        bytes32 initCodeHash =
            keccak256(abi.encodePacked(type(HoodToken).creationCode, abi.encode(name, symbol, totalSupply_)));
        bytes32 h = keccak256(
            abi.encodePacked(bytes1(0xff), address(this), keccak256(abi.encodePacked(caller, salt)), initCodeHash)
        );
        return address(uint160(uint256(h)));
    }

    function tokenInitCodeHash(string calldata name, string calldata symbol, uint256 totalSupply_)
        external
        pure
        returns (bytes32)
    {
        return keccak256(abi.encodePacked(type(HoodToken).creationCode, abi.encode(name, symbol, totalSupply_)));
    }

    function tokens(uint256 offset, uint256 limit) external view returns (address[] memory page) {
        uint256 n = allTokens.length;
        if (offset >= n) return new address[](0);
        uint256 end = offset + limit;
        if (end > n) end = n;
        page = new address[](end - offset);
        for (uint256 i = offset; i < end; i++) {
            page[i - offset] = allTokens[i];
        }
    }

    // -------------------------------------------------------------- helpers

    function _requireLiveCurve(address token) internal view returns (Curve storage c) {
        c = curves[token];
        if (c.creator == address(0)) revert UnknownToken();
        if (c.graduated) revert AlreadyGraduated();
    }
}

library Math {
    function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {
        return a == 0 ? 0 : (a - 1) / b + 1;
    }
}
Chain explorer1303msChain node78ms