Skip to content

Smart Contracts

Detailed reference for the Rate Swap Protocol smart contracts.

State Accounts

Pool

rust
#[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 collateral
  • total_trader_collateral: Tracks aggregate trader deposits for LP NAV calculation
  • markets: Enables multi-market pools (up to 16 markets per pool)

Market

rust
#[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:

rust
#[repr(u8)]
pub enum MarketStatus {
    Normal = 0,      // All trades allowed
    ClosingOnly = 1, // Only risk-reducing trades
    Halted = 2,      // Only liquidations and admin ops
}

Margin

rust
#[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

rust
#[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

rust
#[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:

  1. Per-second bound: |dF| <= max_abs_df_per_sec_wad * dt
  2. 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

rust
#[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

rust
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_mint
  • authority (signer)
  • system_program, token_program, rent

Validation:

  • 0 < max_rate_move_bps <= 5000
  • 0 < util_kink_wad <= WAD
  • util_kink_wad < util_max_wad <= WAD
  • 0 < min_risk_scalar_wad <= WAD

deposit_pool

rust
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 account
  • depositor_shares (mut) - depositor's shares account
  • token_program

Logic:

  1. Read LP NAV: vault_balance - protocol_fees - total_trader_collateral
  2. Transfer amount from depositor to vault
  3. Mint shares: floor(amount × total_shares / NAV)
  4. First deposit: Mint 1:1

withdraw_pool

rust
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:

  1. Read LP NAV
  2. Calculate DV01 reserve lock across all markets
  3. Calculate available LP equity: LP_NAV - reserved
  4. Burn shares
  5. 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

rust
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)
  • oracle
  • authority (signer, must be pool authority)
  • system_program

Validation:

  • seed_time > 0
  • maturity > seed_time
  • min_abs_rate > 0
  • max_abs_rate > min_abs_rate
  • min_abs_rate <= initial_internal_rate <= max_abs_rate
  • 0 < risk_weight_wad <= 5 × WAD
  • total_liquidity_weight_bps + liquidity_weight_bps <= 10000
  • twap_bucket_secs > 0
  • twap_window_secs > 0
  • twap_window_secs % twap_bucket_secs == 0
  • (twap_window_secs / twap_bucket_secs) + 1 <= 256
  • rate_floor_wad >= 0
  • max_rate_deviation_factor_bps >= 1 && <= 10000

Market Registration: Adds market pubkey to pool.markets array.

refresh_market_liquidity

rust
pub fn refresh_market_liquidity(
    ctx: Context<RefreshMarketLiquidity>,
) -> Result<()>

Accounts:

  • market (mut)
  • pool
  • quote_vault

Logic: Updates market's cached liquidity depth from current pool NAV.

remove_market

rust
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

rust
pub fn init_margin(
    ctx: Context<InitMargin>,
) -> Result<()>

Accounts:

  • margin (init, PDA)
  • pool
  • authority (signer)
  • system_program

Initialization:

  • collateral_amount = 0
  • positions = [default; 8]

deposit_margin

rust
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:

  1. Update margin.collateral_amount += amount
  2. Update pool.total_trader_collateral += amount
  3. Transfer quote tokens from depositor to vault

withdraw_margin

rust
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:

  1. Settle ALL positions' funding (accounting-only)
  2. Compute portfolio equity: collateral + sum(realized_pnl) + sum(unrealized_mtm)
  3. Cap withdrawal to equity amount
  4. If withdrawing from realized PnL, deduct proportionally from positions
  5. Update margin.collateral_amount and pool.total_trader_collateral
  6. Verify post-withdrawal health >= 0
  7. Transfer quote tokens from vault to withdrawer

Critical: Health check prevents undercollateralization.

Position Operations

init_position

rust
pub fn init_position(
    ctx: Context<InitPosition>,
) -> Result<()>

Accounts:

  • margin (mut)
  • market
  • authority (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

rust
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)
  • oracle
  • pool
  • authority (signer)

Logic:

  1. Check market status (circuit breaker)
  2. Find/allocate position slot (lazy creation supported)
  3. Settle funding (accounting-only): updates position.realized_pnl_wad
  4. Check pre-swap health
  5. Compute effective liquidity depth:
    • For risk-increasing trades: Apply DV01 gating and utilization scalar
    • For risk-reducing trades: Use full allocated depth
  6. Execute AMM swap against effective depth
  7. Calculate and deduct swap fee (accounting-only):
    rust
    fee_wad = |notional_delta| × swap_fee_bps / 10000
    position.realized_pnl_wad -= fee_wad
  8. Update position: notional_wad, entry_fixed_rate_wad, float_exposure_wad
  9. Update pool counterparty: pool_net_notional_wad, pool_net_entry_fixed_rate_wad
  10. Update market OI and DV01
  11. Check post-swap health and initial margin (for risk-increasing trades)
  12. If position closed: settle realized PnL to collateral

Circuit Breaker Enforcement:

  • Normal: All trades allowed
  • ClosingOnly: Only if |new_notional| <= |old_notional|
  • Halted: Blocked (liquidations use separate instruction)

OI/DV01 Cap Enforcement: Risk-increasing trades rejected if caps exceeded.

liquidate

rust
pub fn liquidate(
    ctx: Context<Liquidate>,
) -> Result<()>

Accounts:

  • margin (mut) - victim's margin
  • market (mut)
  • oracle
  • pool
  • liquidator (signer) - permissionless crank

Logic:

  1. Find victim's position on the market
  2. Settle victim's funding (accounting-only)
  3. Compute health - require health < 0
  4. Calculate close amount using closed-form approximation:
    K = |health| × WAD / (maintenance_rate - penalty_rate - sign_adjusted_mtm)
  5. Execute AMM close trade against allocated depth
  6. Update victim position (reduce notional)
  7. Deduct penalty from victim (accounting-only):
    rust
    victim.realized_pnl_wad -= penalty_wad
  8. Credit penalty to pool (accounting-only):
    rust
    market.pool_rate_accumulator_wad += penalty_wad
  9. Update market OI and DV01
  10. 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

rust
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)
  • pool
  • authority (signer, must be pool authority)
  • payer (signer)
  • system_program

Validation:

  • max_staleness_secs > 0
  • max_abs_df_per_sec_wad > 0
  • max_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::MAX for both bounds

update_oracle

rust
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)
  • pool
  • authority (signer, must be pool authority)

Logic:

  1. Compute dt = now_ts - oracle.last_update_ts
  2. Compute dF = new_rate_index_wad - oracle.rate_index_wad
  3. 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
  4. If validation passes, update oracle.rate_index_wad and oracle.last_update_ts
  5. If validation fails, transaction reverts (no state changes)

Error Codes:

  • OracleTimestampNotIncreasing: dt <= 0
  • OracleDeltaOutOfBounds: |dF| > max_allowed
  • MathOverflow: 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

rust
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:

  1. require_oracle_fresh(oracle, now_ts)
  2. Compute settlement:
    rust
    rate_delta = oracle.rate_index_wad - position.last_funding_index_wad
    rate_settled_wad = position.notional_wad × rate_delta / WAD
  3. 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).

rust
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:

rust
equity = collateral_value + sum(realized_pnl) + sum(unrealized_mtm)

lp_nav.rs

rust
pub fn compute_lp_nav(
    vault_balance: u64,
    protocol_fees: u64,
    total_trader_collateral: u64,
) -> u64

Formula:

rust
LP_NAV = vault_balance - protocol_fees - total_trader_collateral

dv01.rs

rust
pub fn compute_dv01(
    notional_wad: i128,
    time_to_maturity_secs: i64,
    min_time_floor_secs: i64,
) -> u128

Formula:

rust
T_eff = max(time_to_maturity, min_time_floor)
DV01 = |notional| × T_eff / (SECONDS_PER_YEAR × 10000)

liquidity.rs

rust
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,
) -> u128

Logic for risk-increasing trades:

rust
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 / WAD

oracle.rs

rust
pub fn require_oracle_fresh(
    oracle: &RateOracle,
    now_ts: i64,
) -> Result<()>

Validation:

rust
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

rust
/// 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

rust
/// 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_wad
  • fixed_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: Returns Err(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_000

Both checks must pass:

  1. |r_exec_vwap - mark| <= max_deviation
  2. |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 != 0 required: All swaps require valid float output (reverts with ZeroFloatOut)
  • 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_wad prevents small trades from moving the mark

TWAP Initialization

TWAP becomes active when BOTH conditions hold:

  1. Enough history: now - oldest_obs_ts >= min(window_secs - bucket_secs, 10 × bucket_secs)
  2. 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_index advances modulo ring_len in update_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_len modulus 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_secs if observations are sparse
  • Negative rates: Handled correctly via signed arithmetic (i128)
  • float_out == 0: Swap reverts with ZeroFloatOut (applies to ALL swaps unconditionally)
  • Dust trades: Small trades below min_obs_notional_wad don't update last_rate_wad (mark manipulation protection)
  • Unix epoch (t=0): Valid timestamp; uses obs_count > 0 for init check, not last_obs_ts == 0

oracle_sanity.rs

Pure validation functions for oracle delta sanity checks:

rust
/// 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:

  1. Compute dt = now_ts - last_ts, require dt > 0
  2. Compute dF = new_index - old_index
  3. Compute |dF| (fails for i128::MIN)
  4. Compute max_allowed = min(max_abs_df_per_sec_wad * dt, max_abs_df_per_update_wad)
  5. Require |dF| <= max_allowed

Error Codes

Defined in errors.rs:

rust
#[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!

rust
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

Released under the ISC License.