# Graduation

When the curve's sellable allocation reaches zero — the same moment its real reserve reaches
the graduation threshold — the launch graduates. That is two steps, and understanding why
they are separate is the difference between a working integration and a stuck token.

## The two steps

```
factory.graduate(token)            phase 0 -> 1   sweeps the curve into the factory
factory.createGraduatedPool(token) phase 1 -> 2   seeds the V4 pool, locks the position
```

Both are **permissionless** — anyone may call either. Both normally run automatically inside
the buy that crosses the threshold, wrapped in `try/catch` so that a failure to graduate
never fails the trade.

## The gas trap

That `try/catch` is also the trap.

Seeding the pool costs roughly **900,000 gas** on its own. Under EIP-150's 63/64 rule the
nested call only receives 63/64 of whatever the buy has left. If the buy was sized by a naive
`eth_estimateGas`, there is not enough left and the seed dies.

And estimation **cannot** discover this. The estimator simulates the whole transaction
including the `try/catch`; the seed fails inside it; the catch swallows the failure; the
transaction *succeeds*. So the estimator returns a limit sized for the **seed-fails** path —
which is by construction never enough for the seed-succeeds path.

This is deterministic, not flaky. With naive estimation the auto-seed fails **every time**,
and reports success while doing it.

Measured on Arc testnet, two launches identical except the gas limit:

| Gas limit | Result |
|---|---|
| 1,154,267 (from `estimateGas`) | `AutoSeedFailed(token, 27828)` — stuck in phase 1 |
| 3,000,000 (explicit) | seeded in the same transaction |

The shortfall was about 150,000 gas.

> [!WARNING]
> On any buy that might cross the threshold, send an explicit gas limit — roughly
> `estimate + 1,200,000`, or simply 3,000,000. You cannot control third-party routers'
> gas limits, so run the keeper below regardless.

## Watch for the failure events

The curve tells you when it happened:

```solidity
event AutoGraduationFailed(address indexed token, uint256 gasRemaining);
event AutoSeedFailed(address indexed token, uint256 gasRemaining);
```

Both are the signal to finish the job manually. Alert on them, and have a keeper call the
matching entrypoint:

```ts
// finish a launch stuck in phase 1
const gas = await client.estimateContractGas({
  address: FACTORY, abi: factoryAbi, functionName: "createGraduatedPool", args: [token], account,
});
await walletClient.writeContract({
  address: FACTORY, abi: factoryAbi, functionName: "createGraduatedPool",
  args: [token], gas: gas * 2n,
});
```

Estimation *is* reliable here — called directly there is no `try/catch` hiding the cost.

A launch in phase 1 is not broken and nothing is lost: the swept reserves sit safely in the
factory and the seed is retryable by anyone, indefinitely. It simply has no venue until
somebody calls it.

## What the seed does

1. Computes the token side as `sweptTokens · sweptQuote / (sweptQuote + phantomQuote)`.
2. **Permanently locks the remainder** in the launch locker — that supply is burned in every
   sense that matters; the locker has no withdrawal path for anyone, including its owner.
3. Initialises the V4 pool at the curve's closing price, with the foci hook attached.
4. Mints a full-range position **directly to the locker**, which is why the liquidity can
   never be pulled.

At the shipped curve shape, 71.43% of supply is sold on the curve, 20.41% is seeded into the
pool, and **8.16% is burned forever**.

## Checking the phase

```ts
const launch = await client.readContract({
  address: FACTORY, abi: factoryAbi, functionName: "getLaunchedToken", args: [token],
});
// 0 curve · 1 swept (no venue) · 2 pool · 3 rescued
```

`getLaunchedToken` returns a **zeroed struct** for an unknown token rather than reverting —
check `.exists` before trusting `.phase`, or an unknown address looks like a live curve.