BondingCurve
0x3379fd8acc45d60cb6f96cbc5c7aafea0cf9c7f1
Verification
Unverified
v0.8.24+commit.e11b9ed9
Type
Contract
6,745 bytes
ABI entries
59
26 read · 8 write
License
none
Contract information
- Address
- 0x3379fd8acc45d60cb6f96cbc5c7aafea0cf9c7f1
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0x56Cc4630B7…d954D3FBe4
- Creation tx
- 0xe4b59a9b08…cb8efc80d4
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 explorer7219msChain node89ms