BondingCurve
0xe4b43a99f49cd63bc4e97a9c31d3fec767c6ac24
Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
2,557 bytes
ABI entries
26
13 read · 2 write
License
none
Contract information
- Address
- 0xe4b43a99f49cd63bc4e97a9c31d3fec767c6ac24
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.28+commit.7893614a
- Optimization
- Enabled
- Creator
- 0x7A5f1e159E…5e13967CDc
- Creation tx
- 0x3e5fec0e39…6d3ce81f30
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (13)
BPS_DENOMINATOR() → uint256
GRADUATION_FLOOR() → uint256
TRADING_FEE_BPS() → uint256
VIRTUAL_ETH_RESERVE() → uint256
VIRTUAL_TOKEN_RESERVE() → uint256
feeRecipient() → address
graduated() → bool
migrationManager() → address
price() → uint256
realEthReserve() → uint256
token() → address
virtualEthReserve() → uint256
virtualTokenReserve() → uint256
Events (3)
BuyGraduatedSell
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "_token",
"type": "address"
},
{
"internalType": "address",
"name": "_migrationManager",
"type": "address"
},
{
"internalType": "address",
"name": "_feeRecipient",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "AlreadyGraduated",
"type": "error"
},
{
"inputs": [],
"name": "InsufficientEthIn",
"type": "error"
},
{
"inputs": [],
"name": "InsufficientOutput",
"type": "error"
},
{
"inputs": [],
"name": "InsufficientTokensIn",
"type": "error"
},
{
"inputs": [],
"name": "MigrationFailed",
"type": "error"
},
{
"inputs": [],
"name": "Reentrancy",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"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": "ethInAfterFee",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokensOut",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "virtualEth",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "virtualToken",
"type": "uint256"
}
],
"name": "Buy",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethToMigration",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "leftoverTokens",
"type": "uint256"
}
],
"name": "Graduated",
"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": "ethOutBeforeFee",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethOutAfterFee",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "virtualEth",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "virtualToken",
"type": "uint256"
}
],
"name": "Sell",
"type": "event"
},
{
"inputs": [],
"name": "BPS_DENOMINATOR",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "GRADUATION_FLOOR",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TRADING_FEE_BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "VIRTUAL_ETH_RESERVE",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "VIRTUAL_TOKEN_RESERVE",
"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": [],
"name": "feeRecipient",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "graduated",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "migrationManager",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "price",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "realEthReserve",
"outputs": [
{
"internalType": "uint256",
"name": "",
"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": "token",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "virtualEthReserve",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "virtualTokenReserve",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]Source code
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.28;
import { ReentrancyGuard } from "solady/utils/ReentrancyGuard.sol";
import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol";
import { IBondingCurve } from "../interfaces/IBondingCurve.sol";
import { IMigrationManager } from "../interfaces/IMigrationManager.sol";
import { IERC20 } from "../interfaces/IERC20.sol";
/// @title BondingCurve
/// @notice Virtual-reserve constant-product (x*y=k) curve for a single LaunchToken.
/// 1% trading fee taken on the ETH side (in on buy, out on sell).
/// Graduates when a buy drives `virtualTokenReserve` down to
/// `GRADUATION_FLOOR`; at that point all leftover token supply +
/// collected ETH are forwarded to the MigrationManager which moves
/// them into a Thor V3 pool (LP position NFT permanently locked).
///
/// @dev Constants per docs/adr/0001-bonding-curve-constants.md, amended by
/// docs/adr/0009-graduation-raise-5-eth.md (ratios taken verbatim
/// from pump.fun's verified on-chain layout; the ETH scale is set so
/// the curve raises ≈ 5 ETH at graduation).
///
/// Self-consistency property: graduation triggers exactly when the
/// curve's tradable allocation (`VIRTUAL_TOKEN_RESERVE −
/// GRADUATION_FLOOR == 793_100_000e18`, equal to
/// `LaunchpadFactory.BONDING_CURVE_SUPPLY`) is depleted. Unlike a
/// stand-alone ETH-raised threshold, this trigger cannot drift from
/// the curve's actual carrying capacity.
contract BondingCurve is IBondingCurve, ReentrancyGuard {
using SafeTransferLib for address;
// ---------------- Constants ----------------
uint256 public constant VIRTUAL_ETH_RESERVE = 1.7646 ether;
uint256 public constant VIRTUAL_TOKEN_RESERVE = 1_073_000_000e18;
/// @notice Floor below which `virtualTokenReserve` cannot fall — the buy
/// that drives `newVirtualToken` to this value graduates the curve.
/// @dev Equals `VIRTUAL_TOKEN_RESERVE − BONDING_CURVE_SUPPLY` =
/// 1.073B − 793.1M = 279.9M. At this point the curve has sold its
/// entire allocation and `realEthReserve` ≈ 5 ETH (exactly
/// 5.000015219721329047 ETH; raised = VIRTUAL_ETH_RESERVE ×
/// 793.1M/279.9M), corresponding to ~$72.5k FDV at ETH ≈ $3k.
uint256 public constant GRADUATION_FLOOR = 279_900_000e18;
uint256 public constant TRADING_FEE_BPS = 100;
uint256 public constant BPS_DENOMINATOR = 10_000;
// ---------------- Immutables ----------------
address public immutable override token;
address public immutable override migrationManager;
address public immutable override feeRecipient;
// ---------------- Storage ----------------
uint256 public override virtualEthReserve;
uint256 public override virtualTokenReserve;
bool public override graduated;
// ---------------- Errors ----------------
error ZeroAddress();
error AlreadyGraduated();
error InsufficientEthIn();
error InsufficientTokensIn();
error InsufficientOutput();
error MigrationFailed();
// ---------------- Modifiers ----------------
modifier whenNotGraduated() {
if (graduated) revert AlreadyGraduated();
_;
}
// ---------------- Construction ----------------
constructor(
address _token,
address _migrationManager,
address _feeRecipient
) {
if (_token == address(0) || _migrationManager == address(0) || _feeRecipient == address(0)) {
revert ZeroAddress();
}
token = _token;
migrationManager = _migrationManager;
feeRecipient = _feeRecipient;
virtualEthReserve = VIRTUAL_ETH_RESERVE;
virtualTokenReserve = VIRTUAL_TOKEN_RESERVE;
}
// ---------------- Views ----------------
/// @inheritdoc IBondingCurve
function realEthReserve() external view override returns (uint256) {
return virtualEthReserve - VIRTUAL_ETH_RESERVE;
}
/// @inheritdoc IBondingCurve
/// @dev Spot price scaled by 1e18 (ETH-wei per token-wei × 1e18, i.e. ETH per token).
function price() external view override returns (uint256) {
return (virtualEthReserve * 1e18) / virtualTokenReserve;
}
// ---------------- Trade: Buy ----------------
/// @inheritdoc IBondingCurve
function buy(
uint256 minTokensOut
) external payable override nonReentrant whenNotGraduated returns (uint256 tokensOut) {
if (msg.value == 0) revert InsufficientEthIn();
uint256 vEth = virtualEthReserve;
uint256 vTok = virtualTokenReserve;
uint256 k = vEth * vTok;
uint256 fee = (msg.value * TRADING_FEE_BPS) / BPS_DENOMINATOR;
uint256 ethInAfterFee = msg.value - fee;
// x*y=k: new x = x + dx, new y = ⌈K/new x⌉, dy = old y - new y
// Ceil-divide on newVirtualToken so K never decreases (rounding favors the pool;
// the user receives at most the fair-math output, never more by 1 wei).
uint256 newVirtualEth = vEth + ethInAfterFee;
uint256 newVirtualToken = (k + newVirtualEth - 1) / newVirtualEth;
// slither-disable-next-line uninitialized-local
uint256 refund;
// slither-disable-next-line uninitialized-local
bool willGraduate;
// If this buy would push virtualToken below the graduation floor, clamp at
// the floor and recompute consumed ETH (and the proportional fee) so the
// user is refunded the unused portion. The buy that crosses the floor is
// exactly the buy that graduates the curve.
if (newVirtualToken <= GRADUATION_FLOOR) {
newVirtualToken = GRADUATION_FLOOR;
// ceil-divide: ensure newVirtualEth * newVirtualToken >= k (K monotone).
newVirtualEth = (k + newVirtualToken - 1) / newVirtualToken;
uint256 consumedAfterFee = newVirtualEth - vEth;
// Recover the gross msg.value that produced exactly `consumedAfterFee`
// post-fee. Ceil-divide so the pool isn't underfunded by 1 wei.
uint256 consumedMsgValue = (consumedAfterFee * BPS_DENOMINATOR + (BPS_DENOMINATOR - TRADING_FEE_BPS) - 1)
/ (BPS_DENOMINATOR - TRADING_FEE_BPS);
// Belt-and-suspenders: rounding edge could in principle nudge over by 1 wei.
if (consumedMsgValue > msg.value) consumedMsgValue = msg.value;
fee = consumedMsgValue - consumedAfterFee;
refund = msg.value - consumedMsgValue;
willGraduate = true;
}
tokensOut = vTok - newVirtualToken;
if (tokensOut < minTokensOut) revert InsufficientOutput();
// Effects.
virtualEthReserve = newVirtualEth;
virtualTokenReserve = newVirtualToken;
// Interactions.
if (fee > 0) feeRecipient.safeTransferETH(fee);
token.safeTransfer(msg.sender, tokensOut);
if (refund > 0) msg.sender.safeTransferETH(refund);
emit Buy(msg.sender, msg.value - refund, newVirtualEth - vEth, tokensOut, newVirtualEth, newVirtualToken);
if (willGraduate) {
_graduate();
}
}
// ---------------- Trade: Sell ----------------
/// @inheritdoc IBondingCurve
function sell(
uint256 tokensIn,
uint256 minEthOut
) external override nonReentrant whenNotGraduated returns (uint256 ethOut) {
if (tokensIn == 0) revert InsufficientTokensIn();
// Pull tokens from seller into the curve.
token.safeTransferFrom(msg.sender, address(this), tokensIn);
// x*y=k: new y = y + dy, new x = ⌈K/new y⌉, dx = old x - new x
// Ceil-divide on newVirtualEth so K never decreases (rounding favors the pool;
// ethOutGross ≤ what the curve actually holds, no 1-wei underflow on transfer).
uint256 newVirtualToken = virtualTokenReserve + tokensIn;
uint256 k = virtualEthReserve * virtualTokenReserve;
uint256 newVirtualEth = (k + newVirtualToken - 1) / newVirtualToken;
uint256 ethOutGross = virtualEthReserve - newVirtualEth;
uint256 fee = (ethOutGross * TRADING_FEE_BPS) / BPS_DENOMINATOR;
ethOut = ethOutGross - fee;
if (ethOut < minEthOut) revert InsufficientOutput();
// Effects.
virtualEthReserve = newVirtualEth;
virtualTokenReserve = newVirtualToken;
// Interactions.
feeRecipient.safeTransferETH(fee);
msg.sender.safeTransferETH(ethOut);
emit Sell(msg.sender, tokensIn, ethOutGross, ethOut, newVirtualEth, newVirtualToken);
}
// ---------------- Graduation ----------------
/// @dev Forward all leftover tokens + all real ETH to MigrationManager and lock the curve.
/// MigrationManagerV3 creates/adopts a Thor V3 pool, seeds it (with the LaunchpadFactory's
/// DEX_RESERVE_SUPPLY also added), and permanently locks the LP position NFT in the vault.
function _graduate() private {
graduated = true;
uint256 ethToMigration = address(this).balance;
uint256 leftoverTokens = IERC20(token).balanceOf(address(this));
if (leftoverTokens > 0) {
token.safeTransfer(migrationManager, leftoverTokens);
}
// The migration call is the last interaction; if it reverts the entire buy
// (including state changes) reverts. We surface the failure with a custom error.
// The slither-disable-next-line below: migrationManager is `address public
// immutable`, set in the constructor with a zero-address check (see line 79).
// Slither's heuristic doesn't track immutable state, so it sees a
// "potentially user-controlled" address — false positive.
// slither-disable-next-line arbitrary-send-eth
try IMigrationManager(migrationManager).migrate{ value: ethToMigration }(token, leftoverTokens) {
emit Graduated(token, ethToMigration, leftoverTokens);
} catch {
revert MigrationFailed();
}
}
}
Chain explorer8600msChain node69ms