BondingCurve

0x44a880f7c1d96b47f3ea6a5cbdf0e7600747ea17

Verification
Unverified
v0.8.24+commit.e11b9ed9
Type
Contract
6,745 bytes
ABI entries
59
26 read · 8 write
License
none

Contract information

Address
0x44a880f7c1d96b47f3ea6a5cbdf0e7600747ea17
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0x763210d9db…2acec7d6ac

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (26)

BPS_DENOMINATOR()uint256
GRADUATION_FEE()uint256
GRAD_THRESHOLD()uint256
K()uint256
TOTAL_SUPPLY()uint256
TRADE_FEE_BPS()uint256
VIRTUAL_ETH()uint256
VIRTUAL_TOKENS()uint256
configured()bool
creator()address
creatorFeeBps()uint256
creatorFeesAccrued(address)uint256
currentPriceX96()uint256
factory()address
gradBounty()uint256
graduationParams()uint256, uint256, uint256
liquidityManager()address
pool()address
realEthReserve()uint256
reserves()uint256, uint256
sqrtPriceX96Expected()uint160
state()uint8
token()address
tokenBalance()uint256
tokensSold()uint256
treasury()address

Events (8)

BuyCreatorFeesClaimedFeeRoutedGraduatedGraduationConfiguredInitializedSellThresholdReached

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "treasury_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AlreadyConfigured",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "AlreadyInitialized",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EthTransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ExceedsSold",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientReserve",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotConfigured",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotGraduating",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotInitialized",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotTrading",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "OnlyFactory",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SlippageExceeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroTokens",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroValue",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "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": "fee",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "realEthReserve",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensSold",
        "type": "uint256"
      }
    ],
    "name": "Buy",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "CreatorFeesClaimed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "creatorCut",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "treasuryCut",
        "type": "uint256"
      }
    ],
    "name": "FeeRouted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "caller",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethToLP",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "bounty",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "graduationFee",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "Graduated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "liquidityManager",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint160",
        "name": "sqrtPriceX96Expected",
        "type": "uint160"
      }
    ],
    "name": "GraduationConfigured",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "Initialized",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "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": "fee",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "realEthReserve",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensSold",
        "type": "uint256"
      }
    ],
    "name": "Sell",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "realEthReserve",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensSold",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethToLP",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensToLP",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "finalPriceX96",
        "type": "uint256"
      }
    ],
    "name": "ThresholdReached",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "BPS_DENOMINATOR",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "GRADUATION_FEE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "GRAD_THRESHOLD",
    "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": "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_TOKENS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      }
    ],
    "name": "buy",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      }
    ],
    "name": "buyFor",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "claimCreatorFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "creator_",
        "type": "address"
      }
    ],
    "name": "claimCreatorFeesFor",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "uint256",
            "name": "tradeFeeBps",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "graduationFee",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "gradBounty",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "threshold",
            "type": "uint256"
          },
          {
            "internalType": "address",
            "name": "liquidityManager",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "pool",
            "type": "address"
          },
          {
            "internalType": "uint160",
            "name": "sqrtPriceX96Expected",
            "type": "uint160"
          },
          {
            "internalType": "address",
            "name": "creator",
            "type": "address"
          },
          {
            "internalType": "uint256",
            "name": "creatorFeeBps",
            "type": "uint256"
          }
        ],
        "internalType": "struct BondingCurve.GradConfig",
        "name": "c",
        "type": "tuple"
      }
    ],
    "name": "configureGraduation",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "configured",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "creator",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "creatorFeeBps",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "creatorFeesAccrued",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "currentPriceX96",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "factory",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "gradBounty",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "graduate",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "graduationParams",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "ethToLP",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "tokensToLP",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "finalPriceX96",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "contract IERC20",
        "name": "token_",
        "type": "address"
      }
    ],
    "name": "initialize",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "liquidityManager",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pool",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "realEthReserve",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "reserves",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "x",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "y",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokensIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minEthOut",
        "type": "uint256"
      }
    ],
    "name": "sell",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "ethOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "sqrtPriceX96Expected",
    "outputs": [
      {
        "internalType": "uint160",
        "name": "",
        "type": "uint160"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "state",
    "outputs": [
      {
        "internalType": "enum BondingCurve.State",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "token",
    "outputs": [
      {
        "internalType": "contract IERC20",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokenBalance",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokensSold",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "treasury",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

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

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";

interface ILiquidityManagerLike {
    function provideLiquidity(
        address token,
        address pool,
        uint160 sqrtPriceX96Expected,
        address creator,
        uint256 creatorShareNum,
        uint256 creatorShareDenom
    ) external payable returns (uint256 tokenId);
}

/// @title BondingCurve
/// @notice Virtual-reserve constant-product (x*y=k) bonding curve, ETH-reserved,
///         Pump.fun style. 100% of the token supply is minted to this curve; users
///         buy/sell against virtual reserves until the real ETH reserve reaches the
///         graduation threshold, at which point the curve freezes (Graduating).
/// @dev Graduation *execution* (Uniswap seeding) is M2; here we only freeze and
///      expose `graduationParams()`. Rounding always favors the pool. ETH accounting
///      uses the `realEthReserve` storage variable, never `address(this).balance`,
///      so force-fed ETH (selfdestruct) cannot corrupt it.
contract BondingCurve is ReentrancyGuard {
    using SafeERC20 for IERC20;

    // --------------------------------------------------------------------- //
    //                          Pinned curve params                          //
    // --------------------------------------------------------------------- //

    /// @notice Virtual ETH reserve seeding the curve. Scaled with the 4 ETH threshold
    ///         to preserve the curve shape (Ve/threshold ratio) and start/grad mcap ratio.
    uint256 public constant VIRTUAL_ETH = 1.6 ether;
    /// @notice Virtual token reserve seeding the curve.
    uint256 public constant VIRTUAL_TOKENS = 1_073_000_000e18;
    /// @notice Total token supply (100% minted to this curve at creation).
    uint256 public constant TOTAL_SUPPLY = 1_000_000_000e18;
    /// @notice Basis-points denominator.
    uint256 public constant BPS_DENOMINATOR = 10_000;

    // --- Economics snapshotted at configuration (immutable once configured). ---
    // Defaults below are the M1 economics; the factory overwrites them exactly once
    // via `configureGraduation` inside the createToken tx, after which they never
    // change (a live curve's economics are immutable).

    /// @notice Real ETH reserve (net of trade fees) that triggers graduation.
    uint256 public GRAD_THRESHOLD = 4 ether;
    /// @notice Trade fee in basis points, taken in ETH on every trade (1.50%).
    uint256 public TRADE_FEE_BPS = 150;
    /// @notice Graduation fee (ETH) routed to the Treasury on graduate().
    uint256 public GRADUATION_FEE = 0;
    /// @notice Graduation bounty (ETH) paid to whoever calls graduate().
    uint256 public gradBounty = 0;
    /// @notice Creator's share of the trade fee, in bps of the trade (out of
    ///         TRADE_FEE_BPS). e.g. 100 of 150 -> creator 2/3, green.fun 1/3. 0 = all
    ///         to Treasury (M1 default). Snapshotted at configuration. The SAME ratio
    ///         (creatorFeeBps / TRADE_FEE_BPS) is applied to post-grad LP fees.
    uint256 public creatorFeeBps = 0;
    /// @notice Constant product invariant target `k = VIRTUAL_ETH * VIRTUAL_TOKENS`.
    uint256 public constant K = VIRTUAL_ETH * VIRTUAL_TOKENS;
    /// @notice Q96 fixed-point scale (2**96) for `finalPriceX96`.
    uint256 internal constant Q96 = 0x1000000000000000000000000;

    // --------------------------------------------------------------------- //
    //                                State                                  //
    // --------------------------------------------------------------------- //

    /// @notice Lifecycle state. Trading -> Graduating -> Graduated.
    enum State {
        Trading,
        Graduating,
        Graduated
    }

    /// @notice Full economics + graduation config the factory snapshots at creation.
    struct GradConfig {
        uint256 tradeFeeBps;
        uint256 graduationFee;
        uint256 gradBounty;
        uint256 threshold;
        address liquidityManager;
        address pool;
        uint160 sqrtPriceX96Expected;
        address creator;
        uint256 creatorFeeBps;
    }

    /// @notice The factory that deployed this curve (only caller of privileged fns).
    address public immutable factory;
    /// @notice Recipient of trade fees.
    address public immutable treasury;

    /// @notice The launch token traded by this curve (set once via `initialize`).
    IERC20 public token;
    /// @notice The token's immutable creator (snapshot at configuration). Receives
    ///         `creatorFeeBps`% of every trade fee, PULL-based via claimCreatorFees.
    address public creator;
    /// @notice Pull-based creator fee balances (never pushed inline). A creator that
    ///         reverts on receive() can only block their OWN claim, never a trade.
    mapping(address => uint256) public creatorFeesAccrued;
    /// @notice Real ETH held by the curve, net of fees. NOT `address(this).balance`.
    uint256 public realEthReserve;
    /// @notice Net tokens sold to users (curve released minus sold back).
    uint256 public tokensSold;
    /// @notice Current lifecycle state.
    State public state;

    // --- Graduation wiring (set once by the factory via configureGraduation). ---
    /// @notice Whether graduation params have been configured by the factory.
    bool public configured;
    /// @notice LiquidityManager that seeds the v3 position at graduation.
    address public liquidityManager;
    /// @notice The v3 pool created + initialized in the createToken tx.
    address public pool;
    /// @notice The precomputed pool price graduate() requires the pool to still hold.
    uint160 public sqrtPriceX96Expected;

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

    /// @notice Emitted once when the factory wires the token into the curve.
    event Initialized(address indexed token);
    /// @notice Emitted on every buy.
    event Buy(
        address indexed buyer,
        uint256 ethIn,
        uint256 tokensOut,
        uint256 fee,
        uint256 realEthReserve,
        uint256 tokensSold
    );
    /// @notice Emitted on every sell.
    event Sell(
        address indexed seller,
        uint256 tokensIn,
        uint256 ethOut,
        uint256 fee,
        uint256 realEthReserve,
        uint256 tokensSold
    );
    /// @notice Emitted once when the curve crosses the graduation threshold.
    event ThresholdReached(
        uint256 realEthReserve, uint256 tokensSold, uint256 ethToLP, uint256 tokensToLP, uint256 finalPriceX96
    );
    /// @notice Emitted once when the factory configures graduation wiring.
    event GraduationConfigured(address liquidityManager, address pool, uint160 sqrtPriceX96Expected);
    /// @notice Emitted once when the curve graduates and seeds the v3 position.
    event Graduated(address indexed caller, uint256 ethToLP, uint256 bounty, uint256 graduationFee, uint256 tokenId);
    /// @notice Emitted on every trade with the fee split (creator accrual / treasury).
    event FeeRouted(uint256 fee, uint256 creatorCut, uint256 treasuryCut);
    /// @notice Emitted when a creator pulls their accrued fees.
    event CreatorFeesClaimed(address indexed creator, uint256 amount);

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

    error OnlyFactory();
    error AlreadyInitialized();
    error AlreadyConfigured();
    error NotConfigured();
    error NotInitialized();
    error NotTrading();
    error NotGraduating();
    error ZeroValue();
    error ZeroTokens();
    error ZeroAddress();
    error SlippageExceeded();
    error ExceedsSold();
    error InsufficientReserve();
    error EthTransferFailed();

    /// @param treasury_ Recipient of trade fees (non-zero).
    constructor(address treasury_) {
        if (treasury_ == address(0)) revert ZeroAddress();
        factory = msg.sender;
        treasury = treasury_;
        state = State.Trading;
    }

    // --------------------------------------------------------------------- //
    //                             Initialization                            //
    // --------------------------------------------------------------------- //

    /// @notice Wire the launch token into the curve. Callable once, by the factory.
    /// @param token_ The launch token whose full supply is held by this curve.
    function initialize(IERC20 token_) external {
        if (msg.sender != factory) revert OnlyFactory();
        if (address(token) != address(0)) revert AlreadyInitialized();
        if (address(token_) == address(0)) revert ZeroAddress();
        token = token_;
        emit Initialized(address(token_));
    }

    /// @notice Snapshot economics + graduation wiring. Callable once, by the factory,
    ///         in the createToken tx (before any dev-buy). After this the curve's
    ///         economics are immutable; later FeeManager changes never affect it.
    /// @param c The full graduation + economics config (snapshotted, immutable after).
    function configureGraduation(GradConfig calldata c) external {
        if (msg.sender != factory) revert OnlyFactory();
        if (configured) revert AlreadyConfigured();
        if (c.liquidityManager == address(0) || c.pool == address(0)) revert ZeroAddress();
        TRADE_FEE_BPS = c.tradeFeeBps;
        GRADUATION_FEE = c.graduationFee;
        gradBounty = c.gradBounty;
        GRAD_THRESHOLD = c.threshold;
        liquidityManager = c.liquidityManager;
        pool = c.pool;
        sqrtPriceX96Expected = c.sqrtPriceX96Expected;
        creator = c.creator;
        creatorFeeBps = c.creatorFeeBps;
        configured = true;
        emit GraduationConfigured(c.liquidityManager, c.pool, c.sqrtPriceX96Expected);
    }

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

    /// @notice Buy tokens with ETH (msg.value). Fee is taken in ETH to the treasury.
    /// @dev If the buy would push the real reserve to/over `GRAD_THRESHOLD`, it is
    ///      partially filled up to exactly the threshold, the excess ETH is refunded,
    ///      and the curve graduates. This capping is what guarantees I8 feasibility.
    /// @param minTokensOut Minimum acceptable tokens out (mandatory slippage bound).
    /// @return tokensOut Tokens transferred to the buyer.
    function buy(uint256 minTokensOut) external payable nonReentrant returns (uint256 tokensOut) {
        return _buy(msg.sender, minTokensOut);
    }

    /// @notice Buy on behalf of `recipient`, used by the factory for the dev-buy.
    /// @param recipient Address credited with the purchased tokens and any refund.
    /// @param minTokensOut Minimum acceptable tokens out (mandatory slippage bound).
    /// @return tokensOut Tokens transferred to the recipient.
    function buyFor(address recipient, uint256 minTokensOut)
        external
        payable
        nonReentrant
        returns (uint256 tokensOut)
    {
        if (msg.sender != factory) revert OnlyFactory();
        return _buy(recipient, minTokensOut);
    }

    /// @notice Sell `tokensIn` tokens back to the curve for ETH (net of fee).
    /// @dev Caller must have approved this curve for `tokensIn`.
    /// @param tokensIn Amount of tokens to sell (must be > 0).
    /// @param minEthOut Minimum acceptable net ETH out (mandatory slippage bound).
    /// @return ethOut Net ETH sent to the seller.
    function sell(uint256 tokensIn, uint256 minEthOut) external nonReentrant returns (uint256 ethOut) {
        if (address(token) == address(0)) revert NotInitialized();
        if (state != State.Trading) revert NotTrading();
        if (tokensIn == 0) revert ZeroTokens();
        if (tokensIn > tokensSold) revert ExceedsSold();

        uint256 x = VIRTUAL_ETH + realEthReserve;
        uint256 y = VIRTUAL_TOKENS - tokensSold;

        // gross ETH out, floored -> favors the pool
        uint256 grossEthOut = Math.mulDiv(x, tokensIn, y + tokensIn);
        if (grossEthOut == 0) revert ZeroValue();
        if (grossEthOut > realEthReserve) revert InsufficientReserve();

        uint256 fee = (grossEthOut * TRADE_FEE_BPS) / BPS_DENOMINATOR;
        ethOut = grossEthOut - fee;
        if (ethOut < minEthOut) revert SlippageExceeded();

        uint256 creatorCut = _creatorCut(fee);

        // effects
        realEthReserve -= grossEthOut;
        tokensSold -= tokensIn;
        if (creatorCut > 0) creatorFeesAccrued[creator] += creatorCut; // PULL — never pushed

        // interactions
        token.safeTransferFrom(msg.sender, address(this), tokensIn);
        uint256 treasuryCut = fee - creatorCut; // dust rounds to Treasury, never creator
        if (treasuryCut > 0) _sendEth(treasury, treasuryCut);
        _sendEth(msg.sender, ethOut);

        emit Sell(msg.sender, tokensIn, ethOut, fee, realEthReserve, tokensSold);
        emit FeeRouted(fee, creatorCut, treasuryCut);
    }

    // --------------------------------------------------------------------- //
    //                              Graduation                               //
    // --------------------------------------------------------------------- //

    /// @notice Permanently graduate the curve: pay the graduation fee to Treasury
    ///         and the bounty to the caller, then seed the Uniswap v3 position via
    ///         the LiquidityManager. Permissionless — anyone may call once Graduating.
    /// @dev CEI + nonReentrant + one-shot state transition. If the downstream mint
    ///      reverts (e.g. a manipulated pool price), the whole call reverts, state
    ///      rolls back to Graduating, and graduate() can be retried — no funds lost.
    /// @return tokenId The minted (and locked) Uniswap v3 position NFT id.
    function graduate() external nonReentrant returns (uint256 tokenId) {
        if (state != State.Graduating) revert NotGraduating();
        if (!configured) revert NotConfigured();

        // effects
        state = State.Graduated;
        uint256 realEth = realEthReserve;
        uint256 fee = GRADUATION_FEE;
        uint256 bounty = gradBounty;
        uint256 ethToLP = realEth - fee - bounty;
        realEthReserve = 0;
        uint256 tokenBal = token.balanceOf(address(this));

        // interactions
        if (fee > 0) _sendEth(treasury, fee);
        if (bounty > 0) _sendEth(msg.sender, bounty);
        token.safeTransfer(liquidityManager, tokenBal);
        // LP fees split with the SAME ratio as trade fees: creatorFeeBps / TRADE_FEE_BPS.
        tokenId = ILiquidityManagerLike(liquidityManager).provideLiquidity{value: ethToLP}(
            address(token), pool, sqrtPriceX96Expected, creator, creatorFeeBps, TRADE_FEE_BPS
        );

        emit Graduated(msg.sender, ethToLP, bounty, fee, tokenId);
    }

    /// @notice Pull the caller's accrued creator fees. Reentrancy-safe (CEI +
    ///         nonReentrant); zeroes the balance before paying, cannot be drained
    ///         twice, and reverts on a zero balance.
    /// @return amount The ETH paid to the caller.
    function claimCreatorFees() external nonReentrant returns (uint256 amount) {
        return _claimCreatorFees(msg.sender);
    }

    /// @notice Claim `creator_`'s accrued fees, paying THEM directly (never the
    ///         caller). Lets a stateless router (FeeRouter) batch-claim on a
    ///         creator's behalf without ever custodying funds. Permissionless is
    ///         safe: the payout is hard-bound to `creator_`, so a caller can never
    ///         redirect another creator's fees to themselves.
    /// @param creator_ The creator whose fees are claimed and paid out.
    /// @return amount The ETH paid to `creator_`.
    function claimCreatorFeesFor(address creator_) external nonReentrant returns (uint256 amount) {
        return _claimCreatorFees(creator_);
    }

    /// @dev Shared claim body. Identical semantics to the original claim: zeroes
    ///      before paying (CEI), reverts on zero, pays the creator in ETH.
    function _claimCreatorFees(address creator_) internal returns (uint256 amount) {
        amount = creatorFeesAccrued[creator_];
        if (amount == 0) revert ZeroValue();
        creatorFeesAccrued[creator_] = 0;
        emit CreatorFeesClaimed(creator_, amount);
        _sendEth(creator_, amount);
    }

    /// @dev Creator's share of a fee (creatorFeeBps out of the total TRADE_FEE_BPS).
    ///      Zero for unconfigured (M1) curves, so all of the fee routes to the
    ///      Treasury exactly as before. Remainder (incl. rounding dust) -> Treasury.
    function _creatorCut(uint256 fee) internal view returns (uint256) {
        if (creator == address(0) || creatorFeeBps == 0) return 0;
        return (fee * creatorFeeBps) / TRADE_FEE_BPS;
    }

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

    /// @notice Real token balance held by the curve (the unsold remainder).
    /// @return The curve's ERC20 balance.
    function tokenBalance() external view returns (uint256) {
        return token.balanceOf(address(this));
    }

    /// @notice Effective reserves used by the curve math.
    /// @return x Effective ETH reserve (virtual + real).
    /// @return y Effective token reserve (virtual - sold).
    function reserves() external view returns (uint256 x, uint256 y) {
        x = VIRTUAL_ETH + realEthReserve;
        y = VIRTUAL_TOKENS - tokensSold;
    }

    /// @notice Current marginal price (ETH-wei per token-wei) in Q96 fixed point.
    /// @return Marginal price `x/y` scaled by 2**96.
    function currentPriceX96() external view returns (uint256) {
        return Math.mulDiv(VIRTUAL_ETH + realEthReserve, Q96, VIRTUAL_TOKENS - tokensSold);
    }

    /// @notice Graduation seed parameters for M2's LiquidityManager to consume.
    /// @dev Reverts unless the curve is in the Graduating state.
    /// @return ethToLP ETH earmarked for the LP seed (`realEth - GRADUATION_FEE`).
    /// @return tokensToLP Tokens earmarked for the LP seed (`ethToLP / finalPrice`).
    /// @return finalPriceX96 Final curve price (ETH-wei per token-wei) in Q96.
    function graduationParams()
        external
        view
        returns (uint256 ethToLP, uint256 tokensToLP, uint256 finalPriceX96)
    {
        if (state != State.Graduating) revert NotGraduating();
        return _graduationParams();
    }

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

    function _buy(address recipient, uint256 minTokensOut) internal returns (uint256 tokensOut) {
        if (address(token) == address(0)) revert NotInitialized();
        if (state != State.Trading) revert NotTrading();
        if (recipient == address(0)) revert ZeroAddress();
        uint256 value = msg.value;
        if (value == 0) revert ZeroValue();

        uint256 x = VIRTUAL_ETH + realEthReserve;
        uint256 y = VIRTUAL_TOKENS - tokensSold;

        uint256 fee = (value * TRADE_FEE_BPS) / BPS_DENOMINATOR;
        uint256 netIn = value - fee;
        uint256 refund = 0;
        bool graduating = false;

        // If this buy reaches/overshoots the threshold, cap the net to land the
        // real reserve at EXACTLY GRAD_THRESHOLD, refund the excess, and graduate.
        if (realEthReserve + netIn >= GRAD_THRESHOLD) {
            graduating = true;
            uint256 netNeeded = GRAD_THRESHOLD - realEthReserve;
            uint256 grossNeeded =
                Math.mulDiv(netNeeded, BPS_DENOMINATOR, BPS_DENOMINATOR - TRADE_FEE_BPS, Math.Rounding.Ceil);
            fee = grossNeeded - netNeeded;
            netIn = netNeeded;
            refund = value - grossNeeded;
        }

        tokensOut = Math.mulDiv(y, netIn, x + netIn); // floor -> favors the pool
        if (tokensOut == 0) revert ZeroTokens();
        if (tokensOut < minTokensOut) revert SlippageExceeded();

        uint256 creatorCut = _creatorCut(fee);

        // effects
        realEthReserve += netIn; // == GRAD_THRESHOLD exactly when graduating
        tokensSold += tokensOut;
        if (graduating) state = State.Graduating;
        if (creatorCut > 0) creatorFeesAccrued[creator] += creatorCut; // PULL — never pushed

        // interactions
        token.safeTransfer(recipient, tokensOut);
        uint256 treasuryCut = fee - creatorCut; // dust rounds to Treasury, never creator
        if (treasuryCut > 0) _sendEth(treasury, treasuryCut);
        if (refund > 0) _sendEth(recipient, refund);

        emit Buy(recipient, netIn, tokensOut, fee, realEthReserve, tokensSold);
        emit FeeRouted(fee, creatorCut, treasuryCut);
        if (graduating) {
            (uint256 ethToLP, uint256 tokensToLP, uint256 finalPriceX96) = _graduationParams();
            emit ThresholdReached(realEthReserve, tokensSold, ethToLP, tokensToLP, finalPriceX96);
        }
    }

    function _graduationParams()
        internal
        view
        returns (uint256 ethToLP, uint256 tokensToLP, uint256 finalPriceX96)
    {
        uint256 x = VIRTUAL_ETH + realEthReserve; // == VIRTUAL_ETH + GRAD_THRESHOLD
        uint256 y = VIRTUAL_TOKENS - tokensSold;
        ethToLP = realEthReserve - GRADUATION_FEE - gradBounty;
        tokensToLP = Math.mulDiv(ethToLP, y, x); // floor -> maximizes headroom
        finalPriceX96 = Math.mulDiv(x, Q96, y);
    }

    function _sendEth(address to, uint256 amount) internal {
        (bool ok,) = payable(to).call{value: amount}("");
        if (!ok) revert EthTransferFailed();
    }
}
Chain explorer4118msChain node90ms