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)
- 14 lessons across 5 modules. Orientation → Types (3 lessons) → Pure compute (4 lessons, including liquidation_fee + close-outcome decomposition) → Insurance fund (3 lessons) → Scanner & capstone (3 lessons).
- Three SHAs because three sub-systems. Pure compute (computing equity / margin ratio / health), Insurance fund (managing the absorption buffer), Scanner (orchestration). Each pinned separately.
- Layer 1 = scanner. Detects + force-closes. Layer 2 = insurance fund. Layer 3 = ADL (separate course).
- Prerequisites. openhl Funding + perp primer + Liquidation L9 of perp primer.
- Pure compute throughout the math. Reusable in test + production.
- Production parallel. Hyperliquid liquidation engine uses this exact discipline.
Worked example + steps
Build OpenHL Liquidation — perpetual position liquidation engine
What you'll build
The previous course (building-openhl-funding) 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:
- 3 source files / ~600 LOC in a new
openhl-liquidationcrate. - 24+ tests passing at the Stage 10a milestone, more by 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 pure state machine (with its own transition rules —
deposit/withdraw/absorb_deficitinvariants), not just a plainu64balance entry. - 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) was your first deep encounter with this discipline; this course is the second.
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 —
MarginRationewtype +MarginHealthenum (Safe/AtRisk/Liquidatable/Underwater). Why four states, what each authorizes. - Lesson 3 —
AccountSnapshot+CloseOrderSpec. Why a new snapshot type (notfunding::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 whyprop_assume!is the right fix. - Lesson 6 —
margin_healthclassification. 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 —
InsuranceFundstruct +deposit/withdraw. The single-balance state machine. - Lesson 9 —
absorb_deficit: how an Underwater liquidation drains the fund. - Lesson 10 —
credit_fee: liquidation fee flows from collateral into the fund. Composition test: a single liquidation can both credit a fee and absorb a deficit when the position is severely underwater.
Module 4 — Scanner + Capstone (Lessons 11–13) — Stage 10c
- Lesson 11 — Scanner type vocabulary:
CloseOutcomeKind,LiquidationRecord,ScanReport,LiquidationScanner. The scaffolding types and builder API that the scan loop will compose against. - Lesson 12 —
scan— the orchestration heart of the safety cascade: iterate&[AccountSnapshot], classify each, emit close orders forLiquidatableandUnderwater, return insurance-fund deltas. 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:
| Module | Lessons | openhl SHA |
|---|---|---|
| 0 | Lesson 0 | 22eedf9 (Stage 10a) |
| 1 | Lessons 1–3 | 22eedf9 (Stage 10a) |
| 2 | Lessons 4–7 | 22eedf9 (Stage 10a) |
| 3 | Lessons 8–10 | Stage 10b — TBD |
| 4 | Lessons 11–12 | Stage 10c — TBD |
The TBD rows update as Stage 10b and 10c ship. Until then, modules 3 and 4 are skeleton — the modules 1-2 content (the entire pure-compute side) is fully built against 22eedf9 and ready to take you through Stage 10a end-to-end.
Prerequisites
To get the most from this course you should have:
- Course 9 (openhl-funding) 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.
- Course 7 (openhl-clob) 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 courses 6-9:
- 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 testcommand + expected output. - Design reflection — 3-5 load-bearing decisions encoded in this lesson's code.
- Answer key —
git diffagainst 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 course 7. 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)
- Build openhl liquidation = scanner (Layer 1) + insurance fund (Layer 2). 14 lessons / 5 modules.
- Pinned to 3 SHAs (compute / insurance / scanner). Prerequisites: openhl Funding + perp primer.
- Pure compute for math; insurance fund is the first non-pure piece. Production parallel: Hyperliquid.