Skip to content

Architecture

Technical architecture overview of the Rate Swap Protocol.

System Overview

The Rate Swap Protocol is a Solana program built with the Anchor framework that implements margined, mark-to-market interest rate swaps using a quote-only closed system with accounting-only settlement.

Quote-Only Closed System

The protocol uses a single accounting currency called the quote token (e.g., USDC) that serves as:

  • The unit of collateral for traders' margin accounts
  • The vault currency for LP liquidity
  • The settlement medium for all PnL accounting

All settlement is accounting-only: no SPL token transfers occur during funding settlement. Tokens only move during explicit deposit/withdraw operations.

Core Components

  1. Pool: Liquidity vault and LP management, holds quote tokens
  2. Market: Swap instrument with AMM pricing and pool-side counterparty accounting
  3. Margin: User collateral account (raw token amount, not shares)
  4. Position: User swap positions (embedded in Margin), tracks realized PnL
  5. RateOracle: Rate index data source for settlement
  6. AMM: Rate curve pricing engine (external crate)

Program Structure

programs/rate-swap/src/
├── lib.rs                  # Program entrypoint, instruction routing
├── state/
│   └── mod.rs              # Account structures (Pool, Market, Margin, Position, RateOracle)
├── instructions/           # Instruction handlers
│   ├── init_pool.rs        # Pool creation with risk parameters
│   ├── deposit_pool.rs     # LP deposits
│   ├── withdraw_pool.rs    # LP withdrawals
│   ├── init_market.rs      # Market creation with caps and fees
│   ├── remove_market.rs    # Expired market cleanup
│   ├── refresh_market_liquidity.rs
│   ├── init_margin.rs      # Trader margin account
│   ├── deposit_margin.rs   # Collateral deposit
│   ├── withdraw_margin.rs  # Collateral withdrawal
│   ├── init_position.rs    # Position slot allocation
│   ├── swap.rs             # Trading with DV01 gating
│   ├── liquidate.rs        # Crank-only liquidation
│   ├── init_oracle.rs      # Oracle initialization
│   └── update_oracle.rs    # Oracle updates
├── math/                   # Core calculations
│   ├── lp_nav.rs           # LP NAV calculations
│   ├── liquidity.rs        # Liquidity depth and DV01 gating
│   ├── settle.rs           # Accounting-only settlement and health
│   ├── dv01.rs             # DV01 calculations
│   ├── margin_floor.rs     # Per-position margin floors
│   ├── shares.rs           # Equity calculations
│   ├── fees.rs             # Swap fee calculations
│   ├── oracle.rs           # Oracle validation
│   └── conversions.rs      # WAD conversions
├── errors.rs               # Custom error types
└── macros.rs               # Helper macros (require_msg!)

Account Architecture

Pool Account

PDA Seeds: ["pool", shares_mint]

Fields:

  • authority: Pool admin
  • shares_mint: LP share token mint
  • quote_mint: Quote token mint (e.g., USDC)
  • quote_vault: Quote token vault holding all liquidity
  • total_trader_collateral: Sum of all trader collateral (u64)
  • max_rate_move_bps: Max rate move for DV01 reserve (basis points)
  • total_liquidity_weight_bps: Sum of market liquidity weights
  • util_kink_wad: Utilization kink for scalar (WAD)
  • util_max_wad: Max utilization (WAD)
  • min_risk_scalar_wad: Minimum scalar at max utilization (WAD)
  • markets: Array of 16 market pubkeys

Purpose: Central liquidity vault, tracks aggregate trader collateral for LP NAV

Market Account

PDA Seeds: ["market", pool, amm_seed (u64 le bytes)]

Fields:

  • pool: Associated pool
  • oracle: Rate oracle reference
  • amm: AMM state (rate curve)
  • pool_rate_accumulator_wad: Pool's cumulative rate payments (WAD, signed)
  • pool_net_notional_wad: Pool's net exposure (WAD, signed)
  • pool_net_entry_fixed_rate_wad: Pool's weighted-avg entry rate (WAD)
  • pool_last_funding_index_wad: Pool's funding index snapshot (WAD)
  • lp_fee_accrued: Accumulated LP fees (token units)
  • protocol_fee_accrued: Accumulated protocol fees (token units)
  • swap_fee_bps: Trading fee (basis points)
  • maintenance_margin_bps: Maintenance margin requirement
  • initial_margin_bps: Initial margin requirement
  • liquidation_penalty_bps: Liquidation penalty rate
  • protocol_fee_share_bps: Protocol's share of fees
  • min_rate_floor_wad: Minimum rate for margin floor (WAD)
  • im_mult_wad: Initial margin floor multiplier (WAD)
  • mm_mult_wad: Maintenance margin floor multiplier (WAD)
  • status: Circuit breaker (Normal/ClosingOnly/Halted)
  • oi_gross_notional_wad: Gross open interest (WAD)
  • oi_cap_notional_wad: OI cap (WAD)
  • dv01_gross_wad: Gross DV01 (WAD)
  • dv01_cap_wad: DV01 cap (WAD)
  • risk_weight_wad: DV01 weight for reserve calc (WAD)
  • min_time_floor_secs: Minimum time for DV01 (seconds)
  • liquidity_weight_bps: Market's share of pool liquidity

Purpose: Defines swap instrument, manages AMM pricing and pool-side counterparty accounting

Margin Account

PDA Seeds: ["margin", pool, authority]

Fields:

  • authority: User owner
  • pool: Associated pool
  • collateral_amount: Deposited collateral in token units (u64, not WAD)
  • positions: Array of up to 8 positions (embedded)

Purpose: User's collateral and position container

Position Struct (Embedded)

Fields:

  • market: Associated market (Pubkey::default() = empty slot)
  • notional_wad: Position size (WAD-scaled, signed)
  • entry_fixed_rate_wad: Weighted-average entry rate (WAD)
  • last_funding_index_wad: Last funding index snapshot (WAD)
  • float_exposure_wad: Cumulative AMM float exposure (WAD)
  • realized_pnl_wad: Accumulated funding + closed PnL (WAD, signed)
  • opened_at: Position open timestamp
  • last_updated_at: Last modification timestamp

Purpose: Tracks individual swap position state and accumulated PnL

Sign Convention:

  • notional_wad > 0: Pay fixed, receive floating (long rates)
  • notional_wad < 0: Receive fixed, pay floating (short rates)

RateOracle Account

Type: Regular keypair account (not PDA)

Fields:

  • rate_index_wad: Cumulative rate index (WAD, signed)
  • last_update_ts: Last update timestamp
  • max_staleness_secs: Maximum age before stale

Purpose: Provides cumulative rate index for settlement

Data Flow

Opening a Position (Swap)

User → swap instruction

1. Load margin, market, oracle, pool accounts
2. Check market status (circuit breaker)
3. Find/allocate position slot
4. Settle existing position (accounting-only)
5. Check pre-swap health
6. Compute DV01-based liquidity depth:
   a. Calculate gross LP equity
   b. Subtract DV01 reserve for risk-increasing trades
   c. Apply utilization-based scalar
7. Execute AMM swap against effective depth
8. Deduct swap fee from realized_pnl_wad (accounting-only)
9. Update position: notional, entry_rate, float_exposure
10. Update pool counterparty: net_notional, entry_rate
11. Update market OI and DV01
12. Check post-swap health and initial margin
13. If position closed: settle realized PnL to collateral

Accounting-Only Settlement

settle_position()

1. require_oracle_fresh() - Check staleness
2. Compute rate settlement:
   rate_delta = oracle.rate_index_wad - position.last_funding_index_wad
   rate_settled_wad = notional_wad × rate_delta
3. Apply settlement (accounting-only, no token transfer):
   position.realized_pnl_wad += rate_settled_wad
   market.pool_rate_accumulator_wad -= rate_settled_wad
4. Update position.last_funding_index_wad

Settlement is idempotent: calling twice with same oracle produces same result.

Liquidation (Crank-Only)

liquidate instruction (permissionless)

1. Settle victim's funding (accounting-only)
2. Compute health - require health < 0
3. Calculate close amount using closed-form approximation:
   K = |health| × WAD / (maintenance_rate - penalty_rate - sign_adjusted_mtm)
4. Execute AMM close trade
5. Update victim position (reduce notional)
6. Deduct penalty from victim (accounting-only):
   victim.realized_pnl_wad -= penalty_wad
7. Credit penalty to pool (accounting-only):
   market.pool_rate_accumulator_wad += penalty_wad
8. Verify post-liquidation health improved

Key difference from traditional liquidations: No tokens transfer to liquidator. The penalty goes entirely to the pool via accounting entries.

LP Deposit/Withdraw

Deposit:

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

Withdraw:

1. Read LP NAV
2. Calculate DV01 reserve lock:
   reserved = weighted_total_dv01 × max_rate_move_bps / 10000
3. Calculate available LP equity:
   available = LP_NAV - reserved (saturating at 0)
4. Burn shares
5. Transfer assets: min(floor(shares × NAV / total_shares), available)

Math & Calculations

LP NAV Calculation

rust
LP_NAV = vault_balance - protocol_fees - total_trader_collateral

Future: Add pool_rate_accumulator_wad for unrealized pool PnL.

DV01-Based Liquidity Gating

For risk-increasing trades:

rust
// Step 1: Calculate weighted DV01 reserve
weighted_total_dv01 = Σ(market.dv01_gross × market.risk_weight / WAD)
reserved = weighted_total_dv01 × max_rate_move_bps / 10000

// Step 2: Get market's allocated depth
allocated_depth = lp_equity × market.liquidity_weight_bps / 10000

// Step 3: Calculate risk budget
risk_budget = allocated_depth - reserved  // saturating at 0

// Step 4: Apply utilization scalar
util = reserved / allocated_depth
if util <= util_kink: scalar = WAD
else if util >= util_max: scalar = min_risk_scalar
else: scalar = linear_interpolation(WAD, min_risk_scalar)

effective_depth = risk_budget × scalar / WAD

Risk-reducing trades use full allocated_depth (no gating).

Equity Calculation

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

where:
  collateral_value = token_amount_to_wad(margin.collateral_amount, decimals)
  realized_pnl = sum(position.realized_pnl_wad) across positions
  unrealized_mtm = sum(compute_mtm(position)) across positions

MTM Calculation

rust
time_to_maturity = maturity - current_time
MTM = (current_rate - entry_rate) × notional × time_to_maturity / SECONDS_PER_YEAR

Health Calculation

rust
maintenance_req = sum(|notional| × maintenance_margin_bps / 10000)
health = equity - maintenance_req

Healthy if health >= 0

Margin Floor (Per-Position)

rust
R_eff = max(abs(mark_rate), min_rate_floor)
T_eff = max(time_to_maturity, min_time_floor)

floor_IM = |notional| × R_eff × T_eff × im_mult / (WAD × SECONDS_PER_YEAR)
floor_MM = |notional| × R_eff × T_eff × mm_mult / (WAD × SECONDS_PER_YEAR)

effective_IM = max(bps_IM, floor_IM)
effective_MM = max(bps_MM, floor_MM)

Security Model

PDA Validation

All PDAs derived on-chain with seeds validation. Prevents account substitution attacks.

Zero-Copy Accounts

Efficient storage, prevents stack overflow on large accounts.

Checked Arithmetic

All math uses checked_* operations to prevent overflow/underflow.

Oracle Freshness

require_oracle_fresh() prevents stale data from affecting settlement.

Health Before Transfer

NAV and health checked before token transfers prevent manipulation.

Idempotent Settlement

Same oracle index → same settlement result. No double-settlement possible.

Circuit Breakers

Market status controls trading:

  • Normal: All trades allowed
  • ClosingOnly: Only risk-reducing trades (|new_notional| <= |old_notional|)
  • Halted: Only liquidations and admin operations

Program ID

CPrPi4AnqM2RhQXZ9n7gyWb4i7BcTJDDWruhonuRU1jc

External Dependencies

rate-swap-amm

Git submodule in .deps/rate-swap-amm/

Provides AmmState struct with rate curve logic:

  • Maps net exposure → implied fixed rate
  • Handles rate bounds and liquidity depth

Deployment Model

  1. Testnet: Initial development and testing
  2. Devnet: Community testing, integration testing
  3. Mainnet: Production deployment (after audits)

Upgradeability

Anchor programs are upgradeable via:

  • Program upgrade authority
  • Future: Governance-controlled upgrades

Next Steps

Released under the ISC License.