Skip to content
← Build OpenHL Liquidation — perpetual position liquidation engine
Orientation
Lesson 1 of 14·CONTENT15 min50 XP
Course
Build OpenHL Liquidation — perpetual position liquidation engine
Lesson role
CONTENT
Sequence
1 / 14

Build OpenHL Liquidation — perpetual position liquidation engine

Question

Build openhl's liquidation engine in Rust. The scanner detects positions below maintenance margin and force-closes them; the insurance fund absorbs shortfalls. Layer 1 + Layer 2 of the safety-net cascade. Pinned to openhl SHA 22eedf9 (compute), 260883b (insurance fund), 0a8464e (scanner).

Principle (minimum model)

  • A perp is a levered position; when losses eat into collateral and equity / notional falls below the maintenance margin, the position is force-closed. Opposite side, full size; the liquidation fee is taken from collateral and credited to the insurance fund, and any positive remaining equity returns to the account. If equity is negative (underwater), the insurance fund absorbs the shortfall.
  • Liquidation runs inside consensus. Every validator, every block, independently computes which accounts are below maintenance, from the same data with the same code. Outsourcing it to an off-chain bot turns the detection-to-close RPC round-trip into chain losses (a 50× position can flip from healthy to underwater in seconds).
  • No floats — they break consensus. If one validator classifies an account as Liquidatable and another as AtRisk, close orders / fees / insurance-fund deltas differ, blocks diverge, and the chain forks. Fixed-point MARGIN_SCALE = 10_000 (bps) + saturating arithmetic + i128 intermediates for any product that can overflow i64.
  • Four-state margin classification. Safe / AtRisk / Liquidatable / Underwater — each state decides what the engine is allowed to do.
  • The insurance fund is a small state machine, not just a balance number. It has its own transition rules (deposit / withdraw_shortfall) and a balance ≥ 0 invariant every operation preserves.
  • Three sub-stages, three pinned SHAs. Pure compute 22eedf9 (Stage 10a), insurance fund + close-outcome decomposition 260883b (Stage 10b), multi-account scanner 0a8464e (Stage 10c). ADL (Layer 3) is a separate course.

Worked example + steps

Build OpenHL Liquidation — perpetual position liquidation engine

What you'll build

The openhl funding course added the funding-rate state machine — perpetual contracts now have a mechanism that keeps the mark price anchored to the index. This course builds the next openhl primitive: the liquidation engine that force-closes positions when an account's losses exceed its deposited collateral.

By the end of this course, you'll have shipped:

  • 5 source files / ~1,800 lines in a new openhl-liquidation crate at 0a8464e (lib.rs, types.rs, compute.rs, insurance.rs, scanner.rs; the Stage 10a milestone alone is 3 files / ~600 lines).
  • 24 tests passing at the Stage 10a milestone, 69 by the capstone: hand-traced unit tests for each compute function + proptests for margin-ratio monotonicity and determinism + insurance-fund conservation invariants.
  • 3 building blocks: a fixed-point types module, a pure compute module (margin math), and a state machine (insurance fund, Stage 10b) plus a multi-account scanner (Stage 10c).
  • A four-state margin classification (Safe, AtRisk, Liquidatable, Underwater) that every validator computes identically.

You'll understand:

  • Why a perp DEX cannot outsource liquidations to an off-chain process and still claim consensus solvency.
  • The Hyperliquid-shape margin model: cross-margin, mark-vs-entry, initial-vs-maintenance.
  • The four states of margin health and what each state authorizes the engine to do.
  • The non-monotonic edge case in margin_ratio — when collateral dominates notional, the ratio can move against the direction of mark, and why that doesn't break liquidations.
  • Why the insurance fund is a small state machine (with its own transition rules — deposit / withdraw_shortfall — and a balance ≥ 0 invariant), not just a plain balance field.
  • How auto-deleveraging (ADL) lives at the edge of this design — and why we leave it out of Stage 10.

Why liquidations matter (1-paragraph perp recap)

Perpetual contracts are levered positions. A trader deposits collateral (USDC) and opens a position of size (signed: positive = long, negative = short) at an entry price. The position's unrealized PnL moves with the mark price: a long profits when mark > entry, loses when mark < entry. When the loss eats into collateral far enough that equity / notional drops below the maintenance margin requirement, the account can no longer cover its losses — the engine force-closes the position at market (opposite side, full size), debits a liquidation fee to the insurance fund, and (if equity remained positive) returns the remainder to the account. If equity went negative before the close — the "underwater" case — the insurance fund absorbs the deficit. That's the entire mechanism.

Why an L1 perp DEX runs liquidations in consensus

Some derivatives venues outsource liquidations to off-chain liquidator processes — bots that scan account state and call a liquidate(account) endpoint when they find a target. This works for low-frequency settlement systems (think credit default swaps) but breaks at perp speed: a 50× levered HYPE position can flip from healthy to underwater in seconds during a news cascade, and any RPC-round-trip delay between detection and close is loss the chain absorbs.

Hyperliquid runs liquidations in consensus. Every validator, every block, computes which accounts are below maintenance — independently, from the same data, with the same code. The engine's output (close orders + insurance-fund movements) becomes part of the block. That's the only way the chain stays solvent in adversarial market moves.

The price you pay for this guarantee is the determinism discipline: float arithmetic is forbidden, every classification must be byte-identical across validators, every overflow must saturate rather than panic. The funding course (openhl-funding) applies the same discipline; this course applies it again, under leverage.

Why liquidations can't use floats

Same answer as funding: consensus determinism. A validator that classifies an account as Liquidatable while a peer validator classifies the same account as AtRisk will produce a different block — different close orders, different fees, different insurance-fund deltas. Block proposals diverge, the chain forks.

The fix: signed integers + saturating arithmetic (operations that, on overflow, neither panic nor wrap but clamp to the type boundary — i64::MAX / i64::MIN — via Rust's saturating_add / saturating_mul etc.) + i128 intermediate products for any multiplication that can overflow i64. We use MARGIN_SCALE = 10_000 (basis points) as the fixed-point unit for MarginRatio. Bps is the conventional unit for margin in TradFi and in crypto perp venues — Hyperliquid, Binance, Drift all express margin requirements in bps. MarginRatio(1_000) is exactly 10%; MarginRatio(MARGIN_SCALE) is exactly 100%.

(Funding used RATE_SCALE = 1_000_000_000 because it needed parts-per-billion precision for tiny per-interval rates. Liquidation needs less precision but the same discipline.)

The 14 lessons

Module 0 — Orientation

  • Lesson 0 (this lesson) — Why liquidations, why margin model, three-sub-stage roadmap.

Module 1 — Types (Lessons 1–3)

  • Lesson 1 — MARGIN_SCALE = 1e4 (bps) + LiquidationParams + hyperliquid_default() (10% / 2% / 1.5%). Why bps, why these defaults.
  • Lesson 2 — MarginRatio newtype + MarginHealth enum (Safe / AtRisk / Liquidatable / Underwater). Why four states, what each authorizes.
  • Lesson 3 — AccountSnapshot + CloseOrderSpec. Why a new snapshot type (not funding::Position) — separating the read-only, immutable snapshot type keeps the risk-calculation core decoupled from whatever mutable state shape the upstream layers (bridge / clearing) carry — and how the bridge layer assembles it.

Module 2 — Pure compute (Lessons 4–7) — Stage 10a

  • Lesson 4 — notional_value + unrealized_pnl. The signed-multiplication trick that gets the sign right for both longs and shorts.
  • Lesson 5 — account_equity + margin_ratio. The proptest that uncovers the non-monotonic edge case (= the surprising regime where the price seems to move favorably, yet under certain conditions the margin ratio appears to worsen in the opposite direction) when collateral dominates notional, and why prop_assume! is the right fix.
  • Lesson 6 — margin_health classification. Strict-less-than at every boundary and what that buys you.
  • Lesson 7 — close_order_spec. The market-order discipline: liquidation takes any available price. Stage 10a complete.

Module 3 — Insurance fund (Lessons 8–10) — Stage 10b

  • Lesson 8 — InsuranceFund struct + constructors + deposit, plus the WithdrawOutcome enum. The single-balance state machine.
  • Lesson 9 — withdraw_shortfall: how an Underwater liquidation drains the fund, and the Covered / PartiallyDrained / Depleted boundary to ADL.
  • Lesson 10 — liquidation_fee + solvent_close_outcome / underwater_close_outcome: each close decomposes into a fee credited to the fund plus either a residual returned to the trader or a shortfall the fund must absorb. A barely-solvent close does both: it pays a partial fee and needs the fund to cover the rest.

Module 4 — Scanner + Capstone (Lessons 11–13) — Stage 10c

  • Lesson 11 — Scanner type vocabulary: CloseOutcomeKind, LiquidationRecord, ScanReport, LiquidationScanner. The scaffolding types plus constructors and accessors (no builder) that the scan loop will compose against.
  • Lesson 12 — scan — the orchestration heart of the safety cascade: iterate &[AccountSnapshot], classify each, emit close orders for Liquidatable and Underwater, mutate the insurance fund, and return a ScanReport. The composition layer.
  • Lesson 13 — Capstone — 6 nuanced unit tests + 4 invariant proptests + the Stage 10 retrospective. Synthesis, bridge integration preview, market structure context: how on-chain CLOB liquidations differ from CEX liquidations and from ADL.

SHA pinning per module

Every lesson cites the openhl commit it builds against. For this course, lessons span three commits across Stage 10a → 10c:

ModuleLessonsopenhl SHA
0Lesson 022eedf9 (Stage 10a)
1Lessons 1–322eedf9 (Stage 10a)
2Lessons 4–722eedf9 (Stage 10a)
3Lessons 8–10260883b (Stage 10b)
4Lessons 11–130a8464e (Stage 10c)

Check out the SHA for the module you're on before diffing: 22eedf9 for the pure-compute side (Lessons 0–7), 260883b for the insurance fund (Lessons 8–10), 0a8464e for the scanner (Lessons 11–13).

Prerequisites

To get the most from this course you should have:

  • The openhl funding course in your head. You don't need to remember every lesson, but the fixed-point / saturating-arithmetic / pure-state-machine pattern from funding is the same pattern here. If funding was hard, this will be hard.
  • The openhl CLOB course for AccountId, Side, Qty. We reuse these directly. You don't need the matching engine internals.
  • Familiarity with margin math at the basic level. If you've ever seen "initial margin = 10%, maintenance = 2%" and not been confused, you're set. If you haven't, the perp recap above plus the Hyperliquid help center is enough.
  • No EVM, no precompile knowledge needed. Liquidation is pure state-machine math, just like funding.

You do NOT need:

  • A running openhl node — the crate has zero I/O.
  • Experience with risk engines at exchanges — the model here is small.
  • Quantitative finance background — basic algebra is enough.

Setup

# In your openhl workspace root:
cd ~/code/my-openhl
git checkout main
cargo build --workspace  # baseline — should pass before Lesson 1

Reference checkout (for the answer-key diff at the end of each lesson):

cd ~/code/openhl-reference  # separate checkout from your work tree
git checkout 22eedf9

(Or use the same workspace and git stash between lookups. Either works.)

Course style

Each lesson follows the build-along format established in the earlier openhl courses (consensus, CLOB, precompiles, funding):

  • Goal — what passes / what's built by the end.
  • Recap — where the previous lesson left off.
  • Plan — the specific edits, numbered.
  • Predict callouts (🛑 with "Before scrolling...") — questions before answers.
  • Anti-fluency callouts (🛑 with common misconceptions named explicitly) — preempt the "couldn't we just...?" reflex.
  • Walk-through — step-by-step code edits.
  • Test — the cargo test command + expected output.
  • Design reflection — 3-5 load-bearing decisions encoded in this lesson's code.
  • Answer key — git diff against the openhl reference SHA.
  • Common questions — 3-5 grounded answers.

Module 2 (pure compute) is more proof-heavy than code-heavy compared to the matching engine in the CLOB course. Plan to slow down at the edge cases — the leveraged-regime non-monotonicity in Lesson 5 is where most readers' first mental model breaks. We rebuild it.

Ready

Onward to Lesson 1, where we set up MARGIN_SCALE and the LiquidationParams struct that the network's risk parameters live in.

Summary (3 lines)

  • Liquidation force-closes, inside consensus, any position whose losses have pushed equity / notional below maintenance; the fee goes to the insurance fund, and if the account is underwater the fund absorbs the shortfall.
  • No floats — a classification disagreement (AtRisk vs Liquidatable) diverges blocks and forks the chain — so classification uses bps-scaled (MARGIN_SCALE = 10_000) signed integers with saturating arithmetic.
  • Three building blocks (fixed-point types → margin math → insurance fund + scanner), pinned to 22eedf9 / 260883b / 0a8464e. The hardest step is Lesson 5's levered-regime non-monotonicity. ADL is out of scope (separate course).