Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.multiliquid.xyz/llms.txt

Use this file to discover all available pages before exploring further.

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]

VaultAuthority PDA

PDA Seeds: ["vault_authority", liquidity_provider] The vault_authority PDA is LP-specific and owns that LP’s vault token accounts. The vault token accounts themselves are associated token accounts for (mint, vault_authority), so they are keyed by LP and mint without using the global program authority.

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-9900 BPS)
discount_rate_bpsu16Fee for RWA → Stable (0-9900 BPS)

Required Accounts

#[derive(Accounts)]
#[instruction(liquidity_provider: Pubkey)]
pub struct InitPair<'info> {
    #[account(mut)]
    pub admin: Signer<'info>,

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

    pub stable_coin_mint_address: InterfaceAccount<'info, Mint>,
    pub asset_token_mint_address: InterfaceAccount<'info, Mint>,

    #[account(
        seeds = [VAULT_AUTHORITY_PREFIX, liquidity_provider.as_ref()],
        bump,
    )]
    pub lp_vault_authority: UncheckedAccount<'info>,

    #[account(
        init_if_needed,
        payer = admin,
        associated_token::mint = stable_coin_mint_address,
        associated_token::authority = lp_vault_authority,
        associated_token::token_program = token_program_stable,
    )]
    pub stable_coin_vault_token_account: InterfaceAccount<'info, TokenAccount>,

    #[account(
        init_if_needed,
        payer = admin,
        associated_token::mint = asset_token_mint_address,
        associated_token::authority = lp_vault_authority,
        associated_token::token_program = token_program_asset,
    )]
    pub asset_token_vault_token_account: InterfaceAccount<'info, TokenAccount>,

    #[account(
        init_if_needed,
        payer = admin,
        space = 8 + UserVaultInfo::INIT_SPACE,
        seeds = [USER_VAULT_PREFIX, stable_coin_mint_address.key().as_ref(), liquidity_provider.as_ref()],
        bump,
    )]
    pub lp_vault_stable_pda: Account<'info, UserVaultInfo>,

    #[account(
        init_if_needed,
        payer = admin,
        space = 8 + UserVaultInfo::INIT_SPACE,
        seeds = [USER_VAULT_PREFIX, asset_token_mint_address.key().as_ref(), liquidity_provider.as_ref()],
        bump,
    )]
    pub lp_vault_asset_pda: Account<'info, UserVaultInfo>,

    #[account(
        seeds = [GLOBAL_CONFIG_PREFIX],
        bump = global_config.bump,
        has_one = admin
    )]
    pub global_config: Account<'info, GlobalConfig>,

    #[account(
        init_if_needed,
        payer = admin,
        space = 8 + LpStableConfig::INIT_SPACE,
        seeds = [LP_STABLE_CONFIG_PREFIX, stable_coin_mint_address.key().as_ref(), liquidity_provider.as_ref()],
        bump,
    )]
    pub lp_stable_config: Account<'info, LpStableConfig>,

    #[account(
        mut,
        seeds = [ASSET_CONFIG_PREFIX, stable_coin_mint_address.key().as_ref()],
        bump = asset_config_stable.bump,
    )]
    pub asset_config_stable: Account<'info, AssetConfig>,

    #[account(
        mut,
        seeds = [ASSET_CONFIG_PREFIX, asset_token_mint_address.key().as_ref()],
        bump = asset_config_rwa.bump,
    )]
    pub asset_config_rwa: Account<'info, AssetConfig>,

    pub token_program_stable: Interface<'info, TokenInterface>,
    pub token_program_asset: Interface<'info, TokenInterface>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
}

Behavior

  • Creates Pair PDA account
  • Creates or updates LpStableConfig for LP/stablecoin
  • Creates vault ATAs owned by the LP’s vault_authority PDA if they don’t exist
  • Creates UserVaultInfo accounts to track vault usage
  • Increments used_in_pairs_count on both asset configs
  • Initializes redemption_fee_bps and discount_rate_bps from the instruction arguments
  • Pair pause state defaults to unpaused unless the LP later pauses it with update_pair

Access Control

Access: Admin only
Swaps still require every pause gate to be open: global config, both asset configs, the LP stable config, and the pair itself.

Example

await program.methods
  .initPair(
    liquidityProviderPubkey,
    50,   // 0.5% redemption fee
    25    // 0.25% discount rate
  )
  .accounts({
    admin: adminWallet.publicKey,
    pair,
    stableCoinMintAddress: stableMint,
    assetTokenMintAddress: assetMint,
    lpVaultAuthority,
    stableCoinVaultTokenAccount: stableVault,
    assetTokenVaultTokenAccount: rwaVault,
    lpVaultStablePda: stableUserVaultInfo,
    lpVaultAssetPda: rwaUserVaultInfo,
    globalConfig,
    lpStableConfig,
    assetConfigStable: stableAssetConfig,
    assetConfigRwa: rwaAssetConfig,
    tokenProgramStable: TOKEN_PROGRAM_ID,
    tokenProgramAsset: TOKEN_PROGRAM_ID,
    associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
    systemProgram: SystemProgram.programId,
  })
  .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, 0-9900 BPS)
discount_rate_bpsu16New discount rate (RWA → Stable, 0-9900 BPS)
pausedboolNew pause state

Required Accounts

#[derive(Accounts)]
pub struct UpdatePair<'info> {
    pub liquidity_provider: Signer<'info>,

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

    pub stable_coin_mint_address: InterfaceAccount<'info, Mint>,
    pub asset_token_mint_address: InterfaceAccount<'info, Mint>,

    pub global_config: Account<'info, GlobalConfig>,
}

Behavior

  • Updates fee configuration
  • Updates pause state
  • Requires program to be unpaused
  • Validates both fee values are <= 9900

Access Control

Access: LP (pair owner) only

Example

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

close_pair

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

Required Accounts

#[derive(Accounts)]
pub struct ClosePair<'info> {
    #[account(mut)]
    pub admin: UncheckedAccount<'info>,

    pub liquidity_provider: Signer<'info>,

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

    pub stable_coin_mint_address: InterfaceAccount<'info, Mint>,
    pub asset_token_mint_address: InterfaceAccount<'info, Mint>,

    #[account(
        seeds = [VAULT_AUTHORITY_PREFIX, liquidity_provider.key().as_ref()],
        bump,
    )]
    pub lp_vault_authority: UncheckedAccount<'info>,

    #[account(
        mut,
        associated_token::mint = stable_coin_mint_address,
        associated_token::authority = lp_vault_authority,
        associated_token::token_program = token_program_stable,
    )]
    pub stable_coin_vault_token_account: InterfaceAccount<'info, TokenAccount>,

    #[account(
        mut,
        associated_token::mint = asset_token_mint_address,
        associated_token::authority = lp_vault_authority,
        associated_token::token_program = token_program_asset,
    )]
    pub asset_token_vault_token_account: InterfaceAccount<'info, TokenAccount>,

    #[account(
        mut,
        token::mint = stable_coin_mint_address,
        token::authority = liquidity_provider,
        token::token_program = token_program_stable,
    )]
    pub lp_stable_token_account: InterfaceAccount<'info, TokenAccount>,

    #[account(
        mut,
        token::mint = asset_token_mint_address,
        token::authority = liquidity_provider,
        token::token_program = token_program_asset,
    )]
    pub lp_asset_token_account: InterfaceAccount<'info, TokenAccount>,

    #[account(
        mut,
        seeds = [USER_VAULT_PREFIX, stable_coin_mint_address.key().as_ref(), liquidity_provider.key().as_ref()],
        bump = lp_vault_stable_pda.bump,
    )]
    pub lp_vault_stable_pda: Account<'info, UserVaultInfo>,

    #[account(
        mut,
        seeds = [USER_VAULT_PREFIX, asset_token_mint_address.key().as_ref(), liquidity_provider.key().as_ref()],
        bump = lp_vault_asset_pda.bump,
    )]
    pub lp_vault_asset_pda: Account<'info, UserVaultInfo>,

    #[account(
        mut,
        seeds = [ASSET_CONFIG_PREFIX, stable_coin_mint_address.key().as_ref()],
        bump = asset_config_stable.bump,
    )]
    pub asset_config_stable: Account<'info, AssetConfig>,

    #[account(
        mut,
        seeds = [ASSET_CONFIG_PREFIX, asset_token_mint_address.key().as_ref()],
        bump = asset_config_rwa.bump,
    )]
    pub asset_config_rwa: Account<'info, AssetConfig>,

    #[account(
        seeds = [GLOBAL_CONFIG_PREFIX],
        bump = global_config.bump,
        has_one = admin
    )]
    pub global_config: Account<'info, GlobalConfig>,

    pub token_program_stable: Interface<'info, TokenInterface>,
    pub token_program_asset: Interface<'info, TokenInterface>,
}

Behavior

  • Closes Pair account
  • Requires program to be unpaused
  • Decrements used_in_pairs_count on both asset configs
  • Returns remaining vault tokens to LP
  • Closes vault ATAs if no longer used by other pairs
  • Closes the corresponding UserVaultInfo PDAs when their used count reaches 0
  • 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, mint_address.key().as_ref(), liquidity_provider.key().as_ref()],
        bump = lp_stable_config.bump,
    )]
    pub lp_stable_config: Account<'info, LpStableConfig>,

    /// CHECK: checked by seeds
    pub mint_address: UncheckedAccount<'info>,

    /// CHECK: checked by seeds
    pub liquidity_provider: UncheckedAccount<'info>,

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

Behavior

  • Updates pause state on LpStableConfig
  • Affects ALL pairs using this LP/stablecoin combination
  • Requires program to be unpaused

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 liquidity_provider: Signer<'info>,

    pub mint_address: InterfaceAccount<'info, Mint>,

    #[account(
        mut,
        token::mint = mint_address,
        token::authority = liquidity_provider,
        token::token_program = token_program,
    )]
    pub lp_token_account: InterfaceAccount<'info, TokenAccount>,

    #[account(
        mut,
        associated_token::mint = mint_address,
        associated_token::authority = lp_vault_authority,
        associated_token::token_program = token_program,
    )]
    pub vault_token_account: InterfaceAccount<'info, TokenAccount>,

    #[account(
        seeds = [VAULT_AUTHORITY_PREFIX, liquidity_provider.key().as_ref()],
        bump,
    )]
    pub lp_vault_authority: UncheckedAccount<'info>,

    #[account(
        seeds = [GLOBAL_CONFIG_PREFIX],
        bump = global_config.bump,
    )]
    pub global_config: Account<'info, GlobalConfig>,

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

Behavior

  • Transfers tokens from LP’s account to vault
  • Vault is the mint’s ATA owned by the LP’s vault_authority PDA
  • 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({
    liquidityProvider: lpWallet.publicKey,
    mintAddress: stableMint,
    lpTokenAccount: lpStableTokenAccount,
    vaultTokenAccount: stableVault,
    lpVaultAuthority,
    globalConfig,
    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
  • Vault is the mint’s ATA owned by the LP’s vault_authority PDA
  • 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. Each LP has a vault_authority PDA, and each vault is the associated token account for (mint, vault_authority):
LP Alice → vault_authority PDA ["vault_authority", Alice]
USDC mint + Alice vault_authority → One USDC vault ATA
ULTRA mint + Alice vault_authority → One ULTRA vault ATA

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)
OutOfRangePair fee BPS exceeds 9900 during pair creation or update
AssetConfigInUseCannot change asset type while in use

Next: Price Sources

Learn about NAV pricing sources and oracle integration