Auto-Deleveraging (ADL)
Auto-deleveraging is Layer 3 of the liquidation cascade. It is the last-resort mechanism — the “break glass in case of emergency” tool that fires only when the insurance fund cannot absorb an underwater position.
When it triggers
ADL is eligible when all of the following hold, checked on-chain by auto_deleverage.rs:
- The underwater position’s margin ratio is ≤ 13.33% (below the backstop threshold).
- The insurance fund cannot absorb — either
max_backstop_exposure == 0(unconfigured) or absorbing this position would pushcurrent_backstop_exposure + underwater.sizepastmax_backstop_exposure. - The market has
adl_enabled = true(default on market initialization). - The caller has specified an
adl_target_positionthat is:- On the opposing side of the book (
adl_target.direction != underwater.direction). - Profitable at the current oracle price (
pnl > 0).
- On the opposing side of the book (
If any check fails, the instruction reverts. There is no silent degradation.
One call closes the underwater position
SportsPerp’s ADL instruction closes exactly one target position per invocation, and always fully closes the underwater position on the same call. The close size against the target is capped at the underwater position’s size:
adl_close_size = min(adl_target.size, underwater.size)If adl_target.size > underwater.size, the target is partially closed — its size and collateral reduced proportionally, entry price preserved. If adl_target.size ≤ underwater.size, the target is fully closed and its PDA account is freed (rent refunded to the target’s owner).
Either way, the underwater position is closed in the same instruction by the account constraint (close = underwater_owner). If the chosen target was smaller than the underwater size, the residual loss — the portion of the underwater position that no single target fully absorbed — is socialized rather than carried forward. There is no “partial underwater” state; the underwater PDA is released on the first successful call.
Keepers therefore have an incentive to pick the largest profitable opposing target available: a larger target absorbs more of the loss at oracle price and minimizes what gets socialized. Picking an undersized target is permitted but transfers more residual loss to the remaining open-interest holders.
Target selection happens off-chain
The program does not rank opposing positions. The caller (liquidator) chooses which target to ADL. The program only validates that the chosen target meets the eligibility criteria above.
This is a deliberate design choice:
- On-chain ranking is expensive. Iterating every opposing position to compute a global PnL × leverage score would be cost-prohibitive per invocation.
- Off-chain keepers already have the data. The liquidator bot maintains an index of all open positions and can compute a PnL × leverage ranking cheaply off-chain, then submit the top-ranked candidate as the
adl_target_positionparameter. - Permissionless fairness is enforced by competition. Any account can invoke ADL. A keeper picking a suboptimal target leaves reward on the table (note: ADL itself pays no reward — see below) and wastes the gas. Rational keepers converge on reasonable choices.
The reference ranking used by SportsPerp’s own liquidator is PnL × leverage — the industry-standard heuristic. Both inputs come from on-chain position state:
pnl_pct = unrealized_pnl / collateral (at oracle price)
effective_leverage = size / max(collateral + pnl, 1)
adl_score = pnl_pct × effective_leverageHigher score → closed first. Third-party integrators can implement their own ranking; the program will accept any valid target.
ADL executes at oracle price
The underwater position’s eligibility is evaluated against the EMA-smoothed mark price (mark_price_ema). But the settlement of both the underwater close and the ADL target’s payout uses the oracle price (market.oracle_price):
// auto_deleverage.rs
let adl_pnl = calculate_pnl(
&adl_dir, adl_target_size, adl_target_entry_price,
market.oracle_price, // NOT mark_price_ema
)?;Why the split:
- Eligibility on mark EMA → resistant to a single-push manipulation attempt (the EMA dampens flash dislocations, preventing forced ADL from a blip).
- Settlement on oracle → the oracle is the external-data anchor. A vAMM-influenced mark could be locally dislocated at the moment of ADL; paying the ADL target against the oracle gives them a price tied to the OBV-derived fair value, not to the instantaneous vAMM state.
In practice the two prices are close (the mark EMA is computed from the oracle plus a vAMM delta), but the oracle is the canonical ADL settlement reference.
What happens to the ADL target
- Funding is settled on the target’s proportional size (S2-04 fix in auto_deleverage.rs:113-132).
- Payout is computed:
adl_payout = max(0, close_collateral + close_pnl − close_funding). - USDC is transferred directly from the global
pool_tokenSPL account to the target’s token account (gated only onpool_token.amount ≥ adl_payout— ADL payouts can never be blocked by therisk_floorgate). - Position is updated:
- If fully closed: size and collateral zeroed, account closed, rent refunded to the owner.
- If partially closed: size and collateral reduced proportionally; entry price and cumulative-funding snapshot preserved.
- OI updated:
total_long_oiortotal_short_oireduced by the closed size.
No fees are charged on ADL. The 10-bps taker fee applies only to voluntary opens/closes and trigger executions. ADL is involuntary for the target, so no fee is deducted.
The ADL target keeps their profit. Payout = collateral + realized_pnl − funding. If the target was up 30% when ADL fired, they walk away with 30% realized.
What happens to the underwater position
After the ADL target is settled, the underwater position is closed entirely as part of the same instruction:
- The position account is closed (
close = underwater_owner), rent returned to the underwater trader. - OI is reduced by the full underwater size.
- Residual loss: if
underwater.collateral + underwater.pnl < 0, that shortfall is the “bad debt” — but because the ADL target has already been closed and paid, and the underwater position is also closed, the global pool’s net position is balanced. Any residual is socialized across remaining open-interest holders via adjusted funding accrual patterns in subsequent intervals.
In a stress event with multiple underwater positions, each is closed by its own ADL invocation against its own opposing target. A single underwater position is never carried across calls — it is always fully closed on the first successful ADL invocation, with any residual beyond the chosen target’s cover socialized.
Why no liquidator reward on ADL?
Layer 1 and Layer 2 liquidations pay 5% and 3% rewards respectively, drawn from the liquidated position’s collateral. ADL has no equivalent — the caller receives nothing directly.
Rationale: the “reward” for invoking ADL is systemic. By closing an underwater position that can’t be absorbed by insurance, the caller restores market solvency. Participants with large open positions have strong incentive to run an ADL keeper themselves to prevent their own collateral being at risk to bad debt.
In practice, the same liquidator operating Layer 1 and Layer 2 will naturally also operate Layer 3 — the marginal cost of the additional logic is small, and the reputational value (preventing bad-debt socialization) is high.
How likely is ADL?
Rare, by design. ADL only fires when:
- A position is deeply underwater (< 13.33% margin — already past partial liquidation).
- AND absorbing it would push
current_backstop_exposure + underwater.sizepast the insurance fund’smax_backstop_exposurecap (or the fund is unconfigured with a cap of 0).
Note: the on-chain check is on outstanding backstop exposure, not on the fund’s USDC balance. A fund with ample balance but already at its exposure cap will still route to ADL.
For the exposure cap to be reached requires either:
- A large sudden move that absorbs multiple positions into the backstop faster than
unwind_backstopretires them, pushingcurrent_backstop_exposurepast the cap. - A sustained adverse trend that accumulates outstanding backstop exposure over many absorption events.
- A single or several concurrent absorbed positions large enough to push
current_backstop_exposurepast the 50K cap on their own.
The Insurance Fund design — specifically the 10K target balance and the 50% replenishment allocation from Layer 1 partials — is tuned so ADL should fire only in unprecedented stress.
Disabling ADL per market
The adl_enabled flag on MarketConfig defaults to true but can be toggled off by admin via update_market_params. When disabled, a position that would fall to Layer 3 instead remains in Layer 2 backstop indefinitely, and the market is effectively on admin watch until either:
- Insurance can be topped up manually and absorb the residual.
- The admin pauses or force-settles the market.
Disabling ADL is a governance-level action, not something expected in normal operation. The v1 expectation is adl_enabled = true for every market.
Integrator considerations
Applications built on SportsPerp should surface ADL risk in the UI:
- Warn when a user’s position ranks in the top decile of profitable opposing positions on their market (a reasonable proxy for ADL selection probability, even though on-chain selection is caller-driven).
- Display the current insurance fund balance and
current_backstop_exposureso users can see how close Layer 2 is to exhaustion. - Don’t silently re-open positions closed by ADL. An ADL close is a risk signal, not a transient glitch.
The insurance_fund PDA and every UserPosition are public on-chain state, readable without permission.
Further reading
- Three-Layer Cascade — the full liquidation flow.
- Insurance Fund — Layer 2’s buffer and its relationship to ADL.
- Liquidations — why ADL exists and the zero-bad-debt invariant.