Skip to main content
Every launch trades on its own 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

Only callable once per clone (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.
After 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

With x = 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

Anyone may call them directly after approving the quote asset (buy) or the launch token (sell) to the curve; the Router and the factory are conveniences that add dollar routing, deadlines and auto-graduation. 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

Permissionless. Reverts 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.
After graduation 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

In 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 in contracts/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 covers realQuote / quoteScale + creatorFeesAccrued + protocolFeesAccrued.
  • invariant_tokenConservation: the curve’s token balance plus tokensSold equals the total supply, and tokensSold never exceeds curveSupply.
  • invariant_reserveBounded: realQuote never exceeds graduationQuote, and total quote paid out never exceeds total quote taken in.
The curve has no external price input, so front-running and sandwiching a trade are bounded only by the trader’s own minTokensOut / minQuoteOutRaw. See Security model.