> ## 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.

# Pair Management

> Trading pair creation, configuration, and liquidity management

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

```rust theme={null}
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

```rust theme={null}
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

```rust theme={null}
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]`

<Note>
  For permissioned mints (tokens), the issuer must separately whitelist (allowlist) both the `liquidity_provider` address and the derived `vault_authority` PDA. The LP address owns the source token account; the PDA owns the destination vault ATA. Approval of one address does not apply to the other.

  The corresponding LP-owned token account and PDA-owned vault ATA must also be thawed before a transfer can succeed. Creating the pair or vault ATA does not perform issuer whitelisting or thawing.
</Note>

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. The LP signs and pays for all account creation; the configured global-config admin is referenced as a non-signing account and checked by `has_one`.

```rust theme={null}
pub fn init_pair(
    ctx: Context<InitPair>,
    redemption_fee_bps: u16,
    discount_rate_bps: u16,
) -> Result<()>
```

#### Parameters

| Parameter            | Type  | Description                       |
| -------------------- | ----- | --------------------------------- |
| `redemption_fee_bps` | `u16` | Fee for Stable → RWA (0-9900 BPS) |
| `discount_rate_bps`  | `u16` | Fee for RWA → Stable (0-9900 BPS) |

#### Required Accounts

```rust theme={null}
#[derive(Accounts)]
pub struct InitPair<'info> {
    /// CHECK: checked by has_one constraint on global_config
    pub admin: UncheckedAccount<'info>,

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

    #[account(
        init,
        payer = liquidity_provider,
        space = 8 + Pair::INIT_SPACE,
        seeds = [PAIR_PREFIX, liquidity_provider.key().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.key().as_ref()],
        bump,
    )]
    pub lp_vault_authority: UncheckedAccount<'info>,

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

    #[account(
        init_if_needed,
        payer = liquidity_provider,
        space = 8 + UserVaultInfo::INIT_SPACE,
        seeds = [USER_VAULT_PREFIX, asset_token_mint_address.key().as_ref(), liquidity_provider.key().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 = liquidity_provider,
        space = 8 + LpStableConfig::INIT_SPACE,
        seeds = [LP_STABLE_CONFIG_PREFIX, stable_coin_mint_address.key().as_ref(), liquidity_provider.key().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, paid for by the LP
* Validates that `asset_config_stable.asset_type == Stable` and `asset_config_rwa.asset_type == Rwa`
* 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
* Requires program to be unpaused
* Pair pause state defaults to **unpaused** unless the LP later pauses it with `update_pair`

#### Access Control

**Access**: LP (signs as `liquidity_provider`). The `admin` account must be provided and is verified against `global_config.admin` via `has_one`, but admin does not sign.

<Note>
  Swaps still require every pause gate to be open: global config, both asset configs, the LP stable config, and the pair itself.
</Note>

#### Example

```typescript theme={null}
await program.methods
  .initPair(
    50,   // 0.5% redemption fee
    25    // 0.25% discount rate
  )
  .accounts({
    admin: globalConfigAdmin,
    liquidityProvider: lpWallet.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,
  })
  .signers([lpWallet])
  .rpc();
```

***

### update\_pair

Update pair configuration (fees and pause state).

```rust theme={null}
pub fn update_pair(
    ctx: Context<UpdatePair>,
    redemption_fee_bps: u16,
    discount_rate_bps: u16,
    paused: bool,
) -> Result<()>
```

#### Parameters

| Parameter            | Type   | Description                                   |
| -------------------- | ------ | --------------------------------------------- |
| `redemption_fee_bps` | `u16`  | New redemption fee (Stable → RWA, 0-9900 BPS) |
| `discount_rate_bps`  | `u16`  | New discount rate (RWA → Stable, 0-9900 BPS)  |
| `paused`             | `bool` | New pause state                               |

#### Required Accounts

```rust theme={null}
#[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

```typescript theme={null}
// 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. The LP signs and receives the reclaimed rent for the pair account and any closed vault accounts; the configured global-config admin is referenced as a non-signing account and checked by `has_one`.

```rust theme={null}
pub fn close_pair(
    ctx: Context<ClosePair>,
) -> Result<()>
```

#### Required Accounts

```rust theme={null}
#[derive(Accounts)]
pub struct ClosePair<'info> {
    /// CHECK: checked by has_one constraint on global_config
    #[account(mut)]
    pub admin: UncheckedAccount<'info>,

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

    #[account(
        mut,
        close = liquidity_provider,
        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 the LP's recipient token accounts
* Closes vault ATAs if no longer used by other pairs
* Closes the corresponding UserVaultInfo PDAs when their `used` count reaches 0
* Returns reclaimed rent (Pair PDA, closed vault ATAs, and closed UserVaultInfo PDAs) to the LP

<Warning>
  Closing a pair reclaims rent and clears the pair's configuration (fees, pause state). The LP can re-initialize a pair for the same (stable, asset) combination later via `init_pair`, but it will start with fresh configuration — prior fee settings are not restored.
</Warning>

#### Access Control

**Access**: LP (pair owner). The `admin` account must be provided and is verified against `global_config.admin` via `has_one`, but admin does not sign.

***

### set\_paused\_for\_lp\_stable\_config

Set pause state for all pairs of an LP/stablecoin combination.

```rust theme={null}
pub fn set_paused_for_lp_stable_config(
    ctx: Context<SetPausedForLpStableConfig>,
    paused: bool,
) -> Result<()>
```

#### Parameters

| Parameter | Type   | Description     |
| --------- | ------ | --------------- |
| `paused`  | `bool` | New pause state |

#### Required Accounts

```rust theme={null}
#[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.

```rust theme={null}
pub fn add_liquidity(
    ctx: Context<AddLiquidity>,
    amount: u64,
) -> Result<()>
```

#### Parameters

| Parameter | Type  | Description                 |
| --------- | ----- | --------------------------- |
| `amount`  | `u64` | Amount of tokens to deposit |

#### Required Accounts

```rust theme={null}
#[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

```typescript theme={null}
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.

```rust theme={null}
pub fn remove_liquidity(
    ctx: Context<RemoveLiquidity>,
    amount: u64,
) -> Result<()>
```

#### Parameters

| Parameter | Type  | Description                  |
| --------- | ----- | ---------------------------- |
| `amount`  | `u64` | Amount 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 ↔ USTB (uses USDC vault + new USTB 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

| Error                   | Description                                              |
| ----------------------- | -------------------------------------------------------- |
| `Unauthorized`          | Caller is not admin or LP owner                          |
| `ProgramPaused`         | Program is paused                                        |
| `PairPaused`            | Pair is paused                                           |
| `AmountMustBePositive`  | Amount is zero                                           |
| `InsufficientLiquidity` | Vault has insufficient balance                           |
| `InvalidAssetType`      | Asset type mismatch (RWA vs Stable)                      |
| `OutOfRange`            | Pair fee BPS exceeds 9900 during pair creation or update |
| `AssetConfigInUse`      | Cannot change asset type while in use                    |

***

<Card title="Next: Price Sources" icon="file-code" href="/svm/instructions/price-sources">
  Learn about NAV pricing sources and oracle integration
</Card>
