PumpFactory

0xf34a44f20979f66939637f59282ff80b3bc17dc5

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
15,844 bytes
ABI entries
71
31 read · 13 write
License
none

Contract information

Address
0xf34a44f20979f66939637f59282ff80b3bc17dc5
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0x6f7c3a2cf7…c200fe0758

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (31)

CURVE_SUPPLY()uint256
DEAD()address
DEX_SUPPLY()uint256
K()uint256
MAX_DESCRIPTION_LEN()uint256
MAX_IMAGE_URI_LEN()uint256
MAX_NAME_LEN()uint256
MAX_SYMBOL_LEN()uint256
MIGRATION_TOLERANCE_BPS()uint256
TOTAL_SUPPLY()uint256
TRADE_FEE_BPS()uint256
VIRTUAL_ETH()uint256
VIRTUAL_TOKEN()uint256
accruedFees()uint256
allTokens(uint256)address
canFinalize(address)bool
creationFee()uint256
currentPrice(address)uint256
curves(address)bool, bool, bool, address, uint256, uint256
feeRecipient()address
getTokenInfo(address)tuple
getTokenInfos(uint256, uint256)tuple[]
getTokens(uint256, uint256)address[]
owner()address
pendingOwner()address
progressBps(address)uint256
quoteBuy(address, uint256)uint256
quoteSell(address, uint256)uint256
tokenCount()uint256
uniswapRouter()address
weth()address

Events (10)

BuyFeesClaimedGraduatedMigratedOwnershipTransferStartedOwnershipTransferredRescuedSellTokenCreatedUniswapConfigured

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "feeRecipient_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AlreadyMigrated",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "CurveClosed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "CurveSoldOut",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientBalance",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientCreationFee",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidMetadata",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "MigrationNotConfigured",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotGraduated",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotOwner",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SlippageExceeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "UnknownToken",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAmount",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "buyer",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newPrice",
        "type": "uint256"
      }
    ],
    "name": "Buy",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "FeesClaimed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethReserve",
        "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": "tokenAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "liquidity",
        "type": "uint256"
      }
    ],
    "name": "Migrated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferStarted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      }
    ],
    "name": "Rescued",
    "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": "ethOut",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newPrice",
        "type": "uint256"
      }
    ],
    "name": "Sell",
    "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"
      }
    ],
    "name": "TokenCreated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "router",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "weth",
        "type": "address"
      }
    ],
    "name": "UniswapConfigured",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "CURVE_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "DEAD",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "DEX_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "K",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_DESCRIPTION_LEN",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_IMAGE_URI_LEN",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_NAME_LEN",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_SYMBOL_LEN",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIGRATION_TOLERANCE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TOTAL_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TRADE_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "VIRTUAL_ETH",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "VIRTUAL_TOKEN",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "accruedFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "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"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      }
    ],
    "name": "buy",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "canFinalize",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "claimFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "description",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "imageURI",
        "type": "string"
      }
    ],
    "name": "createToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "creationFee",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "currentPrice",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "curves",
    "outputs": [
      {
        "internalType": "bool",
        "name": "exists",
        "type": "bool"
      },
      {
        "internalType": "bool",
        "name": "graduated",
        "type": "bool"
      },
      {
        "internalType": "bool",
        "name": "migrated",
        "type": "bool"
      },
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "ethReserve",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "tokensSold",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeRecipient",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "finalizeGraduation",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "getTokenInfo",
    "outputs": [
      {
        "components": [
          {
            "internalType": "address",
            "name": "token",
            "type": "address"
          },
          {
            "internalType": "string",
            "name": "name",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "symbol",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "imageURI",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "description",
            "type": "string"
          },
          {
            "internalType": "address",
            "name": "creator",
            "type": "address"
          },
          {
            "internalType": "uint256",
            "name": "price",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "ethReserve",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "tokensSold",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "progressBps",
            "type": "uint256"
          },
          {
            "internalType": "bool",
            "name": "graduated",
            "type": "bool"
          },
          {
            "internalType": "bool",
            "name": "migrated",
            "type": "bool"
          }
        ],
        "internalType": "struct PumpFactory.TokenInfo",
        "name": "info",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "start",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "count",
        "type": "uint256"
      }
    ],
    "name": "getTokenInfos",
    "outputs": [
      {
        "components": [
          {
            "internalType": "address",
            "name": "token",
            "type": "address"
          },
          {
            "internalType": "string",
            "name": "name",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "symbol",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "imageURI",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "description",
            "type": "string"
          },
          {
            "internalType": "address",
            "name": "creator",
            "type": "address"
          },
          {
            "internalType": "uint256",
            "name": "price",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "ethReserve",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "tokensSold",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "progressBps",
            "type": "uint256"
          },
          {
            "internalType": "bool",
            "name": "graduated",
            "type": "bool"
          },
          {
            "internalType": "bool",
            "name": "migrated",
            "type": "bool"
          }
        ],
        "internalType": "struct PumpFactory.TokenInfo[]",
        "name": "page",
        "type": "tuple[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "start",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "count",
        "type": "uint256"
      }
    ],
    "name": "getTokens",
    "outputs": [
      {
        "internalType": "address[]",
        "name": "page",
        "type": "address[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "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": "token",
        "type": "address"
      }
    ],
    "name": "progressBps",
    "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"
      }
    ],
    "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"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      }
    ],
    "name": "rescueGraduated",
    "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": [
      {
        "internalType": "uint256",
        "name": "ethOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      }
    ],
    "name": "setCreationFee",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "r",
        "type": "address"
      }
    ],
    "name": "setFeeRecipient",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "router",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "weth_",
        "type": "address"
      }
    ],
    "name": "setUniswap",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "router",
        "type": "address"
      }
    ],
    "name": "setUniswapRouter",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "weth_",
        "type": "address"
      }
    ],
    "name": "setWeth",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokenCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "uniswapRouter",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "weth",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

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

import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {MemeToken} from "./MemeToken.sol";
import {IUniswapV2Router02} from "./interfaces/IUniswapV2Router02.sol";
import {IUniswapV2Factory} from "./interfaces/IUniswapV2Factory.sol";

/// @title PumpFactory
/// @notice A pump.fun-style launchpad for Robinhood Chain. Anyone can create an
///         ERC-20 that trades against a constant-product bonding curve with
///         virtual reserves (the same shape pump.fun uses on Solana). Buyers pay
///         ETH into the curve and receive tokens; sellers return tokens for ETH.
///         A flat trade fee is skimmed to a fee recipient. When the curve sells
///         out its allocation the token "graduates" (a milestone event).
///
/// @dev    Pricing uses a constant product k = (vEth) * (vToken):
///           - vEth   = VIRTUAL_ETH + realEthReserve
///           - vToken = VIRTUAL_TOKEN - tokensSold
///         k is constant, so any trade preserves vEth * vToken = k.
///         Virtual reserves give the curve a sane, non-zero starting price.
contract PumpFactory is ReentrancyGuard {
    // ----------------------------------------------------------------------
    // Curve constants (18-decimal fixed point)
    // ----------------------------------------------------------------------

    /// @notice Total supply minted per token.
    uint256 public constant TOTAL_SUPPLY = 1_000_000_000 ether; // 1B
    /// @notice Portion of supply sold through the curve. The remainder stays in
    ///         the factory (reserved for post-graduation DEX liquidity).
    uint256 public constant CURVE_SUPPLY = 800_000_000 ether; // 800M
    /// @notice Supply reserved for the Uniswap pool at graduation. Equals the
    ///         tokens the factory still holds once the curve is fully sold out
    ///         (TOTAL_SUPPLY - CURVE_SUPPLY = 200M).
    uint256 public constant DEX_SUPPLY = TOTAL_SUPPLY - CURVE_SUPPLY; // 200M
    /// @notice Virtual token reserve — must exceed CURVE_SUPPLY so vToken stays
    ///         positive across the whole curve.
    uint256 public constant VIRTUAL_TOKEN = 1_073_000_000 ether; // ~1.073B
    /// @notice Virtual ETH reserve — sets the initial price / total raise
    ///         (raise = VIRTUAL_ETH × CURVE_SUPPLY / (VIRTUAL_TOKEN −
    ///         CURVE_SUPPLY) = VIRTUAL_ETH × 800/273). Chosen so selling out
    ///         the curve raises exactly 3 ETH: 1.02375 × 800/273 = 3.
    uint256 public constant VIRTUAL_ETH = 1.02375 ether;
    /// @notice Product invariant.
    uint256 public constant K = VIRTUAL_ETH * VIRTUAL_TOKEN;

    /// @notice Trade fee in basis points (100 = 1%).
    uint256 public constant TRADE_FEE_BPS = 100;
    /// @notice Migration accepts fills down to this fraction of the desired
    ///         amounts (9500 = 95%), so a mildly pre-seeded/skewed pair can't
    ///         permanently block graduation. Extreme skews still revert; see
    ///         `rescueGraduated` for the owner escape hatch.
    uint256 public constant MIGRATION_TOLERANCE_BPS = 9_500;
    uint256 private constant BPS = 10_000;

    /// @notice Burn sink for LP tokens — sending them here locks liquidity
    ///         permanently (pump.fun style), so it can never be pulled.
    address public constant DEAD = 0x000000000000000000000000000000000000dEaD;

    /// @notice Metadata length caps (bytes). Bounded so a single token can
    ///         never bloat `getTokenInfos` pages past RPC gas limits and break
    ///         enumeration for everyone. The image cap is sized to the
    ///         EIP-7825 per-transaction gas cap (2^24 ≈ 16.7M): creation with a
    ///         20kB data URI measures ~15.7M gas, leaving room for an opening
    ///         buy. The frontend compresses uploads to fit this budget.
    uint256 public constant MAX_NAME_LEN = 64;
    uint256 public constant MAX_SYMBOL_LEN = 32;
    uint256 public constant MAX_DESCRIPTION_LEN = 1_000;
    uint256 public constant MAX_IMAGE_URI_LEN = 20_000;

    // ----------------------------------------------------------------------
    // Storage
    // ----------------------------------------------------------------------

    struct Curve {
        bool exists;
        bool graduated; // curve sold out; trading is closed
        bool migrated; // liquidity moved to Uniswap and LP burned
        address creator;
        uint256 ethReserve; // real ETH held for this token
        uint256 tokensSold; // tokens dispensed from the curve so far
    }

    /// @notice Per-token curve state.
    mapping(address => Curve) public curves;
    /// @notice All tokens ever created, for enumeration by the frontend.
    address[] public allTokens;

    /// @notice Fee configuration. `owner` may update the recipient.
    address public owner;
    /// @notice Ownership handover is two-step: the new owner must accept, so a
    ///         typo can never brick the admin surface (setUniswap, rescue).
    address public pendingOwner;
    address public feeRecipient;
    /// @notice Flat fee (in wei) charged on token creation. Default 0.
    uint256 public creationFee;
    /// @notice Fees that could not be delivered to `feeRecipient` (it rejected
    ///         ETH). Claimable later via `claimFees`. Trading never blocks on a
    ///         misbehaving fee recipient.
    uint256 public accruedFees;

    // ----------------------------------------------------------------------
    // Uniswap migration config (owner-settable; address(0) = disabled)
    // ----------------------------------------------------------------------

    /// @notice Uniswap v2 Router02 used to seed and lock DEX liquidity at
    ///         graduation. On Robinhood Chain (chain id 4663) the canonical
    ///         address is 0x89e5db8b5aa49aa85ac63f691524311aeb649eba. Left as
    ///         address(0) until the owner configures it; while unset, tokens
    ///         graduate but liquidity is not migrated (reserves stay here).
    address public uniswapRouter;
    /// @notice Canonical WETH the router pairs against. On Robinhood Chain:
    ///         0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73.
    address public weth;

    // ----------------------------------------------------------------------
    // Events
    // ----------------------------------------------------------------------

    event TokenCreated(
        address indexed token,
        address indexed creator,
        string name,
        string symbol
    );
    event Buy(
        address indexed token,
        address indexed buyer,
        uint256 ethIn,
        uint256 tokensOut,
        uint256 newPrice
    );
    event Sell(
        address indexed token,
        address indexed seller,
        uint256 tokensIn,
        uint256 ethOut,
        uint256 newPrice
    );
    event Graduated(address indexed token, uint256 ethReserve);
    event FeesClaimed(address indexed to, uint256 amount);
    event Rescued(
        address indexed token,
        address indexed to,
        uint256 ethAmount,
        uint256 tokenAmount
    );
    event Migrated(
        address indexed token,
        address indexed pair,
        uint256 tokenAmount,
        uint256 ethAmount,
        uint256 liquidity
    );
    event UniswapConfigured(address router, address weth);
    event OwnershipTransferStarted(address indexed from, address indexed to);
    event OwnershipTransferred(address indexed from, address indexed to);

    // ----------------------------------------------------------------------
    // Errors
    // ----------------------------------------------------------------------

    error NotOwner();
    error UnknownToken();
    error ZeroAmount();
    error InsufficientCreationFee();
    error SlippageExceeded();
    error CurveSoldOut();
    error CurveClosed();
    error TransferFailed();
    error NotGraduated();
    error AlreadyMigrated();
    error MigrationNotConfigured();
    error InsufficientBalance();
    error InvalidMetadata();
    error ZeroAddress();

    modifier onlyOwner() {
        if (msg.sender != owner) revert NotOwner();
        _;
    }

    constructor(address feeRecipient_) {
        owner = msg.sender;
        feeRecipient = feeRecipient_ == address(0) ? msg.sender : feeRecipient_;
    }

    // ----------------------------------------------------------------------
    // Creation
    // ----------------------------------------------------------------------

    /// @notice Launch a new token with a fresh bonding curve.
    function createToken(
        string calldata name,
        string calldata symbol,
        string calldata description,
        string calldata imageURI
    ) external payable nonReentrant returns (address token) {
        if (msg.value < creationFee) revert InsufficientCreationFee();
        // Bounded metadata: keeps every token enumerable via getTokenInfos.
        if (
            bytes(name).length == 0 ||
            bytes(name).length > MAX_NAME_LEN ||
            bytes(symbol).length == 0 ||
            bytes(symbol).length > MAX_SYMBOL_LEN ||
            bytes(description).length > MAX_DESCRIPTION_LEN ||
            bytes(imageURI).length > MAX_IMAGE_URI_LEN
        ) revert InvalidMetadata();

        MemeToken t = new MemeToken(
            name,
            symbol,
            description,
            imageURI,
            msg.sender,
            TOTAL_SUPPLY
        );
        token = address(t);

        curves[token] = Curve({
            exists: true,
            graduated: false,
            migrated: false,
            creator: msg.sender,
            ethReserve: 0,
            tokensSold: 0
        });
        allTokens.push(token);

        if (creationFee > 0) _payFee(creationFee);

        emit TokenCreated(token, msg.sender, name, symbol);

        // If the creator sent extra ETH beyond the creation fee, treat it as an
        // opening buy so they can grab the first tokens atomically.
        uint256 extra = msg.value - creationFee;
        if (extra > 0) _buy(token, extra, 0, msg.sender);
    }

    // ----------------------------------------------------------------------
    // Trading
    // ----------------------------------------------------------------------

    /// @notice Buy `token` with ETH. `minTokensOut` guards against slippage.
    function buy(address token, uint256 minTokensOut)
        external
        payable
        nonReentrant
        returns (uint256 tokensOut)
    {
        if (msg.value == 0) revert ZeroAmount();
        return _buy(token, msg.value, minTokensOut, msg.sender);
    }

    /// @notice Sell `tokenAmount` of `token` back to the curve for ETH.
    ///         Caller must approve this factory for `tokenAmount` first.
    function sell(
        address token,
        uint256 tokenAmount,
        uint256 minEthOut
    ) external nonReentrant returns (uint256 ethOut) {
        Curve storage c = curves[token];
        if (!c.exists) revert UnknownToken();
        // Once graduated the curve is closed; trading happens on Uniswap.
        if (c.graduated) revert CurveClosed();
        if (tokenAmount == 0) revert ZeroAmount();

        // Gross ETH from the curve for returning `tokenAmount`.
        uint256 grossEth = _ethOutForTokens(c, tokenAmount);
        if (grossEth == 0) revert ZeroAmount(); // dust: rounds to no payout
        uint256 fee = (grossEth * TRADE_FEE_BPS) / BPS;
        ethOut = grossEth - fee;
        if (ethOut < minEthOut) revert SlippageExceeded();

        // Update curve: tokens flow back in, ETH flows out.
        c.tokensSold -= tokenAmount;
        c.ethReserve -= grossEth;

        // Pull tokens from seller into the curve.
        if (!IERC20(token).transferFrom(msg.sender, address(this), tokenAmount)) {
            revert TransferFailed();
        }

        if (fee > 0) _payFee(fee);
        _payout(msg.sender, ethOut);

        emit Sell(token, msg.sender, tokenAmount, ethOut, currentPrice(token));
    }

    /// @dev Shared buy logic used by `buy` and by the opening buy in `createToken`.
    function _buy(
        address token,
        uint256 ethIn,
        uint256 minTokensOut,
        address to
    ) internal returns (uint256 tokensOut) {
        Curve storage c = curves[token];
        if (!c.exists) revert UnknownToken();
        // Closed once graduated (also covers the sold-out state defensively).
        if (c.graduated) revert CurveClosed();

        uint256 remaining = CURVE_SUPPLY - c.tokensSold;
        if (remaining == 0) revert CurveSoldOut();

        uint256 fee = (ethIn * TRADE_FEE_BPS) / BPS;
        uint256 ethForCurve = ethIn - fee;

        tokensOut = _tokensOutForEth(c, ethForCurve);

        // Cap at the curve's remaining supply and refund the ETH overpayment.
        // The curve absorbs *exactly* the ETH needed for the last tokens (so the
        // constant-product invariant holds and the contract stays solvent); the
        // fee is charged on top of that, and everything above is refunded.
        uint256 refund;
        if (tokensOut > remaining) {
            tokensOut = remaining;
            uint256 ethNeededForCurve = _ethInForTokens(c, remaining);
            uint256 cappedFee =
                (ethNeededForCurve * TRADE_FEE_BPS) / (BPS - TRADE_FEE_BPS);
            uint256 grossNeeded = ethNeededForCurve + cappedFee;
            ethForCurve = ethNeededForCurve;
            fee = cappedFee;
            refund = ethIn > grossNeeded ? ethIn - grossNeeded : 0;
        }

        // Dust guard (mirror of sell's): an input so small it rounds to zero
        // tokens would otherwise silently donate the ETH to the curve.
        if (tokensOut == 0) revert ZeroAmount();
        if (tokensOut < minTokensOut) revert SlippageExceeded();

        c.tokensSold += tokensOut;
        c.ethReserve += ethForCurve;

        if (fee > 0) _payFee(fee);
        if (refund > 0) _payout(to, refund);
        if (!IERC20(token).transfer(to, tokensOut)) revert TransferFailed();

        emit Buy(token, to, ethForCurve, tokensOut, currentPrice(token));

        // Graduation milestone: the curve has dispensed its full allocation.
        // We only flip the flag + emit here — we deliberately DO NOT run the
        // Uniswap migration inline (see finalizeGraduation). That keeps this
        // buy cheap and un-brickable: a failing router call can never revert the
        // trade that crosses the threshold, and migration becomes a separate,
        // permissionless, retryable step.
        if (!c.graduated && c.tokensSold >= CURVE_SUPPLY) {
            c.graduated = true;
            emit Graduated(token, c.ethReserve);
        }
    }

    // ----------------------------------------------------------------------
    // Graduation → Uniswap migration
    // ----------------------------------------------------------------------

    /// @notice Whether a token can be migrated right now (graduated, not yet
    ///         migrated, and Uniswap is configured).
    function canFinalize(address token) external view returns (bool) {
        Curve storage c = curves[token];
        return
            c.exists &&
            c.graduated &&
            !c.migrated &&
            uniswapRouter != address(0) &&
            weth != address(0);
    }

    /// @notice Permissionless: seed a Uniswap v2 pool with the reserved DEX
    ///         supply (200M tokens) paired against the ETH collected on the
    ///         curve, then burn the LP so liquidity is locked forever. Anyone may
    ///         call this once a token has graduated (typically the frontend/a bot
    ///         right after the graduating buy).
    ///
    /// @dev    Kept separate from buy() on purpose (see the note in _buy):
    ///         the graduating trade must never depend on an external router call.
    ///         Because this is its own permissionless, retryable entrypoint, a
    ///         transient router failure can't brick the token — anyone can retry.
    ///
    ///         Price continuity: the pool opens at ethReserve / DEX_SUPPLY. At
    ///         graduation ethReserve = 3 ETH, so the pool price is
    ///         3 / 200e6 = 1.50e-8 ETH/token, essentially equal to the
    ///         curve's final marginal price (vEth/vToken ≈ 4.02 / 0.273e9 ≈
    ///         1.47e-8). Price is continuous across the migration (~2%).
    function finalizeGraduation(address token) external nonReentrant {
        Curve storage c = curves[token];
        if (!c.exists) revert UnknownToken();
        if (!c.graduated) revert NotGraduated();
        if (c.migrated) revert AlreadyMigrated();

        address router = uniswapRouter;
        address weth_ = weth;
        if (router == address(0) || weth_ == address(0)) {
            revert MigrationNotConfigured();
        }

        uint256 ethAmount = c.ethReserve;
        uint256 tokenAmount = DEX_SUPPLY;

        // --- EFFECTS (before any external call) ---
        // Flip migrated first so a reentrant finalize reverts (AlreadyMigrated),
        // and clear the reserve since it is leaving the curve for the pool.
        c.migrated = true;
        c.ethReserve = 0;

        // Solvency invariant: the factory must actually hold the ETH it's about
        // to deposit. Real ETH balance = sum of all tokens' ethReserve (fees and
        // refunds are paid out immediately), so this always holds — but assert it.
        if (address(this).balance < ethAmount) revert InsufficientBalance();

        // --- INTERACTIONS ---
        // Approve exactly the tokens the router may pull, then reset to 0.
        IERC20(token).approve(router, tokenAmount);

        // Min-amounts at a 95% tolerance: a fresh pair consumes the desired
        // amounts exactly, and a mildly pre-seeded/skewed pair (a known v2
        // griefing vector) still migrates instead of reverting forever. An
        // attacker skewing the ratio beyond the tolerance only burns their own
        // capital into a pool whose LP we then deepen; extreme skews revert and
        // are recoverable via `rescueGraduated`. Leftovers that the router does
        // not consume are handled below, so no value is stranded.
        (uint256 usedToken, uint256 usedEth, uint256 liquidity) =
            IUniswapV2Router02(router).addLiquidityETH{value: ethAmount}(
                token,
                tokenAmount,
                (tokenAmount * MIGRATION_TOLERANCE_BPS) / BPS, // amountTokenMin
                (ethAmount * MIGRATION_TOLERANCE_BPS) / BPS, // amountETHMin
                DEAD, // LP recipient: burn address → liquidity locked forever
                block.timestamp // deadline: executes this block
            );

        // Reset the allowance (nonzero when the router consumed < tokenAmount).
        IERC20(token).approve(router, 0);

        // Leftovers: unconsumed tokens are burned (they were earmarked for the
        // pool, never for anyone's wallet); unconsumed ETH (refunded to this
        // contract by the router) goes to the fee recipient via the tolerant
        // path so it can never block the migration.
        uint256 leftoverTokens = tokenAmount - usedToken;
        if (leftoverTokens > 0) {
            if (!IERC20(token).transfer(DEAD, leftoverTokens)) {
                revert TransferFailed();
            }
        }
        uint256 leftoverEth = ethAmount - usedEth;
        if (leftoverEth > 0) _payFee(leftoverEth);

        address pair =
            IUniswapV2Factory(IUniswapV2Router02(router).factory()).getPair(
                token,
                weth_
            );
        emit Migrated(token, pair, usedToken, usedEth, liquidity);
    }

    /// @notice Owner escape hatch for a graduated token whose migration is
    ///         permanently stuck (e.g. an attacker skewed the pair beyond the
    ///         tolerance, or no router is configured). Moves the reserved
    ///         DEX_SUPPLY tokens and the collected ETH to `to` so liquidity can
    ///         be set up manually. Only reachable while `graduated && !migrated`
    ///         — it can never touch a live curve's funds.
    function rescueGraduated(address token, address to)
        external
        onlyOwner
        nonReentrant
    {
        Curve storage c = curves[token];
        if (!c.exists) revert UnknownToken();
        if (!c.graduated) revert NotGraduated();
        if (c.migrated) revert AlreadyMigrated();

        c.migrated = true;
        uint256 ethAmount = c.ethReserve;
        c.ethReserve = 0;

        if (!IERC20(token).transfer(to, DEX_SUPPLY)) revert TransferFailed();
        if (ethAmount > 0) _payout(to, ethAmount);
        emit Rescued(token, to, ethAmount, DEX_SUPPLY);
    }

    // ----------------------------------------------------------------------
    // Curve math (pure helpers)
    // ----------------------------------------------------------------------

    /// @dev Tokens received for adding `ethIn` to the curve.
    function _tokensOutForEth(Curve storage c, uint256 ethIn)
        internal
        view
        returns (uint256)
    {
        uint256 vEth = VIRTUAL_ETH + c.ethReserve;
        uint256 vToken = VIRTUAL_TOKEN - c.tokensSold;
        // newVToken = K / (vEth + ethIn); tokensOut = vToken - newVToken.
        uint256 newVToken = K / (vEth + ethIn);
        return vToken - newVToken;
    }

    /// @dev Gross ETH released for returning `tokenAmount` to the curve.
    function _ethOutForTokens(Curve storage c, uint256 tokenAmount)
        internal
        view
        returns (uint256)
    {
        uint256 vEth = VIRTUAL_ETH + c.ethReserve;
        uint256 vToken = VIRTUAL_TOKEN - c.tokensSold;
        // newVEth = K / (vToken + tokenAmount); ethOut = vEth - newVEth.
        uint256 newVEth = K / (vToken + tokenAmount);
        return vEth - newVEth;
    }

    /// @dev ETH that must enter the curve to dispense exactly `tokenAmount`.
    function _ethInForTokens(Curve storage c, uint256 tokenAmount)
        internal
        view
        returns (uint256)
    {
        uint256 vEth = VIRTUAL_ETH + c.ethReserve;
        uint256 vToken = VIRTUAL_TOKEN - c.tokensSold;
        // newVEth = K / (vToken - tokenAmount); ethIn = newVEth - vEth.
        uint256 newVEth = K / (vToken - tokenAmount);
        return newVEth - vEth;
    }

    // ----------------------------------------------------------------------
    // Views
    // ----------------------------------------------------------------------

    /// @notice Instantaneous price: ETH per whole token (18-decimal fixed point).
    function currentPrice(address token) public view returns (uint256) {
        Curve storage c = curves[token];
        if (!c.exists) return 0;
        uint256 vEth = VIRTUAL_ETH + c.ethReserve;
        uint256 vToken = VIRTUAL_TOKEN - c.tokensSold;
        // price = vEth / vToken, scaled by 1e18.
        return (vEth * 1 ether) / vToken;
    }

    /// @notice Quote: tokens out for a given ETH input (net of fee).
    function quoteBuy(address token, uint256 ethIn)
        external
        view
        returns (uint256 tokensOut)
    {
        Curve storage c = curves[token];
        if (!c.exists) revert UnknownToken();
        uint256 ethForCurve = ethIn - (ethIn * TRADE_FEE_BPS) / BPS;
        tokensOut = _tokensOutForEth(c, ethForCurve);
        uint256 remaining = CURVE_SUPPLY - c.tokensSold;
        if (tokensOut > remaining) tokensOut = remaining;
    }

    /// @notice Quote: ETH out for selling a given token amount (net of fee).
    function quoteSell(address token, uint256 tokenAmount)
        external
        view
        returns (uint256 ethOut)
    {
        Curve storage c = curves[token];
        if (!c.exists) revert UnknownToken();
        uint256 grossEth = _ethOutForTokens(c, tokenAmount);
        ethOut = grossEth - (grossEth * TRADE_FEE_BPS) / BPS;
    }

    /// @notice Fraction of the curve sold, in basis points (10000 = graduated).
    function progressBps(address token) external view returns (uint256) {
        Curve storage c = curves[token];
        if (!c.exists) return 0;
        return (c.tokensSold * BPS) / CURVE_SUPPLY;
    }

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

    /// @notice Everything the frontend needs about one coin in a single call.
    struct TokenInfo {
        address token;
        string name;
        string symbol;
        string imageURI;
        string description;
        address creator;
        uint256 price; // ETH per whole token, 1e18 fixed point
        uint256 ethReserve; // real ETH collected in the curve
        uint256 tokensSold; // tokens dispensed from the curve
        uint256 progressBps; // 0..10000 fraction of the curve sold
        bool graduated;
        bool migrated; // liquidity moved to Uniswap and LP burned
    }

    function getTokenInfo(address token)
        public
        view
        returns (TokenInfo memory info)
    {
        Curve storage c = curves[token];
        if (!c.exists) revert UnknownToken();
        MemeToken t = MemeToken(token);
        info = TokenInfo({
            token: token,
            name: t.name(),
            symbol: t.symbol(),
            imageURI: t.imageURI(),
            description: t.description(),
            creator: c.creator,
            price: currentPrice(token),
            ethReserve: c.ethReserve,
            tokensSold: c.tokensSold,
            progressBps: (c.tokensSold * BPS) / CURVE_SUPPLY,
            graduated: c.graduated,
            migrated: c.migrated
        });
    }

    /// @notice Paginated coin feed: newest last in `allTokens`; callers usually
    ///         reverse the page for a newest-first display.
    function getTokenInfos(uint256 start, uint256 count)
        external
        view
        returns (TokenInfo[] memory page)
    {
        uint256 len = allTokens.length;
        if (start >= len) return new TokenInfo[](0);
        uint256 end = start + count;
        if (end > len) end = len;
        page = new TokenInfo[](end - start);
        for (uint256 i = start; i < end; i++) {
            page[i - start] = getTokenInfo(allTokens[i]);
        }
    }

    /// @notice Paginated token list for the frontend (newest-agnostic; index order).
    function getTokens(uint256 start, uint256 count)
        external
        view
        returns (address[] memory page)
    {
        uint256 len = allTokens.length;
        if (start >= len) return new address[](0);
        uint256 end = start + count;
        if (end > len) end = len;
        page = new address[](end - start);
        for (uint256 i = start; i < end; i++) {
            page[i - start] = allTokens[i];
        }
    }

    // ----------------------------------------------------------------------
    // Admin
    // ----------------------------------------------------------------------

    function setFeeRecipient(address r) external onlyOwner {
        // address(0) accepts ETH via low-level call, so fees would burn silently.
        if (r == address(0)) revert ZeroAddress();
        feeRecipient = r;
    }

    function setCreationFee(uint256 fee) external onlyOwner {
        creationFee = fee;
    }

    /// @notice Two-step ownership handover: propose here, new owner accepts.
    function transferOwnership(address newOwner) external onlyOwner {
        if (newOwner == address(0)) revert ZeroAddress();
        pendingOwner = newOwner;
        emit OwnershipTransferStarted(owner, newOwner);
    }

    function acceptOwnership() external {
        if (msg.sender != pendingOwner) revert NotOwner();
        emit OwnershipTransferred(owner, msg.sender);
        owner = msg.sender;
        pendingOwner = address(0);
    }

    /// @notice Configure the Uniswap v2 router + WETH used for migration. Setting
    ///         either back to address(0) disables migration (graduated tokens
    ///         simply keep their reserves here — the pre-Uniswap fallback).
    /// @dev    Split from the migration path so a bad address can never be set
    ///         mid-migration; only the owner can point at a router.
    function setUniswapRouter(address router) external onlyOwner {
        uniswapRouter = router;
        emit UniswapConfigured(router, weth);
    }

    function setWeth(address weth_) external onlyOwner {
        weth = weth_;
        emit UniswapConfigured(uniswapRouter, weth_);
    }

    /// @notice Convenience: set both router and WETH in one call.
    function setUniswap(address router, address weth_) external onlyOwner {
        uniswapRouter = router;
        weth = weth_;
        emit UniswapConfigured(router, weth_);
    }

    // ----------------------------------------------------------------------
    // Internal
    // ----------------------------------------------------------------------

    function _payout(address to, uint256 amount) internal {
        (bool ok, ) = payable(to).call{value: amount}("");
        if (!ok) revert TransferFailed();
    }

    /// @dev Fee delivery that can never block trading: if `feeRecipient`
    ///      rejects ETH, the fee accrues and can be claimed later. User payouts
    ///      (refunds, sell proceeds) intentionally still hard-revert.
    function _payFee(uint256 amount) internal {
        (bool ok, ) = payable(feeRecipient).call{value: amount}("");
        if (!ok) accruedFees += amount;
    }

    /// @notice Deliver fees that previously failed to send. Callable by anyone;
    ///         funds only ever go to the current `feeRecipient`.
    function claimFees() external nonReentrant {
        uint256 amount = accruedFees;
        if (amount == 0) revert ZeroAmount();
        accruedFees = 0;
        _payout(feeRecipient, amount);
        emit FeesClaimed(feeRecipient, amount);
    }

    /// @dev Accept ETH refunds from the Uniswap router (addLiquidityETH returns
    ///      unused ETH when the pool ratio differs from the desired amounts).
    receive() external payable {}
}
Chain explorer2481msChain node81ms