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
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