🚧 SportsPerp is currently live on devnet. Mainnet target: before Jun 12, 2026 (World Cup kickoff).
TradingThree-Layer Cascade

Three-Layer Cascade

Detailed per-layer mechanics for Liquidations. Every constant below is sourced from programs/obv-perps/src/math/liquidation.rs.

Determining the layer

determine_liquidation_layer() inspects the position’s current margin ratio and returns the applicable layer:

margin_ratio = (collateral + pnl) × 10_000 / size
 
if ratio > MAINTENANCE_MARGIN_BPS          (2000, i.e., 20%)  → 0  (healthy)
if ratio > BACKSTOP_THRESHOLD_BPS          (1333, i.e., 13.33%)→ 1  (partial)
if insurance can absorb                                       → 2  (backstop)
else3  (ADL)

The BACKSTOP_THRESHOLD_BPS = 1333 is literally floor(2000 × 2 / 3) — two-thirds of maintenance margin. Below that ratio, a single partial close wouldn’t recover the position, so the cascade escalates immediately.

Layer 1: Partial Liquidation

Closes 20% of an underwater position to restore the margin ratio while leaving 80% of the position (and its exposure) intact.

Mechanics

  1. Eligibility check
    • MAINTENANCE_MARGIN_BPS ≥ ratio > BACKSTOP_THRESHOLD_BPS (20% ≥ ratio > 13.33%)
    • last_partial_liquidation_time + 30s ≤ now — 30-second cooldown
    • Anti-manipulation: position must have pnl < 0 OR have lost ≥ 18.3% of effective collateral vs its last margin transfer
  2. Close 20%
    • close_size = position.size × 2000 / 10_000
    • PnL and collateral are computed proportionally on the closed portion
    • Liquidation executes at the current mark EMA price
  3. Reward + insurance split on the closed slice’s remaining equity
    • The “remaining” base is max(0, slice_collateral + slice_pnl − slice_funding) — i.e. what’s left on the closed slice after funding and PnL are applied and clamped to zero.
    • 5% of remaining is paid to the liquidator (LAYER1_REWARD_BPS = 500).
    • 50% of (remaining − liquidator_reward) is transferred to the insurance fund.
    • The other 47.5% stays in the global liquidity pool. It is not credited back to the trader’s remaining position. The trader’s collateral is reduced by the full proportional slice; only the liquidator reward and insurance allocation leave the pool.
  4. State updates
    • Position size reduced by 20%
    • Position collateral reduced by the proportional slice
    • last_partial_liquidation_time = now

Example

Long position, size 1,000 USDC, collateral 200 USDC, oracle has fallen so the slice’s PnL on the EMA mark = −40 USDC. Margin ratio = (200 − 40) × 10,000 / 1,000 = 1,600 → 16%. Layer 1 eligible (between 13.33% and 20%).

Assume zero pending funding on this slice for simplicity.

  • close_size = 200 USDC (20% of 1,000)
  • Closed proportional collateral: 40 USDC (20% of 200)
  • Closed slice PnL: −8 USDC (20% of −40)
  • Slice’s remaining equity: max(0, 40 − 8 − 0) = 32 USDC
  • Liquidator reward: 32 × 5% = 1.6 USDC (paid out of pool to liquidator)
  • Insurance allocation: (32 − 1.6) × 50% = 15.2 USDC (transferred pool → insurance)
  • Stays in the pool: 32 − 1.6 − 15.2 = 15.2 USDC (not credited back to the trader)
  • Remaining position: size 800, collateral 160 (the proportional 40 was deducted in full)

The trader effectively loses the full closed slice. The 47.5% “stays in the pool” portion is part of the pool’s LP-side balance — it offsets future shortfalls but isn’t returned to this trader. If this seems harsh: it is the price of having let the position reach 16% margin ratio. The protocol prefers the trader keep watching their position (or place an SL) rather than relying on partial liquidation as a forgiving rescue.

After the close, the position’s margin ratio is recomputed against the new size and new collateral. If still below 20%, another partial can fire after the 30-second cooldown.

Why 20% close, not 100%

A full close at the maintenance threshold is too aggressive — a brief mark dip could liquidate a position that would have recovered. 20% is empirically the sweet spot: it materially improves the margin ratio, recovers substantial insurance allocation per event, and leaves the trader in a position to continue managing the remaining 80%.

Why 30s cooldown

Prevents the same position from being liquidated repeatedly in a single block or within a single mark movement window. A position that has just been partialized deserves a brief “breath” before the next check — if the market is still adverse after 30 seconds, another partial (or Layer 2) fires naturally.

Layer 2: Insurance Backstop

When margin_ratio ≤ 13.33%, the position is too underwater for a partial to rescue. The insurance fund absorbs it at the current mark price.

Mechanics

  1. Eligibility
    • margin_ratio ≤ BACKSTOP_THRESHOLD_BPS (1333)
    • Insurance fund’s current_backstop_exposure + position_notional ≤ max_backstop_exposure (50,000 USDC at launch). Note: the eligibility check is purely on exposure; an insurance fund with low balance but available exposure headroom can still absorb — the balance only matters when subsequent unwind chunks realise losses.
  2. Absorptionbackstop_liquidate
    • 3% of the absorbed position’s remaining collateral is paid to the caller of backstop_liquidate as the absorption reward (LAYER2_REWARD_BPS = 300). This is the only reward in the Layer 2 flow.
    • The position is transferred to a BackstopPosition PDA, owned by the insurance fund.
    • The trader’s remaining collateral (net of the absorption reward) flows into the insurance token account.
    • The original trader’s UserPosition is closed.
    • current_backstop_exposure += position_notional.
  3. Gradual unwindunwind_backstop can be called repeatedly to retire the absorbed position:
    • Each call closes up to 10% of the backstop position’s size (MAX_UNWIND_FRACTION_BPS = 1000).
    • Closes against the oracle price at call time; PnL flows to/from the insurance fund.
    • No reward is paid to the cranker on individual unwind chunks in v1. Unwind cadence is driven by the in-house keeper; introducing an unwind-time reward is on the post-launch list.
    • current_backstop_exposure decreases proportionally.
    • When size reaches zero, the BackstopPosition account is closed.

Why gradual unwind

A 50,000 USDC market-order unwind at a single instant would move the vAMM mid and cascade into other positions’ liquidation. Unwinding in 10% chunks spread across many blocks lets the market absorb the flow without a second-order crisis. A dedicated unwind keeper handles the cadence automatically (keeper/liquidator.mjs).

Layer 3: ADL (Auto-Deleveraging)

When insurance can’t absorb (either exposure cap hit or balance depleted), the protocol closes opposing profitable positions to cover the loss. See Auto-Deleveraging for details.

Liquidator economics

Liquidators are third-party (or first-party) actors incentivized to keep the cascade running. Their payouts, per invocation:

ActionRewardOn what base
Layer 1 partial5%Closed slice’s remaining equity (max(0, collateral + pnl − funding) proportional to the closed slice)
Layer 2 absorb3%Trader’s remaining collateral at absorption time, capped by pool balance
Layer 2 unwind chunk— (no reward in v1)Unwind cadence is driven by the in-house keeper
Layer 3 ADL— (protocol-driven)

A single competitive keeper covers all 68 markets with ~$10/day of Solana compute. The Layer 1 and Layer 2-absorb rewards keep at least one keeper online even in quiet markets.

Invariants

The program enforces these post-conditions on every liquidation:

  • Pool balance never negative. Layer 1 close + insurance allocation + reward ≤ closed slice’s collateral + closed slice’s PnL.
  • Insurance exposure bounded. current_backstop_exposure ≤ max_backstop_exposure.
  • Insurance balance non-negative. Unwind payments are capped at the fund’s current balance.
  • Every closed position settles fully. No hanging “residual” slivers — a position either exists fully or is closed; no half-states.

Further reading