> ## Documentation Index
> Fetch the complete documentation index at: https://docs.windrose.market/llms.txt
> Use this file to discover all available pages before exploring further.

# BondingCurve

> The per-launch constant-product curve with virtual reserves: quotes, buys, sells, the graduation target, pool seeding and pull-claimed fees.

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](/protocol/contracts/liquidity-locker).

Source: `contracts/src/launch/BondingCurve.sol` (`ReentrancyGuardTransient`). Interface: `contracts/src/interfaces/IBondingCurve.sol`. The implementation address per chain is in [Addresses](/protocol/addresses); each launch's clone is in `LaunchCreated` and `LaunchFactory.curveOfToken`.

## Initialisation

```solidity theme={"system"}
struct Init {
    address token;
    address quoteAsset;
    bytes32 code;
    address creator;
    uint256 virtualToken;
    uint256 virtualQuote;      // 18-decimal quote units, already converted at the oracle rate
    uint256 curveSupply;
    uint256 lpSupply;
    uint16  tradeFeeBps;
    uint16  creatorFeeShareBps;
    uint16  graduationFeeBps;
}

function initialize(Init calldata i) external;
```

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.

```
graduationQuote = ceil(virtualQuote * curveSupply / virtualToken), rounded up to a multiple of quoteScale
```

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

| Variable                                    | Unit          | Meaning                                                                              |
| ------------------------------------------- | ------------- | ------------------------------------------------------------------------------------ |
| `tokensSold`                                | 18-dec tokens | net tokens sold on the curve (buys minus sells)                                      |
| `realQuote`                                 | quote units   | net quote in the reserve after fees, always a multiple of `quoteScale`               |
| `creatorFeesAccrued`, `protocolFeesAccrued` | raw quote     | fees waiting to be pulled                                                            |
| `ready`                                     | bool          | set by the buy that reaches `graduationQuote` or sells out the curve; trading closed |
| `graduated`                                 | bool          | `graduate()` has run                                                                 |
| `poolId`, `positionTokenId`                 |               | the Uniswap v4 pool and the locked position after graduation                         |

## The maths

With `x = virtualToken + curveSupply - tokensSold` and `y = virtualQuote + realQuote` (both 18-decimal), the product `x * y` is constant across trades.

| Operation                              | Formula                                                                 |
| -------------------------------------- | ----------------------------------------------------------------------- |
| Spot price                             | `spotPrice() = y * 1e18 / x` (quote units per whole token, 1e18)        |
| Buy with `net` quote units (after fee) | `tokensOut = x * net / (y + net)`                                       |
| Sell `tokensIn`                        | `gross = y * tokensIn / (x + tokensIn)`, then the fee comes off `gross` |
| Progress                               | `progressBps() = realQuote * 10000 / graduationQuote`                   |
| Remaining                              | `tokensRemaining() = curveSupply - tokensSold`                          |

### Quotes

```solidity theme={"system"}
function quoteBuy(uint256 quoteInRaw) external view returns (uint256 tokensOut, uint256 quoteUsedRaw, uint256 feeRaw);
function quoteSell(uint256 tokensIn) external view returns (uint256 quoteOutRaw, uint256 feeRaw);
```

`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

```solidity theme={"system"}
function buy(uint256 quoteInRaw, uint256 minTokensOut, address recipient) external nonReentrant returns (uint256 tokensOut);
function sell(uint256 tokensIn, uint256 minQuoteOutRaw, address recipient) external nonReentrant returns (uint256 quoteOutRaw);
```

Anyone may call them directly after approving the quote asset (buy) or the launch token (sell) to the curve; the [Router](/protocol/contracts/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

```solidity theme={"system"}
function graduate() external nonReentrant returns (uint256 positionTokenId);
```

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.

<Steps>
  <Step title="Take the graduation fee">
    `reserveRaw = realQuote / quoteScale`; `gradFee = reserveRaw * graduationFeeBps / 10000` is added to `protocolFeesAccrued`; `lpQuoteRaw = reserveRaw - gradFee` goes to the pool.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

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](/protocol/contracts/liquidity-locker).

## Fees

```solidity theme={"system"}
function claimCreatorFees(address to) external nonReentrant;   // creator only
function sweepProtocolFees() external nonReentrant;            // anyone; pays factory.feeCollector()
```

`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

```solidity theme={"system"}
function token() / quoteAsset() / creator() / factory() external view returns (address);
function code() external view returns (bytes32);
function quoteScale() / virtualToken() / virtualQuote() / curveSupply() / lpSupply() / graduationQuote() external view returns (uint256);
function tradeFeeBps() / creatorFeeShareBps() / graduationFeeBps() external view returns (uint16);
function tokensSold() / realQuote() / creatorFeesAccrued() / protocolFeesAccrued() external view returns (uint256);
function ready() / graduated() external view returns (bool);
function poolId() external view returns (PoolId);
function positionTokenId() external view returns (uint256);
function spotPrice() / progressBps() / tokensRemaining() external view returns (uint256);
```

## Events

```solidity theme={"system"}
event Buy(address indexed buyer, address indexed recipient, uint256 quoteIn, uint256 fee, uint256 tokensOut, uint256 tokensSold, uint256 realQuote);
event Sell(address indexed seller, address indexed recipient, uint256 tokensIn, uint256 quoteOut, uint256 fee, uint256 tokensSold, uint256 realQuote);
event ReadyToGraduate(uint256 realQuote, uint256 tokensSold);
event Graduated(PoolId indexed poolId, uint256 positionTokenId, uint256 lpQuote, uint256 lpTokens, uint256 burned);
event CreatorFeesClaimed(address indexed to, uint256 amount);
event ProtocolFeesSwept(address indexed to, uint256 amount);
```

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

| Error                  | When                                                                  |
| ---------------------- | --------------------------------------------------------------------- |
| `AlreadyInitialized()` | second `initialize`, or any call on the implementation                |
| `InvalidInit()`        | out-of-bounds `Init`, quote decimals above 18, zero graduation target |
| `CurveClosed()`        | `buy` or `sell` after `ready`                                         |
| `ZeroAmount()`         | zero input                                                            |
| `Slippage()`           | output below the minimum, or zero                                     |
| `ExceedsSold()`        | selling more than `tokensSold`                                        |
| `NotReady()`           | `graduate` before the curve closes                                    |
| `AlreadyGraduated()`   | second `graduate`                                                     |
| `NotCreator()`         | `claimCreatorFees` by another account                                 |
| `NoLiquidity()`        | the position would have zero liquidity                                |

## 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](/protocol/integrate/security-model).
