Skip to main content

Overview

Pair accounts link RWA tokens to stablecoins, enabling trading between them. Each pair is owned by a specific Liquidity Provider (LP) who controls its configuration, fees, and liquidity.

Account Structures

Pair Account

pub struct Pair {
    pub redemption_fee_bps: u16,            // Fee for Stable → RWA swaps
    pub discount_rate_bps: u16,             // Fee for RWA → Stable swaps
    pub stable_coin_mint_address: Pubkey,   // Stablecoin token mint
    pub asset_token_mint_address: Pubkey,   // RWA token mint
    pub liquidity_provider: Pubkey,         // LP owner address
    pub paused: bool,                       // Pair-level pause flag
    pub bump: u8,                           // PDA bump seed
}
PDA Seeds: ["pair", liquidity_provider, stable_mint, asset_mint]

LpStableConfig Account

pub struct LpStableConfig {
    pub stable_coin_mint_address: Pubkey,   // Stablecoin mint
    pub paused: bool,                       // Pause all pairs with this config
    pub liquidity_provider: Pubkey,         // LP address
    pub bump: u8,                           // PDA bump seed
}
PDA Seeds: ["lp_stable_config", stable_mint, liquidity_provider]

UserVaultInfo Account

pub struct UserVaultInfo {
    pub user: Pubkey,                       // LP wallet
    pub mint_address: Pubkey,               // Token mint
    pub used: u16,                          // Number of pairs using this vault
    pub bump: u8,                           // PDA bump seed
}
PDA Seeds: ["user_vault_info", mint_address, liquidity_provider]

Pair Instructions

init_pair

Create a new trading pair for an LP.
pub fn init_pair(
    ctx: Context<InitPair>,
    liquidity_provider: Pubkey,
    redemption_fee_bps: u16,
    discount_rate_bps: u16,
) -> Result<()>

Parameters

ParameterTypeDescription
liquidity_providerPubkeyLP wallet that will control the pair
redemption_fee_bpsu16Fee for Stable → RWA (0-10000 BPS)
discount_rate_bpsu16Fee for RWA → Stable (0-10000 BPS)

Required Accounts

#[derive(Accounts)]
pub struct InitPair<'info> {
    pub global_config: Account<'info, GlobalConfig>,

    #[account(
        seeds = [ASSET_PREFIX, stable_mint.key().as_ref()],
        bump = stable_asset_config.bump,
        constraint = stable_asset_config.asset_type == AssetType::Stable
    )]
    pub stable_asset_config: Account<'info, AssetConfig>,

    #[account(
        seeds = [ASSET_PREFIX, asset_mint.key().as_ref()],
        bump = rwa_asset_config.bump,
        constraint = rwa_asset_config.asset_type == AssetType::Rwa
    )]
    pub rwa_asset_config: Account<'info, AssetConfig>,

    #[account(
        init,
        payer = payer,
        space = 8 + Pair::INIT_SPACE,
        seeds = [PAIR_PREFIX, liquidity_provider.as_ref(), stable_mint.key().as_ref(), asset_mint.key().as_ref()],
        bump,
    )]
    pub pair: Account<'info, Pair>,

    // LP stable config (created if needed)
    #[account(
        init_if_needed,
        payer = payer,
        space = 8 + LpStableConfig::INIT_SPACE,
        seeds = [LP_STABLE_CONFIG_PREFIX, stable_mint.key().as_ref(), liquidity_provider.as_ref()],
        bump,
    )]
    pub lp_stable_config: Account<'info, LpStableConfig>,

    // Vaults and vault info accounts...
    // (created if needed for RWA and stablecoin)

    pub stable_mint: InterfaceAccount<'info, Mint>,
    pub asset_mint: InterfaceAccount<'info, Mint>,

    #[account(
        constraint = global_config.admin == admin.key()
            @ MultiliquidError::Unauthorized
    )]
    pub admin: Signer<'info>,

    #[account(mut)]
    pub payer: Signer<'info>,

    pub system_program: Program<'info, System>,
    pub token_program: Interface<'info, TokenInterface>,
    pub stable_token_program: Interface<'info, TokenInterface>,
}

Behavior

  • Creates Pair PDA account
  • Creates or updates LpStableConfig for LP/stablecoin
  • Creates vault accounts if they don’t exist
  • Creates UserVaultInfo accounts to track vault usage
  • Increments used_in_pairs_count on both asset configs
  • Pair starts paused by default

Access Control

Access: Admin only
Pairs start paused by default. The LP must call update_pair to set fees and unpause the pair before trading can begin.

Example

await program.methods
  .initPair(
    liquidityProviderPubkey,
    50,   // 0.5% redemption fee
    25    // 0.25% discount rate
  )
  .accounts({
    globalConfig,
    stableAssetConfig,
    rwaAssetConfig,
    pair,
    lpStableConfig,
    stableVault,
    rwaVault,
    stableUserVaultInfo,
    rwaUserVaultInfo,
    stableMint,
    assetMint,
    admin: adminWallet.publicKey,
    payer: adminWallet.publicKey,
    systemProgram: SystemProgram.programId,
    tokenProgram: TOKEN_PROGRAM_ID,
    stableTokenProgram: TOKEN_PROGRAM_ID,
  })
  .rpc();

update_pair

Update pair configuration (fees and pause state).
pub fn update_pair(
    ctx: Context<UpdatePair>,
    redemption_fee_bps: u16,
    discount_rate_bps: u16,
    paused: bool,
) -> Result<()>

Parameters

ParameterTypeDescription
redemption_fee_bpsu16New redemption fee (Stable → RWA)
discount_rate_bpsu16New discount rate (RWA → Stable)
pausedboolNew pause state

Required Accounts

#[derive(Accounts)]
pub struct UpdatePair<'info> {
    pub global_config: Account<'info, GlobalConfig>,

    #[account(
        mut,
        seeds = [PAIR_PREFIX, liquidity_provider.key().as_ref(), stable_mint.key().as_ref(), asset_mint.key().as_ref()],
        bump = pair.bump,
        has_one = liquidity_provider,
    )]
    pub pair: Account<'info, Pair>,

    pub stable_mint: InterfaceAccount<'info, Mint>,
    pub asset_mint: InterfaceAccount<'info, Mint>,

    pub liquidity_provider: Signer<'info>,
}

Behavior

  • Updates fee configuration
  • Updates pause state
  • Requires program to be unpaused

Access Control

Access: LP (pair owner) only

Example

// Unpause the pair and set final fees
await program.methods
  .updatePair(
    50,    // 0.5% redemption fee
    25,    // 0.25% discount rate
    false  // Unpause
  )
  .accounts({
    globalConfig,
    pair,
    stableMint,
    assetMint,
    liquidityProvider: lpWallet.publicKey,
  })
  .rpc();

close_pair

Permanently close a trading pair.
pub fn close_pair(
    ctx: Context<ClosePair>,
) -> Result<()>

Required Accounts

#[derive(Accounts)]
pub struct ClosePair<'info> {
    pub global_config: Account<'info, GlobalConfig>,

    pub stable_asset_config: Account<'info, AssetConfig>,
    pub rwa_asset_config: Account<'info, AssetConfig>,

    #[account(
        mut,
        close = admin,
        seeds = [PAIR_PREFIX, liquidity_provider.key().as_ref(), stable_mint.key().as_ref(), asset_mint.key().as_ref()],
        bump = pair.bump,
        has_one = liquidity_provider,
    )]
    pub pair: Account<'info, Pair>,

    // Vault accounts to close (if usage count reaches 0)
    pub stable_vault: InterfaceAccount<'info, TokenAccount>,
    pub rwa_vault: InterfaceAccount<'info, TokenAccount>,

    // User vault info accounts
    pub stable_user_vault_info: Account<'info, UserVaultInfo>,
    pub rwa_user_vault_info: Account<'info, UserVaultInfo>,

    // LP stable config (closed if no more pairs)
    pub lp_stable_config: Account<'info, LpStableConfig>,

    pub stable_mint: InterfaceAccount<'info, Mint>,
    pub asset_mint: InterfaceAccount<'info, Mint>,

    pub liquidity_provider: Signer<'info>,

    #[account(
        constraint = global_config.admin == admin.key()
    )]
    pub admin: SystemAccount<'info>,  // Receives rent

    pub token_program: Interface<'info, TokenInterface>,
    pub stable_token_program: Interface<'info, TokenInterface>,
}

Behavior

  • Closes Pair account
  • Decrements used_in_pairs_count on both asset configs
  • Returns remaining vault tokens to LP
  • Closes vault accounts if no longer used by other pairs
  • Closes LpStableConfig if no more pairs
  • Returns rent to admin
Pair closure is permanent. Once closed, a pair cannot be reopened. Admin must create a new pair if trading is needed again.

Access Control

Access: LP (pair owner) only

set_paused_for_lp_stable_config

Set pause state for all pairs of an LP/stablecoin combination.
pub fn set_paused_for_lp_stable_config(
    ctx: Context<SetPausedForLpStableConfig>,
    paused: bool,
) -> Result<()>

Parameters

ParameterTypeDescription
pausedboolNew pause state

Required Accounts

#[derive(Accounts)]
pub struct SetPausedForLpStableConfig<'info> {
    pub global_config: Account<'info, GlobalConfig>,

    #[account(
        mut,
        seeds = [LP_STABLE_CONFIG_PREFIX, stable_mint.key().as_ref(), liquidity_provider.key().as_ref()],
        bump = lp_stable_config.bump,
    )]
    pub lp_stable_config: Account<'info, LpStableConfig>,

    pub stable_mint: InterfaceAccount<'info, Mint>,

    // Either admin OR liquidity_provider can call
    pub authority: Signer<'info>,

    /// CHECK: LP address for verification
    pub liquidity_provider: UncheckedAccount<'info>,
}

Behavior

  • Updates pause state on LpStableConfig
  • Affects ALL pairs using this LP/stablecoin combination

Access Control

Access: Admin OR LP (config owner)

Liquidity Instructions

add_liquidity

Deposit tokens into a vault.
pub fn add_liquidity(
    ctx: Context<AddLiquidity>,
    amount: u64,
) -> Result<()>

Parameters

ParameterTypeDescription
amountu64Amount of tokens to deposit

Required Accounts

#[derive(Accounts)]
pub struct AddLiquidity<'info> {
    pub global_config: Account<'info, GlobalConfig>,

    #[account(
        mut,
        seeds = [VAULT_PREFIX, mint.key().as_ref(), liquidity_provider.key().as_ref()],
        bump,
    )]
    pub vault: InterfaceAccount<'info, TokenAccount>,

    #[account(
        mut,
        constraint = lp_token_account.owner == liquidity_provider.key()
    )]
    pub lp_token_account: InterfaceAccount<'info, TokenAccount>,

    pub mint: InterfaceAccount<'info, Mint>,

    pub liquidity_provider: Signer<'info>,

    pub token_program: Interface<'info, TokenInterface>,
}

Behavior

  • Transfers tokens from LP’s account to vault
  • Requires program to be unpaused
  • Amount must be greater than 0

Access Control

Access: LP (vault owner) only

Example

await program.methods
  .addLiquidity(new BN(1000_000000))  // 1000 tokens
  .accounts({
    globalConfig,
    vault: stableVault,
    lpTokenAccount: lpStableTokenAccount,
    mint: stableMint,
    liquidityProvider: lpWallet.publicKey,
    tokenProgram: TOKEN_PROGRAM_ID,
  })
  .rpc();

remove_liquidity

Withdraw tokens from a vault.
pub fn remove_liquidity(
    ctx: Context<RemoveLiquidity>,
    amount: u64,
) -> Result<()>

Parameters

ParameterTypeDescription
amountu64Amount of tokens to withdraw

Required Accounts

Same as add_liquidity.

Behavior

  • Transfers tokens from vault to LP’s account
  • Requires program to be unpaused
  • Amount must be greater than 0
  • Vault must have sufficient balance

Access Control

Access: LP (vault owner) only

Vault Architecture

Shared Vault Model

Vaults are shared across pairs for the same LP/token combination:
LP Alice + USDC mint → One USDC vault
LP Alice + ULTRA mint → One ULTRA vault

Pair 1: Alice's USDC ↔ ULTRA (uses both vaults)
Pair 2: Alice's USDC ↔ JTRSY (uses USDC vault + new JTRSY vault)

UserVaultInfo Tracking

The UserVaultInfo account tracks how many pairs use each vault:
  • Incremented when pair is created
  • Decremented when pair is closed
  • Vault closed only when used reaches 0
This ensures vaults aren’t closed while still in use by other pairs.

Error Codes

ErrorDescription
UnauthorizedCaller is not admin or LP owner
ProgramPausedProgram is paused
PairPausedPair is paused
AmountMustBePositiveAmount is zero
InsufficientLiquidityVault has insufficient balance
InvalidAssetTypeAsset type mismatch (RWA vs Stable)
FeesOutOfRangeFee BPS exceeds 9900
AssetConfigInUseCannot change asset type while in use

Next: Price Sources

Learn about NAV pricing sources and oracle integration