BondingCurve
0x7fc77cb2d6667d8d450e88fa8933517fc504412d
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
11,923 bytes
ABI entries
39
30 read · 4 write
License
none
Contract information
- Address
- 0x7fc77cb2d6667d8d450e88fa8933517fc504412d
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0x85a9fb0b18…f0156B0aC5
- Creation tx
- 0xa895edc75f…497701b1d6
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (30)
PLATFORM_FEE_BPS() → uint16
POOL_FEE() → uint24
TICK_SPACING() → int24
creatorFeeBps() → uint16
creatorFeeReceiver() → address
currentPriceX96() → uint160
curveK() → uint256
deployer() → address
dexFactory() → address
ethReserve() → uint256
graduated() → bool
graduationProgressBps() → uint16
graduationThreshold() → uint256
lpLockerAddress() → address
lpTokenId() → uint256
marketCapWei() → uint256
onERC721Received(address, address, uint256, bytes) → bytes4
platformFeeReceiver() → address
poolAfterGraduation() → address
positionManager() → address
quoteBuy(uint256) → uint256, uint256, uint256, uint256, uint256
quoteSell(uint256) → uint256, uint256, uint256, uint256, uint256
state() → tuple
stocksVault() → address
token() → address
tokenReserve() → uint256
vaultBps() → uint16
virtualEthReserve() → uint256
virtualTokenReserve() → uint256
weth() → address
Events (3)
BuyGraduatedSell
ABI
[
{
"inputs": [
{
"internalType": "contract LemonToken",
"name": "_token",
"type": "address"
},
{
"internalType": "contract IWETH",
"name": "_weth",
"type": "address"
},
{
"internalType": "contract IUniswapV3Factory",
"name": "_dexFactory",
"type": "address"
},
{
"internalType": "contract INonfungiblePositionManager",
"name": "_positionManager",
"type": "address"
},
{
"internalType": "address",
"name": "_deployer",
"type": "address"
},
{
"internalType": "address",
"name": "_creatorFeeReceiver",
"type": "address"
},
{
"internalType": "address",
"name": "_platformFeeReceiver",
"type": "address"
},
{
"internalType": "uint16",
"name": "_creatorFeeBps",
"type": "uint16"
},
{
"internalType": "uint256",
"name": "_graduationThreshold",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_virtualTokenReserve",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_virtualEthReserve",
"type": "uint256"
},
{
"internalType": "address",
"name": "_stocksVault",
"type": "address"
},
{
"internalType": "uint16",
"name": "_vaultBps",
"type": "uint16"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"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": "ethReserveAfter",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenReserveAfter",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "timestamp",
"type": "uint256"
}
],
"name": "Buy",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "lpTokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "wethSeeded",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokensSeeded",
"type": "uint256"
},
{
"indexed": false,
"internalType": "address",
"name": "lpLocker",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "timestamp",
"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": "ethOut",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethReserveAfter",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenReserveAfter",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "timestamp",
"type": "uint256"
}
],
"name": "Sell",
"type": "event"
},
{
"inputs": [],
"name": "PLATFORM_FEE_BPS",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "POOL_FEE",
"outputs": [
{
"internalType": "uint24",
"name": "",
"type": "uint24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TICK_SPACING",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "_graduateExternal",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "minTokensOut",
"type": "uint256"
}
],
"name": "buy",
"outputs": [
{
"internalType": "uint256",
"name": "tokensOut",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "creatorFeeBps",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "creatorFeeReceiver",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "currentPriceX96",
"outputs": [
{
"internalType": "uint160",
"name": "",
"type": "uint160"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "curveK",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "deployer",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "dexFactory",
"outputs": [
{
"internalType": "contract IUniswapV3Factory",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "ethReserve",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "graduate",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "graduated",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "graduationProgressBps",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "graduationThreshold",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "lpLockerAddress",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "lpTokenId",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "marketCapWei",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"name": "onERC721Received",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [],
"name": "platformFeeReceiver",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poolAfterGraduation",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "positionManager",
"outputs": [
{
"internalType": "contract INonfungiblePositionManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "ethIn",
"type": "uint256"
}
],
"name": "quoteBuy",
"outputs": [
{
"internalType": "uint256",
"name": "tokensOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "ethIntoCurve",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "platformFee",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "creatorFee",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "vaultFee",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokensIn",
"type": "uint256"
}
],
"name": "quoteSell",
"outputs": [
{
"internalType": "uint256",
"name": "ethOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "grossEthOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "platformFee",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "creatorFee",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "vaultFee",
"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": "state",
"outputs": [
{
"components": [
{
"internalType": "uint256",
"name": "tokenReserve",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "ethReserve",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "graduationThreshold",
"type": "uint256"
},
{
"internalType": "bool",
"name": "graduated",
"type": "bool"
},
{
"internalType": "uint16",
"name": "creatorFeeBps",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "platformFeeBps",
"type": "uint16"
},
{
"internalType": "address",
"name": "poolAfterGraduation",
"type": "address"
},
{
"internalType": "uint256",
"name": "virtualTokenReserve",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "virtualEthReserve",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "curveK",
"type": "uint256"
}
],
"internalType": "struct BondingCurve.CurveState",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "stocksVault",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "token",
"outputs": [
{
"internalType": "contract LemonToken",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tokenReserve",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "vaultBps",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"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"
},
{
"inputs": [],
"name": "weth",
"outputs": [
{
"internalType": "contract IWETH",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {LemonToken} from "./LemonToken.sol";
import {LpLocker} from "./LpLocker.sol";
import {IUniswapV3Factory, IUniswapV3Pool, INonfungiblePositionManager, IWETH, IERC20} from "./interfaces/IUniswap.sol";
/// @notice Pump.fun-style bonding curve for a single token.
/// - Holds ALL supply of the paired LemonToken at construction.
/// - Trades follow constant-product with virtual reserves: (x + h)(y + r) = K
/// where x = tokens remaining in curve, y = WETH raised, r/h virtual.
/// - Buys pay ETH, receive tokens minus 1% platform fee + creator fee (bps).
/// - Sells burn tokens back to curve for WETH minus same fees.
/// - When `y >= graduationThreshold`, `_graduate()` seeds a Sushi V3
/// pool with remaining tokens + raised WETH, then locks the LP NFT.
contract BondingCurve {
// --- Immutable config --------------------------------------------------
LemonToken public immutable token;
IWETH public immutable weth;
IUniswapV3Factory public immutable dexFactory;
INonfungiblePositionManager public immutable positionManager;
address public immutable deployer;
address public immutable creatorFeeReceiver;
address public immutable platformFeeReceiver;
address public immutable stocksVault; // zero for regular launches
uint16 public immutable creatorFeeBps; // 0..1000 (0..10%)
uint16 public immutable vaultBps; // 0..500 (0..5%) → StocksVault
uint16 public constant PLATFORM_FEE_BPS = 100; // 1%
uint256 public immutable graduationThreshold; // in WETH (wei)
uint24 public constant POOL_FEE = 10_000; // 1%
int24 public constant TICK_SPACING = 200;
// Curve params — (x + h)(y + r) = K
uint256 public immutable virtualTokenReserve; // h
uint256 public immutable virtualEthReserve; // r
uint256 public immutable curveK; // K
// --- Mutable state -----------------------------------------------------
uint256 public tokenReserve; // x
uint256 public ethReserve; // y
bool public graduated;
address public poolAfterGraduation;
uint256 public lpTokenId;
address public lpLockerAddress; // set at graduation; before that = address(0)
bool private _entered;
// Enriched indexing events — post-trade reserves and timestamps
// travel with every trade so terminals (DEX Screener, GeckoTerminal, Photon, BullX, GMGN,
// Axiom, etc.) can render progress bars + price without a follow-up eth_call to state().
event Buy(
address indexed buyer,
uint256 ethIn,
uint256 tokensOut,
uint256 ethReserveAfter,
uint256 tokenReserveAfter,
uint256 timestamp
);
event Sell(
address indexed seller,
uint256 tokensIn,
uint256 ethOut,
uint256 ethReserveAfter,
uint256 tokenReserveAfter,
uint256 timestamp
);
// pool indexed so terminals can subscribe by pool address; lpLocker surfaces the LP-lock
// trust signal that GT Score / DS Trust consume without decoding constructor traces.
event Graduated(
address indexed token,
address indexed pool,
uint256 lpTokenId,
uint256 wethSeeded,
uint256 tokensSeeded,
address lpLocker,
uint256 timestamp
);
modifier nonReentrant() {
require(!_entered, "reentrant");
_entered = true;
_;
_entered = false;
}
constructor(
LemonToken _token,
IWETH _weth,
IUniswapV3Factory _dexFactory,
INonfungiblePositionManager _positionManager,
address _deployer,
address _creatorFeeReceiver,
address _platformFeeReceiver,
uint16 _creatorFeeBps,
uint256 _graduationThreshold,
uint256 _virtualTokenReserve,
uint256 _virtualEthReserve,
address _stocksVault,
uint16 _vaultBps
) {
require(_creatorFeeBps <= 1000, "creatorFee>10%");
require(_vaultBps <= 500, "vault>5%");
require(_graduationThreshold > 0, "threshold=0");
if (_stocksVault == address(0)) require(_vaultBps == 0, "vaultBps w/o vault");
token = _token;
weth = _weth;
dexFactory = _dexFactory;
positionManager = _positionManager;
deployer = _deployer;
creatorFeeReceiver = _creatorFeeReceiver == address(0) ? _deployer : _creatorFeeReceiver;
platformFeeReceiver = _platformFeeReceiver;
creatorFeeBps = _creatorFeeBps;
graduationThreshold = _graduationThreshold;
virtualTokenReserve = _virtualTokenReserve;
virtualEthReserve = _virtualEthReserve;
stocksVault = _stocksVault;
vaultBps = _vaultBps;
// x starts at full supply
tokenReserve = _token.totalSupply();
// K = (x + h)(0 + r)
curveK = (tokenReserve + _virtualTokenReserve) * _virtualEthReserve;
}
// --- Views -------------------------------------------------------------
struct CurveState {
uint256 tokenReserve;
uint256 ethReserve;
uint256 graduationThreshold;
bool graduated;
uint16 creatorFeeBps;
uint16 platformFeeBps;
address poolAfterGraduation;
uint256 virtualTokenReserve;
uint256 virtualEthReserve;
uint256 curveK;
}
function state() external view returns (CurveState memory) {
return CurveState({
tokenReserve: tokenReserve,
ethReserve: ethReserve,
graduationThreshold: graduationThreshold,
graduated: graduated,
creatorFeeBps: creatorFeeBps,
platformFeeBps: PLATFORM_FEE_BPS,
poolAfterGraduation: poolAfterGraduation,
virtualTokenReserve: virtualTokenReserve,
virtualEthReserve: virtualEthReserve,
curveK: curveK
});
}
/// @notice Preview a buy: given ethIn (gross, before fees), return tokensOut.
function quoteBuy(uint256 ethIn) public view returns (
uint256 tokensOut,
uint256 ethIntoCurve,
uint256 platformFee,
uint256 creatorFee,
uint256 vaultFee
) {
(platformFee, creatorFee, vaultFee) = _splitFees(ethIn);
ethIntoCurve = ethIn - platformFee - creatorFee - vaultFee;
// (x + h)(y + r) = K → new x' = K / (y + ethIn + r) − h; tokensOut = x − x'
uint256 newY = ethReserve + ethIntoCurve;
uint256 newXPlusH = curveK / (newY + virtualEthReserve);
uint256 xPlusH = tokenReserve + virtualTokenReserve;
if (newXPlusH >= xPlusH) return (0, ethIntoCurve, platformFee, creatorFee, vaultFee);
tokensOut = xPlusH - newXPlusH;
if (tokensOut > tokenReserve) tokensOut = tokenReserve;
}
/// @notice Preview a sell: given tokensIn, return net ETH out to seller.
function quoteSell(uint256 tokensIn) public view returns (
uint256 ethOut,
uint256 grossEthOut,
uint256 platformFee,
uint256 creatorFee,
uint256 vaultFee
) {
uint256 newX = tokenReserve + tokensIn;
uint256 newYPlusR = curveK / (newX + virtualTokenReserve);
uint256 yPlusR = ethReserve + virtualEthReserve;
if (newYPlusR >= yPlusR) return (0, 0, 0, 0, 0);
grossEthOut = yPlusR - newYPlusR;
if (grossEthOut > ethReserve) grossEthOut = ethReserve;
(platformFee, creatorFee, vaultFee) = _splitFees(grossEthOut);
ethOut = grossEthOut - platformFee - creatorFee - vaultFee;
}
function _splitFees(uint256 amount) internal view returns (uint256 platformFee, uint256 creatorFee, uint256 vaultFee) {
platformFee = (amount * PLATFORM_FEE_BPS) / 10_000;
creatorFee = (amount * creatorFeeBps) / 10_000;
vaultFee = (amount * vaultBps) / 10_000;
}
// --- Terminal-friendly views ------------------------------------------
// Cheap eth_call helpers so indexers (DEX Screener launchpad module,
// GeckoTerminal, Photon, GMGN, Axiom, etc.) can render progress / price /
// marketcap without decoding events.
/// @notice Graduation progress in basis points (0..10000).
/// Uses both ETH-raised model (our threshold) and tokens-sold
/// model, whichever is further along, so generic parsers work.
function graduationProgressBps() external view returns (uint16) {
if (graduated) return 10_000;
uint256 ethBps = graduationThreshold == 0 ? 0 : (ethReserve * 10_000) / graduationThreshold;
uint256 totalSupply_ = token.totalSupply();
uint256 sold = totalSupply_ > tokenReserve ? totalSupply_ - tokenReserve : 0;
uint256 tokenBps = totalSupply_ == 0 ? 0 : (sold * 10_000) / totalSupply_;
uint256 best = ethBps > tokenBps ? ethBps : tokenBps;
if (best > 10_000) best = 10_000;
return uint16(best);
}
/// @notice Marginal price on the curve as wei-per-token in Q64.96 format,
/// same shape Uniswap V3 uses so terminals can reuse existing sqrtPriceX96 math.
/// price = (y + r) / (x + h). Returned as sqrt(price) * 2^96.
function currentPriceX96() external view returns (uint160) {
uint256 num = ethReserve + virtualEthReserve;
uint256 den = tokenReserve + virtualTokenReserve;
if (den == 0) return 0;
uint256 ratioX96 = (num << 96) / den;
uint256 s = _sqrt(ratioX96);
uint256 result = s << 48;
if (result > type(uint160).max) return type(uint160).max;
return uint160(result);
}
/// @notice Implied market cap in wei = price × totalSupply.
function marketCapWei() external view returns (uint256) {
uint256 num = ethReserve + virtualEthReserve;
uint256 den = tokenReserve + virtualTokenReserve;
if (den == 0) return 0;
return (num * token.totalSupply()) / den;
}
// --- Trades ------------------------------------------------------------
function buy(uint256 minTokensOut) external payable nonReentrant returns (uint256 tokensOut) {
require(!graduated, "graduated");
require(msg.value > 0, "ethIn=0");
uint256 ethIntoCurve;
uint256 platformFee;
uint256 creatorFee;
uint256 vaultFee;
(tokensOut, ethIntoCurve, platformFee, creatorFee, vaultFee) = quoteBuy(msg.value);
require(tokensOut >= minTokensOut, "slippage");
require(tokensOut > 0, "out=0");
// Effects
tokenReserve -= tokensOut;
ethReserve += ethIntoCurve;
// Fee payouts (ETH)
if (platformFee > 0) _safePayETH(platformFeeReceiver, platformFee);
if (creatorFee > 0) _safePayETH(creatorFeeReceiver, creatorFee);
if (vaultFee > 0 && stocksVault != address(0)) _safePayETH(stocksVault, vaultFee);
// Send tokens to buyer
require(token.transfer(msg.sender, tokensOut), "xfer");
emit Buy(msg.sender, msg.value, tokensOut, ethReserve, tokenReserve, block.timestamp);
// Try to graduate; if it reverts (e.g. pool.initialize race, gas spike),
// the buy still succeeds and anyone can call graduate() to retry.
if (ethReserve >= graduationThreshold) {
try this._graduateExternal() {} catch {}
}
}
/// @notice Public retry — anyone can push the curve into graduation once the
/// ETH threshold has been reached. Prevents funds from being stranded in the
/// curve if the atomic graduation inside `buy()` reverted for any reason.
function graduate() external nonReentrant {
require(!graduated, "graduated");
require(ethReserve >= graduationThreshold, "threshold");
_graduate();
}
/// @dev External wrapper so `buy()` can call `_graduate()` in a subcontext
/// that can be try/catch'd. Only self-callable.
function _graduateExternal() external {
require(msg.sender == address(this), "self");
_graduate();
}
function sell(uint256 tokensIn, uint256 minEthOut) external nonReentrant returns (uint256 ethOut) {
require(!graduated, "graduated");
require(tokensIn > 0, "in=0");
uint256 grossEthOut;
uint256 platformFee;
uint256 creatorFee;
uint256 vaultFee;
(ethOut, grossEthOut, platformFee, creatorFee, vaultFee) = quoteSell(tokensIn);
require(ethOut >= minEthOut, "slippage");
require(ethOut > 0, "out=0");
// Pull tokens
require(token.transferFrom(msg.sender, address(this), tokensIn), "xferFrom");
// Effects
tokenReserve += tokensIn;
ethReserve -= grossEthOut;
// Payouts
if (platformFee > 0) _safePayETH(platformFeeReceiver, platformFee);
if (creatorFee > 0) _safePayETH(creatorFeeReceiver, creatorFee);
if (vaultFee > 0 && stocksVault != address(0)) _safePayETH(stocksVault, vaultFee);
_safePayETH(msg.sender, ethOut);
emit Sell(msg.sender, tokensIn, ethOut, ethReserve, tokenReserve, block.timestamp);
}
// --- Graduation --------------------------------------------------------
function _graduate() internal {
graduated = true;
uint256 wethAmount = ethReserve;
uint256 tokenAmount = tokenReserve;
require(wethAmount > 0 && tokenAmount > 0, "empty");
// Wrap ETH into WETH
weth.deposit{value: wethAmount}();
// Determine ordering
address t = address(token);
address w = address(weth);
bool tokenIsToken0 = t < w;
(address token0, address token1) = tokenIsToken0 ? (t, w) : (w, t);
// Compute sqrtPriceX96 from final curve reserves
// price (token1 per token0) = reserve1 / reserve0
uint256 reserve0 = tokenIsToken0 ? tokenAmount : wethAmount;
uint256 reserve1 = tokenIsToken0 ? wethAmount : tokenAmount;
uint160 sqrtPriceX96 = _sqrtPriceX96(reserve1, reserve0);
// Create + init pool. If the pool exists and is already initialized
// (race with an external actor), skip init and reuse the existing sqrtPrice.
address pool = dexFactory.getPool(token0, token1, POOL_FEE);
if (pool == address(0)) {
pool = dexFactory.createPool(token0, token1, POOL_FEE);
}
try IUniswapV3Pool(pool).initialize(sqrtPriceX96) {} catch {}
poolAfterGraduation = pool;
// Approve position manager
require(token.approve(address(positionManager), tokenAmount), "approve tok");
require(IERC20(w).approve(address(positionManager), wethAmount), "approve weth");
// Full range
int24 minTick = -887272 / TICK_SPACING * TICK_SPACING;
int24 maxTick = 887272 / TICK_SPACING * TICK_SPACING;
(uint256 tokenId,,,) = positionManager.mint(INonfungiblePositionManager.MintParams({
token0: token0,
token1: token1,
fee: POOL_FEE,
tickLower: minTick,
tickUpper: maxTick,
amount0Desired: reserve0,
amount1Desired: reserve1,
amount0Min: 0,
amount1Min: 0,
recipient: address(this),
deadline: block.timestamp
}));
lpTokenId = tokenId;
// Lock LP: transfer to a fresh LpLocker for this token.
LpLocker locker = new LpLocker(
positionManager, weth, t,
platformFeeReceiver, creatorFeeReceiver,
/*platformBps*/ 100, /*deployerBps*/ 100, // fixed 50/50 split of Sushi 1% pool fee
tokenId
);
positionManager.safeTransferFrom(address(this), address(locker), tokenId);
lpLockerAddress = address(locker);
// Wire the real (now seeded) pool into the token so honeypot scanners
// that read `token.pool()` simulate against real liquidity. Wrapped in
// try/catch so a re-entered or already-set pool cannot brick graduation.
try token.setPool(pool) {} catch {}
// Zero out reserves (all funds now in pool)
tokenReserve = 0;
ethReserve = 0;
emit Graduated(t, pool, tokenId, wethAmount, tokenAmount, address(locker), block.timestamp);
}
// --- helpers -----------------------------------------------------------
function _safePayETH(address to, uint256 amount) internal {
(bool ok, ) = to.call{value: amount}("");
require(ok, "ETH pay");
}
/// @dev sqrtPriceX96 = sqrt(reserve1 / reserve0) * 2^96, computed as sqrt(reserve1 * 2^192 / reserve0).
function _sqrtPriceX96(uint256 reserve1, uint256 reserve0) internal pure returns (uint160) {
require(reserve0 > 0, "r0=0");
// Use mulDiv-safe path: sqrt((reserve1 << 192) / reserve0)
// reserve1 is at most ~4e18, so << 192 fits since Solidity 0.8 handles 256-bit.
// But (reserve1 << 192) may overflow if reserve1 > 2^64. So use two-step: sqrt(reserve1/reserve0) * 2^96.
// For our range (reserve1 up to 100 ether = 1e20, reserve0 up to 1e27 tokens), we need higher precision.
// Approach: compute ratio = (reserve1 * 2^96) / reserve0 (safe since reserve1 * 2^96 <= 2^96 * 1e20 < 2^160),
// then sqrt(ratio) * 2^48 == sqrt(ratio * 2^96). Actually sqrt(reserve1/reserve0) * 2^96 = sqrt(reserve1 * 2^192 / reserve0).
// Safer: use 512-bit intermediate via unchecked * shift; we'll approximate with 2-step.
uint256 ratioX96 = (reserve1 << 96) / reserve0; // Q96 ratio of prices
// sqrt(ratio) with Q96 → sqrt(ratioX96 * 2^96) = sqrt(ratioX96) * 2^48
uint256 sqrtRatioX48 = _sqrt(ratioX96);
uint256 result = sqrtRatioX48 << 48;
require(result <= type(uint160).max, "sqrtPrice ovf");
return uint160(result);
}
function _sqrt(uint256 x) internal pure returns (uint256) {
if (x == 0) return 0;
uint256 z = (x + 1) / 2;
uint256 y = x;
while (z < y) { y = z; z = (x / z + z) / 2; }
return y;
}
function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
return this.onERC721Received.selector;
}
receive() external payable {}
}
Chain explorer8792msChain node82ms