-
Notifications
You must be signed in to change notification settings - Fork 3
sync: engine v12.19 baseline (96 commits) #88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
28a161c
512252d
63abb5f
b78a9d3
94df734
331810a
ffbf54e
f486a22
d761f0f
6b6321b
cf4f9d3
443de02
ef2a754
38bcb32
1b397cc
b87b5b5
5666357
9cbc515
9c0fdde
3bf4ca1
aed6012
4445b92
ea5205c
ea735af
088b668
498543d
43c8bfc
6619269
7435d77
23abe05
ad47bb0
6e9d2a5
8b1a012
04a111c
13512f4
8685828
0f28779
379c3ca
072a62e
af18b09
470b273
190656d
c602ce9
8837b45
42f4790
2f51f96
b8f0572
fea80d8
d0906ae
2b34c86
8264871
43a08e7
bf9c093
95665cb
0b02e8f
dd5b34c
79f1734
335d74c
b47eb93
0eae228
7e698b9
54a2365
5f6075b
3f55f87
ab2a4f1
3eda21d
28d0213
1b3de8c
ce2cc7a
d550cb7
9f5ce98
5d2e841
bc7f119
70857de
f8af0c7
e52d337
d27d4b7
70c88b6
ff3863d
7420a0d
f248d80
85aad85
5678746
229f530
e56d350
346994a
fe1d2d8
e8774bf
6652411
33c2b04
bc88671
1299bbd
babfe83
f93336b
7d89767
3969c31
3234d2c
b29df2e
c32bc0b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,29 +4,33 @@ | |
|
|
||
| Risk engine library for permissionless perpetual futures on Solana. | ||
|
|
||
| A predictable alternative to ADL queues. | ||
| A predictable perpetual-futures risk engine built around backed exits, lazy | ||
| overhang clearing, and bounded cranks. | ||
|
|
||
| If you want the `xy = k` of perpetual futures risk engines -- something you can reason about, audit, and run without human intervention -- the cleanest move is simple: stop treating profit like money. Treat it like what it really is in a stressed exchange: a junior claim on a shared balance sheet. | ||
|
|
||
| > No user can ever withdraw more value than actually exists on the exchange balance sheet. | ||
|
|
||
| ## Two Problems, Two Mechanisms | ||
| ## Three Invariants | ||
|
|
||
| A perp exchange has two fairness problems: | ||
| A stressed perp exchange has three jobs: | ||
|
|
||
| 1. **Exit fairness:** when the vault is stressed, who gets paid and how much? | ||
| 2. **Overhang clearing:** when positions go bankrupt, how does the opposing side absorb the residual without deadlocking the market? | ||
| 1. **Backed exits:** when the vault is stressed, nobody can extract more value than the balance sheet can pay. | ||
| 2. **Fair overhang clearing:** when positions go bankrupt, the residual is absorbed pro rata instead of by a discretionary ADL queue. | ||
| 3. **Bounded cranks:** when the oracle moves, the live book is repriced only inside the configured one-step risk budget. | ||
|
|
||
| Percolator solves them with two independent mechanisms that compose cleanly: | ||
| Percolator composes three mechanisms: | ||
|
|
||
| - **H** (the haircut ratio) keeps all exits fair. | ||
| - **A/K** (the lazy side indices) keeps all residual overhang clearing fair, and guarantees markets always return to healthy. | ||
| - **H** (the haircut ratio) makes positive PnL a junior claim on residual value. | ||
| - **A/K/F** (lazy side indices) settles mark moves, funding, and ADL overhang without selecting individual losers. | ||
| - **The price/funding envelope** bounds every exposed accrual step before K/F/price/slot state can mutate. | ||
|
|
||
| --- | ||
|
|
||
| ## H: Fair Exits | ||
| ## H: Backed Exits | ||
|
|
||
| Capital is senior. Profit is junior. A single global ratio determines how much profit is real. | ||
| Capital is senior. Profit is junior. A single global ratio determines how much | ||
| released positive PnL is actually backed. | ||
|
|
||
| ``` | ||
| Residual = max(0, V - C_tot - I) | ||
|
|
@@ -36,44 +40,62 @@ Residual = max(0, V - C_tot - I) | |
| PNL_matured_pos_tot | ||
| ``` | ||
|
|
||
| If fully backed, `h = 1`. If stressed, `h < 1`. Every profitable account sees the same fraction of its *released* profit: | ||
| If fully backed, `h = 1`. If stressed, `h < 1`. Every profitable account sees | ||
| the same fraction of its *released* positive PnL: | ||
|
|
||
| ``` | ||
| ReleasedPos_i = max(PNL_i, 0) - R_i | ||
| effective_pnl_i = floor(ReleasedPos_i * h) | ||
| ``` | ||
|
|
||
| Fresh profit sits in a per-account reserve `R_i` and converts to released (matured) profit through a warmup period. Only matured profit enters the haircut denominator (`PNL_matured_pos_tot`) and the per-account effective PnL. This is the core oracle-manipulation defense — an attacker who spikes a price sees their unrealized gain locked in `R_i`, excluded from both the ratio and their withdrawable amount, until the warmup window passes. | ||
| Fresh profit sits in a per-account reserve `R_i` and converts to released | ||
| (matured) profit through admission and warmup. Only admitted matured profit | ||
| enters the haircut denominator (`PNL_matured_pos_tot`) and per-account effective | ||
| PnL. | ||
|
|
||
| This is the core anti-oracle-manipulation defense. An attacker who spikes a | ||
| price sees live gain locked in reserve, excluded from both the ratio and their | ||
| withdrawable amount, until the instruction policy admits it. Public wrappers | ||
| using untrusted live oracle or execution-price PnL must use nonzero admission | ||
| warmup; stress-threshold gating is not a substitute. | ||
|
|
||
| No rankings, no queue priority, no first-come advantage. The floor rounding is conservative — the sum of all effective PnL never exceeds what exists in the vault. | ||
|
|
||
| When the system is stressed, `h` falls and less converts. When losses settle or buffers recover, `h` rises. Self-healing. | ||
| When the system is stressed, `h` falls and less profit converts. When losses | ||
| settle or buffers recover, `h` rises. Self-healing. | ||
|
|
||
| Flat accounts are always protected — `h` only gates profit extraction, never touches deposited capital. | ||
|
|
||
| --- | ||
|
|
||
| ## A/K: Fair Overhang Clearing | ||
| ## A/K/F: Fair Overhang Clearing | ||
|
|
||
| When a leveraged account goes bankrupt, two things need to happen: remove the position quantity from open interest, and distribute any uncovered deficit across the opposing side. | ||
|
|
||
| Traditional ADL queues pick specific counterparties and force-close them. Percolator replaces the queue with two global coefficients per side: | ||
| Traditional ADL queues pick specific counterparties and force-close them. | ||
| Percolator replaces the queue with lazy side indices: | ||
|
|
||
| - **A** scales everyone's effective position equally. | ||
| - **K** accumulates all PnL events (mark, funding, deficit socialization) into one index. | ||
| - **K** accumulates mark and ADL overhang effects. | ||
| - **F** accumulates funding effects. | ||
|
|
||
| ``` | ||
| effective_pos(i) = floor(basis_i * A / a_basis_i) | ||
| pnl_delta(i) = floor(|basis_i| * (K - k_snap_i) / (a_basis_i * POS_SCALE)) | ||
| pnl_delta(i) = | ||
| floor(|basis_i| * ((K - k_snap_i) * FUNDING_DEN + (F - f_snap_i)) | ||
| / (a_basis_i * POS_SCALE * FUNDING_DEN)) | ||
| ``` | ||
|
|
||
| When a liquidation reduces OI, `A` decreases — every account on that side shrinks by the same ratio. When a deficit is socialized, `K` shifts — every account absorbs the same per-unit loss. | ||
| When a liquidation reduces OI, `A` decreases -- every account on that side | ||
| shrinks by the same ratio. When a deficit is socialized, `K` shifts -- every | ||
| account absorbs the same per-unit loss. Funding moves through `F` the same way: | ||
| accounts settle against their snapshots when touched. | ||
|
|
||
| No account is singled out. Settlement is O(1) per account and order-independent. | ||
|
|
||
| ### Markets Always Return to Healthy | ||
| ### Markets Return to Healthy | ||
|
|
||
| A/K guarantees forward progress through a deterministic cycle: | ||
| A/K/F guarantees forward progress through a deterministic cycle: | ||
|
|
||
| **DrainOnly** — when `A` drops below a precision threshold, no new OI can be added. Positions can only close. | ||
|
|
||
|
|
@@ -85,22 +107,71 @@ No admin intervention. No governance vote. The state machine always makes progre | |
|
|
||
| --- | ||
|
|
||
| ## Price/Funding Envelope | ||
|
|
||
| The third invariant is a system bound: an exposed market cannot be cranked | ||
| through an arbitrary oracle or funding jump in one step. | ||
|
|
||
| For any crank that advances the engine price while open interest exists, the | ||
| allowed price move is capped by elapsed slots: | ||
|
|
||
| ``` | ||
| abs(P_new - P_last) * 10_000 | ||
| <= max_price_move_bps_per_slot * dt * P_last | ||
| ``` | ||
|
Comment on lines
+118
to
+121
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a language tag to the fenced formula block (markdownlint MD040). Line 118 opens a fence without a language identifier. Please set one (for example Suggested patch-```
+```text
abs(P_new - P_last) * 10_000
<= max_price_move_bps_per_slot * dt * P_last🧰 Tools🪛 markdownlint-cli2 (0.22.1)[warning] 118-118: Fenced code blocks should have a language specified (MD040, fenced-code-language) 🤖 Prompt for AI Agents |
||
|
|
||
| Equivalently, the normalized move is bounded by | ||
| `max_price_move_bps_per_slot * dt / 10_000`. | ||
|
|
||
| At a high level, the maximum price movement between exposed cranks is bounded by | ||
| the system's risk budget. If the market is configured around `L` times leverage, | ||
| the safe one-step move is roughly on the order of `1 / L`, with room reserved | ||
| for funding, liquidation fees, integer rounding, and fee floors/caps. | ||
|
|
||
| This turns "crank often enough" into a hard solvency boundary rather than an | ||
| operator preference. A stale or fast-moving oracle target must be fed into the | ||
| engine as a capped staircase of effective prices. Same-slot exposed cranks use | ||
| the previous price; they cannot mark live OI through a zero-time jump. | ||
|
|
||
| Active price or funding accrual also has a maximum elapsed-slot window; beyond | ||
| that, ordinary live catch-up fails closed and the wrapper must use recovery or | ||
| resolution. | ||
|
|
||
| Initialization proves a per-risk-notional envelope for the worst allowed | ||
| price/funding step plus liquidation fees. At runtime, before any K/F/price/slot | ||
| mutation, the engine checks that the next effective step stays inside that | ||
| envelope. If it does not fit, the crank fails closed instead of moving the | ||
| market into an unbudgeted state. | ||
|
|
||
| --- | ||
|
|
||
| ## How They Compose | ||
|
|
||
| | | H | A/K | | ||
| |---|---|---| | ||
| | **Solves** | Exit fairness | Overhang clearing | | ||
| | **Math** | Pro-rata profit scaling | Pro-rata position/deficit scaling | | ||
| | **Triggered by** | Withdrawal or conversion | Bankrupt liquidation | | ||
| | **Recovery** | Automatic as Residual improves | Deterministic three-phase reset | | ||
| | | H | A/K/F | Price/funding envelope | | ||
| |---|---|---|---| | ||
| | **Solves** | Backed exits | Bankrupt overhang clearing | Bounded live repricing | | ||
| | **Math** | Pro-rata profit scaling | Pro-rata position, mark, funding, and deficit scaling | Exact per-risk-notional loss budget | | ||
| | **Triggered by** | Withdrawal, conversion, settlement | Mark, funding, liquidation, reset | Live accrual/crank | | ||
| | **Failure mode** | Less profit is released | Side drains and resets | Crank fails closed or wrapper stair-steps | | ||
|
|
||
| Together: | ||
| - No user can withdraw more than exists. | ||
| - No user is singled out for forced closure. | ||
| - Markets always recover. | ||
| - Flat accounts keep their deposits. | ||
|
|
||
| A/K fairness is exact for open-position economics. H fairness is exact only for the currently stored realized claim set, not for the economically "true" claim set you would get after globally cranking everyone. | ||
| - Risk-increasing trades cannot count their own favorable execution slippage as margin. | ||
| - Markets recover through deterministic side resets. | ||
| - Exposed cranks are bounded to the configured price/funding budget. | ||
| - Raw oracle targets are wrapper-owned; the engine only sees capped effective prices. | ||
|
|
||
| A/K/F fairness is exact for open-position economics. H fairness is exact for the | ||
| currently stored realized claim set, not for the economically "true" claim set | ||
| you would get after globally touching every account. | ||
|
|
||
| The engine is not the whole public protocol by itself. A compliant wrapper must | ||
| enforce authorization, source and clamp oracle/funding inputs, use nonzero live | ||
| PnL admission for untrusted public flows, sync recurring fees when enabled, and | ||
| reject extraction-sensitive actions while raw oracle target and effective engine | ||
| price diverge. | ||
|
|
||
| --- | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,47 @@ | ||
| fn main() { | ||
| println!("ACCOUNTS_OFF={}", core::mem::offset_of!(percolator::RiskEngine, accounts)); | ||
| println!("C_TOT={}", core::mem::offset_of!(percolator::RiskEngine, c_tot)); | ||
| println!("PNL_POS_TOT={}", core::mem::offset_of!(percolator::RiskEngine, pnl_pos_tot)); | ||
| println!("ADL_MULT_LONG={}", core::mem::offset_of!(percolator::RiskEngine, adl_mult_long)); | ||
| println!("ADL_MULT_SHORT={}", core::mem::offset_of!(percolator::RiskEngine, adl_mult_short)); | ||
| println!("ADL_EPOCH_LONG={}", core::mem::offset_of!(percolator::RiskEngine, adl_epoch_long)); | ||
| println!("ADL_EPOCH_SHORT={}", core::mem::offset_of!(percolator::RiskEngine, adl_epoch_short)); | ||
| println!("NUM_USED={}", core::mem::offset_of!(percolator::RiskEngine, num_used_accounts)); | ||
| println!("BITMAP={}", core::mem::offset_of!(percolator::RiskEngine, used)); | ||
| println!("ENGINE_SIZE={}", std::mem::size_of::<percolator::RiskEngine>()); | ||
| println!("ACCOUNT_SIZE={}", std::mem::size_of::<percolator::Account>()); | ||
| println!( | ||
| "ACCOUNTS_OFF={}", | ||
| core::mem::offset_of!(percolator::RiskEngine, accounts) | ||
| ); | ||
| println!( | ||
| "C_TOT={}", | ||
| core::mem::offset_of!(percolator::RiskEngine, c_tot) | ||
| ); | ||
| println!( | ||
| "PNL_POS_TOT={}", | ||
| core::mem::offset_of!(percolator::RiskEngine, pnl_pos_tot) | ||
| ); | ||
| println!( | ||
| "ADL_MULT_LONG={}", | ||
| core::mem::offset_of!(percolator::RiskEngine, adl_mult_long) | ||
| ); | ||
| println!( | ||
| "ADL_MULT_SHORT={}", | ||
| core::mem::offset_of!(percolator::RiskEngine, adl_mult_short) | ||
| ); | ||
| println!( | ||
| "ADL_EPOCH_LONG={}", | ||
| core::mem::offset_of!(percolator::RiskEngine, adl_epoch_long) | ||
| ); | ||
| println!( | ||
| "ADL_EPOCH_SHORT={}", | ||
| core::mem::offset_of!(percolator::RiskEngine, adl_epoch_short) | ||
| ); | ||
| println!( | ||
| "NUM_USED={}", | ||
| core::mem::offset_of!(percolator::RiskEngine, num_used_accounts) | ||
| ); | ||
| println!( | ||
| "BITMAP={}", | ||
| core::mem::offset_of!(percolator::RiskEngine, used) | ||
| ); | ||
| println!( | ||
| "ENGINE_SIZE={}", | ||
| std::mem::size_of::<percolator::RiskEngine>() | ||
| ); | ||
| println!( | ||
| "ACCOUNT_SIZE={}", | ||
| std::mem::size_of::<percolator::Account>() | ||
| ); | ||
| println!("MAX_ACCOUNTS={}", percolator::MAX_ACCOUNTS); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Version signaling is inconsistent with the rest of the document.
Lines 14–27 present the v12.19 model, but later sections still label the feature/spec baseline as v12.17 (e.g., Line 180 and Line 209). For operator and auditor traceability, keep one explicit version target throughout or add a clear “mixed-version” note.
🤖 Prompt for AI Agents