> For the complete documentation index, see [llms.txt](https://docs.kumbaya.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kumbaya.xyz/developers/launchpad/graduation.md).

# Bonding curve and graduation

A launchpad token's life on-chain has three states: **bonding**, **grace**, and **graduated**. This page describes how a token moves between them and what each transition does.

## The bonding curve

When `FireLaunch.ignite()` runs, `FireGraduator.createPositions()` deploys **`numPositions` concentrated V3 positions** (must be in `[5, 50]` per registry bounds) covering `[tickLower, tickUpper]`. Tokens-per-position is `tokensToSell / numPositions` (integer division; remainder goes to the last position). The bonding-curve effect emerges from the way these stacked positions are consumed in order as price moves through them.

There's also a **tail position** for post-graduation liquidity. It's anchored at the graduation tick and extends *outward*:

* **Token is `token0`** → tail covers `[graduationTick, MAX_TICK_aligned]`
* **Token is `token1`** → tail covers `[MIN_TICK_aligned, graduationTick]`

(Tick bounds are aligned inward to the fee tier's tick spacing using `TickMath.minUsableTick` / `maxUsableTick`.) Even before graduation, the tail provides liquidity outside the curve range so the pool never goes empty.

Trading on the pool is unmodified V3 - buyers swap via `SwapRouter02` like for any other token. The "curve" is just an emergent property of how the seed positions are stacked.

## Recording the graduation condition

```solidity
function recordGraduationCondition(address token) external;
```

Anyone can call this. It checks whether the pool's current tick has crossed the graduation tick (the comparison direction depends on `token0`/`token1` ordering: token0 launches need `currentTick ≥ graduationTick`, token1 launches need `currentTick ≤ graduationTick`). On success it sets `graduationConditionMetAt = block.timestamp`, which **starts the grace period**.

* **One-shot, not idempotent:** the function reverts with `ConditionAlreadyRecorded` if `graduationConditionMetAt != 0`. The grace timer is set once and never reset.
* **Pre-graduation only:** reverts with `AlreadyGraduated` if the token has already graduated.
* **Owner-disable switch:** the registry owner can flip `graduationRecordingEnabled` (via `FireLaunch.setGraduationRecordingEnabled`) to pause this entry point in an emergency. Disabled state reverts with `GraduationRecordingDisabled`.
* **Caller incentive:** none baked into the contract - but UIs and bots are motivated to call it because they want to trigger graduation downstream.

## The grace period

Configured in `FireRegistry`:

* **`gracePeriodDuration`** - bounded `[1 hour, 7 days]`. Read the live mainnet value with `FireRegistry.gracePeriodDuration()`.
* **`forceGraduationDelay`** - applied from the token's `createdAt`. Read with `FireRegistry.forceGraduationDelay()`.

Two roles can graduate during/after grace:

| Caller             | When they can graduate                                                                                                                      |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| **Guardian**       | Immediately, as long as `canGraduate(token)` returns `true` (no grace period required)                                                      |
| **Anyone**         | After `gracePeriodDuration` elapses since `graduationConditionMetAt`, *and* `canGraduate(token)` is `true`                                  |
| **Anyone (force)** | After `block.timestamp ≥ createdAt + forceGraduationDelay`, even if `recordGraduationCondition` was never called. Bypasses the grace check. |

The grace period exists to give the protocol a window to react to abnormal conditions before liquidity reshapes.

## `graduate()`

```solidity
function graduate(address token) external nonReentrant whenNotPaused;
```

What it does, in order - straight from `FireGraduator._graduate`:

1. **`FireToken.setGraduated()`** - runs first. Sets `graduated = true`, `vestingStart = block.timestamp`, and emits `Graduated()`. From this moment on the skim hook is disabled and the on-token vesting clock starts.
2. **Burn every bonding-curve and tail position.** Loops over `positions[token]`, calling `IUniswapV3Pool.burn` and `collect` for each. Tracks principal and fees separately.
3. **Distribute pre-graduation fees** via `_distributeFees`: all to the protocol (the contract splits `graduationFeeBps` to the registry `treasury` and the remainder to its `integrator`, but both are the same Kumbaya protocol address). See [**Fees**](/developers/launchpad/fees-and-credits.md).
4. **Mint a full-range NFT** via `NonfungiblePositionManager.mint` with `tickLower = TickMath.minUsableTick(tickSpacing)`, `tickUpper = TickMath.maxUsableTick(tickSpacing)`. Recipient is `FireStream`. Any unused principal (mint dust) is sent to the protocol.
5. **`FireStream.receiveNFT(token, nftId, creator)`** - registers the NFT for fee streaming and pins the creator address.
6. **`FuelVault.onGraduated(token)`** - sets `graduated[token] = true` and `burnTime[token] = block.timestamp + registry.burnCountdownDuration()`.
7. **`FireLaunch.setGraduated(token, nftId)`** - flips the launch state's `graduated` flag and stores `graduatedNftId`.

After `graduate` completes the pool is just an ordinary Uniswap V3 pool. There's nothing launchpad-specific in the trading path anymore.

## State you can read

Use **`FireLaunch.getLaunchState(token)`** to inspect a token's lifecycle state:

```solidity
function getLaunchState(address token) external view returns (LaunchState memory);

struct LaunchState {
    address  token;                       // FireToken address
    uint24   feeTier;
    int24    tickSpacing;
    uint16   graduationFeeBps;            // pre-grad protocol share, snapshotted at launch
    bool     isToken0;
    bool     graduated;
    address  numeraire;
    int24    graduationTick;              // = tickUpper if isToken0 else tickLower
    address  pool;
    address  creator;                     // ignite() caller
    uint256  graduatedNftId;              // 0 until graduation
    uint256  graduationConditionMetAt;    // 0 until recordGraduationCondition succeeds
    uint256  createdAt;
}
```

`FireGraduator.canGraduate(token)` returns `true` when graduation is possible (price reached, OR `forceGraduationDelay` elapsed since `createdAt`).

You can also read `FireToken.graduated` (public bool) and `FireToken.vestingStart` (public uint256) directly off the token contract.

## What happens to the tail liquidity?

The tail position is **burned at graduation**, just like the bonding-curve positions. It does not survive into the graduated state as a separate position. The full-range NFT held by `FireStream` is the only launchpad-related liquidity afterward - alongside whatever organic LPs add post-graduation.

## Errors to handle

`FireGraduator` errors:

| Error                   | When                                                                                                                  |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `AlreadyGraduated`      | `graduate()` called twice, or `claimFees()` called post-graduation on the graduator                                   |
| `ConditionNotMet`       | `graduate()` called but `canGraduate(token)` returns false (price not at tick AND force-graduation delay not elapsed) |
| `GracePeriodNotElapsed` | Non-guardian called `graduate()` after `recordGraduationCondition` but before `gracePeriodDuration` elapsed           |

`FireLaunch` errors relevant to graduation:

| Error                         | When                                                                                   |
| ----------------------------- | -------------------------------------------------------------------------------------- |
| `AlreadyGraduated`            | `recordGraduationCondition` called on a graduated token                                |
| `ConditionAlreadyRecorded`    | `recordGraduationCondition` called twice for the same token                            |
| `ConditionNotMet`             | `recordGraduationCondition` called when the price hasn't crossed the graduation tick   |
| `GraduationRecordingDisabled` | `recordGraduationCondition` called while the registry owner has paused the entry point |

> Note: `OnlyGuardian` is a **`FuelVault`** error (used for guardian-relayed gifts), not a graduation error. The guardian path on `graduate()` is gated by an `if (isGuardian || forceGraduationAvailable)` check rather than a single revert.

## What changes for integrators after graduation

* **Pool address is unchanged** - it's still the same V3 pool. Quotes, swaps, and existing tooling continue to work.
* **NFT ownership** - `NonfungiblePositionManager.ownerOf(positionTokenId) == FireStream`.
* **Fee claim path** - pre-grad fees came from `FireGraduator.claimFees()`; post-grad fees come from `FireStream.claimFees()`. See [**Fees and credits**](/developers/launchpad/fees-and-credits.md).
* **Vesting** - `FireToken.releaseVested()` becomes meaningful (the schedule unlocks linearly from graduation).
* **FuelVault burn timer** is now ticking. After it elapses, anyone can call `executeBurn(token)` to permanently destroy unclaimed user credits (creator-bucket credits are protected).
