Kerf is a first release and may still have small bugs. Our team is building it around the clock - follow updates and new features onEarly release - small bugs possible. Updates onx.com/UseKerf
Kerf
Chapter 08 · Launch & pre-market perp

Launch & pre-market perp

Track, graduate, and a vAMM with no counterparty for tokens no venue lists yet.

A new token has no vault, no perp market and no history. LaunchPipeline gives it the first, over time and only once two things are true on chain; PreMarketPerp gives it the second immediately, on a curve with no counterparty and a documented way of losing money. The one fact to carry out of this chapter: neither contract has an owner, an allow-list or an undo, so a Graduated event and a MarketCreated event are both signals that someone paid gas, not endorsements.

Track and graduate

LaunchPipeline is two permissionless calls and one immutable number. track(token) writes down when a token/WETH pool was first seen and starts a clock; graduate(token) deploys a vault once MIN_AGE (3 days) has passed and the token's pools hold at least MIN_LIQUIDITY between them. MIN_LIQUIDITY is a constructor parameter rather than a constant, and the deploy (KerfWiring) sets it to 10 ether. There is no way to un-track or un-graduate: the contract only ever adds a VaultFactory clone, which itself has no admin.

  1. 01
    track(token)
    Reverts AlreadyTracked if firstSeen[token] is set, then asks bestPool(token) for a token/WETH pool on any of the four fee tiers (100, 500, 3000, 10 000) and reverts NoPool if there is none. Otherwise it stores block.timestamp and emits Tracked(token, firstSeen). Nothing about depth is checked here - a pool with zero liquidity is still a pool.
  2. 02
    Wait MIN_AGE
    Three days from firstSeen, measured on chain. A token nobody tracked has firstSeen == 0 and is TooYoung forever, which is why the graduator bot's first action on a fresh Established pool is track, not graduate.
  3. 03
    graduate(token): the gate
    In order: AlreadyGraduated if vaultOf[token] is set; TooYoung if untracked or younger than MIN_AGE; then one pass of _scan over the four fee tiers, summing each pool's liquidity() and remembering the deepest. NoPool if the scan found nothing, ThinPool if the sum is below MIN_LIQUIDITY. The gate is on the sum because that is the spec's depth test; the vault goes on the single deepest pool because a vault can only hold one position.
  4. 04
    graduate(token): the vault
    _initFor(token, pool) builds an LPVault.Init - WETH as the asset, a CURVE range snapped to the pool's spacing, the policy in the table below, a 5 WETH cap - and hands it to vaultFactory.createV3Vault. The clone's address is stored in vaultOf[token] and Graduated(token, vault, pool) is emitted.
track and graduateTwo permissionless calls. Red chips are the reverts, in the order the contract tests them; the dashed arrow is the three-day clock between the two calls. Market cap and trade count are never read here.
yesnonoyesMIN_AGE = 3 daysyesnonoyesnoyesnoyestrack(token)firstSeen[token]!= 0?AlreadyTrackedbestPool(token)exists?NoPoolfirstSeen[token] = nowTracked(token, firstSeen)graduate(token)vaultOf[token]!= 0?AlreadyGraduatedtracked andnow − firstSeen ≥ 3 d?TooYoung_scan(token)getPool(token, WETH, fee) × 4 tiersdeepest pool · Σ liquidity()any pool?NoPoolΣ liquidity ≥MIN_LIQUIDITY (10 ether)?ThinPool_initFor(token, pool)WETH side · CURVE ±25 % snappedhysteresis 10 spacings · 6 h100 bps · bounty 50 · cap 5 ETHvaultFactory.createV3VaultEIP-1167 clone · no adminGraduated(token, vault, pool)Established is a labelMarket cap > $1M and ≥ 100 trades arethe indexer’s flag, shown on /launch,never checked here.Depth is measured, not trustedliquidity() is in-range liquidity atthe instant of the call: a flash loancan lift a thin pool over the linefor one block. Graduated is a signal.

canGraduate(token) runs the same four tests as a view and returns (ok, reason), where reason is the four-byte selector of the error graduate would revert with, or bytes4(0). That is what the disabled Graduate button on /launch shows: lib/abi/launchPipeline.ts derives the selectors with toFunctionSelector rather than pasting them, so a renamed error shows up as a missing label, not a wrong one.

canGraduate reasonSelector ofButton label
Already has a vaultAlreadyGraduated()Already graduated
Untracked, or younger than 3 daysTooYoung()Needs 3 days tracked
No token/WETH pool on any tierNoPool()No token/WETH pool
Summed in-range liquidity below MIN_LIQUIDITYThinPool()Pool too thin

NoPool is unreachable from graduate and canGraduate in practice: a token with no pool cannot have been tracked, so the age test answers first, and Uniswap pools are never deleted. The branch stays because the pool address is read at call time, not remembered from track.

Two more views exist for the UI. bestPool(token) returns the deepest pool, its fee tier and its in-range liquidity; the tie-break is >=, so a token whose pools are all empty still reports one of them and callers see NoPool only when there really is no pool. curveWidth(spacing) returns 2 × ((CURVE_HALF_TICKS / spacing) × spacing) with CURVE_HALF_TICKS = 2231 - ln(1.25) / ln(1.0001) floored - so a 0.30 % pool (spacing 60) gets a range 4 440 ticks wide, ±25 % less the snap.

The vault's name and symbol come from the token's symbol() through a try: a token that reverts, answers with a bytes32, or answers with an empty string still graduates, as Kerf TOKEN Vault / kTOKEN.

The graduated vault

Every graduation produces the same vault, and it is the same vault the deploy seeds for ETH and USDG: KerfWiring uses identical constants, so a graduated token is managed exactly like the flagship pools. The only things that vary are the pool, the range snap and the name.

LPVault.Init a graduation passes to createV3Vault
ParameterRangeDefaultMeaning
assetWETH-deposits, withdrawals and the cap are in WETH; the token is the other leg
pooldeepest token/WETH pool-the tier with the most in-range liquidity at the moment of the call
policy.widthTickscurveWidth(spacing) ticks4 440 at spacing 60CURVE, ±25 % snapped down to the pool spacing on each side
policy.hysteresisTicksspacing × 10 ticks600 at spacing 60HYSTERESIS_SPACINGS - how far the price must leave the range before a rebalance counts
policy.minInterval6 hours6 hMIN_INTERVAL between two keeper actions on the vault
policy.maxSlippageBps100 bps100MAX_SLIPPAGE_BPS - the swap bound on every rebalance
policy.compoundtrue-harvested fees are re-minted into the range
policy.bountyBps50 bps50BOUNTY_BPS - advertised to keeper bots; LPVault pays its own HARVEST_BOUNTY_BPS
cap5 ether5 WETHVAULT_CAP, the beta cap, immutable once the vault exists
name / symbol"Kerf SYMBOL Vault" / "kSYMBOL"TOKEN when symbol() failsfrom the token, with the fallback above

What a graduated vault then does - pricing behind the TWAP guard, harvest, rebalance, the cap - is the Vaults chapter; the keeper that services it is Keeper. A graduated vault starts empty and is uncapped by anyone's money but its own depositors', which matters for the caveat below.

Established is a label, not a gate

The screener's definition of Established lives in @kerf/core's isEstablished: ageDays >= ESTABLISHED_MIN_AGE_DAYS (3), marketCapUsd > ESTABLISHED_MIN_MC_USD (1 000 000, strictly greater) and trades >= ESTABLISHED_MIN_TRADES (100). Only the first is visible to a contract. Market cap needs a price and a supply; a trade count needs the swap history. Both come from the indexer, and /launch shows them as a three-part progress strip beside every tracked token precisely because graduate cannot check them.

The join between the two rule sets is the graduator bot in the API process (apps/api/src/bots/graduator.ts). Each tick it asks the indexer for every pool it has flagged Established, drops the ones whose token has already graduated, reads firstSeen for the rest through Multicall3, and then: track for any token with firstSeen == 0; graduate for any token whose clock has run three days (GRADUATION_AGE_SECONDS); nothing for the ones in between. Neither call pays a bounty, so both go out as "send if it simulates", and a ThinPool or TooYoung revert is logged as a skip rather than an error - it is the normal outcome for most candidates on most ticks. The action guard's 15-minute failure backoff keeps a thin pool from being re-simulated every minute. How the bots are wired is in Indexer, API and bots.

The indexer keeps one row per token in its graduation table: Tracked creates it with firstSeen, Graduated fills in vault, pool and graduatedAt. That one table is both the tracked list and the graduated list the API's GET /launch reads; the API adds live firstSeen and MIN_LIQUIDITY reads from the chain, so poolDeepEnough on a row is the indexer's depth measured against the contract's own threshold rather than a number of the API's choosing.

What 10 ether of liquidity means is worth being precise about, because it is not ten ETH of TVL. Uniswap liquidity for a full-range position is sqrt(x × y), so for a token/WETH pool holding W wei of WETH against an equally valued 18-decimal token, full-range L = W × sqrt(price) - exactly W at parity. MIN_LIQUIDITY = 10 ether is therefore "about ten ETH of full-range depth" for an 18-decimal token near parity, and it scales with sqrt(price) away from that. It is a floor against dust pools, not a valuation.

The pre-market perp

PreMarketPerp is a constant-product virtual AMM for tokens no order book lists. There is no counterparty and no liquidity provider: the curve invents both sides, and the contract's USDG balance is the only thing that can pay a winner. Collateral is USDG (6 decimals), margin is isolated per position, and every price the contract quotes comes either from its own x × y = k curve (the mark) or from a Uniswap v3 TWAP times Chainlink ETH/USD (the index). Four things bound what that can cost: leverage is capped at 3x and open interest at five times the market's advertised depth; a third of every taker fee accrues to an insurance fund; when the fund is empty, bad debt becomes a deficit that the next profitable closers pay down; and a payout never touches another open position's collateral or the insurance fund, so a winner whose loser is still open is paid what is free and owed the rest.

Index and mark

The index is the token's dollar price from the spot market, in USDG's 6 decimals: the arithmetic-mean tick of the token/WETH pool over TWAP_WINDOW (30 minutes), turned into wei per token by Oracle.getQuoteAtTick, times Chainlink ETH/USD. The window is shortened to whatever the pool's observation ring actually reaches (Oracle.oldestObservationSecondsAgo), which is what makes a freshly grown ring usable at all; a shorter window is easier to move, so a market's first half hour is its most manipulable one. indexPrice(token) reverts BadOracle when the pool cannot be observed, the feed reverts or answers zero, or the product rounds to zero; previewIndexPrice(token, fee) reads the same number for a pool that has no market yet, which is the create-market form's pre-flight.

The mark is the curve's own price, quoteReserve × 1e18 / baseReserve, and it is the only price a fill or a liquidation ever uses. The index drives funding and nothing else. That split is deliberate: a 30-minute mean tick on a thin pool is expensive but not impossible to move, and confining it to funding means a manipulated index can push the hourly rate to its ±1 % cap and no further.

Creating a market

createMarket(token, fee, virtualLiquidityUsd) is permissionless and needs four things in this order: no market yet (MarketExists), virtualLiquidityUsd ≥ MIN_VIRTUAL_LIQ_USD (10 000 USDG, TooSmall), a token/WETH pool on that fee tier (NoPool), and that pool's observationCardinalityNext ≥ MIN_OBS_CARDINALITY (60, LowCardinality). The ring is grown by prepare(token, fee), split out as its own transaction because it is cheap, idempotent, and anyone can pay for it; the fork suite checks it against a live Robinhood Chain pool.

The curve is then seeded at the index: quoteReserve = virtualLiquidityUsd, baseReserve = virtualLiquidityUsd × 1e18 / index, k = baseReserve × quoteReserve, fixed for the life of the market. maxOpenInterestUsd = OI_MULTIPLE × virtualLiquidityUsd is the cap on the sum of entry notionals of every open position. The creator picks the depth and pays nothing for it - the reserves are virtual - which is why the cap is expressed as a multiple of the depth: a market can never owe more than five times what it advertised, and MIN_VIRTUAL_LIQ_USD keeps a 10 USDG market from existing.

Constants

PreMarketPerp
ParameterRangeDefaultMeaning
MAX_LEVERAGE_X3 x-open reverts Leverage for 0 or anything above; whole numbers only
MAINT_MARGIN_BPS625 bps-equity below 6.25 % of the mark notional is liquidatable
TAKER_FEE_BPS30 bps-of the notional, on the way in and on the way out
INSURANCE_SHARE_BPS3333 bps-of the fee to insuranceFund - about 10 bps of notional; the rest to FeeRouter
LIQ_BOUNTY_BPS100 bps-of the remaining margin, to whoever calls liquidate
TWAP_WINDOW30 minutes-the index window, shortened to the ring reach when shorter
MIN_OBS_CARDINALITY60-observation slots a pool needs before it can back a market
MIN_VIRTUAL_LIQ_USD10 000e6 USDG-smallest virtual depth a market may be created with
FUNDING_CAP_X180.01e18-hourly funding is clamped to ±1 %
OI_MULTIPLE5 x-open interest may not exceed five times the virtual depth
FUNDING_INTERVAL1 hours-funding accrues per whole hour elapsed

Open, close and liquidate

open(token, isLong, collateral, leverageX, limitPrice) checks the market, a non-zero collateral and 1 ≤ leverageX ≤ 3, then pokes funding so the position starts on a fresh cumulative. notional = collateral × leverageX is checked against the OI cap before any transfer. The collateral is pulled, and the entry fee - 30 bps of the notional - is charged before the position is stored: TooSmall if the fee would reach the collateral, otherwise _settleFee splits it and only collateral − fee is recorded. The curve then fills the notional: a long adds quote and takes base out, a short takes quote out and adds base, with the reserve rounding chosen so the trader's size rounds down and the curve never leaks. A short is separately bounded by the quote reserve - _openShort reverts OiCap when the notional would reach it - which on a market whose depth is its quote reserve is a tighter bound than the cap. The average fill price = notional × 1e18 / size is checked against limitPrice last: a maximum for a long, a minimum for a short, zero for no bound, Slippage otherwise.

Because the fee comes out first and the exposure is sized from the gross notional, a position is a little more levered than the slider says: effectiveLeverage = notional / (collateral − fee), 3.03x for a 3x open. The trade box on /launch mirrors the contract's arithmetic (lib/launch/view.ts tradeEstimate) and prints the liquidation price from the effective leverage L and m = 625 / 10 000: entry × (L − 1) / (L × (1 − m)) for a long, entry × (L + 1) / (L × (1 + m)) for a short, funding left out. The drawer's default limit price is the mark ± 1 % against you.

open and close on the vAMMThe entry fee comes out of the collateral before the position exists, and an exit is paid only from USDG the venue holds free - the rest is a deferred claim. Red chips are the reverts.
open(token, isLong, collateral, leverageX, limitPrice)close(id, limitPrice) · liquidate(id)noyesyesnoyesnoyesnoyesyesnoyesnoyesopen(token, isLong, …)collateral · leverageX · limitPricemarket exists?NoMarketcollateral > 0 and1 ≤ leverageX ≤ 3?ZeroAmountLeveragepokeFunding(token)accrue whole hours firstopenInterest + notional≤ 5 × virtual depth?OiCapUSDG.transferFrom(collateral)fee = 30 bps of notionalfee < collateral?TooSmall_settleFee(fee)⅓ → insuranceFund⅔ → FeeRouter.take(USDG)_openLong / _openShortx · y = k · size = base out/inshort: notional < quoteReserveOiCap · shortlimitPrice = 0 orprice within it?Slippagepositions[id] = …collateral net of fee · sizeentryPrice · entryFundingX18Opened(id, trader, token, …)close(id, limitPrice)or liquidate(id) · anyoneopen andmsg.sender == trader?NotOpenNotTraderpokeFunding(token)then back through the curve_unwind(p)pnl at the mark, funding includedexitPrice · exitNotionallimitPrice = 0 orexitPrice within it?Slippage_affordableFee(exitNotional)gross = collateral + pnl30 bps, capped at gross · 0 if ≤ 0_retire · _settleFeecollateral leaves the booknet = gross − fee> 0?_absorb(loss)insuranceFund firstthen deficit += rest_payOut(id, to, net)haircut = min(profit,deficit)clamp to _payableNowrest → deferred[to]Closed(id, pnl, exitPrice) · Socialised(…)liquidate(id) differs after _settleFeemargin ≤ 0 → _absorb; otherwise 100 bpsto the caller (clamped to _payableNow)and the rest to insuranceFund. Thetrader keeps nothing.

close(id, limitPrice) is the trader's only exit and NotTrader for anyone else. It pokes funding, pushes the position back through the curve (_unwind: a long returns its base for quote, a short returns quote for its base), realises pnl net of funding and checks the average exit price against limitPrice - a minimum for a long, a maximum for a short. Then the money: gross = collateral + pnl; the exit fee is _affordableFee(exitNotional, gross) - 30 bps of the exit notional, capped at gross, and zero when gross ≤ 0, because charging an exit fee into bad debt would only deepen the hole the insurance fund has to fill; _retire marks the position closed and takes its collateral out of totalCollateral; _settleFee splits what was affordable. If net = gross − fee is not positive the shortfall goes to _absorb; otherwise _payOut pays it, subject to the haircut and the clamp described below. Closed(id, pnl, exitPrice) is emitted either way.

liquidate(id) is permissionless and reverts NotLiquidatable unless _liquidatable: equity at or below zero, or below MAINT_MARGIN_BPS of the mark notional, where equity = collateral + unrealised pnl at the mark − funding owed. It unwinds and fees exactly as close does, then diverges: a non-positive margin is absorbed; a positive one pays LIQ_BOUNTY_BPS (100) of it to the caller, clamped to what the venue holds free, and credits the rest to the insurance fund. The liquidated trader keeps nothing - a venue with no order book to unwind into has to make liquidating worth doing, and on a thin market with a half-hour TWAP a liquidation that arrives late is not a remote case. Both Liquidated(id, liquidator, bounty) and Closed are emitted.

Funding

pokeFunding(token) is public, permissionless and idempotent; open, close and liquidate all call it first. It does nothing until a whole FUNDING_INTERVAL has passed since lastFundingAt, then reads the index and, if the index is unreadable, returns silently without advancing the clock - so an oracle outage can never freeze close or liquidate, and the hours it missed accrue at the next readable poke. Otherwise rate = (mark − index) / index, clamped to ±FUNDING_CAP_X18, is added to cumulativeFundingX18 once per whole hour elapsed - the same rate for every missed hour, because the contract only knows the prices it sees at poke time - and Funding(token, rate, cumulative) is emitted. A position owes (cumulative − entryFundingX18) × entryNotional, charged on its entry notional rather than its mark notional; longs pay when the sum is positive (the mark has led the index), shorts receive it, and it is settled inside _unwind when the position closes.

index, mark and fundingThe index is read from the spot pool and Chainlink; the mark is the curve itself. Only funding looks at the index - fills and liquidations use the mark - so a manipulated index can push funding to its cap and no further.
noyesindexmarktoken/WETH v3 poolobserve(): 30 min mean tickring ≥ 60 (prepare)Chainlink ETH/USDlatestRoundData() > 0indexPriceTWAP × ETH/USD → USD 1e6window ≤ ring reachunreadable → silent no-opmarkPricequoteReserve / baseReservepokeFunding(token) · anyonea whole hoursince last?no-oprate = (mark − index) / indexclamped to ±1 % (FUNDING_CAP_X18)cumulativeFunding += rate × hFunding(token, rate, cumulative)per position, at closeΔcum × entryNotional · longs pay +Index drives only fundingFills and liquidations use themark. A manipulated index canpush funding to its cap andno further.

Insurance, deficit and socialised losses

_settleFee puts INSURANCE_SHARE_BPS (3333) of every fee into insuranceFund and transfers the rest to the FeeRouter, where take(USDG, amount, trader) credits the treasury and, if the trader was referred, 20 % to the referrer (Fees & referrals). InsuranceChanged(delta, balance) fires on every credit and debit.

A position that closes or is liquidated with a negative net is bad debt, and _absorb(loss) pays it in a fixed order: the insurance fund first, down to zero; whatever is left becomes deficit. Nothing is confiscated at that moment. The deficit is instead burned down by _payOut: a closing position's profit = owed − collateral is haircut by min(profit, deficit), the haircut is subtracted from both, and Socialised(id, haircut, deferredAmount) records it. The haircut bites profit only - your collateral is never used to pay someone else's loss - and it bites the first profitable closers after a blow-up, in full, until the hole is paid. That is the socialisation the design asks for and it is deliberately unfair in a documented direction: whoever exits first after a liquidation that arrived too late pays for it, and whoever waits pays less.

haircutBps() is the contract's gauge for it: deficit / totalCollateral in basis points, capped at 10 000 (the /launch page shows the insurance fund's balance from GET /launch; the deficit and the outstanding deferred total are in the indexer's perpGlobal row). It is a measure of how unfunded the venue currently is, not a rate the next close will be cut by - a small profit against a large deficit is cut entirely, a large profit against a small deficit loses only the deficit.

Deferred claims

A vAMM has no auto-deleveraging, so there is a second way a close can be paid less than it earned. _payableNow() is the contract's USDG balance minus everything encumbered - totalCollateral plus insuranceFund - and _payOut clamps every payout to it. Consider a long that closes at a profit while the short that took the other side is still open: the short's loss is real at the mark but not yet realised in USDG, so the venue does not hold the long's profit free. The long is paid what is free, the remainder is added to deferred[trader] and totalDeferred, and the same Socialised event carries it as deferredAmount. When the short closes and its loss lands in the contract, claimDeferred() pays as much of the claim as is free right then - possibly in pieces, over several calls - and emits DeferredClaimed; it reverts NothingToClaim when nothing is owed or nothing is free.

This is the beta limitation to know before opening a position: a profit is not always liquid the moment you close it. A couple of units of a claim can even outlast the loser's exit, because the exit fee that funded it was itself split and a third of it went to insurance. The alternative - paying the winner out of the still-open short's collateral or out of the insurance fund - is exactly what the solvency invariant forbids.

The solvency invariant

invariant_perpCoversItsObligations in contracts/test/invariants/PerpSolvency.t.sol states the whole design in one line: the contract's USDG balance is at least totalCollateral() + insuranceFund() − deficit(). Every open position's collateral and the whole insurance fund are always there to be paid, less only the bad debt that has been written down and is being socialised. A handler drives four traders through random open, close, liquidate, claimDeferred and warp steps (1 to 12 hours, poking funding each time), catching reverts rather than letting them propagate, because most reverts - a short bigger than the quote reserve, a liquidation of a healthy position - are the design working and the fuzzer needs to get deep into the book. It runs 24 sequences of depth 32 (forge-config), against the repository's default of 64 × 32, and a second invariant asserts the handler actually closes or liquidates something once it has opened more than twenty positions, so the first one cannot pass vacuously. The unit suite asserts the same floor after every bad-debt scenario with _assertSolvent.

Errors and events

ErrorContractWhen
NoPoolLaunchPipelinetrack or graduate on a token with no token/WETH pool on any tier
TooYoungLaunchPipelinegraduate before MIN_AGE, or on an untracked token
ThinPoolLaunchPipelinesummed in-range liquidity below MIN_LIQUIDITY
AlreadyGraduatedLaunchPipelinegraduate twice
AlreadyTrackedLaunchPipelinetrack twice
ZeroAddressLaunchPipelineconstructor only
NoMarketPreMarketPerpopen, markPrice or pokeFunding on a token with no market
MarketExistsPreMarketPerpcreateMarket twice
NoPoolPreMarketPerpprepare, createMarket or previewIndexPrice on a missing pool
LowCardinalityPreMarketPerpcreateMarket before the ring reaches 60
LeveragePreMarketPerpleverageX of 0 or above MAX_LEVERAGE_X
OiCapPreMarketPerpthe OI cap, or a short that would reach the quote reserve
SlippagePreMarketPerpthe average fill or exit price crossed limitPrice
NotOpenPreMarketPerpclose, liquidate or equity on a closed position
NotTraderPreMarketPerpclose by anyone but the position's trader
NotLiquidatablePreMarketPerpliquidate on a position above maintenance margin
TooSmallPreMarketPerpdepth below MIN_VIRTUAL_LIQ_USD, a base reserve of zero, or an entry fee that reaches the collateral
ZeroAmountPreMarketPerpzero collateral, or a fill that would round to zero size
NothingToClaimPreMarketPerpclaimDeferred with nothing owed, or nothing free to pay it with
BadOraclePreMarketPerpthe TWAP or the Chainlink feed could not be read, or the index is zero
EventEmitted byCarries
Tracked(token, firstSeen)trackthe clock start the indexer's graduation row keeps
Graduated(token, vault, pool)graduatethe clone and the pool it manages
MarketCreated(token, pool, virtualLiquidityUsd)createMarketthe index pool and the depth the cap is a multiple of
Opened(positionId, trader, token, isLong, collateral, size, price)opencollateral net of the entry fee, the base size, the average fill
Closed(positionId, pnl, price)close, liquidatesigned realised PnL, funding included, and the exit price
Liquidated(positionId, liquidator, bounty)liquidatewhat the caller was paid
Funding(token, rateX18, cumulativeX18)pokeFundingthe clamped hourly rate and the running sum
InsuranceChanged(delta, balance)fees, liquidations, _absorbevery movement of the fund
Socialised(positionId, haircut, deferredAmount)_payOutthe part burned against the deficit and the part owed later
DeferredClaimed(trader, amount)claimDeferreda claim collected, in whole or in part

The indexer folds these into perpMarket (one row per token: reserves' depth, open interest, volume, last funding), perpGlobal (the insurance fund, the socialised deficit and the outstanding deferred total, one row), perpPosition (one row per id with status open, closed or liquidated) and perpEvent (Socialised and DeferredClaimed). GET /launch adds the live chain reads - markPrice, indexPrice, open interest and the cap per market, insuranceFund, and equity plus liquidatable for the visitor's open positions - and computes the funding1h column off chain as the same clamped (mark − index) / index.

Read the code
  • contracts/src/LaunchPipeline.sol track, graduate, canGraduate, bestPool, curveWidth, _scan, _initFor
  • contracts/script/KerfWiring.sol MIN_LIQUIDITY = 10 ether and the seed-vault policy the graduated vault copies
  • contracts/test/LaunchPipeline.t.sol every revert, the policy assertions, the symbol() fallback
  • contracts/src/PreMarketPerp.sol createMarket, open, close, liquidate, pokeFunding, _absorb, _payOut, _payableNow, claimDeferred
  • contracts/src/libraries/uniswap/Oracle.sol the mean tick, the ring reach, getQuoteAtTick
  • contracts/test/PreMarketPerp.t.sol fee split, OI cap, funding clamp, deferred claims, the bad-debt path
  • contracts/test/invariants/PerpSolvency.t.sol invariant_perpCoversItsObligations and its handler
  • contracts/test/fork/PerpFork.t.sol prepare and previewIndexPrice against a live Robinhood Chain pool
  • packages/core/src/established.ts isEstablished - the three-part screener test
  • apps/api/src/bots/graduator.ts the bot that tracks and graduates Established pools
  • apps/api/src/launch/build.ts how GET /launch joins indexer rows with chain reads
  • apps/web/lib/launch/view.ts launchAction, establishedChecks, tradeEstimate