Skip to main content

RiskEngine

Struct RiskEngine 

Source
#[repr(C)]
pub struct RiskEngine {
Show 30 fields pub vault: U128, pub insurance_fund: InsuranceFund, pub params: RiskParams, pub current_slot: u64, pub funding_index_qpb_e6: I128, pub last_funding_slot: u64, pub funding_rate_bps_per_slot_last: i64, pub last_crank_slot: u64, pub max_crank_staleness_slots: u64, pub total_open_interest: U128, pub c_tot: U128, pub pnl_pos_tot: U128, pub liq_cursor: u16, pub gc_cursor: u16, pub last_full_sweep_start_slot: u64, pub last_full_sweep_completed_slot: u64, pub crank_cursor: u16, pub sweep_start_idx: u16, pub lifetime_liquidations: u64, pub lifetime_force_realize_closes: u64, pub net_lp_pos: I128, pub lp_sum_abs: U128, pub lp_max_abs: U128, pub lp_max_abs_sweep: U128, pub used: [u64; 64], pub num_used_accounts: u16, pub next_account_id: u64, pub free_head: u16, pub next_free: [u16; 4096], pub accounts: [Account; 4096],
}
Expand description

Main risk engine state - fixed slab with bitmap

Fields§

§vault: U128

Total vault balance (all deposited funds)

§insurance_fund: InsuranceFund

Insurance fund

§params: RiskParams

Risk parameters

§current_slot: u64

Current slot (for warmup calculations)

§funding_index_qpb_e6: I128

Global funding index (quote per 1 base, scaled by 1e6)

§last_funding_slot: u64

Last slot when funding was accrued

§funding_rate_bps_per_slot_last: i64

Funding rate (bps per slot) in effect starting at last_funding_slot. This is the rate used for the interval [last_funding_slot, next_accrual). Anti-retroactivity: state changes at slot t can only affect funding for slots >= t.

§last_crank_slot: u64

Last slot when keeper crank was executed

§max_crank_staleness_slots: u64

Maximum allowed staleness before crank is required (in slots)

§total_open_interest: U128

Total open interest = sum of abs(position_size) across all accounts This measures total risk exposure in the system.

§c_tot: U128

Sum of all account capital: C_tot = Σ C_i Maintained incrementally via set_capital() helper.

§pnl_pos_tot: U128

Sum of all positive PnL: PNL_pos_tot = Σ max(PNL_i, 0) Maintained incrementally via set_pnl() helper.

§liq_cursor: u16

Cursor for liquidation scan (wraps around MAX_ACCOUNTS)

§gc_cursor: u16

Cursor for garbage collection scan (wraps around MAX_ACCOUNTS)

§last_full_sweep_start_slot: u64

Slot when the current full sweep started (step 0 was executed)

§last_full_sweep_completed_slot: u64

Slot when the last full sweep completed

§crank_cursor: u16

Cursor: index where the next crank will start scanning

§sweep_start_idx: u16

Index where the current sweep started (for completion detection)

§lifetime_liquidations: u64

Total number of liquidations performed (lifetime)

§lifetime_force_realize_closes: u64

Total number of force-realize closes performed (lifetime)

§net_lp_pos: I128

Net LP position: sum of position_size across all LP accounts Updated incrementally in execute_trade and close paths

§lp_sum_abs: U128

Sum of abs(position_size) across all LP accounts Updated incrementally in execute_trade and close paths

§lp_max_abs: U128

Max abs(position_size) across all LP accounts (monotone upper bound) Only increases; reset via bounded sweep at sweep completion

§lp_max_abs_sweep: U128

In-progress max abs for current sweep (reset at sweep start, committed at completion)

§used: [u64; 64]

Occupancy bitmap (4096 bits = 64 u64 words)

§num_used_accounts: u16

Number of used accounts (O(1) counter, fixes H2: fee bypass TOCTOU)

§next_account_id: u64

Next account ID to assign (monotonically increasing, never recycled)

§free_head: u16

Freelist head (u16::MAX = none)

§next_free: [u16; 4096]

Freelist next pointers

§accounts: [Account; 4096]

Account slab (4096 accounts)

Implementations§

Source§

impl RiskEngine

Source

pub fn new(params: RiskParams) -> Self

Create a new risk engine (stack-allocates the full struct - avoid in BPF!)

WARNING: This allocates ~6MB on the stack at MAX_ACCOUNTS=4096. For Solana BPF programs, use init_in_place instead.

Source

pub fn init_in_place(&mut self, params: RiskParams)

Initialize a RiskEngine in place (zero-copy friendly).

PREREQUISITE: The memory backing self MUST be zeroed before calling. This method only sets non-zero fields to avoid touching the entire ~6MB struct.

This is the correct way to initialize RiskEngine in Solana BPF programs where stack space is limited to 4KB.

Source

pub fn is_used(&self, idx: usize) -> bool

Source

pub fn set_pnl(&mut self, idx: usize, new_pnl: i128)

Mandatory helper: set account PnL and maintain pnl_pos_tot aggregate (spec §4.2). All code paths that modify PnL MUST call this.

Source

pub fn set_capital(&mut self, idx: usize, new_capital: u128)

Helper: set account capital and maintain c_tot aggregate (spec §4.1).

Source

pub fn recompute_aggregates(&mut self)

Recompute c_tot and pnl_pos_tot from account data. For test use after direct state mutation.

Source

pub fn haircut_ratio(&self) -> (u128, u128)

Compute haircut ratio (h_num, h_den) per spec §3.2. h = min(Residual, PNL_pos_tot) / PNL_pos_tot where Residual = max(0, V - C_tot - I). Returns (1, 1) when PNL_pos_tot == 0.

Source

pub fn effective_pos_pnl(&self, pnl: i128) -> u128

Compute effective positive PnL after haircut for a given account PnL (spec §3.3). PNL_eff_pos_i = floor(max(PNL_i, 0) * h_num / h_den)

Source

pub fn effective_equity(&self, account: &Account) -> u128

Compute effective realized equity per spec §3.3. Eq_real_i = max(0, C_i + min(PNL_i, 0) + PNL_eff_pos_i)

Source

pub fn add_user(&mut self, fee_payment: u128) -> Result<u16>

Add a new user account

Source

pub fn add_lp( &mut self, matching_engine_program: [u8; 32], matching_engine_context: [u8; 32], fee_payment: u128, ) -> Result<u16>

Add a new LP account

Source

pub fn settle_maintenance_fee( &mut self, idx: u16, now_slot: u64, oracle_price: u64, ) -> Result<u128>

Settle maintenance fees for an account.

Returns the fee amount due (for keeper rebate calculation).

Algorithm:

  1. Compute dt = now_slot - account.last_fee_slot
  2. If dt == 0, return 0 (no-op)
  3. Compute due = fee_per_slot * dt
  4. Deduct from fee_credits; if negative, pay from capital to insurance
  5. If position exists and below maintenance after fee, return Err
Source

pub fn set_owner(&mut self, idx: u16, owner: [u8; 32]) -> Result<()>

Set owner pubkey for an account

Source

pub fn deposit_fee_credits( &mut self, idx: u16, amount: u128, now_slot: u64, ) -> Result<()>

Pre-fund fee credits for an account.

The wrapper must have already transferred amount tokens into the vault. This pre-pays future maintenance fees: vault increases, insurance receives the amount as revenue (since credits are a coupon — spending them later does NOT re-book into insurance), and the account’s fee_credits balance increases by amount.

Source

pub fn set_risk_reduction_threshold(&mut self, new_threshold: u128)

Set the risk reduction threshold (admin function). This controls when risk-reduction-only mode is triggered.

Source

pub fn risk_reduction_threshold(&self) -> u128

Get the current risk reduction threshold.

Source

pub fn close_account( &mut self, idx: u16, now_slot: u64, oracle_price: u64, ) -> Result<u128>

Close an account and return its capital to the caller.

Requirements:

  • Account must exist
  • Position must be zero (no open positions)
  • fee_credits >= 0 (no outstanding fees owed)
  • pnl must be 0 after settlement (positive pnl must be warmed up first)

Returns Err(PnlNotWarmedUp) if pnl > 0 (user must wait for warmup). Returns Err(Undercollateralized) if pnl < 0 (shouldn’t happen after settlement). Returns the capital amount on success.

Source

pub fn garbage_collect_dust(&mut self) -> u32

Garbage collect dust accounts.

A “dust account” is a slot that can never pay out anything:

  • position_size == 0
  • capital == 0
  • reserved_pnl == 0
  • pnl <= 0

Any remaining negative PnL is socialized via ADL waterfall before freeing. No token transfers occur - this is purely internal bookkeeping cleanup.

Called at end of keeper_crank after liquidation/settlement has already run.

Returns the number of accounts closed.

Source

pub fn require_fresh_crank(&self, now_slot: u64) -> Result<()>

Check if a fresh crank is required before state-changing operations. Returns Err if the crank is stale (too old).

Source

pub fn require_recent_full_sweep(&self, now_slot: u64) -> Result<()>

Check if a full sweep started recently. For risk-increasing ops, we require a sweep to have STARTED recently. The priority-liquidation phase runs every crank, so once a sweep starts, the worst accounts are immediately addressed.

Source

pub fn keeper_crank( &mut self, caller_idx: u16, now_slot: u64, oracle_price: u64, funding_rate_bps_per_slot: i64, allow_panic: bool, ) -> Result<CrankOutcome>

Keeper crank entrypoint - advances global state and performs maintenance.

Returns CrankOutcome with flags indicating what happened.

Behavior:

  1. Accrue funding
  2. Advance last_crank_slot if now_slot > last_crank_slot
  3. Settle maintenance fees for caller (50% discount)
  4. Process up to ACCOUNTS_PER_CRANK occupied accounts:
    • Liquidation (if not in force-realize mode)
    • Force-realize (if insurance at/below threshold)
    • Socialization (haircut profits to cover losses)
    • LP max tracking
  5. Detect and finalize full sweep completion

This is the single permissionless “do-the-right-thing” entrypoint.

  • Always attempts caller’s maintenance settle with 50% discount (best-effort)
  • Only advances last_crank_slot when now_slot > last_crank_slot
  • Returns last_cursor: the index where this crank stopped
  • Returns sweep_complete: true if this crank completed a full sweep

When the system has fewer than ACCOUNTS_PER_CRANK accounts, one crank covers all accounts and completes a full sweep.

Source

pub fn mark_pnl_for_position(pos: i128, entry: u64, oracle: u64) -> Result<i128>

Compute mark PnL for a position at oracle price (pure helper, no side effects). Returns the PnL from closing the position at oracle price.

  • Longs: profit when oracle > entry
  • Shorts: profit when entry > oracle
Source

pub fn compute_liquidation_close_amount( &self, account: &Account, oracle_price: u64, ) -> (u128, bool)

Compute how much position to close for liquidation (closed-form, single-pass).

Returns (close_abs, is_full_close) where:

  • close_abs = absolute position size to close
  • is_full_close = true if this is a full position close (including dust kill-switch)
§Algorithm:
  1. Compute target_bps = maintenance_margin_bps + liquidation_buffer_bps
  2. Compute max safe remaining position: abs_pos_safe_max = floor(E_mtm * 10_000 * 1_000_000 / (P * target_bps))
  3. close_abs = abs_pos - abs_pos_safe_max
  4. If remaining position < min_liquidation_abs, do full close (dust kill-switch)

Uses MTM equity (capital + realized_pnl + mark_pnl) for correct risk calculation. This is deterministic, requires no iteration, and guarantees single-pass liquidation.

Source

pub fn liquidate_at_oracle( &mut self, idx: u16, now_slot: u64, oracle_price: u64, ) -> Result<bool>

Liquidate a single account at oracle price if below maintenance margin.

Returns Ok(true) if liquidation occurred, Ok(false) if not needed/possible. Per spec: close position, settle losses, write off unpayable PnL, charge fee. No ADL — haircut ratio h reflects any undercollateralization.

Source

pub fn withdrawable_pnl(&self, account: &Account) -> u128

Calculate withdrawable PNL for an account after warmup

Source

pub fn update_warmup_slope(&mut self, idx: u16) -> Result<()>

Update warmup slope for an account NOTE: No warmup rate cap (removed for simplicity)

Source

pub fn accrue_funding(&mut self, now_slot: u64, oracle_price: u64) -> Result<()>

Accrue funding globally in O(1) using the stored rate (anti-retroactivity).

This uses funding_rate_bps_per_slot_last - the rate in effect since last_funding_slot. The rate for the NEXT interval is set separately via set_funding_rate_for_next_interval.

Anti-retroactivity guarantee: state changes at slot t can only affect funding for slots >= t.

Source

pub fn set_funding_rate_for_next_interval(&mut self, new_rate_bps_per_slot: i64)

Set the funding rate for the NEXT interval (anti-retroactivity).

MUST be called AFTER accrue_funding() to ensure the old rate is applied to the elapsed interval before storing the new rate.

This implements the “rate-change rule” from the spec: state changes at slot t can only affect funding for slots >= t.

Source

pub fn accrue_funding_with_rate( &mut self, now_slot: u64, oracle_price: u64, funding_rate_bps_per_slot: i64, ) -> Result<()>

Convenience: Set rate then accrue in one call.

This sets the rate for the interval being accrued, then accrues. For proper anti-retroactivity in production, the rate should be set at the START of an interval via set_funding_rate_for_next_interval, then accrued later.

Source

pub fn touch_account(&mut self, idx: u16) -> Result<()>

Touch an account (settle funding before operations)

Source

pub fn settle_mark_to_oracle( &mut self, idx: u16, oracle_price: u64, ) -> Result<()>

Settle mark-to-market PnL to the current oracle price (variation margin).

This realizes all unrealized PnL at the given oracle price and resets entry_price = oracle_price. After calling this, mark_pnl_for_position will return 0 for this account at this oracle price.

This makes positions fungible: any LP can close any user’s position because PnL is settled to a common reference price.

Source

pub fn touch_account_full( &mut self, idx: u16, now_slot: u64, oracle_price: u64, ) -> Result<()>

Full account touch: funding + mark settlement + maintenance fees + warmup. This is the standard “lazy settlement” path called on every user operation. Triggers liquidation check if fees push account below maintenance margin.

Source

pub fn deposit(&mut self, idx: u16, amount: u128, now_slot: u64) -> Result<()>

Deposit funds to account.

Settles any accrued maintenance fees from the deposit first, with the remainder added to capital. This ensures fee conservation (fees are never forgiven) and prevents stuck accounts.

Source

pub fn withdraw( &mut self, idx: u16, amount: u128, now_slot: u64, oracle_price: u64, ) -> Result<()>

Withdraw capital from an account. Relies on Solana transaction atomicity: if this returns Err, the entire TX aborts.

Source

pub fn account_equity(&self, account: &Account) -> u128

Realized-only equity: max(0, capital + realized_pnl).

DEPRECATED for margin checks: Use account_equity_mtm_at_oracle instead. This helper is retained for reporting, PnL display, and test assertions that specifically need realized-only equity.

Source

pub fn account_equity_mtm_at_oracle( &self, account: &Account, oracle_price: u64, ) -> u128

Mark-to-market equity at oracle price with haircut (the ONLY correct equity for margin checks). equity_mtm = max(0, C_i + min(PNL_i, 0) + PNL_eff_pos_i + mark_pnl) where PNL_eff_pos_i = floor(max(PNL_i, 0) * h_num / h_den) per spec §3.3.

FAIL-SAFE: On overflow, returns 0 (worst-case equity) to ensure liquidation can still trigger. This prevents overflow from blocking liquidation.

Source

pub fn is_above_margin_bps_mtm( &self, account: &Account, oracle_price: u64, bps: u64, ) -> bool

MTM margin check: is equity_mtm > required margin? This is the ONLY correct margin predicate for all risk checks.

FAIL-SAFE: Returns false on any error (treat as below margin / liquidatable).

Source

pub fn is_above_maintenance_margin_mtm( &self, account: &Account, oracle_price: u64, ) -> bool

MTM maintenance margin check (fail-safe: returns false on overflow)

Source

pub fn execute_trade<M: MatchingEngine>( &mut self, matcher: &M, lp_idx: u16, user_idx: u16, now_slot: u64, oracle_price: u64, size: i128, ) -> Result<()>

Risk-reduction-only mode is entered when the system is in deficit. Warmups are frozen so pending PNL cannot become principal. Withdrawals of principal (capital) are allowed (subject to margin). Risk-increasing actions are blocked; only risk-reducing/neutral operations are allowed. Execute a trade between LP and user. Relies on Solana transaction atomicity: if this returns Err, the entire TX aborts.

Source

pub fn settle_loss_only(&mut self, idx: u16) -> Result<()>

Settle loss only (§6.1): negative PnL pays from capital immediately. If PnL still negative after capital exhausted, write off via set_pnl(i, 0). Used in two-pass settlement to ensure all losses are realized (increasing Residual) before any profit conversions use the haircut ratio.

Source

pub fn settle_warmup_to_capital(&mut self, idx: u16) -> Result<()>

Settle warmup: loss settlement + profit conversion per spec §6

§6.1 Loss settlement: negative PnL pays from capital immediately. If PnL still negative after capital exhausted, write off via set_pnl(i, 0).

§6.2 Profit conversion: warmable gross profit converts to capital at haircut ratio h. y = floor(x * h_num / h_den), where (h_num, h_den) is computed pre-conversion.

Source

pub fn top_up_insurance_fund(&mut self, amount: u128) -> Result<bool>

Top up insurance fund

Adds tokens to both vault and insurance fund. Returns true if the top-up brings insurance above the risk reduction threshold.

Source

pub fn check_conservation(&self, oracle_price: u64) -> bool

Check conservation invariant (spec §3.1)

Primary invariant: V >= C_tot + I

Extended check: vault >= sum(capital) + sum(positive_pnl_clamped) + insurance with bounded rounding slack from funding/mark settlement.

We also verify the full accounting identity including settled/unsettled PnL: vault >= sum(capital) + sum(settled_pnl + mark_pnl) + insurance The difference (slack) must be bounded by MAX_ROUNDING_SLACK.

Source

pub fn advance_slot(&mut self, slots: u64)

Advance to next slot (for testing warmup)

Trait Implementations§

Source§

impl Clone for RiskEngine

Source§

fn clone(&self) -> RiskEngine

Returns a duplicate of the value. Read more
1.0.0 · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for RiskEngine

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl PartialEq for RiskEngine

Source§

fn eq(&self, other: &RiskEngine) -> bool

Tests for self and other values to be equal, and is used by ==.
1.0.0 · Source§

fn ne(&self, other: &Rhs) -> bool

Tests for !=. The default implementation is almost always sufficient, and should not be overridden without very good reason.
Source§

impl Eq for RiskEngine

Source§

impl StructuralPartialEq for RiskEngine

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> AbiExample for T

Source§

default fn example() -> T

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<'a, T, E> AsTaggedExplicit<'a, E> for T
where T: 'a,

Source§

fn explicit(self, class: Class, tag: u32) -> TaggedParser<'a, Explicit, Self, E>

Source§

impl<'a, T, E> AsTaggedImplicit<'a, E> for T
where T: 'a,

Source§

fn implicit( self, class: Class, constructed: bool, tag: u32, ) -> TaggedParser<'a, Implicit, Self, E>

Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Checks if this value is equivalent to the given key. Read more
Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Checks if this value is equivalent to the given key. Read more
Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Compare self to key and return true if they are equal.
Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Checks if this value is equivalent to the given key. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more