Create
Documentation
Guides

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#

code
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 limitResult
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.