Design: protected principal + junior profit claims + lazy A/K/F side indices, native 128-bit persistent state. Status: implementation source of truth. Normative terms are MUST, MUST NOT, SHOULD, MAY. Scope: one perpetual DEX risk engine for one quote-token vault.
This revision supersedes v12.19.13. It preserves the v12.19.13 economics and adds the account-free catchup composition rule: wrappers may not use a no-touch catchup instruction to perform equity-active accrual on exposed markets.
The stress-scaled consumption threshold is not an anti-oracle-manipulation warmup. Public or permissionless wrappers using untrusted live oracle or execution-price PnL MUST use a nonzero live admission minimum (
admit_h_min > 0) for positive PnL.admit_h_min = 0is only appropriate for trusted/private deployments or other non-public flows that explicitly accept immediate-release semantics.The engine's
oracle_priceinput is the effective engine price that will be accrued against, not necessarily the raw external oracle target. A public wrapper whose raw normalized target jumps farther than the engine price cap MUST feed the engine a valid capped staircase price, keep the raw target separate from the last effective engine price, and restrict or conservatively shadow-check user value-moving/risk-increasing operations while the target and effective engine price differ.
The engine safety boundary is:
- exact lazy A/K/F accounting for all mark, funding, and ADL effects;
- exact positive-PnL junior-claim haircuts bounded by
Residual = V - (C_tot + I); - mandatory warmup/admission for live positive PnL;
- exact candidate-trade positive-slippage neutralization;
- an exact per-risk-notional solvency envelope checked at initialization;
- per-accrual price-move and funding envelopes checked before any K/F/price/slot mutation;
- wrapper-owned oracle-target catch-up that never feeds a cap-violating raw jump into live exposed accrual; and
- no account-free wrapper instruction may perform equity-active accrual while the market has open interest.
Every top-level instruction is atomic. Any failed precondition, checked arithmetic guard, missing authenticated account proof, context-capacity overflow, or conservative-failure condition MUST roll back every mutation performed by that instruction.
The engine MUST maintain the following properties.
- Flat protected principal is senior. An account with effective position
0MUST NOT have protected principal reduced by another account’s insolvency. - Open opposing positions MAY be subject to explicit deterministic ADL during bankrupt liquidation. ADL MUST be visible protocol state, never hidden execution.
- Live positive PnL MUST pass admission. It MUST NOT be directly withdrawable, converted to principal, or counted as matured collateral unless admitted by the current instruction policy and the engine gates.
- Public or permissionless wrappers with untrusted live oracle or execution-price PnL MUST use
admit_h_min > 0; stress-threshold gating is additive and MUST NOT be treated as a substitute for warmup. - A candidate trade’s own positive execution-slippage PnL MUST be removed from that same trade’s risk-increasing approval metric.
- Explicit protocol fees are collected into
Iimmediately or tracked as account-local fee debt up to collectible headroom. Uncollectible fee tails are dropped, not socialized. - Losses are senior to engine-native fees on the same local capital state.
- Synthetic liquidation close executes at oracle mark; liquidation penalties are explicit fees only.
- Resolved positive payouts MUST wait for all stale accounts and all negative PnL to be reconciled, then use one shared payout snapshot.
- Any arithmetic not proven unreachable by bounds MUST have checked, deterministic behavior. Silent wrap, unchecked panic, and undefined truncation are forbidden.
- Account capacity is finite; empty fully-drained accounts MUST be reclaimable permissionlessly.
- Keeper progress MUST be possible with off-chain candidate discovery and without a mandatory on-chain global scan.
- The wrapper MUST NOT overload raw oracle target state and effective engine price state. Known lag between them MUST NOT become a public free-option: user risk-increasing and extraction-sensitive operations MUST be rejected or checked under a conservative target-price shadow policy while the lag exists.
- Persistent unsigned economic quantities use
u128unless otherwise stated. - Persistent signed economic quantities use
i128and MUST NOT equali128::MIN. wide_unsigned/wide_signedmean exact transient domains at least 256 bits wide, or a formally equivalent comparison-preserving method.- All products involving prices, positions, A/K/F indices, funding numerators, ADL deltas, fee products, haircut numerators, or warmup-release numerators MUST use checked arithmetic or exact multiply-divide helpers.
POS_SCALE = 1_000_000.price: u64is quote atomic units per1base.- Every price input and stored live/resolved price MUST satisfy
0 < price <= MAX_ORACLE_PRICE. - For live accrual,
oracle_pricemeans the wrapper-fed effective engine price. The raw external oracle target is wrapper-owned input state and is not stored or derived by the engine core. basis_pos_q_i: i128stores signed base position scaled byPOS_SCALE.RiskNotional_i = 0ifeffective_pos_q(i) == 0, else:
RiskNotional_i = ceil(abs(effective_pos_q(i)) * oracle_price / POS_SCALE)
This ceiling is load-bearing. A nonzero fractional quote-notional position has nonzero risk notional and cannot evade maintenance by floor rounding. Floor oracle notional MAY be displayed or used by wrapper policy, but MUST NOT be used for margin.
- Trade fees use executed floor notional:
trade_notional = floor(size_q * exec_price / POS_SCALE)
ADL_ONE = 1_000_000_000_000_000
FUNDING_DEN = 1_000_000_000
A_side is dimensionless and scaled by ADL_ONE. K_side has units ADL scale * quote/base. F_side_num has units ADL scale * quote/base * FUNDING_DEN.
MAX_VAULT_TVL = 10_000_000_000_000_000
MAX_ORACLE_PRICE = 1_000_000_000_000
MAX_POSITION_ABS_Q = 100_000_000_000_000
MAX_TRADE_SIZE_Q = MAX_POSITION_ABS_Q
MAX_OI_SIDE_Q = 100_000_000_000_000
MAX_ACCOUNT_NOTIONAL = 100_000_000_000_000_000_000
MAX_PROTOCOL_FEE_ABS = 1_000_000_000_000_000_000_000_000_000_000_000_000
GLOBAL_MAX_ABS_FUNDING_E9_PER_SLOT = 10_000
MAX_TRADING_FEE_BPS = 10_000
MAX_INITIAL_BPS = 10_000
MAX_MAINTENANCE_BPS = 10_000
MAX_LIQUIDATION_FEE_BPS = 10_000
MAX_MATERIALIZED_ACCOUNTS = 1_000_000
MIN_A_SIDE = 100_000_000_000_000
MAX_WARMUP_SLOTS = 18_446_744_073_709_551_615
MAX_RESOLVE_PRICE_DEVIATION_BPS = 10_000
PRICE_MOVE_CONSUMPTION_SCALE = 1_000_000_000
MAX_ACTIVE_POSITIONS_PER_SIDE MUST be finite and MUST NOT exceed MAX_MATERIALIZED_ACCOUNTS.
The market stores immutable:
cfg_h_min, cfg_h_max
cfg_maintenance_bps, cfg_initial_bps
cfg_trading_fee_bps
cfg_liquidation_fee_bps, cfg_liquidation_fee_cap, cfg_min_liquidation_abs
cfg_min_nonzero_mm_req, cfg_min_nonzero_im_req
cfg_resolve_price_deviation_bps
cfg_max_active_positions_per_side
cfg_max_accrual_dt_slots
cfg_max_abs_funding_e9_per_slot
cfg_max_price_move_bps_per_slot
cfg_min_funding_lifetime_slots
cfg_account_index_capacity
Initialization MUST require:
0 < cfg_min_nonzero_mm_req < cfg_min_nonzero_im_req
0 <= cfg_maintenance_bps <= MAX_MAINTENANCE_BPS
cfg_maintenance_bps <= cfg_initial_bps <= MAX_INITIAL_BPS
0 <= cfg_trading_fee_bps <= MAX_TRADING_FEE_BPS
0 <= cfg_liquidation_fee_bps <= MAX_LIQUIDATION_FEE_BPS
0 <= cfg_min_liquidation_abs <= cfg_liquidation_fee_cap <= MAX_PROTOCOL_FEE_ABS
0 <= cfg_h_min <= cfg_h_max <= MAX_WARMUP_SLOTS
cfg_h_max > 0
0 <= cfg_resolve_price_deviation_bps <= MAX_RESOLVE_PRICE_DEVIATION_BPS
0 < cfg_account_index_capacity <= MAX_MATERIALIZED_ACCOUNTS
0 < cfg_max_active_positions_per_side <= MAX_ACTIVE_POSITIONS_PER_SIDE
cfg_max_active_positions_per_side <= cfg_account_index_capacity
0 < cfg_max_accrual_dt_slots <= MAX_WARMUP_SLOTS
0 <= cfg_max_abs_funding_e9_per_slot <= GLOBAL_MAX_ABS_FUNDING_E9_PER_SLOT
0 < cfg_max_price_move_bps_per_slot
Live admission pairs MUST satisfy:
0 <= admit_h_min <= admit_h_max <= cfg_h_max
admit_h_max > 0
admit_h_max >= cfg_h_min
if admit_h_min > 0: admit_h_min >= cfg_h_min
For public or permissionless wrappers with untrusted live oracle or execution-price PnL, wrapper policy MUST additionally enforce admit_h_min > 0.
Initialization MUST validate, in exact wide arithmetic:
ADL_ONE * MAX_ORACLE_PRICE * cfg_max_abs_funding_e9_per_slot * cfg_max_accrual_dt_slots <= i128::MAX
cfg_min_funding_lifetime_slots >= cfg_max_accrual_dt_slots
ADL_ONE * MAX_ORACLE_PRICE * cfg_max_abs_funding_e9_per_slot * cfg_min_funding_lifetime_slots <= i128::MAX
Initialization MUST also validate the exact per-risk-notional envelope below for every integer risk notional N with 1 <= N <= MAX_ACCOUNT_NOTIONAL, by an exact bounded breakpoint/interval proof or by a stronger conservative sufficient proof. Unbounded runtime loops over all N are forbidden on constrained runtimes.
Let:
price_budget_bps = cfg_max_price_move_bps_per_slot * cfg_max_accrual_dt_slots
funding_budget_num = cfg_max_abs_funding_e9_per_slot * cfg_max_accrual_dt_slots * 10_000
loss_budget_num = price_budget_bps * FUNDING_DEN + funding_budget_num
For each N:
price_funding_loss_N = ceil(N * loss_budget_num / (10_000 * FUNDING_DEN))
worst_liq_notional_N = ceil(N * (10_000 + price_budget_bps) / 10_000)
liq_fee_raw_N = ceil(worst_liq_notional_N * cfg_liquidation_fee_bps / 10_000)
liq_fee_N = min(max(liq_fee_raw_N, cfg_min_liquidation_abs), cfg_liquidation_fee_cap)
mm_req_N = max(floor(N * cfg_maintenance_bps / 10_000), cfg_min_nonzero_mm_req)
require price_funding_loss_N + liq_fee_N <= mm_req_N
This law is the construction-level self-neutral-siphon boundary. It accounts for fractional funding, integer rounding, worst adverse post-move liquidation notional, bps fees, fee floors, and fee caps. Implementations MUST NOT substitute floor-funded bps budgeting, pre-move liquidation notional, floor risk notional, or a two-point small-notional shortcut unless accompanied by an exact proof covering every intervening and larger notional.
If a deployment defines permissionless_resolve_stale_slots, initialization MUST require:
permissionless_resolve_stale_slots <= cfg_max_accrual_dt_slots
Oracle normalization, source selection, target storage, and rate limiting are wrapper-owned. The engine only validates and accrues the effective oracle_price passed to it.
A compliant public wrapper SHOULD maintain distinct fields equivalent to:
oracle_target_price // latest validated normalized external target
oracle_target_publish_ts // target source timestamp or publish slot
last_effective_price // last price actually fed into engine accrual, equal to engine P_last when synchronized
The wrapper MUST NOT overload last_effective_price as the raw target. If the external target jumps beyond the engine cap, the wrapper keeps the raw target and feeds a capped staircase of effective prices until caught up.
For an exposed live market (OI_eff_long != 0 || OI_eff_short != 0), the wrapper-fed next effective price SHOULD be computed by the deterministic clamp law:
dt = now_slot - slot_last
if target == P_last or dt == 0:
next_price = P_last
else:
max_delta = floor(P_last * cfg_max_price_move_bps_per_slot * dt / 10_000)
next_price = clamp_toward(P_last, target, max_delta)
The multiplication MUST use exact wide arithmetic; max_delta MAY be capped to the price type maximum after the exact quotient. clamp_toward moves toward target by at most max_delta and never overshoots. The result MUST satisfy the engine cap in §5.3.
Normative consequences:
- Same-slot exposed cranks (
dt == 0) MUST passP_last; price catch-up requires elapsed slots. They MAY still do Phase 1 liquidation checks and Phase 2 round-robin touches at the unchanged effective price. - If exposed
target != P_last,dt > 0, and the computedmax_delta == 0, ordinary live catch-up cannot make progress at the deployed price scale/cap. The wrapper MUST treat this asCatchupRequired/ recovery territory and MUST NOT advanceslot_lastby feeding the unchanged price merely to bypass the lag. - If exposed
dt > cfg_max_accrual_dt_slotsand the target differs fromP_last, ordinary one-step live catch-up is unavailable. The wrapper MUST use an explicit recovery path, privileged degenerate resolution, or a separately specified atomic multi-accrual procedure that preserves all §5.3 mutation-order and cap invariants. - If both OI sides are zero, no live position can lose equity, so the wrapper MAY feed the raw target directly subject to ordinary price validity.
- Feeding a cap-violating raw target into exposed live accrual is non-compliant and should fail before engine state mutation.
While oracle_target_price != P_last, the market is intentionally using a lagged effective engine price. For public wrappers, keeper progress, liquidation attempts, settlement, and structural sweep MAY continue at the effective price, but user operations that are risk-increasing or extraction-sensitive MUST either be rejected or pass a conservative wrapper shadow policy using both the effective engine price and the raw target. At minimum, public wrappers MUST reject risk-increasing user trades during target/effective-price divergence unless they are priced and margin-checked under a stricter dual-price policy that removes the known-lag free option.
Account-free catchup is a wrapper composition boundary. A public wrapper instruction that has no candidate list, no account touch set, and no liquidation/revalidation phase MUST NOT perform equity-active accrual while the market is exposed. Equity-active means either:
price_move_active = (P_last > 0 && next_price != P_last && (OI_eff_long != 0 || OI_eff_short != 0))
funding_active = (funding_rate != 0 && OI_eff_long != 0 && OI_eff_short != 0 && fund_px_last > 0)
Such an instruction MAY prove oracle liveness, update liveness stamps, or advance no-op time when both price_move_active == false and funding_active == false. If price movement or active funding would move account equity, the wrapper MUST reject and require an account-touching path such as keeper crank, liquidation, or another specified procedure that revalidates/touches the affected accounts within the same atomic instruction.
Each materialized account stores:
C_i: u128 protected principal
PNL_i: i128 realized PnL claim
R_i: u128 reserved positive PnL, 0 <= R_i <= max(PNL_i,0)
basis_pos_q_i: i128
a_basis_i: u128
k_snap_i: i128
f_snap_i: i128
epoch_snap_i: u64
fee_credits_i: i128 <= 0, never i128::MIN
last_fee_slot_i: u64
Live accounts additionally store at most one scheduled bucket and one pending bucket.
Scheduled bucket:
sched_present_i: bool
sched_remaining_q_i: u128
sched_anchor_q_i: u128
sched_start_slot_i: u64
sched_horizon_i: u64
sched_release_q_i: u128
Pending bucket:
pending_present_i: bool
pending_remaining_q_i: u128
pending_horizon_i: u64
Live reserve invariants:
R_i = scheduled_remaining + pending_remaining
if sched_present: 0 < sched_remaining <= sched_anchor, cfg_h_min <= sched_horizon <= cfg_h_max, sched_release <= sched_anchor
if pending_present: 0 < pending_remaining, cfg_h_min <= pending_horizon <= cfg_h_max
if R_i == 0: both buckets absent
pending never matures while pending
If basis_pos_q_i != 0, then a_basis_i > 0. Any helper dividing by a_basis_i or a_basis_i * POS_SCALE MUST fail conservatively if the denominator is zero.
On resolved markets, reserve storage is inert and MUST be cleared by prepare_account_for_resolved_touch before mutating resolved PnL.
Wrapper-owned annotation fields MAY exist, but the engine MUST never read them to decide margin, liquidation, fee routing, admission, accrual, resolution, reset, reclamation, conservation, or authorization. They MUST be canonicalized on materialization and cleared on free-slot reset.
The engine stores:
V, I, C_tot, PNL_pos_tot, PNL_matured_pos_tot: u128
current_slot, slot_last: u64
P_last, fund_px_last: u64
A_long, A_short: u128
K_long, K_short: i128
F_long_num, F_short_num: i128
epoch_long, epoch_short: u64
K_epoch_start_long, K_epoch_start_short: i128
F_epoch_start_long_num, F_epoch_start_short_num: i128
OI_eff_long, OI_eff_short: u128
mode_long, mode_short in {Normal, DrainOnly, ResetPending}
stored_pos_count_long, stored_pos_count_short: u64
stale_account_count_long, stale_account_count_short: u64
phantom_dust_bound_long_q, phantom_dust_bound_short_q: u128
materialized_account_count, neg_pnl_account_count: u64
rr_cursor_position, sweep_generation: u64
price_move_consumed_bps_e9_this_generation: u128
last_stress_consumption_slot, last_sweep_generation_advance_slot: u64 or NO_SLOT
stress_reset_pending: bool
market_mode in {Live, Resolved}
resolved_price, resolved_live_price: u64
resolved_slot: u64
resolved_k_long_terminal_delta, resolved_k_short_terminal_delta: i128
resolved_payout_snapshot_ready: bool
resolved_payout_h_num, resolved_payout_h_den: u128
Global invariants:
C_tot <= V <= MAX_VAULT_TVL
I <= V
V >= C_tot + I
0 <= neg_pnl_account_count <= materialized_account_count <= cfg_account_index_capacity <= MAX_MATERIALIZED_ACCOUNTS
0 <= rr_cursor_position < cfg_account_index_capacity
slot_last <= current_slot
F_long_num and F_short_num fit i128
if Live: PNL_matured_pos_tot <= PNL_pos_tot <= MAX_PNL_POS_TOT_LIVE and resolved fields are zero
if Resolved: resolved_price > 0, resolved_live_price > 0, PNL_matured_pos_tot <= PNL_pos_tot
if snapshot not ready: resolved_payout_h_num = resolved_payout_h_den = 0
if snapshot ready: resolved_payout_h_num <= resolved_payout_h_den
Every external index MUST satisfy i < cfg_account_index_capacity. Missing/materialized status MUST come from authenticated engine state; omitted account data is not proof of missingness.
Only deposit(i, amount > 0, now_slot) may materialize a missing account. materialize_account(i, materialize_slot) initializes all fields to zero/canonical defaults, sets last_fee_slot_i = materialize_slot, and increments materialized_account_count.
free_empty_account_slot(i) is the only canonical free path. Preconditions:
account materialized
C_i = 0, PNL_i = 0, R_i = 0
both buckets absent
basis_pos_q_i = 0
fee_credits_i <= 0
Effects: forgive fee debt by setting fee_credits_i = 0, reset local fields to canonical zero-position defaults, clear reserves and wrapper annotations, set last_fee_slot_i = 0, mark the slot missing/reusable in authenticated state, and decrement materialized_account_count. neg_pnl_account_count is unchanged.
For every materialized account with nonzero basis on side s, exactly one holds:
epoch_snap_i == epoch_s
or mode_s == ResetPending and epoch_snap_i + 1 == epoch_s
begin_full_drain_reset(side) requires OI_eff_side == 0 and then snapshots K_side/F_side_num to epoch-start fields, zeros live K_side/F_side_num, increments epoch_side, sets A_side = ADL_ONE, sets stale_account_count_side = stored_pos_count_side, clears phantom dust for that side, and enters ResetPending.
finalize_side_reset(side) requires ResetPending, zero OI, zero stale count, and zero stored position count, then sets mode to Normal.
Before any OI-increasing operation rejects on ResetPending, it MUST call maybe_finalize_ready_reset_sides_before_oi_increase.
Let:
Residual = V - (C_tot + I) // checked, and invariant guarantees nonnegative
PosPNL_i = max(PNL_i, 0)
ReleasedPos_i = PosPNL_i - R_i on Live
ReleasedPos_i = PosPNL_i on Resolved
PendingWarmupTot = PNL_pos_tot - PNL_matured_pos_tot = sum R_i on Live
Canonical haircut pairs:
if PNL_matured_pos_tot == 0: h = (1, 1)
else h = (min(Residual, PNL_matured_pos_tot), PNL_matured_pos_tot)
if PNL_pos_tot == 0: g = (1, 1)
else g = (min(Residual, PNL_pos_tot), PNL_pos_tot)
Then:
PNL_eff_matured_i = floor(ReleasedPos_i * h.num / h.den)
PNL_eff_trade_i = floor(PosPNL_i * g.num / g.den)
Equity lanes, all exact wide signed:
Eq_withdraw_raw_i = C_i + min(PNL_i,0) + PNL_eff_matured_i - FeeDebt_i
Eq_trade_raw_i = C_i + min(PNL_i,0) + PNL_eff_trade_i - FeeDebt_i
Eq_maint_raw_i = C_i + PNL_i - FeeDebt_i
Eq_net_i = max(0, Eq_maint_raw_i)
Candidate trade approval MUST neutralize that trade’s own positive slippage:
TradeGain_i_candidate = max(candidate_trade_pnl_i, 0)
PNL_trade_open_i = PNL_i - TradeGain_i_candidate
PosPNL_trade_open_i = max(PNL_trade_open_i, 0)
PNL_pos_tot_trade_open_i = PNL_pos_tot - PosPNL_i + PosPNL_trade_open_i
compute g_open from PNL_pos_tot_trade_open_i and Residual
Eq_trade_open_raw_i = C_i + min(PNL_trade_open_i,0) + floor(PosPNL_trade_open_i*g_open.num/g_open.den) - FeeDebt_i
Eq_trade_open_raw_i is the only compliant risk-increasing trade approval metric.
set_capital(i, new_C) updates C_tot by the exact signed delta, then writes C_i.
set_position_basis_q(i, new_basis) updates long/short stored position counts exactly once according to old/new sign flags, enforcing cfg_max_active_positions_per_side on any increment, then writes basis_pos_q_i. All position-zeroing settlement branches MUST use this helper or an exactly equivalent path.
promote_pending_to_scheduled(i) does nothing if scheduled exists or pending absent. Otherwise it creates a scheduled bucket from pending with sched_start_slot = current_slot, sched_anchor_q = sched_remaining_q = pending_remaining_q, sched_horizon = pending_horizon, sched_release_q = 0, and clears pending. It MUST NOT change R_i.
append_new_reserve(i, reserve_add, admitted_h_eff) requires positive amount and positive horizon. If no scheduled bucket exists but pending exists, first promote pending. Then:
- if scheduled absent, create scheduled at
current_slot; - else if pending absent and
sched_start_slot == current_slot,sched_horizon == admitted_h_eff, andsched_release_q == 0, merge into scheduled; - else if pending absent, create pending;
- else merge into pending and set
pending_horizon = max(pending_horizon, admitted_h_eff).
Finally increase R_i by reserve_add.
apply_reserve_loss_newest_first(i, reserve_loss) consumes pending before scheduled, decrements R_i, and clears empty buckets.
advance_profit_warmup(i) promotes pending if needed, computes:
elapsed = current_slot - sched_start_slot
effective_elapsed = min(elapsed, sched_horizon)
sched_total = floor(sched_anchor_q * effective_elapsed / sched_horizon)
sched_increment = sched_total - sched_release_q
release = min(sched_remaining_q, sched_increment)
It releases release to PNL_matured_pos_tot. If the scheduled bucket empties, it is cleared completely including sched_release_q = 0, and pending is promoted if present. A non-empty bucket MUST NOT persist with an over-advanced release cursor.
admit_fresh_reserve_h_lock(i, fresh_positive_pnl_i, ctx, admit_h_min, admit_h_max) -> admitted_h_eff requires a live materialized account and valid admission pair. Let:
Residual_now = V - (C_tot + I)
matured_plus_fresh = PNL_matured_pos_tot + fresh_positive_pnl_i
threshold_opt = ctx.admit_h_max_consumption_threshold_bps_opt_shared
Law:
- if
iis inctx.h_max_sticky_accounts, returnadmit_h_max; - if
threshold_opt = Some(threshold_bps), computethreshold_e9 = threshold_bps * PRICE_MOVE_CONSUMPTION_SCALE; ifprice_move_consumed_bps_e9_this_generation >= threshold_e9, chooseadmit_h_max; - otherwise choose
admit_h_miniffmatured_plus_fresh <= Residual_now, elseadmit_h_max; - if
admit_h_maxwas chosen, insertiinto the sticky set.
None disables the stress gate. Some(0) is invalid. The engine enforces only the supplied policy; public-wrapper nonzero-warmup requirements are wrapper obligations.
admit_outstanding_reserve_on_touch(i, ctx) accelerates all outstanding reserve only when all hold:
reserve_total > 0
ctx.admit_h_min_shared == 0
stress threshold is absent or inactive
PNL_matured_pos_tot + reserve_total <= Residual_now
If so it moves the entire reserve into PNL_matured_pos_tot, clears both buckets, and sets R_i = 0. Otherwise it leaves reserve unchanged. It never extends or resets a horizon.
Every persistent PNL_i mutation after materialization MUST use set_pnl, except consume_released_pnl.
set_pnl(i, new_PNL, reserve_mode[, ctx]) where reserve mode is:
UseAdmissionPair(admit_h_min, admit_h_max)
ImmediateReleaseResolvedOnly
NoPositiveIncreaseAllowed
It updates PNL_pos_tot, PNL_matured_pos_tot, R_i, reserve buckets, and neg_pnl_account_count atomically.
For positive increases:
NoPositiveIncreaseAllowedfails;ImmediateReleaseResolvedOnlyrequiresResolved, increasesPNL_matured_pos_tot, and does not reserve;UseAdmissionPairrequiresLive, obtainsadmitted_h_eff, immediately matures iffadmitted_h_eff == 0, otherwise appends reserve.
For non-increases it consumes reserve loss newest-first, then matured loss, updates aggregates and sign count, and requires no reserve remains when live positive PnL becomes zero.
consume_released_pnl(i, x) requires live 0 < x <= ReleasedPos_i, decreases PNL_i, PNL_pos_tot, and PNL_matured_pos_tot by x, and leaves reserve unchanged.
Trading fee:
fee = 0 if cfg_trading_fee_bps == 0 or trade_notional == 0
else ceil(trade_notional * cfg_trading_fee_bps / 10_000)
Liquidation fee for q_close_q:
if q_close_q == 0: liq_fee = 0
else:
closed_notional = floor(q_close_q * oracle_price / POS_SCALE)
liq_fee_raw = ceil(closed_notional * cfg_liquidation_fee_bps / 10_000)
liq_fee = min(max(liq_fee_raw, cfg_min_liquidation_abs), cfg_liquidation_fee_cap)
charge_fee_to_insurance(i, fee_abs) requires fee_abs <= MAX_PROTOCOL_FEE_ABS. It computes collectible headroom from capital plus fee-credit headroom, pays as much as possible from C_i into I, records any collectible shortfall as negative fee_credits_i, and drops the uncollectible tail. It MUST NOT mutate PnL, reserves, positive-PnL aggregates, or K/F indices.
sync_account_fee_to_slot(i, anchor, rate) charges recurring wrapper-owned fees exactly once over [last_fee_slot_i, anchor], caps rate * dt at MAX_PROTOCOL_FEE_ABS without failing on raw-product overflow, routes the capped amount through charge_fee_to_insurance, and advances last_fee_slot_i = anchor. Live anchors must be <= current_slot; resolved anchors must be <= resolved_slot.
fee_debt_sweep(i) pays fee debt from available C_i into I. This preserves Residual because it is a pure C -> I reclassification.
use_insurance_buffer(loss_abs) MUST spend exactly pay = min(loss_abs, I), set I -= pay, and return loss_abs - pay. It MUST NOT drain the full insurance fund when the loss is smaller.
record_uninsured_protocol_loss(loss_abs) may record telemetry but MUST NOT inflate D, C_tot, PNL_pos_tot, PNL_matured_pos_tot, V, or I. The loss remains represented by junior haircuts.
absorb_protocol_loss(loss_abs) calls use_insurance_buffer and records only the returned nonzero remainder.
For account i with nonzero basis on side s:
if epoch_snap_i != epoch_s: effective_pos_q(i) = 0
else effective_abs_pos_q = floor(abs(basis_pos_q_i) * A_s / a_basis_i)
effective_pos_q = sign(basis_pos_q_i) * effective_abs_pos_q
The exact bilateral trade OI after-values are:
OI_long_after = OI_eff_long - old_long_a - old_long_b + new_long_a + new_long_b
OI_short_after = OI_eff_short - old_short_a - old_short_b + new_short_a + new_short_b
They MUST be used for both gating and writeback.
Live touch settlement:
- if basis is zero, return;
- require
a_basis_i > 0and computeden = a_basis_i * POS_SCALEexactly; - if current epoch, compute effective quantity and
pnl_deltawithwide_signed_mul_div_floor_from_kf_pair(abs_basis, k_snap, K_s, f_snap, F_s_num, den); - apply
set_pnl(..., UseAdmissionPair(ctx...)); - if effective quantity floors to zero, increment the side phantom-dust bound by exactly one q-unit, clear basis through
set_position_basis_q(i,0), and reset snapshots; otherwise update snapshots.
Epoch-mismatch settlement requires mode_s == ResetPending, epoch_snap_i + 1 == epoch_s, and positive stale count. It settles against K_epoch_start_s / F_epoch_start_s_num, applies PnL through admission, clears basis through set_position_basis_q(i,0), decrements stale count, and resets snapshots.
Resolved settlement first calls prepare_account_for_resolved_touch, then settles stale one-epoch-lag basis against:
k_terminal_s_exact = K_epoch_start_s + resolved_k_terminal_delta_s
f_terminal_s_exact = F_epoch_start_s_num
using ImmediateReleaseResolvedOnly, then clears basis through set_position_basis_q and decrements stale count.
accrue_market_to(now_slot, oracle_price, funding_rate_e9_per_slot) requires live mode, trusted now_slot >= slot_last, valid oracle price, and funding-rate magnitude within config.
Let:
dt = now_slot - slot_last
funding_active = funding_rate != 0 && OI_eff_long != 0 && OI_eff_short != 0 && fund_px_last > 0
price_move_active = P_last > 0 && oracle_price != P_last && (OI_eff_long != 0 || OI_eff_short != 0)
If either active branch is true, require dt <= cfg_max_accrual_dt_slots.
If price_move_active, before mutating any K/F/price/slot/consumption state, require exactly:
abs(oracle_price - P_last) * 10_000 <= cfg_max_price_move_bps_per_slot * dt * P_last
Then update stress consumption:
consumed = floor(abs_delta_price * 10_000 * PRICE_MOVE_CONSUMPTION_SCALE / P_last)
price_move_consumed_bps_e9_this_generation = saturating_add(price_move_consumed_bps_e9_this_generation, consumed)
If consumed > 0, set last_stress_consumption_slot = now_slot.
The accumulator is a stress signal, not a conservation quantity; overflow MUST saturate at u128::MAX and force slow-lane admission for finite thresholds until an eligible generation reset. A generation reset MUST NOT clear stress consumed in the same slot.
Mark-to-market once:
ΔP = oracle_price - P_last
if OI_long_0 > 0: K_long += A_long * ΔP
if OI_short_0 > 0: K_short -= A_short * ΔP
Funding, if active, uses one exact total:
fund_num_total = fund_px_last * funding_rate_e9_per_slot * dt
F_long_num -= A_long * fund_num_total
F_short_num += A_short * fund_num_total
Persistent K/F overflow fails conservatively. Finally set slot_last = now_slot, P_last = oracle_price, and fund_px_last = oracle_price.
enqueue_adl(ctx, liq_side, q_close_q, D):
- decrements liquidated-side OI by
q_close_q; - spends insurance exactly with
use_insurance_buffer(D); - if opposing OI is zero, records any remainder as uninsured and schedules reset if both sides zero;
- if opposing stored position count is zero, reduces opposing OI by
q_close_q, records remainder, and schedules reset if both sides zero; - otherwise computes opposing quantity decay and optional K loss.
For D_rem > 0, compute:
delta_K_abs = ceil(D_rem * A_old * POS_SCALE / OI_before)
delta_K_exact = -delta_K_abs
If representability, K_opp + delta_K_exact, or future mark headroom |K_candidate| + A_old * MAX_ORACLE_PRICE <= i128::MAX fails, route D_rem to uninsured loss while still continuing quantity socialization.
Then:
OI_post = OI_before - q_close_q
A_candidate = floor(A_old * OI_post / OI_before)
If OI_post == 0, zero opposing OI and schedule reset. If A_candidate > 0, set A_opp, set OI_eff_opp, add the exact ADL dust bound, and enter DrainOnly if A_opp < MIN_A_SIDE. If A_candidate == 0 while OI_post > 0, zero both OI sides and schedule both resets.
At the end of every top-level instruction that can touch accounts, mutate side state, liquidate, or resolved-close, call schedule_end_of_instruction_resets(ctx) exactly once, except for the additional explicit pre-open dust/reset flush inside execute_trade.
If both stored side counts are zero, compute clear_bound = checked_add(phantom_dust_bound_long_q, phantom_dust_bound_short_q). If residual OI or dust exists, require OI symmetry and clear both OI sides only if both are within clear_bound; otherwise fail conservatively.
If exactly one stored side is zero, require OI symmetry and clear both sides only if the empty side’s OI is within that side’s phantom-dust bound; otherwise fail conservatively.
If a side is DrainOnly and its OI is zero, set that side’s pending reset flag.
finalize_end_of_instruction_resets(ctx) begins pending resets and finalizes any ready ResetPending side.
touch_account_live_local(i, ctx):
- requires live materialized account;
- adds
itoctx.touched_accountsor fails on capacity; - calls
admit_outstanding_reserve_on_touch(i, ctx); - advances warmup;
- settles A/K/F side effects;
- settles negative PnL from principal;
- if now authoritative flat and still negative, calls
absorb_protocol_lossand sets PnL to zero; - MUST NOT auto-convert or sweep fee debt.
finalize_touched_accounts_post_live(ctx) computes one shared whole-haircut snapshot after all live local work. It then iterates touched accounts in ascending storage-index order. If an account is flat, has released positive PnL, and the snapshot has h = 1, it uses consume_released_pnl followed by set_capital(C_i + released). It then calls fee_debt_sweep.
After authoritative live touch:
RiskNotional_i = 0 if effective_pos_q(i) == 0
else ceil(abs(effective_pos_q(i)) * oracle_price / POS_SCALE)
MM_req_i = 0 if flat else max(floor(RiskNotional_i * cfg_maintenance_bps / 10_000), cfg_min_nonzero_mm_req)
IM_req_i = 0 if flat else max(floor(RiskNotional_i * cfg_initial_bps / 10_000), cfg_min_nonzero_im_req)
Maintenance healthy iff Eq_net_i > MM_req_i. Withdrawal healthy iff Eq_withdraw_raw_i >= IM_req_i. Risk-increasing trade approval healthy iff Eq_trade_open_raw_i >= IM_req_post_i.
A trade is risk-increasing if it increases absolute effective position, flips sign, or opens from flat. It is strictly risk-reducing if same sign, nonzero before/after, and absolute position decreases.
An account is liquidatable iff after full authoritative live touch it has nonzero effective position and Eq_net_i <= MM_req_i. If recurring fees are enabled, the account MUST be fee-current first.
Partial liquidation requires 0 < q_close_q < abs(old_eff_pos_q_i). It closes synthetically at oracle price, attaches the remaining position, settles losses from principal, charges liquidation fee, invokes enqueue_adl(ctx, liq_side, q_close_q, 0), and requires the remaining nonzero position to be maintenance healthy after the step.
Full-close liquidation closes the whole effective position at oracle price, attaches flat, settles losses from principal, charges liquidation fee, sets D = max(-PNL_i, 0), invokes enqueue_adl if q_close_q > 0 || D > 0, then sets negative PnL to zero with NoPositiveIncreaseAllowed if D > 0.
Live instructions that depend on current market state execute:
- validate slots, effective oracle price, funding-rate bound, admission pair, optional threshold (
Nonedisables;Some(t)requires0 < t <= floor(u128::MAX / PRICE_MOVE_CONSUMPTION_SCALE)), and endpoint inputs; - initialize fresh
ctx; - call
accrue_market_toexactly once; - set
current_slot = now_slot; - sync recurring fees for touched accounts before health-sensitive checks;
- run endpoint logic;
- call
finalize_touched_accounts_post_live(ctx)exactly once if live local touches were used; - schedule and finalize resets exactly once;
- assert OI symmetry for side-mutating/live-exposure instructions;
- require
V >= C_tot + I.
Any early no-op return after state mutation or fee sync MUST still perform the final applicable invariant checks.
Pure public live paths that advance current_slot without calling accrue_market_to MUST call:
require_no_accrual_public_path_within_envelope(now_slot):
require market_mode == Live
require now_slot >= current_slot
require slot_last <= current_slot
if OI_eff_long == 0 && OI_eff_short == 0: return
dt = now_slot - slot_last // checked subtraction
require dt <= cfg_max_accrual_dt_slots
This avoids overflow-prone slot_last + cfg_max_accrual_dt_slots arithmetic and permits zero-OI idle fast-forward.
deposit(i, amount, now_slot) is live-only, no-accrual, and may materialize missing i only if amount > 0. It increases V, increases C_i, settles realized losses from principal, MUST NOT absorb flat negative loss through insurance, and sweeps fee debt only if the account is flat and nonnegative.
deposit_fee_credits(i, amount, now_slot) pays min(amount, FeeDebt_i) into V and I, increases fee_credits_i by that amount, and never makes fee credits positive.
top_up_insurance_fund(amount, now_slot) increases V and I by amount.
charge_account_fee(i, fee_abs, now_slot) routes fee_abs through charge_fee_to_insurance and performs no margin check by itself.
settle_flat_negative_pnl(i, now_slot[, fee_rate]) is live-only, no-accrual, requires flat account with no reserve, syncs fee if enabled, settles losses from principal, then absorbs any remaining negative PnL through insurance/uninsured loss and sets PnL to zero.
reclaim_empty_account(i, now_slot[, fee_rate]) is live-only, no-accrual, syncs fees if enabled, then requires the §2.3 free-slot preconditions and calls free_empty_account_slot.
settle_account runs the standard live lifecycle, touches one account, and finalizes.
withdraw touches and finalizes first. It then requires amount <= C_i; if the account is nonflat, it requires withdrawal health under the hypothetical state where both V and C_tot decrease by amount; then it pays out by decreasing C_i and V.
convert_released_pnl touches first, requires 0 < x_req <= ReleasedPos_i, computes current h, and for flat accounts requires x_req <= max_safe_flat_conversion_released. It consumes released PnL, adds floor(x_req * h.num / h.den) to capital, sweeps fee debt, and if still nonflat requires maintenance health.
close_account touches and finalizes first. It requires flat, zero PnL, no reserve, and no fee debt, pays out all capital by decreasing C_i and V, then calls free_empty_account_slot.
execute_trade(a,b, ..., size_q, exec_price) requires distinct materialized accounts, valid execution price, positive size, computed trade_notional <= MAX_ACCOUNT_NOTIONAL, and standard live lifecycle.
It syncs fees if enabled, touches both accounts in deterministic ascending storage-index order, then runs a pre-open dust/reset flush using a separate reset-only context. It captures pre-trade positions and maintenance state, finalizes ready reset sides, computes candidate positions and exact bilateral OI after-values, enforces position/OI bounds and side-mode gating, applies execution-slippage PnL before fees, attaches positions, writes OI after-values, settles losses, charges trade fees, computes post-trade risk notional and approval metrics, and approves each account independently:
- flat result: fee-neutral negative-shortfall comparison must not worsen;
- risk-increasing: require
Eq_trade_open_raw_i >= IM_req_post_i; - already maintenance healthy: allow;
- strictly risk-reducing while unhealthy: allow only if fee-neutral maintenance shortfall strictly improves and fee-neutral negative equity does not worsen;
- otherwise reject.
liquidate(i, ..., policy) runs standard live lifecycle, syncs fees if enabled, touches the account, requires liquidation eligibility, executes FullClose or ExactPartial(q_close_q), finalizes, schedules/finalizes resets, and checks conservation.
keeper_crank(now_slot, oracle_price, funding_rate, admit_h_min, admit_h_max, threshold_opt, ordered_candidates[], max_revalidations, rr_touch_limit[, fee_fn]) is live-only and accrues exactly once before both phases.
Phase 1 processes keeper-supplied candidates in supplied order until max_revalidations is exhausted or a pending reset is scheduled. Authenticated missing-account skips do not count. If a candidate slot is materialized, its account state MUST be available; omission/unreadability fails conservatively. Liquidation is Phase 1 only.
Phase 2 always runs, even if Phase 1 stopped on pending reset. It does not count against max_revalidations, does not liquidate, and does not stop on pending reset. It greedily scans authenticated index space and touches up to rr_touch_limit materialized accounts:
sweep_limit = cfg_account_index_capacity
i = rr_cursor_position
touched = 0
while i < sweep_limit and touched < rr_touch_limit:
if authenticated engine state proves missing:
i += 1
continue
require account data
touch_account_live_local(i)
touched += 1
i += 1
Then set rr_cursor_position = i if i < sweep_limit. If i >= sweep_limit, set rr_cursor_position = 0 and complete a cursor wrap. A cursor wrap MUST NOT advance sweep_generation more than once per slot. If last_stress_consumption_slot == now_slot, set stress_reset_pending = true and do not clear price_move_consumed_bps_e9_this_generation. Otherwise, if last_sweep_generation_advance_slot != now_slot, increment sweep_generation, set last_sweep_generation_advance_slot = now_slot, clear price_move_consumed_bps_e9_this_generation, and clear stress_reset_pending. A pending same-slot stress reset only clears on such a later eligible cursor wrap; a later slot alone is not sufficient.
Because greedy Phase 2 may inspect authenticated unused slots without touching them, cfg_account_index_capacity MUST be compute-safe for the deployment, or the implementation MUST define a deterministic scan cap that still preserves full-sweep coverage before generation reset. The stress-generation gate is only an admission-lane selector between admit_h_min and admit_h_max; it is not a substitute for the public-wrapper requirement that untrusted positive live PnL use nonzero minimum warmup.
resolve_market(resolve_mode, resolved_price, live_oracle_price, now_slot, funding_rate) is privileged. Branch selection is explicit; value-detected branch selection is forbidden.
Ordinary branch calls accrue_market_to(now_slot, live_oracle_price, funding_rate), sets current_slot, and requires the resolved price to be inside the configured deviation band around the trusted live-sync price. On this branch, live_oracle_price is the effective live-sync price supplied to the engine; if the raw external target is beyond the live cap, feeding it directly will fail and the wrapper must first catch up through valid capped accruals or choose an explicit recovery path.
Degenerate branch requires live_oracle_price == P_last and funding_rate == 0, sets current_slot = slot_last = now_slot, uses P_last as the resolved live price, and skips the ordinary band. It is a privileged recovery path only.
Both branches compute terminal K deltas exactly, store them separately from live K, enter Resolved, set resolved_slot, clear payout snapshot state, set PNL_matured_pos_tot = PNL_pos_tot, zero both OI sides, begin/finalize side resets as applicable, and require conservation.
force_close_resolved(i) is permissionless and takes no caller slot. It requires current_slot == resolved_slot, prepares the account for resolved touch, settles resolved side effects, settles/absorbs losses, finalizes ready reset sides, then:
- if
PNL_i == 0, fee-sweeps, forgives remaining fee debt, pays out capital, and frees the slot; - if
PNL_i > 0and the market is not positive-payout ready, returnsProgressOnly; - if positive-payout ready, captures the shared payout snapshot if needed, pays
floor(PNL_i * snapshot_num / snapshot_den), fee-sweeps, pays out capital, and frees the slot.
A zero payout MUST NOT be the only encoding of progress-only.
- Public wrappers MUST NOT expose arbitrary caller-controlled
admit_h_min,admit_h_max, threshold, or funding-rate inputs. - Public or permissionless wrappers with untrusted live oracle or execution-price PnL MUST use
admit_h_min > 0for instructions that can create or accelerate live positive PnL.admit_h_min = 0is reserved for trusted/private immediate-release deployments. - Stress threshold gating is optional engine machinery. It is a reconciliation/UX stress signal, not a substitute for warmup.
- Resolution is privileged. Wrappers MUST source trusted live and settlement prices, funding rate, and explicit
resolve_mode. - Wrappers MUST monitor accrual envelopes and K/F headroom, and crank or resolve before exposed markets exceed live envelopes.
- Public wrappers MUST separate raw oracle target state from effective engine price state and MUST feed capped staircase prices, not cap-violating raw jumps, into exposed live accrual. Same-slot exposed cranks MUST pass the unchanged engine price. If exposed catch-up would have
target != P_last,dt > 0, andmax_delta == 0, the wrapper MUST enter recovery or wait for enough elapsed slots; it MUST NOT advanceslot_lastwith the unchanged price as a silent bypass. - While raw target and effective engine price differ, public wrappers MUST reject or conservatively shadow-check extraction-sensitive user actions (
withdraw,convert_released_pnl, user-triggered settlement/finalization that can release or convert positive PnL, and any close path whose payout depends on lagged PnL) and MUST reject risk-increasing user trades unless a stricter dual-price policy prices and margin-checks the trade against the lag. - Public wrappers using the sweep-generation stress gate MUST pass nonzero
rr_touch_limiton normal keeper cranks and ensuremax_revalidations + rr_touch_limitfits touched-account capacity and compute budget.rr_touch_limit = 0is reserved for trusted/private compatibility or explicit recovery flows. - Public wrappers SHOULD enforce execution-price admissibility, e.g. bounded deviation from effective engine price and, during oracle catch-up lag, from the raw target as well.
- User value-moving operations must be account-authorized. Intended permissionless paths are settlement, liquidation, reclaim, flat-negative cleanup, resolved close, and keeper crank.
- If recurring fees are enabled, wrappers MUST sync fee-current state before health-sensitive checks, reclaim checks, and resolved terminal close, and MUST use
resolved_sloton resolved markets. - Wrappers own account-materialization anti-spam economics: minimum deposit, recurring fees, and reclaim incentives.
- Runtime configuration MUST bound
max_revalidations + rr_touch_limitto fit actual context capacity, and MUST bound worst-case greedy Phase 2 index inspection to fit compute budget by choosing a compute-safe account-index capacity or an explicit deterministic scan cap.
Implementations and public wrappers MUST test at least:
- conservation
V >= C_tot + Iacross all paths; - PnL aggregate and
neg_pnl_account_countconsistency; - reserve admission, sticky
admit_h_max, pending/scheduled behavior, reserve loss ordering, and no stale release cursor; - public-wrapper policy tests that
admit_h_min = 0is not used for untrusted public live PnL; - outstanding reserve acceleration blocked by nonzero
admit_h_minor active threshold; - exact candidate-trade positive-slippage neutralization;
- fee-debt sweep residual neutrality and actual-fee-impact comparisons;
RiskNotionalceil margin including fractional-notional dust;- exact per-risk-notional init envelope including funding fractions, post-move liquidation notional, fee floor, fee cap, and rounded notionals;
- price-move cap rejection before any K/F/price/slot/consumption mutation;
- wrapper oracle catch-up clamp: raw target is stored separately, next effective price moves toward target by at most
floor(P_last * cap * dt / 10_000), and same-slot exposed cranks passP_last; - target/effective-price divergence policy: public risk-increasing trades and extraction-sensitive actions are rejected or pass a stricter dual-price shadow check;
- account-free catchup rejects exposed price movement and active funding, while still allowing flat/no-op catchup;
- zero-OI no-accrual fast-forward and exposed-market no-accrual envelope rejection using checked subtraction near
u64::MAX; - exact insurance spending
min(loss_abs, I); - stress accumulator floor-at-scaled-bps precision, saturating addition, threshold activation, reset only on eligible generation advance, and no same-slot stress clear;
- deterministic greedy Phase 2 cursor arithmetic over
cfg_account_index_capacity, authenticated missing-slot skips, touched-account limits, generation advancement at most once per slot, and failure on omitted materialized account data; - public keeper wrappers using the stress gate pass nonzero
rr_touch_limiton normal cranks and enforce touched-account budget; - deterministic ascending trade touch order and pre-open dust/reset flush;
- all position zeroing through
set_position_basis_qand all frees throughfree_empty_account_slot; - resolved payout readiness, shared snapshot stability, and explicit progress-vs-close outcome;
- degenerate resolution requires explicit mode and exact degenerate inputs; ordinary resolution never value-detects into degenerate mode;
- ADL exact K deficit computation, overflow fallback to uninsured loss while quantity socialization continues, and phantom-dust clearance bounds;
- self-neutral insurance/oracle-siphon scenarios across multiple valid accrual envelopes;
- exposed
target != P_last,dt > 0,max_delta == 0cannot advanceslot_lastby feedingP_last; it must wait, reject as catch-up-required, or enter explicit recovery; - raw target jumps beyond the cap are never fed directly to exposed live engine accrual except in an explicit recovery/resolution test that confirms conservative failure or privileged recovery semantics.