Joseon docs
AppExplorerLaunch app

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 / migrationToken

With no sells on the curve, that resolves to:

WETH into the pool3.6
Tokens into the pool200,000,000
Opening pool price0.000000018 ETH per token
Pool TVL7.2 ETH
FDV18 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% tolerance

The 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   → JoseonSwapRouter

The 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.