レッスン0 — OpenHL Liquidation を作る(永久先物ポジション liquidation エンジン)
問い
前コース(Funding)で mark と index の乖離を funding payment で抑える仕組みは手に入った。だがトレーダーの損失が預け入れ collateral を超えたら? その時ポジションを consensus 内で force-close し、不足分を吸収するエンジンを、float なしの決定論で作るには?
注: OpenHL コースのコードブロックは原則として手元で実行可能な形で示す。ただし
<file>などのプレースホルダや答え合わせ用コマンドは、各レッスンの指示に従って置換してから実行すること。
原理(最小モデル)
- perp はレバレッジ position。損失が collateral を食い
equity / notionalが maintenance margin を下回ると force-close。 反対 side・フルサイズで close、liquidation fee を collateral から引いて insurance fund へ、equity が正なら残りをアカウントに返す。equity が負(underwater)なら不足分を insurance fund が吸収。 - liquidation は consensus 内で実行する。 全 validator が全 block で、同じデータ・同じコードで maintenance 割れを独立計算する。オフチェーン bot に外注すると、検知〜close の RPC ラウンドトリップ遅延がそのまま chain の損失になる(50× の position は数秒で healthy→underwater に反転しうる)。
- float 禁止 — consensus が壊れる。 あるアカウントを
Liquidatableと分類する validator とAtRiskと分類する validator がいると、close orders / fees / insurance-fund deltas が食い違い block が分岐して fork する。固定小数点MARGIN_SCALE = 10_000(bps)+ saturating + i64 overflow しうる乗算に i128 中間値。 - 4 状態の margin classification。
Safe/AtRisk/Liquidatable/Underwater— 各状態が engine に何を許可するかを決める。 - insurance fund は単なる
u64残高でなく pure state machine。 独自の遷移ルール(deposit/withdraw/absorb_deficitの不変条件)を持つ。
具体例
トレーダーは collateral(USDC) を預け、entry 価格で size(符号付き: 正=long、負=short)の position を開く:
unrealized PnL = size とともに mark で動く(long は mark>entry で利益)
equity = collateral + unrealized_pnl
margin_ratio = equity / notional (notional = |size| × mark)
margin_ratio < maintenance(2%) → Liquidatable → force-close + fee → insurance fund
close 前に equity < 0 → Underwater → 不足分を insurance fund が absorb
bps が margin の慣例単位(HL/Binance/Drift とも)。MarginRatio(1_000) = 10%、MarginRatio(MARGIN_SCALE) = 100%。
失敗例(誤解)
「liquidation はオフチェーンの liquidator bot(アカウントを scan して liquidate(account) を呼ぶ)に外注すればいい」は誤り — perp のスピードでは破綻する。低頻度 settlement(CDS 等)なら機能するが、50× レバレッジの position はニュース cascade で数秒のうちに healthy から underwater に反転する。検知から close までの RPC ラウンドトリップの遅延は丸ごと chain 側の損失として残る。敵対的な市場の動きのもとで chain が支払い能力を保つ手段は、consensus 内 liquidation 以外にない。
ここまでで「なぜ consensus 内 liquidation」「なぜ float 不可」は着地した。ここから先はスコープ・前提・14 レッスンのロードマップに入る。L1 以降は実際に Rust を書く。
🛑 予測。 2 つの validator が同じアカウントの margin health を分類する。片方が
f64のequity / notionalを計算したら、もう片方と bit-exact に一致するか?(答え: 保証されない。float は compiler/CPU/演算順で最下位ビットが食い違いうる。分類が 1 段でもズレると(AtRiskvsLiquidatable)、生成される block が変わり fork する。だから bps スケールの符号付き整数で分類する — 整数演算は全プラットフォームで byte-identical。)
終了時に手にするもの
新規 crates/liquidation/ crate、3 ソースファイル / ~600 LOC:
- 固定小数点 types モジュール —
MARGIN_SCALE、LiquidationParams、MarginRatio、MarginHealthenum、AccountSnapshot、CloseOrderSpec。 - 純粋な compute モジュール(margin math) —
notional_value/unrealized_pnl/account_equity/margin_ratio/margin_health/close_order_spec。 - state machine(insurance fund)+ multi-account scanner —
InsuranceFund(deposit/withdraw/absorb_deficit/credit_fee)とLiquidationScanner(scan)。 - 24+ tests(compute マイルストーン時点、capstone までに増える): hand-traced unit test、margin-ratio の単調性・determinism proptest、insurance fund の保存則 invariant。
終了時にも手にしないもの(意図的な scope cut)
- Auto-deleveraging(ADL) — この設計の端に位置づくが、本コースでは扱わない(別コース
building-openhl-adl)。 - Oracle(mark/index の供給元) — liquidation は mark を 入力 として受け取るだけ。
- Bridge 統合 — liquidation engine は純粋な state machine として完結。
LiveRethEvmBridgeへの plug-in は capstone でプレビューし、実装は下流。
前提
- Step 4(Funding) が頭にあること — fixed-point / saturating 演算 / pure state machine のパターンが再登場する(funding が難しかったなら本コースも難しい)。
- Step 2(CLOB) の
AccountId/Side/Qtyを直接再利用する(matching engine の内部までは不要)。 - 基本レベルの margin math(「initial margin 10% / maintenance 2%」で混乱しない程度)。
- EVM / precompile の知識は不要(liquidation は純粋な state-machine 数学)。
不要: 動作中の openhl ノード(zero I/O)/ 取引所リスクエンジン経験 / 定量金融バックグラウンド。
セットアップ(今やる)
cd ~/code/my-openhl
git checkout main
cargo build --workspace # baseline — L1 前に通るべき
cd ~/code/openhl-reference # answer-key 用の別チェックアウト
git checkout 22eedf9
14 レッスンのロードマップ
| # | build するもの | 終了時テスト |
|---|---|---|
| 0 | Orientation(本レッスン) | セットアップ確認 |
| 1 | MARGIN_SCALE = 1e4(bps)+ LiquidationParams + hyperliquid_default() | cargo check -p openhl-liquidation |
| 2 | MarginRatio newtype + MarginHealth enum(4 状態) | 型がコンパイル |
| 3 | AccountSnapshot + CloseOrderSpec | roster 完成 |
| 4 | notional_value + unrealized_pnl(signed-multiplication) | 符号テスト pass |
| 5 | account_equity + margin_ratio + 非単調性 proptest | proptest pass(prop_assume!) |
| 6 | margin_health 分類カスケード(strict less-than) | 分類テスト pass |
| 7 | close_order_spec(market order の規律) | compute 完成 |
| 8 | InsuranceFund + deposit/withdraw | single-balance state machine |
| 9 | absorb_deficit(underwater が fund を drain) | 保存則 test pass |
| 10 | credit_fee(fee が collateral→fund)+ close-outcome decomposition | composition test pass |
| 11 | Scanner 型語彙(CloseOutcomeKind/LiquidationRecord/ScanReport/LiquidationScanner) | 型がコンパイル |
| 12 | scan(safety cascade の orchestration) | scan test pass |
| 13 | Capstone — 6 unit test + 4 invariant proptest | 全 tests pass |
マイルストーンは レッスン13 — 全 tests が通り、4 状態分類 → insurance fund → multi-account scanner が 1 つの orchestration ループに結合する。レッスン13 で「まだ何が足りないか(oracle / bridge / ADL)」を名指す。
答え合わせの規律
本コースは Liquidation 参照実装の 3 パートにまたがる:
| Module | レッスン | openhl SHA |
|---|---|---|
| 型 + 純粋な compute | L1〜7 | 22eedf9(計算パート) |
| Insurance fund | L8〜10 | 260883b(保険基金パート) |
| Scanner + Capstone | L11〜13 | 0a8464e(スキャナパート) |
# レッスン範囲に応じて checkout:
# 22eedf9 (計算), 260883b (保険基金), 0a8464e (スキャナ)
cd ~/code/openhl-reference && git checkout 22eedf9
diff -u ~/code/my-openhl/crates/liquidation/src/types.rs ./crates/liquidation/src/types.rs
diff -u ~/code/my-openhl/crates/liquidation/src/compute.rs ./crates/liquidation/src/compute.rs
diff -u ~/code/my-openhl/crates/liquidation/src/insurance.rs ./crates/liquidation/src/insurance.rs
diff -u ~/code/my-openhl/crates/liquidation/src/scanner.rs ./crates/liquidation/src/scanner.rs
diff -u ~/code/my-openhl/crates/liquidation/src/lib.rs ./crates/liquidation/src/lib.rs
合格基準
cargo test -p openhl-liquidation --releaseを全 tests 通せる(コース完走時)。- なぜ liquidation を consensus 内で実行するか(オフチェーンだと支払い能力を主張できない)を 1 文で言える。
- 4 状態分類(Safe/AtRisk/Liquidatable/Underwater)と、それぞれが engine に何を許可するかを説明できる。
まとめ(3行)
- liquidation は、損失が collateral を食って
equity/notionalが maintenance を下回った position を consensus 内で force-close するエンジン。fee を insurance fund へ、underwater なら不足分を fund が absorb。 - float は分類のズレ(
AtRiskvsLiquidatable)で block 分岐 → fork を招くので使わず、bps スケール(MARGIN_SCALE = 10_000)の符号付き整数 + saturating で分類する。 - 3 building block(固定小数点 types → margin math → insurance fund + scanner)。最大の山は レッスン5 の levered-regime 非単調性。ADL は scope 外(別コース)。
次のレッスン(レッスン1)
MARGIN_SCALE = 1e4(bps)と、ネットワークのリスクパラメータを収める LiquidationParams 構造体 + hyperliquid_default()(initial 10% / maintenance 2% / fee 1.5%)を作る。