Skip to content
← Build OpenHL ADL — auto-deleveraging, Layer 3 of the safety-net cascade
Orientation
Lesson 1 of 5·CONTENT15 min50 XP
Course
Build OpenHL ADL — auto-deleveraging, Layer 3 of the safety-net cascade
Lesson role
CONTENT
Sequence
1 / 5

Build OpenHL ADL — auto-deleveraging, Layer 3 of the safety-net cascade

Question

When the liquidation scanner leaves a deficit that the insurance fund cannot fully absorb, the system has one last move. Why does it do that move not via the orderbook, but by directly rewriting the books? That is ADL — auto-deleveraging — Layer 3 of the safety-net cascade.

Principle (minimum model)

  • ADL is Layer 3 of a three-layer cascade. Layer 1 = liquidation scanner (force-close). Layer 2 = insurance fund (absorb shortfall). Layer 3 = ADL (when even the fund cannot cover the deficit, haircut the most-profitable opposite-side traders).
  • Three things to internalise. (1) ADL is the last resort, never the first move. (2) It bypasses the orderbook by rewriting positions directly. (3) The trigger is "scanner left a deficit + insurance fund cannot absorb it".
  • Why not the orderbook? Every market order sent against a profitable position would punch through the bid/ask stack and crash the mark further, pushing more positions underwater — a feedback loop that runs away. Closing positions directly in the bookkeeping layer breaks that loop (the reason adl.rs's module doc gives).
  • Why this is controversial. ADL takes profit from a trader who did nothing wrong. Mitigations: well-funded insurance fund (ADL never fires) + transparent ranking (the "lucky" winner is known in advance) + haircut cap (haircut = min(remaining_deficit, pnl_gross), so no one loses more than their unrealized PnL).
  • Four-lesson structure. L1 (types + adl_score = ranking function) → L2 (execute_adl = 5-phase orchestration heart) → L3 (6 nuanced absorption tests) → L4 (Capstone — 5 invariant proptests + Stage 10 quartet retrospective).
  • Stage 10 reference SHA: d66b44a. Every code example pins to this SHA from openhl; the answer key at end of Capstone is byte-for-byte against this SHA.
  • Prerequisite: the openhl liquidation course. This course assumes the WithdrawOutcome proptest semantics (from its withdraw_shortfall lesson) and the scanner's per-scan conservation law (from its scanner capstone) are internalised.

Worked example + steps

Build OpenHL ADL — auto-deleveraging, Layer 3 of the safety-net cascade

What you'll build

The openhl liquidation course shipped the multi-account scanner — the orchestration loop that batches every Liquidatable / Underwater account (an account whose equity has fallen below the maintenance threshold; Underwater specifically means equity has gone negative — collateral can no longer cover the position) into one ScanReport. At the end of its scanner capstone we noted that ScanReport.unfilled_deficit > 0 is the signal the insurance fund couldn't absorb everything, and that Stage 10d (this course) would consume it — taking it as the deficit input and reporting what haircuts couldn't cover as AdlReport.deficit_remaining (the residual, with deficit_absorbed + deficit_remaining == deficit).

This course implements that consumer. By the end you'll have shipped:

  • 1 new source file / ~530 LOC in crates/liquidation/src/adl.rs.
  • 21 tests passing at SHA d66b44a: 16 unit tests covering score / no-candidate / single-winner / multi-winner / tiebreaker / nuanced-absorption cases, plus 5 invariant proptests; total crate test count 69 → 90 after this course.
  • 3 new types (AdlScore, AdlRecord, AdlReport) and 2 new functions (adl_score, execute_adl) — a clean, small module compared to the scanner.
  • A complete 4-layer safety cascade: margin requirement (Layer 0) → force-close fee (Layer 1) → insurance fund (Layer 2) → ADL (Layer 3) → socialized loss / protocol insolvency (Layer 4). Layer 4 is the regime that 0–3 are designed to make unreachable; if AdlReport.deficit_remaining > 0 at the end of Lesson 4 of this course, the chain has formally entered Layer 4 — every depositor takes a haircut, or the protocol halts.

You'll understand:

  • Why ADL bypasses the orderbook entirely — not as an optimization, but because submitting market orders against profitable positions while the mark is already cascading would create a feedback loop that crashes the chain. Bookkeeping-layer mutation is the only safe path.
  • The Hyperliquid score convention: (pnl_pct × leverage) ranks the "luckiest" winners — those who both made the highest relative gain and took the most leveraged risk to get there. They take the haircut first.
  • How the haircut works: each ADL'd winner had unrealized PnL of P; in a normal close they'd receive P in full; with ADL they receive P - haircut, where haircut = min(remaining_deficit, P). The system absorbs the difference toward the unfilled deficit.
  • Deterministic ranking: stable-sort by score descending, tiebreak by AccountId ascending — so two equally-lucky winners produce a byte-identical order across every validator.
  • The fourth-layer exit: when the candidate pool exhausts before the deficit is absorbed, AdlReport.deficit_remaining > 0 and the protocol has run out of mechanisms. That value going non-zero is the moment a chain admits it's insolvent.

Why ADL bypasses the orderbook (the feedback-loop reason)

This is the most important conceptual leap in the course, and it's worth stopping on it before any code.

Stage 10c's scanner submits close orders to the CLOB (matching engine). A Liquidatable account's position gets unwound by a market order that consumes the existing bid/ask stack. That's fine when there are a few liquidations in a quiet market.

But consider the case ADL is designed for: a violent move triggered enough underwater closes that the insurance fund drained. Now imagine we kept the same mechanism for ADL — submit market orders against the profitable counter-positions through the matching engine.

The order book has finite depth. Every additional market sell punches through the bid stack and drops the mark further. The mark dropping further pushes more accounts underwater. Those new underwater accounts also need ADL. The matching engine sees more aggressive sells. Mark drops more. The cycle runs away.

This is exactly the failure mode that killed Mt. Gox in slow-motion, that almost killed Robinhood during GameStop, and that has caused every major perp DEX outage in the last 5 years. The fix is structural: ADL must not touch the orderbook.

Concretely, what we do instead:

  • ADL ranks winners by score, in pure Rust, on every validator independently.
  • The "force-close" is a bookkeeping mutation: credit the trader's collateral by pnl - haircut, set position size to zero, remove from the open-positions table.
  • The matching engine never sees an ADL close. The bid/ask stack is untouched. The mark moves only if someone else trades.

The CloseOrderSpec we still emit in each AdlRecord is purely telemetry — kept for shape parity with Stage 10a's other close paths and for downstream auditing. The bridge (the openhl integration layer meant to call LiquidationScanner::scan and then execute_adl once per block — same component you've been hearing about since the liquidation course's close-outcome decomposition lesson) is expected to apply it as an account-state mutation, not as a CLOB submission. At d66b44a that per-block composition is documented in lib.rs, but nothing calls execute_adl yet; Stage 13's integration coordinator (930bdc8) wires it.

The Stage 10c → 10d handoff in one diagram

   ┌──────────────────────────────────────────────────────────────┐
   │  Stage 10c scanner (last block)                                │
   ├──────────────────────────────────────────────────────────────┤
   │  ScanReport {                                                  │
   │      records:          Vec<LiquidationRecord>,                 │
   │      fund_deposits:    i64,                                    │
   │      fund_withdrawals: i64,                                    │
   │      unfilled_deficit: i64,   ←─── if > 0, ADL fires           │
   │  }                                                             │
   └──────────────────────────────────────────────────────────────┘
                            │
                            ▼ if unfilled_deficit > 0
   ┌──────────────────────────────────────────────────────────────┐
   │  Stage 10d execute_adl                                         │
   ├──────────────────────────────────────────────────────────────┤
   │  Input:  candidates  &[AccountSnapshot]   ← all open positions │
   │          mark         MarkPrice                                │
   │          deficit      i64    (= unfilled_deficit from scanner) │
   │                                                                │
   │  Body:   1. Score each candidate (None if not a winner)        │
   │          2. Stable-sort by score desc, account_id asc          │
   │          3. Iterate descending; haircut each winner            │
   │             until deficit absorbed                             │
   │                                                                │
   │  Output: AdlReport {                                           │
   │              records:           Vec<AdlRecord>,                │
   │              deficit_absorbed:  i64,                           │
   │              deficit_remaining: i64,  ←─ chain insolvent if >0 │
   │          }                                                     │
   └──────────────────────────────────────────────────────────────┘

The contract is one i64 in, one i64 out. The bridge wires:

  • The liquidation course's scanner capstone proved unfilled_deficit > 0 ⇒ fund_balance == 0 (proptest #2).
  • Lesson 0 (this lesson) tells you that this contract is exactly what triggers execute_adl.

The score: "luckiest winners take the haircut"

You'll implement this in Lesson 2. For now: the score is

$$\text{pnl_pct_bps} = \frac{\text{pnl} \times \text{MARGIN_SCALE}}{\text{collateral}}$$

$$\text{leverage_bps} = \frac{\text{notional} \times \text{MARGIN_SCALE}}{\text{equity}}$$

$$\text{score} = \frac{\text{pnl_pct_bps} \times \text{leverage_bps}}{\text{MARGIN_SCALE}}$$

(Reminder from Stage 10a: equity = collateral + unrealized_pnl at the current mark; notional = |position_size| × mark. So collateral is the deposited base, equity is what the position is worth right now, and notional is the gross exposure.)

Both factors are in bps (10000 = 100%). The product is renormalized once. A trader who is up 50% on a 10× position scores higher than one who is up 100% on a 1× position — Hyperliquid's choice, because high-leverage winners are seen as more "structurally" lucky (they took the most risk to win the most).

This is Hyperliquid's actual convention. Other venues use different scores (some use raw pnl_pct, some use absolute pnl); the choice matters for fairness but the mechanism is the same. We follow HL.

The conservation law (the load-bearing invariant)

Same discipline as the liquidation course's withdraw_shortfall, close-outcome decomposition, and scanner-capstone lessons:

$$\text{deficit_absorbed} + \text{deficit_remaining} = \text{input_deficit}$$

execute_adl either absorbs the full deficit (deficit_remaining = 0) or absorbs as much as it can and surfaces the remainder. No deficit is created or destroyed by ADL itself. The proptests in Lesson 4 lock this invariant in across every random (candidates, mark, deficit) triple.

This closes the cascade math — four layers, four conservation identities:

$$\text{Fund withdraw (per fund call):} \quad \text{amount} + \text{unfilled} = \text{shortfall}$$

$$\text{Close outcome (per position close):} \quad \text{fee_to_fund} + \text{residual_to_account} = \text{post_close_equity}$$

$$\text{Scanner (per scan batch):} \quad \text{balance_before} + \sum \text{deposits} - \sum \text{withdrawals} = \text{balance_after}$$

$$\text{ADL, Lesson 4 (per ADL pass):} \quad \text{deficit_absorbed} + \text{deficit_remaining} = \text{input_deficit}$$

Four layers, four identities. After this course, the openhl-liquidation crate's math is closed under every operation.

The 5 lessons

Module 0 — Orientation

  • Lesson 0 (this lesson) — Why ADL, why bypass orderbook, Stage 10c → 10d handoff, score preview, conservation law preview.

Module 1 — ADL implementation

  • Lesson 1 — AdlScore newtype + AdlRecord + AdlReport types + the adl_score(snapshot, mark) -> Option<AdlScore> function. The pure-compute scoring with None for flat / losing / zero-collateral cases. 5 score tests.
  • Lesson 2 — execute_adl(candidates, mark, deficit) -> AdlReport — the orchestration: filter by Option<AdlScore>, stable-sort descending with AccountId tiebreaker, haircut loop. Phase-by-phase walkthrough of the 50-line body + 5 simple unit tests (zero deficit / negative deficit / no candidates / no profitable / single winner absorbing a small deficit).
  • Lesson 3 — Nuanced absorption tests: single winner haircut to zero payout, single winner exhausted with deficit remaining, multi-winner in score order, drain-first-then-partial, tiebreaker by AccountId ascending, "doesn't touch losers or flats" defense. 6 unit tests.
  • Lesson 4 — 5 invariant proptests + Stage 10 quartet retrospective. The conservation law from per-pass to per-block, the 4-layer cascade closed end-to-end.

What's next after this course

The Stage 10 cascade is complete. The openhl roadmap continues:

  • Stages 11–11b — Oracle (1fa955e, 6495ffd, in openhl): the median-aggregating index-price feed, then signed observation verify. No course covers it; the source is the reference.
  • Stage 12 — Vault (1e63e0b): the share-based collateral pooling primitive. No course covers it.
  • Stages 13–13k — bin/openhl (multiple SHAs): the actual runnable single-validator node. No course covers it.

After Lesson 4 of this course, you'll be at the openhl Stage 10 endpoint: the full liquidation cascade, ending in ADL. From there, openhl is your reference for the rest of the build.

License / SHA discipline

Lessons 0–4 cite Stage 10d at SHA d66b44a. The new code lives in crates/liquidation/src/adl.rs (530 lines). The only other liquidation-crate changes between Stage 10c (0a8464e) and Stage 10d are lib.rs (pub mod adl;, the pub use adl::{...} re-export, and the crate-doc roadmap) and one visibility change in compute.rs (saturate_i128_to_i64 becomes pub(crate) so adl.rs can reuse it). Otherwise ADL is a purely additive module.

Summary (3 lines)

  • ADL = Layer 3 of the safety-net cascade. Last resort when scanner leaves a deficit + insurance fund cannot absorb. Bypasses orderbook by rewriting positions directly.
  • Why not orderbook: market orders against winners would crash the mark further and feed the cascade. Why controversial: takes profit from someone who did nothing wrong; mitigated by transparent ranking + a haircut capped at each winner's unrealized PnL.
  • 4 lessons: types + ranking → 5-phase orchestration → 6 absorption tests → 5 invariant proptests Capstone. Pinned to SHA d66b44a. Prerequisite: the openhl liquidation course.