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)
else → 3 (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
- 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 < 0OR have lost ≥ 18.3% of effective collateral vs its last margin transfer
- 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
- 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
remainingis 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
collateralis reduced by the full proportional slice; only the liquidator reward and insurance allocation leave the pool.
- The “remaining” base is
- 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
remainingequity: 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
- 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.
- Absorption —
backstop_liquidate- 3% of the absorbed position’s remaining collateral is paid to the caller of
backstop_liquidateas the absorption reward (LAYER2_REWARD_BPS = 300). This is the only reward in the Layer 2 flow. - The position is transferred to a
BackstopPositionPDA, 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
UserPositionis closed. current_backstop_exposure += position_notional.
- 3% of the absorbed position’s remaining collateral is paid to the caller of
- Gradual unwind —
unwind_backstopcan 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_exposuredecreases proportionally.- When size reaches zero, the
BackstopPositionaccount is closed.
- Each call closes up to 10% of the backstop position’s size (
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:
| Action | Reward | On what base |
|---|---|---|
| Layer 1 partial | 5% | Closed slice’s remaining equity (max(0, collateral + pnl − funding) proportional to the closed slice) |
| Layer 2 absorb | 3% | 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
- Liquidations — high-level cascade overview.
- Auto-Deleveraging — Layer 3 specifics.
- Insurance Fund — the pool that absorbs Layer 2.