Lesson 11 — Capstone — what you built, what's deferred, what comes next
Question
You've built the openhl funding state machine. Retrospective + what's deferred to other courses (Liquidation / ADL / Scanner) + the Stage 10 quartet end-to-end view.
Principle (minimum model)
- You built. 9 newtypes + 4 pure-compute functions (compute_premium / compute_rate / apply_funding + a clock helper) +
FundingClockdiscrete state machine + 12+ tests including 3+ proptests. - Three invariants proved. Zero-sum (Σ payments = 0) + interval-gating (exactly one Fund per interval) + no-catch-up (one Fund = one interval regardless of elapsed time).
- Pinned to SHA
cd94137. Byte-for-byte reproducible against the openhl reference.git checkout cd94137gives the same answer. - What's deferred. Liquidation scanner (separate course) + insurance fund (separate course) + ADL (separate course). All compose with your funding state machine via the type system.
- Stage 10 quartet view. Funding (this course) + Insurance fund + Liquidation scanner + ADL. Four courses compose into the openhl safety-net cascade.
- Composition guarantee. Each layer's output is the next layer's input. Type-system enforced:
FundingPayment → AccountState → LiquidationDecision → AdlReport. - Production parallels. Hyperliquid HyperEVM uses this exact discipline (different code, same shape). What you built is operationally close to a production funding engine.
Worked example + steps
Lesson 11 — Capstone — what you built, what's deferred, what comes next
Goal
By the end of this lesson:
- You can sketch the funding pipeline on a whiteboard from memory:
(mark, index)→ premium → rate → settlements, gated by the clock. - You can name the five deferred items (oracle integration, balance updates, liquidations, multi-market funding, funding-as-EVM-event) and explain why each is out of scope for
crates/funding/. - You can sketch where four extensions would land in a future course.
- You're ready to wire this state machine into a perpetual DEX.
No code in this lesson. Just the mental model.
The pipeline, in one diagram
┌────────────┐ ┌─────────────┐
│ MarkPrice │ │ IndexPrice │ (raw u64, upstream oracle price, off-chain)
└─────┬──────┘ └──────┬──────┘
│ │
▼ ▼
┌─────────────────────┐
│ compute_premium │ → Premium (i64, RATE_SCALE = 1e9 scale)
└──────────┬───────────┘
│
▼
┌─────────────────────┐
│ compute_rate │ ← FundingParams (divisor: u32, rate_cap: FundingRate, …)
└──────────┬───────────┘
│
▼ FundingRate (i64, RATE_SCALE = 1e9 scale, clamped to ±rate_cap)
│
┌─────┴─────┐
│ │
▼ ▼
┌──────────────────────┐
│ apply_funding │ ← &[Position] (account snapshots), MarkPrice
└──────────┬───────────┘
│
▼
Vec<Settlement> → each element = { account: AccountId, delta: Notional }
Notional is i64, raw quote-currency amount (1 unit = 1)
bridge → balance updates (future)
╔═══════════════════════════════════════════════════════╗
║ FundingClock::tick ║
║ ║
║ guard: now ≥ last_settled_at + interval_secs? ║
║ no → return None ║
║ yes → execute pipeline above, advance to `now` ║
╚═══════════════════════════════════════════════════════╝
Read top-to-bottom: prices in, settlements out. The clock wraps the whole pipeline behind a "has enough time elapsed?" gate.
Track-level topology note: this Vec<Settlement> lane is still running outside the EVM mainline (BlockExecutor), in the same way Course 7 carried Vec<Fill> in a parallel lane. At this stage, both fills and settlements are bridge-side lanes that later merge at payload/state application boundaries.
What each module delivered
Module 1 (Determinism + types, Lessons 1–3) — Fixed-point vocabulary:
RATE_SCALE = 1_000_000_000(ppb): the load-bearing constant.- 9 newtypes:
MarkPrice,IndexPrice,Premium,FundingRate,Notional,PositionSize,Position,Settlement,FundingParams. hyperliquid_default(): 3600s interval, ±4% cap, divisor 8.- Lesson learned: newtypes prevent argument-order bugs at compile time; sign conventions live in doc comments at definition site.
Module 2 (Pure compute, Lessons 4–7) — Stateless math:
compute_premium(mark, index) → Premium— graceful onindex == 0, i128 intermediates, saturate.compute_rate(premium, params) → FundingRate— divide-then-clamp, defensive.abs()on cap.apply_funding(positions, mark, rate) → Vec<Settlement>— longs-pay-shorts via unary minus, filters flat positions.saturate_i128_to_i64: 3-line private helper, the only safety net at type boundaries.- 15 tests: 13 hand-traced + 2 proptests (antisymmetry, balanced-book zero-sum).
- Lesson learned: panic-vs-wrap-vs-saturate as a 3-way design tension; saturation is the only consensus-safe choice.
Module 3 (Clock state machine, Lessons 8–10) — Discrete event loop:
FundingClock+FundingTick+tick().- 7 tests covering: guard semantics, boundary cases, interval persistence, no-catch-up.
- Lesson learned: composition tests catch wiring errors; state machines need multi-call tests; design philosophy belongs in doc comments + tests + lesson prose, never in just one place.
The honest deferred
Five things crates/funding/ doesn't do. Each is a real production gap, deliberately deferred to keep this crate a pure state machine.
1. Oracle integration
What we have: compute_premium takes mark: MarkPrice, index: IndexPrice as inputs.
What we don't have: a way to get those prices. The caller must source mark from the CLOB (via something like clob.best_bid_with_qty() mid-price) and index from an external oracle (Pyth, Chainlink, a validator-attested feed).
Why deferred: oracle plumbing is its own discipline — staleness checks, deviation circuit breakers, multi-source aggregation, validator-set sign-off. Bundling it into the funding crate would couple two unrelated concerns. The bridge layer (future course) wires the oracle to tick().
When to revisit: when wiring the funding crate into LiveRethEvmBridge. The bridge's payload-building code will read the latest mark/index just before calling clock.tick(...).
2. Balance updates
What we have: tick() returns Vec<Settlement> — a list of (account, delta) pairs.
What we don't have: any mechanism to apply those deltas to account balances.
Why deferred: balance state lives in EVM storage (or another store maintained by the bridge). The funding crate is intentionally storage-free — it computes, it doesn't persist. The bridge takes the Vec<Settlement> and emits balance-update transactions or direct state mutations.
When to revisit: same as oracle integration. The bridge layer is where settlements meet balances.
3. Liquidations
What we have: settlements that can push an account's balance arbitrarily negative.
What we don't have: any check that an account has capacity to absorb the funding payment, or any logic for what happens when it doesn't.
Why deferred: liquidation is a separate state machine with its own invariants (insurance fund, ADL waterfalls, mark-price triggers). Tying it to funding would conflate two cadences (funding is hourly; liquidation is per-block). Liquidation should be its own crate.
When to revisit: after balance updates. The bridge sees a balance go negative; then the liquidation engine kicks in.
4. Multi-market funding
What we have: a single FundingClock for a single market.
What we don't have: a way to manage funding across multiple perpetual markets (BTC-USD, ETH-USD, SOL-USD, etc.) with potentially different intervals or caps.
Why deferred: the multi-market design is straightforward — one FundingClock per market, all managed by a HashMap<MarketId, FundingClock> at the bridge layer. The crate doesn't need to know about market multiplicity; it just needs to be correct for one.
When to revisit: when openhl adds a second market. Probably never as part of this crate — the multiplexing belongs above.
5. Funding as EVM events
What we have: settlements as Vec<Settlement> returned from tick().
What we don't have: a way for smart contracts to observe a funding tick. A contract that wants to react to funding (e.g., "auto-deleverage when funding exceeds X%") can't subscribe to it as an event.
Why deferred: emitting EVM events from non-EVM code requires plumbing — the bridge would have to convert each Settlement into an EvmLog and inject it into the next block. It's a bridge-layer concern, not a state-machine concern.
When to revisit: when there's a concrete contract use case that demands event-based funding observation. Until then, telemetry can be done at the bridge layer.
What comes next
Four extensions you could ship after this course:
Extension 1: Oracle adapter (2-3 days)
A small crates/oracle/ that pulls index prices from one or more sources (Pyth, Chainlink, validator-signed), aggregates with staleness checks, and exposes fn current_index_price() -> Option<IndexPrice>. The bridge calls this just before clock.tick(...). The hard part is choosing the staleness threshold; the code is straightforward.
Extension 2: Bridge-side funding tick (1 week)
Wire FundingClock into LiveRethEvmBridge. The bridge owns the clock instance, reads mark from the CLOB, reads index from the oracle, gets positions from the perpetuals position store, calls tick(), and applies the resulting settlements to balances. Most of the work is plumbing; the funding crate is self-contained.
Extension 3: Liquidation engine (3-4 weeks)
A separate crates/liquidation/ that monitors balances post-funding-tick, identifies under-margined accounts, and routes them through the insurance fund / ADL waterfall. Big design discussions: insurance fund sizing, partial liquidation, MEV protection. This is its own course.
Extension 4: Multi-market manager (1 week)
A crates/markets/ that maintains HashMap<MarketId, FundingClock> plus per-market position stores. The bridge dispatches funding ticks per market at the right cadences. Conceptually simple; the value is in the per-market isolation.
Course completion — what you've internalized
Five skills that generalize beyond perpetual funding:
- Fixed-point arithmetic for consensus systems. Any time you need to share numerical state across validators — funding, fees, oracle prices, vesting schedules — you'll use signed integers + a scale constant. Stated as the general pattern: real-valued
xandyare encoded with a scale factorSasX = x × SandY = y × S; multiplications widen the intermediate into a larger integer type, then divide bySat the end to land back in the original scale:
(S = scale factor; this course uses S = RATE_SCALE = 1e9)
real-number space: x · y ──► x × y
│ │ │
▼ ▼ ▼
fixed-point space: X = x·S Y = y·S X × Y = (x × y) × S²
│
▼ (received by a wider type, e.g. i128)
(x × y) × S²
│
▼ ÷ S
(x × y) × S ◄── final representation
(back to the original fixed-point scale)
That one identity is the spine of every wrestling-with-intermediates moment in Module 2: the product carries an extra factor of S, so we let i128 hold it, then divide by RATE_SCALE to cancel it back out. RATE_SCALE = 1e9 is the pattern; the constant value is the variable. A fee calculator might use S = 10_000 (basis-points scale) and reach for the same identity; a vesting schedule might use S = 86_400 (seconds-per-day) to build a fixed-point representation along the time axis.
-
Saturation as the consensus-safe overflow strategy. Panic = chain fork via halt. Wrap = chain fork via wrong value. Saturate = bounded, consistent across validators. For any consensus-critical math, saturate is the only choice.
-
Newtype pattern for semantic distinction.
MarkPriceandIndexPriceboth wrapu64, but they're different concepts. The newtype prevents arg-order bugs at compile time, and the doc comment carries the sign convention. 5 lines per newtype; entire bug classes prevented. -
Composition tests for layered code. Each layer (
compute_premium,compute_rate,apply_funding) is tested individually, but the layering itself is a separate concern.tick()tests verify the composition; unit tests verify the pieces. You need both. -
Design philosophy lives in code + doc + tests + prose. The no-catch-up invariant is named in
clock.rs's module doc, enforced bytick()'s implementation, verified byno_catchup_after_long_gap, and explained in this course. Four places to find the rationale; rationale survives even when individual pieces change.
Where this course sits in the L1 Architect track
Courses 1-5 (Reth internals): pipeline, payload building, NodeBuilder, evm crate, RPC.
Course 6 (openhl-consensus): Malachite integration.
Course 7 (openhl-clob): matching engine.
Course 8 (openhl-precompiles): EVM ↔ CLOB bridge via custom precompiles.
Course 9 (this one): funding state machine. Pure state, no I/O — the contrast to course 8's bridge plumbing.
Course 10 (openhl-bridge-integration — future): wires funding + oracle + liquidation into LiveRethEvmBridge. This is where everything from courses 6-9 composes into a runnable perp DEX.
You're now 90% of the way through the L1 Architect track. The patterns from this course (fixed-point, saturation, composition tests) apply across the remaining work.
Final answer key
cd ~/code/openhl-reference
git checkout cd94137
diff -u ~/code/my-openhl/crates/funding/ ./crates/funding/ --recursive
After Lesson 11, the entire crates/funding/ directory should be byte-identical to Stage 8b. You've reproduced 1 commit (~635 LOC across 3 files) by hand, with full understanding of why each line is there. The crate compiles standalone, tests pass standalone, no external dependencies beyond openhl-clob (for AccountId).
Return:
git checkout main
You shipped this
22 tests passing. 3 source files. ~635 LOC of production Rust. A funding state machine that:
- computes deterministic premium/rate/settlement math at signed fixed-point precision;
- saturates rather than panics on pathological inputs;
- gates settlements on a configurable interval;
- refuses to catch up after long gaps (philosophical choice that aligns math with fairness).
That's the entire HL-shape perpetual funding mechanism, in a crate you can drop into any Rust trading system. The next time someone asks "how does perpetual funding work?" — show them this crate.
Go build perpetuals.
Summary (3 lines)
- You built: 9 newtypes + 4 pure-compute functions + FundingClock + 12+ tests including 3 proptests. Three invariants proved.
- Pinned to SHA
cd94137for byte-for-byte answer-keys. Deferred: scanner / insurance fund / ADL (separate courses). - Stage 10 quartet (Funding / Insurance / Scanner / ADL) composes via type system. Production parallel: Hyperliquid HyperEVM. Funding course complete.