Protocol
Graduation
The atomic migration from curve to a JoseonSwap pair.
One transaction, no window#
Graduation is not a job someone runs later. The buy that sells out the curve calls
MigrationManagerV2.graduate() inline, so the pair is created, funded, minted, and locked before that transaction
returns. There is no interval in which the pool exists but is unfunded, and no keeper to trust.
final buy
└─ curve: state ──▶ GRADUATING, emit CurveCompleted
└─ migration manager
1. registry.startGraduation(token)
2. launchFactory.createLaunchPair(token, WETH)
3. assert the pair is completely empty
4. curve.releaseMigrationAssets() ──▶ ETH + 200M tokens, protocol fee paid out
5. assert |curve price - pool price| <= 0.5%
6. wrap ETH, transfer both sides into the pair
7. pair.mint() ──▶ initial LP
8. locker.lock() ──▶ LP held forever
9. registry.completeGraduation(token, pair)If any step fails the whole buy reverts. A launch cannot end up half-migrated.
What moves#
migrationWETH = realEthReserve - creatorFeeAccrued - protocolFeeAccrued
migrationToken = dexTokenReserve
initialDexPrice = migrationWETH / migrationTokenWith no sells on the curve, that resolves to:
| WETH into the pool | 3.6 |
| Tokens into the pool | 200,000,000 |
| Opening pool price | 0.000000018 ETH per token |
| Pool TVL | 7.2 ETH |
| FDV | 18 ETH |
Accrued creator fees stay in the curve contract, claimable afterwards. The protocol fee is paid out during the same call. Neither is treated as liquidity.
getMigrationQuote(token) returns the same three numbers as a view, so you can read what a launch would migrate before
it happens.
The price-gap check#
Curve price and pool price have to agree, or the first arbitrageur takes the difference from whoever bought last. The manager enforces it with a hard assertion:
|curvePrice - dexPrice| × 10000 <= curvePrice × 50 // 0.5% toleranceThe tolerance exists for integer rounding, not for market drift. Because the curve's end state is fixed, the real gap is a rounding error rather than a spread.
Why the pair must be empty#
Before funding, the manager requires both reserves to be zero and both token balances on the pair to be zero. That closes the obvious attack: create the token/WETH pair first, seed it at an absurd ratio, and let the graduation supply liquidity into your price. See launch-pair protection for how the pair itself refuses to be used before its seeded mint.
Permanent liquidity#
The initial LP tokens are minted to the migration manager, approved to the PermanentLiquidityLocker, and pulled in by
the locker in the same transaction. The locker has exactly one external function, lock, and it:
- only accepts calls from the migration manager,
- rejects a token that already has a locked pair,
- verifies the pair is the factory's canonical token/WETH pair,
- verifies the factory considers it a launch pair.
There is no withdraw function. Not gated, not timelocked — absent. The initial position cannot be removed by anyone, including us.
Only the initial LP is locked. Anyone can add or remove their own liquidity on top of it afterwards, and those LP tokens belong to them.
After graduation#
CURVE_ACTIVE → BondingCurvePool
GRADUATING → trading paused
DEX_ACTIVE → JoseonSwapRouterThe curve's buy and sell revert with NOT_ACTIVE once state has moved on, so a stale UI fails loudly rather than
trading against a dead pool.
The indexer normalises the pair's Swap events into the same eth-per-token price it used for curve trades, so charts
and trade history stay continuous across the switch instead of restarting at the pool.
Creator fees do not stop at graduation either — they change source. See fees.