Smart Contracts
Detailed reference for the Rate Swap Protocol smart contracts.
State Accounts
Pool
#[account(zero_copy)]
#[repr(C)]
pub struct Pool {
pub authority: Pubkey, // Pool admin
pub shares_mint: Pubkey, // LP share token mint
pub quote_mint: Pubkey, // Quote token mint (e.g., USDC)
pub quote_vault: Pubkey, // Quote token vault holding all liquidity
pub total_trader_collateral: u64, // Sum of all margin.collateral_amount
// Risk parameters for DV01-based liquidity gating
pub max_rate_move_bps: u16, // Max rate move for DV01 reserve (0-5000 bps)
pub total_liquidity_weight_bps: u16, // Sum of market liquidity weights
pub util_kink_wad: u128, // Utilization kink (WAD-scaled)
pub util_max_wad: u128, // Max utilization (WAD-scaled)
pub min_risk_scalar_wad: u128, // Min scalar at max utilization (WAD-scaled)
pub markets: [Pubkey; 16], // Array of market pubkeys (16 slots)
}PDA Derivation: ["pool", shares_mint]
Key Fields:
quote_vault: Single vault holding LP funds and trader collateraltotal_trader_collateral: Tracks aggregate trader deposits for LP NAV calculationmarkets: Enables multi-market pools (up to 16 markets per pool)
Market
#[account(zero_copy)]
#[repr(C)]
pub struct Market {
pub pool: Pubkey, // Associated pool
pub oracle: Pubkey, // Rate oracle
pub amm: Amm, // AMM state (transparent wrapper)
// Pool-side counterparty accounting
pub pool_rate_accumulator_wad: i128, // Cumulative rate payments (WAD, signed)
pub pool_net_notional_wad: i128, // Pool's net exposure (WAD, signed)
pub pool_net_entry_fixed_rate_wad: i128, // Pool's weighted-avg entry rate (WAD)
pub pool_last_funding_index_wad: i128, // Pool's funding index snapshot (WAD)
// Fee accounting
pub lp_fee_accrued: u64, // Accumulated LP fees (token units)
pub protocol_fee_accrued: u64, // Accumulated protocol fees (token units)
// Fee and margin parameters
pub swap_fee_bps: u16, // Swap fee (0-10000 bps)
pub maintenance_margin_bps: u16, // Maintenance margin requirement
pub initial_margin_bps: u16, // Initial margin requirement
pub liquidation_penalty_bps: u16, // Liquidation penalty rate
pub protocol_fee_share_bps: u16, // Protocol's share of fees
// Margin floor parameters (per-position MRN floors)
pub min_rate_floor_wad: i128, // Min rate for floor calc (WAD)
pub im_mult_wad: u128, // Initial margin multiplier (WAD)
pub mm_mult_wad: u128, // Maintenance margin multiplier (WAD)
// Circuit breaker
pub status: MarketStatus, // Normal/ClosingOnly/Halted
// Risk caps
pub oi_gross_notional_wad: u128, // Gross open interest (WAD)
pub oi_cap_notional_wad: u128, // OI cap (WAD, 0 = no limit)
pub dv01_gross_wad: u128, // Gross DV01 (WAD)
pub dv01_cap_wad: u128, // DV01 cap (WAD)
pub risk_weight_wad: u128, // DV01 weight for reserve calc (WAD)
pub min_time_floor_secs: i64, // Min time for DV01 (seconds)
pub liquidity_weight_bps: u16, // Market's share of pool liquidity
// TWAP/VWAP Mark Deviation Guardrail
pub mark_twap: MarkTwapState, // TWAP ring buffer and config
}PDA Derivation: ["market", pool, amm_seed.to_le_bytes()]
MarketStatus Enum:
#[repr(u8)]
pub enum MarketStatus {
Normal = 0, // All trades allowed
ClosingOnly = 1, // Only risk-reducing trades
Halted = 2, // Only liquidations and admin ops
}Margin
#[account(zero_copy)]
#[repr(C)]
pub struct Margin {
pub authority: Pubkey, // User owner
pub pool: Pubkey, // Associated pool
pub collateral_amount: u64, // Collateral in token units (NOT WAD-scaled)
pub positions: [Position; 8], // Up to 8 embedded positions
}PDA Derivation: ["margin", pool, authority]
MAX_MARGIN_POSITIONS: 8
Important: collateral_amount is stored as raw token units (e.g., USDC with 6 decimals), not WAD-scaled.
Position
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
pub struct Position {
pub market: Pubkey, // Market pubkey (default = empty slot)
pub notional_wad: i128, // Position size (WAD-scaled, signed)
pub entry_fixed_rate_wad: i128, // Weighted-avg entry rate (WAD)
pub last_funding_index_wad: i128, // Last funding index snapshot (WAD)
pub float_exposure_wad: i128, // AMM float exposure (WAD)
pub realized_pnl_wad: i128, // Accumulated PnL (WAD, signed)
pub opened_at: i64, // Open timestamp
pub last_updated_at: i64, // Last modification timestamp
}Embedded: Stored within Margin account, not a separate PDA
Sign Convention:
notional_wad > 0: Pay fixed, receive floating (long rates)notional_wad < 0: Receive fixed, pay floating (short rates)
Realized PnL: Accumulates funding settlements and closed position gains/losses. Can be withdrawn when position is closed.
RateOracle
#[account(zero_copy)]
#[repr(C)]
pub struct RateOracle {
pub rate_index_wad: i128, // Cumulative rate index (WAD, signed)
pub max_abs_df_per_sec_wad: i128, // Max delta per second (WAD/sec, sanity bound)
pub max_abs_df_per_update_wad: i128, // Max delta per update (WAD, sanity bound)
pub last_update_ts: i64, // Last update timestamp
pub max_staleness_secs: i64, // Maximum age before stale
}Account Type: Regular keypair account (not PDA)
Validation: Pool authority can update any oracle used by the pool's markets.
Sanity Bounds: Oracle updates enforce two safety limits:
- Per-second bound:
|dF| <= max_abs_df_per_sec_wad * dt - Per-update bound:
|dF| <= max_abs_df_per_update_wad
Effective max delta: min(max_abs_df_per_sec_wad * dt, max_abs_df_per_update_wad)
Updates violating these bounds are rejected with OracleDeltaOutOfBounds. Updates must also have strictly increasing timestamps (dt > 0), enforced via OracleTimestampNotIncreasing.
Amm
#[repr(transparent)]
pub struct Amm(pub AmmState);Wraps external AmmState from rate-swap-amm crate. Uses #[repr(transparent)] for IDL compatibility.
Instructions
Pool Management
init_pool
pub fn init_pool(
ctx: Context<InitPool>,
args: InitPoolArgs,
) -> Result<()>
pub struct InitPoolArgs {
pub max_rate_move_bps: u16, // Max rate move for DV01 reserve (1-5000)
pub util_kink_wad: u128, // Utilization kink (0 < kink <= WAD)
pub util_max_wad: u128, // Max utilization (kink < max <= WAD)
pub min_risk_scalar_wad: u128, // Min scalar (0 < min <= WAD)
}Accounts:
pool(init, PDA)shares_mint(init)quote_vault(init)quote_mintauthority(signer)system_program,token_program,rent
Validation:
0 < max_rate_move_bps <= 50000 < util_kink_wad <= WADutil_kink_wad < util_max_wad <= WAD0 < min_risk_scalar_wad <= WAD
deposit_pool
pub fn deposit_pool(
ctx: Context<DepositPool>,
amount: u64,
) -> Result<()>Accounts:
pool(mut)quote_vault(mut)shares_mint(mut)depositor(signer)depositor_quote(mut) - depositor's quote token accountdepositor_shares(mut) - depositor's shares accounttoken_program
Logic:
- Read LP NAV:
vault_balance - protocol_fees - total_trader_collateral - Transfer
amountfrom depositor to vault - Mint shares:
floor(amount × total_shares / NAV) - First deposit: Mint 1:1
withdraw_pool
pub fn withdraw_pool(
ctx: Context<WithdrawPool>,
shares: u64,
) -> Result<()>Accounts:
pool(mut)quote_vault(mut)shares_mint(mut)withdrawer(signer)withdrawer_quote(mut)withdrawer_shares(mut)token_program
Logic:
- Read LP NAV
- Calculate DV01 reserve lock across all markets
- Calculate available LP equity:
LP_NAV - reserved - Burn
shares - Transfer:
min(floor(shares × NAV / total_shares), available)
DV01 Reserve Lock: Prevents LPs from withdrawing funds needed to cover potential DV01 losses.
Market Operations
init_market
pub fn init_market(
ctx: Context<InitMarket>,
args: InitMarketArgs,
) -> Result<()>
pub struct InitMarketArgs {
pub amm_seed: u64,
pub maturity: i64,
pub initial_rate_wad: i128,
pub min_abs_rate_wad: u128,
pub max_abs_rate_wad: u128,
pub rate_offset_wad: u128,
pub swap_fee_bps: u16,
pub maintenance_margin_bps: u16,
pub initial_margin_bps: u16,
pub liquidation_penalty_bps: u16,
pub protocol_fee_share_bps: u16,
pub oi_cap_notional_wad: u128,
pub dv01_cap_wad: u128,
pub risk_weight_wad: u128,
pub min_time_floor_secs: i64,
pub liquidity_weight_bps: u16,
pub min_rate_floor_wad: i128,
pub im_mult_wad: u128,
pub mm_mult_wad: u128,
// TWAP/VWAP Mark System Configuration
pub seed_mark_rate_wad: i128, // Initial mark rate before TWAP ready (WAD)
pub twap_bucket_secs: u32, // Bucket duration (default: 60s)
pub twap_window_secs: u32, // TWAP window (default: 3600s)
pub min_obs_notional_wad: u128, // Min notional for rate updates (default: 0)
pub rate_floor_wad: i128, // Floor for deviation scale (default: 0.01×WAD)
pub max_rate_deviation_factor_bps: u16, // Deviation factor (must be >= 1; default: 500)
}Accounts:
market(init, PDA)pool(mut)oracleauthority(signer, must be pool authority)system_program
Validation:
seed_time > 0maturity > seed_timemin_abs_rate > 0max_abs_rate > min_abs_ratemin_abs_rate <= initial_internal_rate <= max_abs_rate0 < risk_weight_wad <= 5 × WADtotal_liquidity_weight_bps + liquidity_weight_bps <= 10000twap_bucket_secs > 0twap_window_secs > 0twap_window_secs % twap_bucket_secs == 0(twap_window_secs / twap_bucket_secs) + 1 <= 256rate_floor_wad >= 0max_rate_deviation_factor_bps >= 1 && <= 10000
Market Registration: Adds market pubkey to pool.markets array.
refresh_market_liquidity
pub fn refresh_market_liquidity(
ctx: Context<RefreshMarketLiquidity>,
) -> Result<()>Accounts:
market(mut)poolquote_vault
Logic: Updates market's cached liquidity depth from current pool NAV.
remove_market
pub fn remove_market(
ctx: Context<RemoveMarket>,
) -> Result<()>Accounts:
market(mut)pool(mut)authority(signer, must be pool authority)
Validation: Market must be expired (maturity < current_time)
Logic: Removes market from pool.markets array and updates total_liquidity_weight_bps.
Margin Operations
init_margin
pub fn init_margin(
ctx: Context<InitMargin>,
) -> Result<()>Accounts:
margin(init, PDA)poolauthority(signer)system_program
Initialization:
collateral_amount = 0positions = [default; 8]
deposit_margin
pub fn deposit_margin(
ctx: Context<DepositMargin>,
args: DepositMarginArgs,
) -> Result<()>
pub struct DepositMarginArgs {
pub amount: u64,
}Accounts:
margin(mut)pool(mut)quote_vault(mut)depositor(signer, must be margin authority)depositor_quote(mut)token_program
Logic:
- Update
margin.collateral_amount += amount - Update
pool.total_trader_collateral += amount - Transfer quote tokens from depositor to vault
withdraw_margin
pub fn withdraw_margin(
ctx: Context<WithdrawMargin>,
args: WithdrawMarginArgs,
) -> Result<()>
pub struct WithdrawMarginArgs {
pub amount: u64,
}Accounts:
margin(mut)pool(mut)quote_vault(mut)withdrawer(signer, must be margin authority)withdrawer_quote(mut)token_program- Remaining accounts: markets and oracles for settlement
Logic:
- Settle ALL positions' funding (accounting-only)
- Compute portfolio equity:
collateral + sum(realized_pnl) + sum(unrealized_mtm) - Cap withdrawal to equity amount
- If withdrawing from realized PnL, deduct proportionally from positions
- Update
margin.collateral_amountandpool.total_trader_collateral - Verify post-withdrawal health >= 0
- Transfer quote tokens from vault to withdrawer
Critical: Health check prevents undercollateralization.
Position Operations
init_position
pub fn init_position(
ctx: Context<InitPosition>,
) -> Result<()>Accounts:
margin(mut)marketauthority(signer)
Logic:
- Find empty position slot in
margin.positions - Initialize with
market, zero notional
Note: Positions can also be lazy-created in swap instruction.
swap
pub fn swap(
ctx: Context<Swap>,
args: SwapArgs,
) -> Result<()>
pub struct SwapArgs {
pub notional_delta_wad: i128, // Signed notional change (WAD-scaled)
}Accounts:
margin(mut)market(mut)oraclepoolauthority(signer)
Logic:
- Check market status (circuit breaker)
- Find/allocate position slot (lazy creation supported)
- Settle funding (accounting-only): updates
position.realized_pnl_wad - Check pre-swap health
- Compute effective liquidity depth:
- For risk-increasing trades: Apply DV01 gating and utilization scalar
- For risk-reducing trades: Use full allocated depth
- Execute AMM swap against effective depth
- Calculate and deduct swap fee (accounting-only):rust
fee_wad = |notional_delta| × swap_fee_bps / 10000 position.realized_pnl_wad -= fee_wad - Update position:
notional_wad,entry_fixed_rate_wad,float_exposure_wad - Update pool counterparty:
pool_net_notional_wad,pool_net_entry_fixed_rate_wad - Update market OI and DV01
- Check post-swap health and initial margin (for risk-increasing trades)
- If position closed: settle realized PnL to collateral
Circuit Breaker Enforcement:
Normal: All trades allowedClosingOnly: Only if|new_notional| <= |old_notional|Halted: Blocked (liquidations use separate instruction)
OI/DV01 Cap Enforcement: Risk-increasing trades rejected if caps exceeded.
liquidate
pub fn liquidate(
ctx: Context<Liquidate>,
) -> Result<()>Accounts:
margin(mut) - victim's marginmarket(mut)oraclepoolliquidator(signer) - permissionless crank
Logic:
- Find victim's position on the market
- Settle victim's funding (accounting-only)
- Compute health - require
health < 0 - Calculate close amount using closed-form approximation:
K = |health| × WAD / (maintenance_rate - penalty_rate - sign_adjusted_mtm) - Execute AMM close trade against allocated depth
- Update victim position (reduce notional)
- Deduct penalty from victim (accounting-only):rust
victim.realized_pnl_wad -= penalty_wad - Credit penalty to pool (accounting-only):rust
market.pool_rate_accumulator_wad += penalty_wad - Update market OI and DV01
- Verify post-liquidation health improved and ≈ 0
Crank-Only Model: No tokens transfer to liquidator. The penalty goes entirely to the pool, benefiting LPs.
Always Succeeds: Liquidations bypass OI caps and circuit breakers to ensure bad positions can always be closed.
Oracle Operations
init_oracle
pub fn init_oracle(
ctx: Context<InitOracle>,
args: InitOracleArgs,
) -> Result<()>
pub struct InitOracleArgs {
pub max_staleness_secs: i64,
pub max_abs_df_per_sec_wad: i128, // Max delta per second (WAD/sec)
pub max_abs_df_per_update_wad: i128, // Max delta per update (WAD)
}Accounts:
oracle(init, keypair)poolauthority(signer, must be pool authority)payer(signer)system_program
Validation:
max_staleness_secs > 0max_abs_df_per_sec_wad > 0max_abs_df_per_update_wad > 0
Recommended Values:
- For funding rate oracles:
max_abs_df_per_sec_wad = WAD / 360(~1% per hour),max_abs_df_per_update_wad = WAD / 10(10% max per update) - For effectively unlimited: Use
i128::MAXfor both bounds
update_oracle
pub fn update_oracle(
ctx: Context<UpdateOracle>,
args: UpdateOracleArgs,
) -> Result<()>
pub struct UpdateOracleArgs {
pub new_rate_index_wad: i128,
}Accounts:
oracle(mut)market(validates oracle belongs to market)poolauthority(signer, must be pool authority)
Logic:
- Compute
dt = now_ts - oracle.last_update_ts - Compute
dF = new_rate_index_wad - oracle.rate_index_wad - Validate delta against sanity bounds:
- Require
dt > 0(strictly increasing timestamp) - Compute
max_allowed = min(oracle.max_abs_df_per_sec_wad * dt, oracle.max_abs_df_per_update_wad) - Require
|dF| <= max_allowed
- Require
- If validation passes, update
oracle.rate_index_wadandoracle.last_update_ts - If validation fails, transaction reverts (no state changes)
Error Codes:
OracleTimestampNotIncreasing:dt <= 0OracleDeltaOutOfBounds:|dF| > max_allowedMathOverflow: Arithmetic overflow in validation
Security: Only pool authority can update oracles. Sanity bounds prevent erroneous or malicious updates from causing protocol-wide damage.
Math Functions
settle.rs
pub fn settle_position(
position: &mut Position,
market: &mut Market,
oracle: &RateOracle,
now_ts: i64,
) -> Result<i128>Returns: Rate settlement amount (rate_settled_wad)
Process:
require_oracle_fresh(oracle, now_ts)- Compute settlement:rust
rate_delta = oracle.rate_index_wad - position.last_funding_index_wad rate_settled_wad = position.notional_wad × rate_delta / WAD - Apply settlement (accounting-only):rust
position.realized_pnl_wad += rate_settled_wad market.pool_rate_accumulator_wad -= rate_settled_wad position.last_funding_index_wad = oracle.rate_index_wad
Idempotency: Same oracle state → same result (no double-settlement).
pub fn compute_health(
margin: &Margin,
positions_mtm: &[i128],
market: &Market,
decimals: u8,
) -> Result<HealthResult>
pub struct HealthResult {
pub equity_wad: i128,
pub unrealized_mtm_wad: i128,
pub health_wad: i128,
pub maintenance_requirement_wad: i128,
pub initial_requirement_wad: i128,
pub rate_settled_wad: i128,
}Equity Formula:
equity = collateral_value + sum(realized_pnl) + sum(unrealized_mtm)lp_nav.rs
pub fn compute_lp_nav(
vault_balance: u64,
protocol_fees: u64,
total_trader_collateral: u64,
) -> u64Formula:
LP_NAV = vault_balance - protocol_fees - total_trader_collateraldv01.rs
pub fn compute_dv01(
notional_wad: i128,
time_to_maturity_secs: i64,
min_time_floor_secs: i64,
) -> u128Formula:
T_eff = max(time_to_maturity, min_time_floor)
DV01 = |notional| × T_eff / (SECONDS_PER_YEAR × 10000)liquidity.rs
pub fn compute_effective_depth(
gross_depth: u128,
reserved: u128,
util_kink_wad: u128,
util_max_wad: u128,
min_risk_scalar_wad: u128,
is_risk_increasing: bool,
) -> u128Logic for risk-increasing trades:
risk_budget = gross_depth.saturating_sub(reserved)
util = reserved × WAD / gross_depth
scalar = interpolate_scalar(util, util_kink, util_max, min_risk_scalar)
effective_depth = risk_budget × scalar / WADoracle.rs
pub fn require_oracle_fresh(
oracle: &RateOracle,
now_ts: i64,
) -> Result<()>Validation:
require!(
now_ts - oracle.last_update_ts <= oracle.max_staleness_secs,
RateSwapError::StaleOracle
)twap.rs
Implements on-chain TWAP (Time-Weighted Average Price) with VWAP-based mark deviation guardrail. This module provides manipulation-resistant mark rate tracking for health, margin, and liquidation calculations.
Feature Overview
Motivation: In thin rate markets, instantaneous AMM implied rates are unsafe:
- A single large trade can spike the implied rate
- Attackers can manipulate rates via flash loans to trigger unfair liquidations
- Using spot rate for mark-to-market creates insolvency risk
Goals:
- Resist single-block manipulation via time-weighted averaging
- Enable price discovery (speed limit, not a cap)
- Provide stable reference for health/liquidations
- Fully on-chain, trade-derived (no external oracle)
Tradeoffs:
- Not anchored to external benchmarks
- Slow drift over time is allowed
- Conservative toward hedgers/long-horizon users
Core Structures
/// Single observation in the TWAP ring buffer (32 bytes)
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
pub struct TwapObs {
pub ts: i64, // Exact timestamp when recorded (Unix seconds)
pub bucket: i64, // Bucket identifier = ts / bucket_secs
pub cum_rate_time: i128, // Cumulative rate×time integral (WAD-scaled × seconds)
}
/// TWAP state embedded in Market (8320 bytes total)
/// WARNING: twap_bucket_secs and twap_window_secs are IMMUTABLE after init.
/// Changing them would corrupt the ring buffer and cause TWAP failures.
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
pub struct MarkTwapState {
// === Configuration (set at init_market) ===
pub seed_mark_rate_wad: i128, // Initial mark before TWAP ready (WAD, signed)
pub min_obs_notional_wad: u128, // Min notional to update last_rate (WAD)
pub rate_floor_wad: i128, // Floor for deviation scale (WAD, >= 0)
// === Runtime State ===
pub last_rate_wad: i128, // Last rate for TWAP integral (WAD, signed)
pub cum_rate_time: i128, // Current cumulative integral (WAD × seconds)
pub last_obs_bucket: i64, // Last observation bucket written
pub last_obs_ts: i64, // Last observation timestamp (Unix seconds)
// === TWAP Parameters ===
pub twap_bucket_secs: u32, // Bucket duration (e.g., 60 = 1 minute)
pub twap_window_secs: u32, // TWAP window (e.g., 3600 = 1 hour)
pub max_rate_deviation_factor_bps: u16, // Deviation factor (must be >= 1; 10000 = 100%)
pub twap_n: u16, // Ring capacity = (window/bucket) + 1
// === Ring Buffer Control ===
pub obs_index: u16, // Current write index in ring buffer
pub twap_initialized: u8, // 0 = not initialized, 1 = active
// obs_count stored as two u8s (supports up to 256, since u8 max is 255)
pub obs_count_lo: u8, // Low byte of observation count
pub obs_count_hi: u8, // High byte of observation count
pub _pad_obs_alignment: [u8; 15], // Padding for 16-byte alignment
// === Ring Buffer (MAX_TWAP_OBS = 256) ===
pub obs: [TwapObs; 256], // 256 × 32 = 8192 bytes
}ABI/Layout Warning: This is a #[repr(C)] account layout and must remain stable. Integrators should decode using the canonical struct in code, not docs. The documentation is descriptive; code is authoritative.
obs_count Sizing Note: The observation count is stored as two u8 fields (obs_count_lo, obs_count_hi) reconstructed via u16::from_le_bytes([lo, hi]). This allows counts up to 256 while maintaining Pod compatibility (u8 max is 255, insufficient for MAX_TWAP_OBS=256).
Key Functions
/// Compute execution VWAP rate from swap amounts
/// Returns Err(DustTrade) if float_out == 0
pub fn compute_exec_vwap_rate_wad(fixed_in: i128, float_out: i128) -> Result<i128>
/// Compute maximum deviation allowed from mark
/// max_dev = max(|mark|, rate_floor) × factor_bps / 10000
pub fn compute_max_deviation_wad(
mark_wad: i128,
rate_floor_wad: i128,
max_rate_deviation_factor_bps: u16,
) -> Result<u128>
/// Check if rate is within deviation band of mark
/// Returns true if |rate - mark| <= max_dev
pub fn is_within_deviation_band(rate_wad: i128, mark_wad: i128, max_dev_wad: u128) -> bool
/// Get the current mark rate (TWAP if initialized, else seed)
/// Returns Err(TwapComputeFailed) if initialized but computation fails
pub fn get_mark_rate_wad(state: &MarkTwapState, now_ts: i64) -> Result<i128>
/// Update TWAP state after a trade
/// Advances cumulative integral, writes observation on bucket change,
/// optionally updates last_rate_wad based on notional filter
pub fn update_twap_state(
state: &mut MarkTwapState,
now_ts: i64,
new_rate_wad: i128,
abs_notional: u128,
update_rate: bool, // false before check, true after
) -> Result<()>
/// Apply rate update after deviation check passes
pub fn apply_rate_update(state: &mut MarkTwapState, new_rate_wad: i128, abs_notional: u128)
/// Validate TWAP configuration parameters
/// Returns twap_n (ring capacity) on success
pub fn validate_twap_config(
twap_bucket_secs: u32,
twap_window_secs: u32,
rate_floor_wad: i128,
max_rate_deviation_factor_bps: u16,
) -> Result<u16>
/// Compute TWAP from ring buffer observations
pub fn compute_twap(
obs: &[TwapObs; MAX_TWAP_OBS],
obs_index: u16,
obs_count: u16,
twap_n: u16,
cum_now: i128,
now_ts: i64,
bucket_secs: u32,
window_secs: u32,
) -> Option<i128>Execution VWAP Definition
For ALL swaps (both risk-increasing and risk-reducing), execution VWAP is computed as:
r_exec_vwap_wad = fixed_in_wad × WAD / float_out_wadfixed_in: Fixed leg payment amount (WAD-scaled, signed)float_out: Float leg received amount (WAD-scaled, signed)- Result: WAD-scaled rate (1e18 = 100%)
- If
float_out == 0: ReturnsErr(ZeroFloatOut)- this applies to ALL swaps unconditionally
Sign conventions: Both values are signed. If float_out is negative, VWAP becomes negative.
Important: All swaps require float_out != 0. This is checked unconditionally in the swap handler before VWAP computation. A swap that produces zero float output is not a valid trade.
Deviation Band Formula (Guardrails)
For risk-increasing swaps only:
scale = max(|mark_rate|, rate_floor_wad)
max_deviation = scale × max_rate_deviation_factor_bps / 10_000Both checks must pass:
|r_exec_vwap - mark| <= max_deviation|r_endpoint - mark| <= max_deviation
Trade rejected with MarkDeviationExceeded if outside band.
Important: max_rate_deviation_factor_bps must be >= 1 (0 is not allowed). Value of 10000 = 100% deviation.
Risk-Reducing Swaps
- VWAP computed: Same as risk-increasing swaps, VWAP is computed from
fixed_in / float_out float_out != 0required: All swaps require valid float output (reverts withZeroFloatOut)- No deviation checks: Must not block deleveraging/risk reduction ("exits always allowed")
- TWAP updates using VWAP: Same rate source as risk-increasing swaps
- Dust filter applies:
min_obs_notional_wadprevents small trades from moving the mark
TWAP Initialization
TWAP becomes active when BOTH conditions hold:
- Enough history:
now - oldest_obs_ts >= min(window_secs - bucket_secs, 10 × bucket_secs) - Valid start observation:
find_observation_at_or_before(start_bucket)succeeds
This invariant ensures compute_twap() will succeed after initialization.
Ring Buffer Indexing
- Ring capacity:
ring_len = min(twap_n, MAX_TWAP_OBS)where MAX_TWAP_OBS = 256 obs_indexadvances moduloring_leninupdate_twap_state- During warm-up:
obs_count < ring_len, observations at indices 0..obs_count - After saturation:
obs_count == ring_len, all slots valid - All scans use consistent
ring_lenmodulus for wrapping
Failure Modes & Liquidation Behavior
Fail-Closed Semantics: If TWAP is initialized but computation fails (e.g., ring buffer edge case), get_mark_rate_wad() returns Err(TwapComputeFailed) rather than silently falling back to seed.
Consequence for Liquidations: Liquidations depend on mark rate for MTM calculation. If TWAP computation fails, liquidations will also fail with TwapComputeFailed. This is accepted as a conservative safety posture for now.
Risk: A solvent protocol could refuse to liquidate underwater positions if TWAP computation fails. While fail-closed is appropriate for trades (better to block than get exploited), it is potentially dangerous for liquidations.
Future Options (not implemented):
- Liquidation-only fallback to last good mark
- Fallback to seed mark with haircut
- Circuit breaker / admin recovery flow
Edge Cases
- First trade / insufficient observations: Uses
seed_mark_rate_wad - Sparse trading: TWAP may span longer than
window_secsif observations are sparse - Negative rates: Handled correctly via signed arithmetic (i128)
float_out == 0: Swap reverts withZeroFloatOut(applies to ALL swaps unconditionally)- Dust trades: Small trades below
min_obs_notional_waddon't updatelast_rate_wad(mark manipulation protection) - Unix epoch (t=0): Valid timestamp; uses
obs_count > 0for init check, notlast_obs_ts == 0
oracle_sanity.rs
Pure validation functions for oracle delta sanity checks:
/// Error types for oracle sanity validation
pub enum OracleSanityError {
DtNotPositive, // dt <= 0
Overflow, // Math overflow
DeltaOutOfBounds { df_abs, max_allowed }, // |dF| > max_allowed
}
/// Validate oracle delta against configured bounds
/// Returns Ok(()) if delta is within bounds, Err otherwise
pub fn validate_oracle_delta(
old_index: i128,
new_index: i128,
now_ts: i64,
last_ts: i64,
max_abs_df_per_sec_wad: i128,
max_abs_df_per_update_wad: i128,
) -> Result<(), OracleSanityError>Validation Logic:
- Compute
dt = now_ts - last_ts, requiredt > 0 - Compute
dF = new_index - old_index - Compute
|dF|(fails for i128::MIN) - Compute
max_allowed = min(max_abs_df_per_sec_wad * dt, max_abs_df_per_update_wad) - Require
|dF| <= max_allowed
Error Codes
Defined in errors.rs:
#[error_code]
pub enum RateSwapError {
#[msg("Oracle data is stale")]
StaleOracle,
#[msg("Position is unhealthy")]
UnhealthyPosition,
#[msg("Insufficient margin")]
InsufficientMargin,
#[msg("Invalid market parameters")]
InvalidMarketParams,
#[msg("Market OI cap exceeded")]
MarketOICapExceeded,
#[msg("Market DV01 cap exceeded")]
MarketDV01CapExceeded,
#[msg("Market is halted")]
MarketHalted,
#[msg("Market is closing-only")]
MarketClosingOnly,
#[msg("Insufficient risk budget depth")]
InsufficientRiskBudgetDepth,
#[msg("Position not liquidatable")]
NotLiquidatable,
#[msg("Math overflow")]
MathOverflow,
// === TWAP/VWAP Mark System Errors ===
#[msg("Mark rate deviates too far from TWAP")]
MarkDeviationExceeded, // Trade rate outside allowed deviation band
#[msg("Invalid TWAP configuration")]
InvalidTwapConfig, // Invalid bucket/window parameters
#[msg("Invalid TWAP deviation factor")]
InvalidTwapDeviationFactor, // factor_bps == 0 or > 10000
#[msg("TWAP computation failed")]
TwapComputeFailed, // TWAP initialized but computation failed (fail-closed)
#[msg("Dust trade")]
DustTrade, // Risk-increasing swap with float_out == 0
#[msg("Clock went backwards")]
ClockWentBackwards, // now_ts < last_obs_ts (timestamp anomaly)
// === Oracle Sanity Check Errors ===
#[msg("Oracle timestamp not increasing")]
OracleTimestampNotIncreasing, // dt <= 0 (requires strictly increasing timestamps)
#[msg("Oracle delta out of bounds")]
OracleDeltaOutOfBounds, // |dF| > max_allowed (sanity bound violation)
// ... more errors
}Helper Macros
require_msg!
macro_rules! require_msg {
($cond:expr, $err:expr, $msg:expr) => {
if !($cond) {
msg!($msg);
return Err($err.into());
}
};
}Purpose: Log context before failing, aids debugging.
Next Steps
- API Reference - Instruction interfaces and examples
- Development Guide - Building and deploying
- Testing - Test suite and scenarios