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 / notionalfalls 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
Liquidatableand another asAtRisk, close orders / fees / insurance-fund deltas differ, blocks diverge, and the chain forks. Fixed-pointMARGIN_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 abalance ≥ 0invariant every operation preserves. - Three sub-stages, three pinned SHAs. Pure compute
22eedf9(Stage 10a), insurance fund + close-outcome decomposition260883b(Stage 10b), multi-account scanner0a8464e(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-liquidationcrate at0a8464e(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 abalance ≥ 0invariant), 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 —
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 + constructors +deposit, plus theWithdrawOutcomeenum. The single-balance state machine. - Lesson 9 —
withdraw_shortfall: how an Underwater liquidation drains the fund, and theCovered/PartiallyDrained/Depletedboundary 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 forLiquidatableandUnderwater, mutate the insurance fund, and return aScanReport. 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 | 260883b (Stage 10b) |
| 4 | Lessons 11–13 | 0a8464e (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 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 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 / notionalbelow maintenance; the fee goes to the insurance fund, and if the account is underwater the fund absorbs the shortfall. - No floats — a classification disagreement (
AtRiskvsLiquidatable) 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).