Skip to main content

Overview

The swap instruction is the core trading operation of the Multiliquid Program, enabling atomic swaps between Real World Asset (RWA) tokens and stablecoins. It supports both swap directions and both exact-in and exact-out modes.

Key Responsibilities

  • Execute bidirectional swaps between RWAs and stablecoins
  • Calculate swap amounts using NAV-based pricing
  • Apply protocol fees and LP fees
  • Validate all pause states before execution
  • Emit comprehensive swap events

Swap Instruction

Function Signature

pub fn swap(
    ctx: Context<Swap>,
    amount: u64,
    min_amount_out: Option<u64>,
    max_amount_in: Option<u64>,
    swap_direction: SwapDirection,
    swap_type: SwapType,
) -> Result<()>

Parameters

ParameterTypeDescription
amountu64Primary amount (input for ExactIn, output for ExactOut)
min_amount_outOption<u64>Minimum output (slippage protection for ExactIn)
max_amount_inOption<u64>Maximum input (slippage protection for ExactOut)
swap_directionSwapDirectionDirection of the swap
swap_typeSwapTypeExactIn or ExactOut mode

Swap Direction

pub enum SwapDirection {
    StableToAsset,  // Stablecoin → RWA
    AssetToStable,  // RWA → Stablecoin
}

Swap Type

pub enum SwapType {
    ExactIn,   // User specifies exact input amount
    ExactOut,  // User specifies exact output amount
}

Required Accounts

#[derive(Accounts)]
pub struct Swap<'info> {
    // Signer
    pub user: Signer<'info>,

    // Pair account (PDA seeded by liquidity_provider, stable mint, asset mint)
    pub pair: Account<'info, Pair>,

    // Program authority PDA (signs vault transfers)
    pub program_authority: UncheckedAccount<'info>,

    // Liquidity provider wallet (referenced in PDA seeds)
    pub liquidity_provider: UncheckedAccount<'info>,

    // Token mints
    pub stable_coin_mint_address: InterfaceAccount<'info, Mint>,
    pub asset_token_mint_address: InterfaceAccount<'info, Mint>,

    // User token accounts
    pub asset_token_user_token_account: InterfaceAccount<'info, TokenAccount>,
    pub stable_coin_user_token_account: InterfaceAccount<'info, TokenAccount>,

    // Vault accounts (PDA-owned, seeded by mint + liquidity_provider)
    pub stable_coin_vault_token_account: InterfaceAccount<'info, TokenAccount>,
    pub asset_token_vault_token_account: InterfaceAccount<'info, TokenAccount>,

    // Configuration accounts
    pub global_config: Account<'info, GlobalConfig>,
    pub fee_token_account: InterfaceAccount<'info, TokenAccount>,
    pub rwa_config: Account<'info, AssetConfig>,
    pub stable_config: Account<'info, AssetConfig>,
    pub lp_stable_config: Account<'info, LpStableConfig>,

    // Token programs (one per mint, supporting Token and Token-2022)
    pub token_program_stable: Interface<'info, TokenInterface>,
    pub token_program_asset: Interface<'info, TokenInterface>,

    // NAV accounts passed via remaining_accounts
    // ... up to 5 NAV source accounts per asset
}

Validation Checks

Before executing the swap, the instruction validates:
  1. Pause States: Program, both assets, pair, and LP config must be unpaused
  2. Asset Types: RWA must be AssetType::Rwa, stablecoin must be AssetType::Stable
  3. Amount: Must be greater than zero
  4. NAV Prices: Both assets must return valid, non-zero NAV prices
  5. Slippage: Calculated output/input must satisfy min/max constraints

Events

#[event]
pub struct SwapExecuted {
    pub requestor: Pubkey,          // User wallet
    pub amount_in: u64,             // Actual input amount
    pub protocol_fee_amount: u64,   // Protocol fees collected
    pub discount_bps: u16,          // LP fee applied (redemption or discount)
    pub amount_out: u64,            // Actual output amount
    pub pair: Pubkey,               // Pair account address
    pub swap_direction: SwapDirection,
    pub swap_type: SwapType,
}

Swap Calculation

Deterministic Pricing: The Multiliquid Program uses NAV-based pricing, not AMM-style liquidity curves. Prices are determined by asset NAV values, not by liquidity depth. There is no price impact from trade size.
The swap amount is calculated using the exchange rate between asset NAVs:
output_amount = input_amount × input_nav / output_nav

Fee Application

Fees are applied in a specific order depending on swap direction:

StableToAsset (Stablecoin → RWA)

  1. Combined Fees: Protocol fees and redemption fee are calculated simultaneously from the input amount
  2. Exchange Rate: Apply NAV ratio to the post-fee amount to calculate RWA output
total_fees_bps = protocol_fees_bps + redemption_fee_bps
protocol_fees = ceil(amount × protocol_fees_bps / 10000)
total_fees = ceil(amount × total_fees_bps / 10000)
amount_after_fees = amount - total_fees
rwa_output = amount_after_fees × stable_nav / rwa_nav

AssetToStable (RWA → Stablecoin)

  1. Exchange Rate: Apply NAV ratio to calculate stablecoin equivalent
  2. Discount Rate: Apply LP discount fee
  3. Protocol Fees: Deducted from discounted amount
stable_equivalent = rwa_amount × rwa_nav / stable_nav
after_discount = stable_equivalent - (stable_equivalent × discount_rate_bps / 10000)
stable_output = after_discount - protocol_fee

Decimal Normalization

All calculations use 128-bit arithmetic with proper decimal handling:
  • NAV prices normalized to 9 decimals
  • Intermediate calculations use combined decimal precision
  • Final amounts rounded appropriately (ceil for fees, floor for outputs)

Usage Examples

ExactIn: Stable → RWA

Swap exactly 1000 USDC for RWA tokens with minimum output protection:
import { BN } from "@coral-xyz/anchor";

const amount = new BN(1000_000000);  // 1000 USDC (6 decimals)
const minAmountOut = new BN(990_000000);  // Minimum 990 RWA tokens

await program.methods
  .swap(
    amount,
    minAmountOut,    // min_amount_out
    null,            // max_amount_in (not used for ExactIn)
    { stableToAsset: {} },
    { exactIn: {} }
  )
  .accounts({
    user: wallet.publicKey,
    pair,
    programAuthority,
    liquidityProvider,
    stableCoinMintAddress,
    assetTokenMintAddress,
    assetTokenUserTokenAccount,
    stableCoinUserTokenAccount,
    stableCoinVaultTokenAccount,
    assetTokenVaultTokenAccount,
    globalConfig,
    feeTokenAccount,
    rwaConfig,
    stableConfig,
    lpStableConfig,
    tokenProgramStable: TOKEN_PROGRAM_ID,
    tokenProgramAsset: TOKEN_PROGRAM_ID,
  })
  .remainingAccounts(navAccounts)  // NAV source accounts
  .rpc();

ExactOut: Stable → RWA

Receive exactly 1000 RWA tokens, spending at most a specified USDC amount:
const amount = new BN(1000_000000);  // Exact 1000 RWA tokens out
const maxAmountIn = new BN(1010_000000);  // Maximum 1010 USDC in

await program.methods
  .swap(
    amount,
    null,            // min_amount_out (not used for ExactOut)
    maxAmountIn,     // max_amount_in
    { stableToAsset: {} },
    { exactOut: {} }
  )
  .accounts({
    // ... same accounts as above
  })
  .remainingAccounts(navAccounts)
  .rpc();

ExactIn: RWA → Stable

Swap exactly 1000 RWA tokens for stablecoins:
const amount = new BN(1000_000000);  // 1000 RWA tokens
const minAmountOut = new BN(995_000000);  // Minimum 995 USDC

await program.methods
  .swap(
    amount,
    minAmountOut,
    null,
    { assetToStable: {} },
    { exactIn: {} }
  )
  .accounts({
    // ... accounts
  })
  .remainingAccounts(navAccounts)
  .rpc();

ExactOut: RWA → Stable

Receive exactly 1000 USDC, spending at most a specified RWA amount:
const amount = new BN(1000_000000);  // Exact 1000 USDC out
const maxAmountIn = new BN(1005_000000);  // Maximum 1005 RWA in

await program.methods
  .swap(
    amount,
    null,
    maxAmountIn,
    { assetToStable: {} },
    { exactOut: {} }
  )
  .accounts({
    // ... accounts
  })
  .remainingAccounts(navAccounts)
  .rpc();

Error Codes

ErrorDescription
ProgramPausedProgram is paused
RwaPausedRWA asset is paused
StablePausedStablecoin or LP stable config is paused
PairPausedTrading pair is paused
AmountMustBePositiveAmount is zero
InvalidAssetTypeAsset types don’t match expected (RWA vs Stable)
AmountOutTooLowOutput less than minimum
AmountInTooHighInput more than maximum
InvalidNavNAV sources returned invalid or divergent data
MathOverflowCalculation overflow

Access Control

Access: Permissionless Any wallet can execute swaps provided:
  • All pause states are inactive
  • User has sufficient token balance
  • User has approved token transfers
  • Valid NAV prices are available

Next: Global Configuration

Learn about program-wide configuration management