BondingCurve, a minimal-proxy clone initialised once by the factory. It sells the curve supply along x * y = k with virtual reserves on both sides, accrues a fee per trade, closes itself when the reserve reaches the graduation target and can then be graduated by anyone into a full-range Uniswap v4 position owned by the LiquidityLocker.
Source: contracts/src/launch/BondingCurve.sol (ReentrancyGuardTransient). Interface: contracts/src/interfaces/IBondingCurve.sol. The implementation address per chain is in Addresses; each launch’s clone is in LaunchCreated and LaunchFactory.curveOfToken.
Initialisation
AlreadyInitialized; the implementation contract locks itself in its constructor). The caller becomes factory. Validation (InvalidInit): non-zero token, quote asset and reserves, tradeFeeBps <= 1000, creatorFeeShareBps <= 10000, graduationFeeBps <= 2000, quote decimals at most 18. The curve reads decimals() from the quote asset and stores quoteScale = 10^(18 - decimals).
The graduation target is derived, not configured: the quote needed to sell the whole curve supply, rounded up to a raw-representable amount.
initialize, token, quoteAsset, code, creator, factory, quoteScale, virtualToken, virtualQuote, curveSupply, lpSupply, graduationQuote, tradeFeeBps, creatorFeeShareBps and graduationFeeBps never change. There is no owner and no setter.
State
The maths
Withx = virtualToken + curveSupply - tokensSold and y = virtualQuote + realQuote (both 18-decimal), the product x * y is constant across trades.
Quotes
quoteBuy takes the fee (tradeFeeBps) off quoteInRaw, then caps the net amount at what is still needed to graduate: remainingRaw = (graduationQuote - realQuote) / quoteScale. When the cap binds, quoteUsedRaw is recomputed as the gross amount that yields exactly remainingRaw net (rounded up) and the rest of the input is not taken. tokensOut is further capped at tokensRemaining(). It returns zeros once ready or for a zero input.
quoteSell computes gross from the formula, caps it at realQuote (the curve never pays out more than it holds), takes the fee off the gross and returns the net. It returns zeros once ready, for zero input, or for tokensIn > tokensSold.
Both are exactly what buy and sell execute, so a quote followed by a trade in the same block is exact.
Trading
buy reverts CurveClosed once ready, ZeroAmount for zero input, and Slippage when the quote yields zero tokens or fewer than minTokensOut. It pulls quoteUsedRaw (not the full input) from msg.sender, adds the net amount to realQuote, adds tokensOut to tokensSold, splits the fee between creator and protocol, sets ready and emits ReadyToGraduate when realQuote >= graduationQuote or tokensSold == curveSupply, transfers the tokens to recipient and emits Buy.
sell reverts CurveClosed, ZeroAmount, ExceedsSold when tokensIn > tokensSold, and Slippage below minQuoteOutRaw. It pulls the tokens, removes gross (net plus fee) from realQuote, reduces tokensSold, accrues the fee and pays recipient.
Fees accrue in raw quote units: creator = fee * creatorFeeShareBps / 10000, the rest to the protocol. They stay in the curve’s balance until claimed.
Graduation
NotReady before the curve closes and AlreadyGraduated afterwards. The router calls it inside the buy that fills the curve (wrapped in try, so a pool-side failure never blocks the trade); anyone can call it later if that attempt failed.
1
Take the graduation fee
reserveRaw = realQuote / quoteScale; gradFee = reserveRaw * graduationFeeBps / 10000 is added to protocolFeesAccrued; lpQuoteRaw = reserveRaw - gradFee goes to the pool.2
Size the token side at the final curve price
lpTokens = lpQuote18 * x / y, capped at lpSupply. With the deployed configuration this is 245,000,000 tokens (the pool holds 2% fewer tokens than lpSupply because of the fee), so the pool opens at exactly the curve’s closing price.3
Initialise the pool
The pool key is
{ currency0: min(token, quoteAsset), currency1: max(token, quoteAsset), fee: factory.poolFee(), tickSpacing: factory.tickSpacing(), hooks: 0x0 }. PositionManager.initializePool is called at sqrt(amount1 / amount0) * 2^96, clamped into the valid tick range; it is a no-op if someone already initialised the pool, and the curve then reads the actual sqrtPriceX96 from the PoolManager and adds liquidity at that price instead.4
Mint the full-range position to the locker
Liquidity is computed for the full usable tick range (
minUsableTick to maxUsableTick of the spacing) from the two amounts, reduced by one part per million as rounding headroom (NoLiquidity if it rounds to zero). The curve approves Permit2 for both currencies for one hour, calls modifyLiquidities with MINT_POSITION + SETTLE_PAIR and factory.locker() as the position owner, records positionTokenId (nextTokenId() before the mint), then revokes the Permit2 allowances.5
Burn the rest and settle dust
Every launch token still in the curve (the unused part of
lpSupply, 5,000,000 with the deployed numbers, plus anything unsold) is sent to 0x…dEaD. Any quote balance above the fees owed is added to protocolFeesAccrued. Graduated(poolId, positionTokenId, lpQuoteRaw, lpTokens, burned) is emitted.tokensSold and realQuote keep their final values, buy, sell and both quote functions are closed, and the token trades on Uniswap v4 (pool id poolId(), price from StateView.getSlot0). Nothing can withdraw the position: see LiquidityLocker.
Fees
claimCreatorFees reverts NotCreator for anyone but creator and sends the accrued raw quote to to. sweepProtocolFees sends the protocol share to the factory’s current feeCollector. Both zero the counter first and can be called at any time, before or after graduation. Fees are in the launch’s quote asset (wINR for an INR launch), not in dollars.
Views
Events
Buy, quoteIn is the gross raw amount taken (fee included) and fee its fee; in Sell, quoteOut is the net raw amount paid and fee the fee taken from the gross. tokensSold and realQuote are the post-trade totals, which is how the indexer recomputes the spot price without a call. buyer / seller is msg.sender, normally the router; the indexer records the transaction sender as the trader.
Errors
Properties the test suite checks
Unit tests incontracts/test/BondingCurve.t.sol cover curve sizing by rate and by quote decimals, the graduation cap on the last buy, pool seeding at the curve price (including a 6-decimal quote, a real EUR stablecoin and a pre-initialised pool), fee claims and the creator’s first buy in either asset. Fuzz tests: testFuzz_buySell_neverProfits (a buy followed by a sell of the same tokens never returns more quote than it took) and testFuzz_manyBuys_respectBounds. The invariant suite (contracts/test/Invariants.t.sol, random buys and sells by three actors) holds:
invariant_curveSolvent: the curve’s quote balance always coversrealQuote / quoteScale + creatorFeesAccrued + protocolFeesAccrued.invariant_tokenConservation: the curve’s token balance plustokensSoldequals the total supply, andtokensSoldnever exceedscurveSupply.invariant_reserveBounded:realQuotenever exceedsgraduationQuote, and total quote paid out never exceeds total quote taken in.
minTokensOut / minQuoteOutRaw. See Security model.