Skip to content

Module 7: DApp 3 - DeFi AMM

Learning Objectives

By the end of this module, you will:

  • Understand Automated Market Maker (AMM) theory and constant product formula
  • Implement liquidity pools with LP token mechanics
  • Build swap functionality with slippage protection
  • Design fee collection and distribution systems
  • Apply flash loan protection patterns

Prerequisites

Complete Module 6: NFT Marketplace first.


7.1 AMM Fundamentals

What is an AMM?

An Automated Market Maker (AMM) is a decentralized exchange mechanism that uses mathematical formulas to price assets instead of traditional order books. AMMs enable permissionless trading by allowing anyone to:

  1. Provide Liquidity: Deposit token pairs into pools
  2. Trade: Swap tokens against the pool reserves
  3. Earn Fees: Liquidity providers earn trading fees

Order Books vs AMMs

Traditional Order Book          Automated Market Maker
┌─────────────────────┐         ┌─────────────────────┐
│ Bids      │  Asks   │         │   Liquidity Pool    │
├───────────┼─────────┤         │  ┌───────────────┐  │
│ 100 @ $99 │ 50 @ $101│        │  │ Token A: 1000 │  │
│ 75 @ $98  │ 80 @ $102│        │  │ Token B: 2000 │  │
│ 200 @ $97 │ 120@ $103│        │  └───────────────┘  │
├───────────┴─────────┤         │   k = 2,000,000     │
│   Price Discovery   │         │   Price: B/A = 2    │
│   via Matching      │         │   via Formula       │
└─────────────────────┘         └─────────────────────┘
Aspect Order Book AMM
Price Discovery Bid/Ask matching Mathematical formula
Liquidity From limit orders From liquidity pools
Counterparty Other traders The pool itself
Always Liquid No (needs orders) Yes (always tradeable)
Slippage Depends on depth Predictable via formula

The Constant Product Formula

The most common AMM design uses the constant product formula:

\[x \times y = k\]

Where: - \(x\) = Reserve of Token A - \(y\) = Reserve of Token B - \(k\) = Constant (invariant)

This creates a hyperbolic curve that ensures: - The pool never runs out of either token - Price increases as supply decreases - Large trades cause more slippage

Price Calculation

The spot price (marginal rate) is the ratio of reserves:

\[\text{Price of A in terms of B} = \frac{y}{x}\]

For example, if pool has: - 1000 Token A - 2000 Token B - Price of A = 2000/1000 = 2 Token B per Token A

Swap Calculation

When trading \(\Delta x\) of Token A for Token B:

\[\Delta y = \frac{y \times \Delta x}{x + \Delta x}\]

Example: Swap 100 Token A - Before: x=1000, y=2000, k=2,000,000 - Input: Δx = 100 - Output: Δy = 2000 × 100 / (1000 + 100) = 181.82 Token B - After: x=1100, y=1818.18, k=2,000,000 ✓

Notice: You get 181.82 instead of 200 (spot price). This is slippage.

Slippage and Price Impact

Slippage is the difference between expected and actual swap output:

\[\text{Slippage} = 1 - \frac{\text{Actual Output}}{\text{Expected at Spot Price}}\]

From our example: - Expected: 100 × 2 = 200 Token B - Actual: 181.82 Token B - Slippage: 1 - 181.82/200 = 9.09%

Price Impact depends on trade size relative to pool reserves:

Trade Size (% of reserve) Approximate Slippage
0.1% 0.1%
1% 1%
10% 9.09%
50% 33.33%

7.2 Liquidity Pool Design

Pool Architecture

┌─────────────────────────────────────────────────┐
│                   Pool Account                   │
├─────────────────────────────────────────────────┤
│  token_a_mint: Pubkey     // Token A mint       │
│  token_b_mint: Pubkey     // Token B mint       │
│  token_a_vault: Pubkey    // Vault holding A    │
│  token_b_vault: Pubkey    // Vault holding B    │
│  lp_mint: Pubkey          // LP token mint      │
│  fee_rate: u16            // Fee in basis pts   │
│  bump: u8                 // PDA bump           │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│              Token Vaults (PDAs)                │
├───────────────────────┬─────────────────────────┤
│    Vault A            │    Vault B              │
│    Holds Token A      │    Holds Token B        │
│    Authority: Pool    │    Authority: Pool      │
└───────────────────────┴─────────────────────────┘
┌─────────────────────────────────────────────────┐
│                 LP Token Mint                   │
│   Represents proportional share of pool         │
│   Mint Authority: Pool PDA                      │
└─────────────────────────────────────────────────┘

Pool State Structure

use anchor_lang::prelude::*;

#[account]
#[derive(InitSpace)]
pub struct Pool {
    /// Token A mint address
    pub token_a_mint: Pubkey,

    /// Token B mint address
    pub token_b_mint: Pubkey,

    /// Vault holding Token A reserves
    pub token_a_vault: Pubkey,

    /// Vault holding Token B reserves
    pub token_b_vault: Pubkey,

    /// LP token mint (minted to liquidity providers)
    pub lp_mint: Pubkey,

    /// Trading fee in basis points (e.g., 30 = 0.3%)
    pub fee_rate: u16,

    /// Total fees collected in Token A
    pub fees_collected_a: u64,

    /// Total fees collected in Token B
    pub fees_collected_b: u64,

    /// Pool PDA bump
    pub bump: u8,

    /// LP mint bump for CPI signing
    pub lp_mint_bump: u8,
}

PDA Derivation

// Pool PDA
[b"pool", token_a_mint.as_ref(), token_b_mint.as_ref()]

// Token A Vault
[b"vault_a", pool.key().as_ref()]

// Token B Vault
[b"vault_b", pool.key().as_ref()]

// LP Mint
[b"lp_mint", pool.key().as_ref()]

Token Ordering

Always order tokens canonically (e.g., by pubkey bytes) to prevent duplicate pools:

fn order_tokens(a: Pubkey, b: Pubkey) -> (Pubkey, Pubkey) {
    if a.to_bytes() < b.to_bytes() {
        (a, b)
    } else {
        (b, a)
    }
}


7.3 LP Token Mechanics

What are LP Tokens?

Liquidity Provider (LP) tokens represent a proportional share of the pool:

  • When you deposit liquidity, you receive LP tokens
  • LP tokens are fungible SPL tokens
  • Burning LP tokens withdraws your share of the pool
  • Your share may grow from trading fees

Initial Liquidity Deposit

The first depositor sets the initial price and receives LP tokens based on geometric mean:

\[LP_{initial} = \sqrt{amount_A \times amount_B}\]

Example: First deposit of 1000 Token A and 2000 Token B - LP tokens minted: √(1000 × 2000) = 1414.21

Subsequent Deposits

Later depositors must deposit at the current ratio and receive proportional LP tokens:

\[LP_{minted} = \min\left(\frac{amount_A}{reserve_A}, \frac{amount_B}{reserve_B}\right) \times LP_{supply}\]

Example: Pool has 1000 A, 2000 B, 1414 LP supply - Depositing 100 A and 200 B - LP minted: min(100/1000, 200/2000) × 1414 = 141.4 LP tokens

Withdrawal

Burning LP tokens withdraws proportional reserves:

\[amount_A = \frac{LP_{burned}}{LP_{supply}} \times reserve_A$$ $$amount_B = \frac{LP_{burned}}{LP_{supply}} \times reserve_B\]

LP Token Implementation

use anchor_spl::token::{self, Mint, MintTo, Burn};

pub fn mint_lp_tokens<'info>(
    pool: &Account<'info, Pool>,
    lp_mint: &Account<'info, Mint>,
    destination: &Account<'info, TokenAccount>,
    amount: u64,
    token_program: &Program<'info, Token>,
    seeds: &[&[u8]],
) -> Result<()> {
    let signer_seeds = &[&seeds[..]];

    token::mint_to(
        CpiContext::new_with_signer(
            token_program.to_account_info(),
            MintTo {
                mint: lp_mint.to_account_info(),
                to: destination.to_account_info(),
                authority: pool.to_account_info(),
            },
            signer_seeds,
        ),
        amount,
    )
}

pub fn burn_lp_tokens<'info>(
    lp_mint: &Account<'info, Mint>,
    source: &Account<'info, TokenAccount>,
    authority: &Signer<'info>,
    amount: u64,
    token_program: &Program<'info, Token>,
) -> Result<()> {
    token::burn(
        CpiContext::new(
            token_program.to_account_info(),
            Burn {
                mint: lp_mint.to_account_info(),
                from: source.to_account_info(),
                authority: authority.to_account_info(),
            },
        ),
        amount,
    )
}

7.4 Swap Implementation

Swap with Fees

Real swaps include trading fees:

\[\Delta y = \frac{y \times \Delta x \times (10000 - fee)}{x \times 10000 + \Delta x \times (10000 - fee)}\]

Where fee is in basis points (e.g., 30 = 0.3%).

Swap Instruction

use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer};

#[derive(Accounts)]
pub struct Swap<'info> {
    #[account(
        seeds = [
            b"pool",
            pool.token_a_mint.as_ref(),
            pool.token_b_mint.as_ref()
        ],
        bump = pool.bump,
    )]
    pub pool: Account<'info, Pool>,

    #[account(
        mut,
        constraint = vault_a.key() == pool.token_a_vault,
    )]
    pub vault_a: Account<'info, TokenAccount>,

    #[account(
        mut,
        constraint = vault_b.key() == pool.token_b_vault,
    )]
    pub vault_b: Account<'info, TokenAccount>,

    #[account(mut)]
    pub user_token_in: Account<'info, TokenAccount>,

    #[account(mut)]
    pub user_token_out: Account<'info, TokenAccount>,

    pub user: Signer<'info>,

    pub token_program: Program<'info, Token>,
}

pub fn handler(
    ctx: Context<Swap>,
    amount_in: u64,
    minimum_amount_out: u64,
    swap_a_to_b: bool,
) -> Result<()> {
    let pool = &ctx.accounts.pool;

    // Determine input/output vaults
    let (vault_in, vault_out) = if swap_a_to_b {
        (&ctx.accounts.vault_a, &ctx.accounts.vault_b)
    } else {
        (&ctx.accounts.vault_b, &ctx.accounts.vault_a)
    };

    // Calculate output with fees
    let amount_out = calculate_swap_output(
        amount_in,
        vault_in.amount,
        vault_out.amount,
        pool.fee_rate,
    )?;

    // Slippage protection
    require!(
        amount_out >= minimum_amount_out,
        AmmError::SlippageExceeded
    );

    // Transfer input tokens to vault
    token::transfer(
        CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            Transfer {
                from: ctx.accounts.user_token_in.to_account_info(),
                to: vault_in.to_account_info(),
                authority: ctx.accounts.user.to_account_info(),
            },
        ),
        amount_in,
    )?;

    // Transfer output tokens from vault to user
    let seeds = &[
        b"pool",
        pool.token_a_mint.as_ref(),
        pool.token_b_mint.as_ref(),
        &[pool.bump],
    ];

    token::transfer(
        CpiContext::new_with_signer(
            ctx.accounts.token_program.to_account_info(),
            Transfer {
                from: vault_out.to_account_info(),
                to: ctx.accounts.user_token_out.to_account_info(),
                authority: pool.to_account_info(),
            },
            &[seeds],
        ),
        amount_out,
    )?;

    emit!(SwapExecuted {
        pool: pool.key(),
        user: ctx.accounts.user.key(),
        amount_in,
        amount_out,
        swap_a_to_b,
    });

    Ok(())
}

Math Library

// src/math.rs

use anchor_lang::prelude::*;
use crate::error::AmmError;

/// Calculate swap output using constant product formula with fees
/// Formula: dy = (y * dx * (10000 - fee)) / (x * 10000 + dx * (10000 - fee))
pub fn calculate_swap_output(
    amount_in: u64,
    reserve_in: u64,
    reserve_out: u64,
    fee_rate: u16,
) -> Result<u64> {
    require!(amount_in > 0, AmmError::ZeroAmount);
    require!(reserve_in > 0 && reserve_out > 0, AmmError::InsufficientLiquidity);

    let amount_in = amount_in as u128;
    let reserve_in = reserve_in as u128;
    let reserve_out = reserve_out as u128;
    let fee_rate = fee_rate as u128;

    // Amount after fee deduction
    let amount_in_with_fee = amount_in
        .checked_mul(10000 - fee_rate)
        .ok_or(AmmError::Overflow)?;

    // Numerator: reserve_out * amount_in_with_fee
    let numerator = reserve_out
        .checked_mul(amount_in_with_fee)
        .ok_or(AmmError::Overflow)?;

    // Denominator: reserve_in * 10000 + amount_in_with_fee
    let denominator = reserve_in
        .checked_mul(10000)
        .ok_or(AmmError::Overflow)?
        .checked_add(amount_in_with_fee)
        .ok_or(AmmError::Overflow)?;

    let amount_out = numerator
        .checked_div(denominator)
        .ok_or(AmmError::Overflow)?;

    Ok(amount_out as u64)
}

/// Calculate LP tokens to mint for initial deposit (geometric mean)
pub fn calculate_initial_lp_tokens(
    amount_a: u64,
    amount_b: u64,
) -> Result<u64> {
    require!(amount_a > 0 && amount_b > 0, AmmError::ZeroAmount);

    let product = (amount_a as u128)
        .checked_mul(amount_b as u128)
        .ok_or(AmmError::Overflow)?;

    Ok(sqrt(product))
}

/// Calculate LP tokens for subsequent deposits
pub fn calculate_lp_tokens_for_deposit(
    amount_a: u64,
    amount_b: u64,
    reserve_a: u64,
    reserve_b: u64,
    lp_supply: u64,
) -> Result<u64> {
    let ratio_a = (amount_a as u128)
        .checked_mul(lp_supply as u128)
        .ok_or(AmmError::Overflow)?
        .checked_div(reserve_a as u128)
        .ok_or(AmmError::Overflow)?;

    let ratio_b = (amount_b as u128)
        .checked_mul(lp_supply as u128)
        .ok_or(AmmError::Overflow)?
        .checked_div(reserve_b as u128)
        .ok_or(AmmError::Overflow)?;

    // Take minimum to prevent manipulation
    let lp_tokens = std::cmp::min(ratio_a, ratio_b);

    Ok(lp_tokens as u64)
}

/// Calculate withdrawal amounts for burning LP tokens
pub fn calculate_withdrawal_amounts(
    lp_amount: u64,
    lp_supply: u64,
    reserve_a: u64,
    reserve_b: u64,
) -> Result<(u64, u64)> {
    require!(lp_amount > 0, AmmError::ZeroAmount);
    require!(lp_supply > 0, AmmError::InsufficientLiquidity);

    let lp_amount = lp_amount as u128;
    let lp_supply = lp_supply as u128;

    let amount_a = lp_amount
        .checked_mul(reserve_a as u128)
        .ok_or(AmmError::Overflow)?
        .checked_div(lp_supply)
        .ok_or(AmmError::Overflow)? as u64;

    let amount_b = lp_amount
        .checked_mul(reserve_b as u128)
        .ok_or(AmmError::Overflow)?
        .checked_div(lp_supply)
        .ok_or(AmmError::Overflow)? as u64;

    Ok((amount_a, amount_b))
}

/// Integer square root using Newton's method
pub fn sqrt(n: u128) -> u64 {
    if n == 0 {
        return 0;
    }

    let mut x = n;
    let mut y = (x + 1) / 2;

    while y < x {
        x = y;
        y = (x + n / x) / 2;
    }

    x as u64
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_sqrt() {
        assert_eq!(sqrt(0), 0);
        assert_eq!(sqrt(1), 1);
        assert_eq!(sqrt(4), 2);
        assert_eq!(sqrt(9), 3);
        assert_eq!(sqrt(2_000_000), 1414); // √(1000 * 2000)
    }

    #[test]
    fn test_swap_output() {
        // Pool: 1000 A, 2000 B, 0.3% fee
        let output = calculate_swap_output(100, 1000, 2000, 30).unwrap();
        // Without fee: 2000 * 100 / 1100 = 181.81
        // With 0.3% fee: slightly less
        assert!(output > 0 && output < 182);
    }
}

7.5 Add Liquidity

Add Liquidity Instruction

#[derive(Accounts)]
pub struct AddLiquidity<'info> {
    #[account(
        mut,
        seeds = [b"pool", pool.token_a_mint.as_ref(), pool.token_b_mint.as_ref()],
        bump = pool.bump,
    )]
    pub pool: Account<'info, Pool>,

    #[account(mut, constraint = vault_a.key() == pool.token_a_vault)]
    pub vault_a: Account<'info, TokenAccount>,

    #[account(mut, constraint = vault_b.key() == pool.token_b_vault)]
    pub vault_b: Account<'info, TokenAccount>,

    #[account(mut, constraint = lp_mint.key() == pool.lp_mint)]
    pub lp_mint: Account<'info, Mint>,

    #[account(mut)]
    pub user_token_a: Account<'info, TokenAccount>,

    #[account(mut)]
    pub user_token_b: Account<'info, TokenAccount>,

    #[account(mut)]
    pub user_lp_account: Account<'info, TokenAccount>,

    pub user: Signer<'info>,

    pub token_program: Program<'info, Token>,
}

pub fn handler(
    ctx: Context<AddLiquidity>,
    amount_a: u64,
    amount_b: u64,
    min_lp_tokens: u64,
) -> Result<()> {
    let pool = &ctx.accounts.pool;
    let vault_a = &ctx.accounts.vault_a;
    let vault_b = &ctx.accounts.vault_b;
    let lp_mint = &ctx.accounts.lp_mint;

    // Calculate LP tokens to mint
    let lp_tokens = if lp_mint.supply == 0 {
        // First deposit - use geometric mean
        calculate_initial_lp_tokens(amount_a, amount_b)?
    } else {
        // Subsequent deposit - proportional
        calculate_lp_tokens_for_deposit(
            amount_a,
            amount_b,
            vault_a.amount,
            vault_b.amount,
            lp_mint.supply,
        )?
    };

    require!(lp_tokens >= min_lp_tokens, AmmError::SlippageExceeded);

    // Transfer Token A to vault
    token::transfer(
        CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            Transfer {
                from: ctx.accounts.user_token_a.to_account_info(),
                to: vault_a.to_account_info(),
                authority: ctx.accounts.user.to_account_info(),
            },
        ),
        amount_a,
    )?;

    // Transfer Token B to vault
    token::transfer(
        CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            Transfer {
                from: ctx.accounts.user_token_b.to_account_info(),
                to: vault_b.to_account_info(),
                authority: ctx.accounts.user.to_account_info(),
            },
        ),
        amount_b,
    )?;

    // Mint LP tokens
    let seeds = &[
        b"pool",
        pool.token_a_mint.as_ref(),
        pool.token_b_mint.as_ref(),
        &[pool.bump],
    ];

    token::mint_to(
        CpiContext::new_with_signer(
            ctx.accounts.token_program.to_account_info(),
            MintTo {
                mint: lp_mint.to_account_info(),
                to: ctx.accounts.user_lp_account.to_account_info(),
                authority: pool.to_account_info(),
            },
            &[seeds],
        ),
        lp_tokens,
    )?;

    emit!(LiquidityAdded {
        pool: pool.key(),
        provider: ctx.accounts.user.key(),
        amount_a,
        amount_b,
        lp_tokens_minted: lp_tokens,
    });

    Ok(())
}

7.6 Remove Liquidity

Remove Liquidity Instruction

#[derive(Accounts)]
pub struct RemoveLiquidity<'info> {
    #[account(
        seeds = [b"pool", pool.token_a_mint.as_ref(), pool.token_b_mint.as_ref()],
        bump = pool.bump,
    )]
    pub pool: Account<'info, Pool>,

    #[account(mut, constraint = vault_a.key() == pool.token_a_vault)]
    pub vault_a: Account<'info, TokenAccount>,

    #[account(mut, constraint = vault_b.key() == pool.token_b_vault)]
    pub vault_b: Account<'info, TokenAccount>,

    #[account(mut, constraint = lp_mint.key() == pool.lp_mint)]
    pub lp_mint: Account<'info, Mint>,

    #[account(mut)]
    pub user_token_a: Account<'info, TokenAccount>,

    #[account(mut)]
    pub user_token_b: Account<'info, TokenAccount>,

    #[account(mut)]
    pub user_lp_account: Account<'info, TokenAccount>,

    pub user: Signer<'info>,

    pub token_program: Program<'info, Token>,
}

pub fn handler(
    ctx: Context<RemoveLiquidity>,
    lp_amount: u64,
    min_amount_a: u64,
    min_amount_b: u64,
) -> Result<()> {
    let pool = &ctx.accounts.pool;
    let vault_a = &ctx.accounts.vault_a;
    let vault_b = &ctx.accounts.vault_b;
    let lp_mint = &ctx.accounts.lp_mint;

    // Calculate withdrawal amounts
    let (amount_a, amount_b) = calculate_withdrawal_amounts(
        lp_amount,
        lp_mint.supply,
        vault_a.amount,
        vault_b.amount,
    )?;

    // Slippage protection
    require!(amount_a >= min_amount_a, AmmError::SlippageExceeded);
    require!(amount_b >= min_amount_b, AmmError::SlippageExceeded);

    // Burn LP tokens
    token::burn(
        CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            Burn {
                mint: lp_mint.to_account_info(),
                from: ctx.accounts.user_lp_account.to_account_info(),
                authority: ctx.accounts.user.to_account_info(),
            },
        ),
        lp_amount,
    )?;

    // Transfer tokens from vaults to user
    let seeds = &[
        b"pool",
        pool.token_a_mint.as_ref(),
        pool.token_b_mint.as_ref(),
        &[pool.bump],
    ];

    token::transfer(
        CpiContext::new_with_signer(
            ctx.accounts.token_program.to_account_info(),
            Transfer {
                from: vault_a.to_account_info(),
                to: ctx.accounts.user_token_a.to_account_info(),
                authority: pool.to_account_info(),
            },
            &[seeds],
        ),
        amount_a,
    )?;

    token::transfer(
        CpiContext::new_with_signer(
            ctx.accounts.token_program.to_account_info(),
            Transfer {
                from: vault_b.to_account_info(),
                to: ctx.accounts.user_token_b.to_account_info(),
                authority: pool.to_account_info(),
            },
            &[seeds],
        ),
        amount_b,
    )?;

    emit!(LiquidityRemoved {
        pool: pool.key(),
        provider: ctx.accounts.user.key(),
        amount_a,
        amount_b,
        lp_tokens_burned: lp_amount,
    });

    Ok(())
}

7.7 Fee Collection and Distribution

Fee Mechanics

Trading fees are automatically collected during swaps and remain in the pool:

  1. Fee is deducted from input amount before swap calculation
  2. Fee tokens stay in the vault, increasing reserves
  3. LP token holders automatically own more underlying tokens
  4. Fees are realized when liquidity is removed

Example Fee Accumulation

Initial State:
- Pool: 1000 A, 2000 B
- LP Supply: 1414
- Your LP tokens: 141 (10% share)
- Your underlying: 100 A, 200 B

After 1000 trades (0.3% fee each):
- Pool: 1050 A, 2100 B (fees accumulated)
- LP Supply: 1414 (unchanged)
- Your LP tokens: 141 (10% share)
- Your underlying: 105 A, 210 B (+5% from fees!)

Protocol Fee (Optional)

Some AMMs take a cut of trading fees for the protocol:

#[account]
pub struct Pool {
    // ... other fields

    /// Protocol fee as fraction of trading fee (e.g., 1/6 = 16.67%)
    pub protocol_fee_rate: u16,

    /// Protocol fee recipient
    pub protocol_fee_recipient: Pubkey,
}

pub fn calculate_fees(
    amount_in: u64,
    trading_fee_rate: u16,
    protocol_fee_rate: u16,
) -> Result<(u64, u64)> {
    let total_fee = amount_in
        .checked_mul(trading_fee_rate as u64)
        .ok_or(AmmError::Overflow)?
        .checked_div(10000)
        .ok_or(AmmError::Overflow)?;

    let protocol_fee = total_fee
        .checked_mul(protocol_fee_rate as u64)
        .ok_or(AmmError::Overflow)?
        .checked_div(10000)
        .ok_or(AmmError::Overflow)?;

    let lp_fee = total_fee.checked_sub(protocol_fee).ok_or(AmmError::Overflow)?;

    Ok((lp_fee, protocol_fee))
}

7.8 Impermanent Loss

What is Impermanent Loss?

Impermanent Loss (IL) occurs when price ratios change after providing liquidity:

  • You deposit at price P
  • Price moves to P'
  • Your holdings are worth less than if you simply held

It's "impermanent" because it reverses if price returns to original.

IL Formula

\[IL = \frac{2\sqrt{r}}{1 + r} - 1\]

Where \(r\) = price ratio change (new_price / old_price)

IL Examples

Price Change Impermanent Loss
±10% 0.11%
±25% 0.64%
±50% 2.02%
±100% (2x) 5.72%
±200% (3x) 13.4%
±400% (5x) 25.5%

IL vs Trading Fees

Liquidity providers hope trading fees exceed impermanent loss. High-volume pools with lower volatility pairs (e.g., stablecoin pairs) typically have lower IL risk.


7.9 Flash Loan Protection

What are Flash Loans?

Flash loans allow borrowing assets without collateral if repaid in the same transaction. They can be used for: - Arbitrage across DEXs - Collateral swaps - Self-liquidation

Flash Loan Attack Vectors

  1. Price Manipulation: Inflate price with flash loan, profit, repay
  2. Reentrancy: Re-enter during vulnerable state
  3. Oracle Manipulation: Manipulate price feeds

Protection Patterns

1. Reentrancy Guard

#[account]
pub struct Pool {
    // ... other fields
    pub locked: bool,
}

pub fn swap(ctx: Context<Swap>, ...) -> Result<()> {
    let pool = &mut ctx.accounts.pool;

    require!(!pool.locked, AmmError::Reentrancy);
    pool.locked = true;

    // ... perform swap ...

    pool.locked = false;
    Ok(())
}

2. TWAP Oracle

Use Time-Weighted Average Price instead of spot price:

#[account]
pub struct Pool {
    // ... other fields

    /// Cumulative price for TWAP calculation
    pub price_cumulative_a: u128,
    pub price_cumulative_b: u128,

    /// Last update timestamp
    pub last_update_timestamp: i64,
}

pub fn update_twap(pool: &mut Pool, reserve_a: u64, reserve_b: u64) -> Result<()> {
    let now = Clock::get()?.unix_timestamp;
    let time_elapsed = now.saturating_sub(pool.last_update_timestamp) as u128;

    if time_elapsed > 0 {
        // Price is reserve_b / reserve_a (scaled by 2^112)
        let price_a = ((reserve_b as u128) << 112) / (reserve_a as u128);
        let price_b = ((reserve_a as u128) << 112) / (reserve_b as u128);

        pool.price_cumulative_a = pool.price_cumulative_a
            .saturating_add(price_a.saturating_mul(time_elapsed));
        pool.price_cumulative_b = pool.price_cumulative_b
            .saturating_add(price_b.saturating_mul(time_elapsed));

        pool.last_update_timestamp = now;
    }

    Ok(())
}

3. Minimum Block Delay

Require deposits and withdrawals to be in different blocks:

#[account]
pub struct UserLiquidity {
    pub owner: Pubkey,
    pub pool: Pubkey,
    pub lp_amount: u64,
    pub deposit_slot: u64,
}

pub fn remove_liquidity(ctx: Context<RemoveLiquidity>, ...) -> Result<()> {
    let current_slot = Clock::get()?.slot;
    let user_liquidity = &ctx.accounts.user_liquidity;

    require!(
        current_slot > user_liquidity.deposit_slot,
        AmmError::SameSlotWithdrawal
    );

    // ... proceed with withdrawal
}

7.10 Complete Program Structure

File Organization

programs/defi-amm/
├── Cargo.toml
├── src/
│   ├── lib.rs
│   ├── state.rs
│   ├── error.rs
│   ├── events.rs
│   ├── math.rs
│   └── instructions/
│       ├── mod.rs
│       ├── initialize_pool.rs
│       ├── add_liquidity.rs
│       ├── remove_liquidity.rs
│       └── swap.rs

Program Entry Point

// src/lib.rs
use anchor_lang::prelude::*;

pub mod error;
pub mod events;
pub mod instructions;
pub mod math;
pub mod state;

use instructions::*;

declare_id!("AMM1111111111111111111111111111111111111111");

#[program]
pub mod defi_amm {
    use super::*;

    pub fn initialize_pool(
        ctx: Context<InitializePool>,
        fee_rate: u16,
    ) -> Result<()> {
        instructions::initialize_pool::handler(ctx, fee_rate)
    }

    pub fn add_liquidity(
        ctx: Context<AddLiquidity>,
        amount_a: u64,
        amount_b: u64,
        min_lp_tokens: u64,
    ) -> Result<()> {
        instructions::add_liquidity::handler(ctx, amount_a, amount_b, min_lp_tokens)
    }

    pub fn remove_liquidity(
        ctx: Context<RemoveLiquidity>,
        lp_amount: u64,
        min_amount_a: u64,
        min_amount_b: u64,
    ) -> Result<()> {
        instructions::remove_liquidity::handler(
            ctx, lp_amount, min_amount_a, min_amount_b
        )
    }

    pub fn swap(
        ctx: Context<Swap>,
        amount_in: u64,
        minimum_amount_out: u64,
        swap_a_to_b: bool,
    ) -> Result<()> {
        instructions::swap::handler(ctx, amount_in, minimum_amount_out, swap_a_to_b)
    }
}

7.11 Interactive Exercise

Try It: Build Your AMM

Open Solana Playground and implement:

  1. Create a pool with 1000 Token A and 2000 Token B
  2. Add liquidity (check LP tokens received)
  3. Execute a swap (verify constant product maintained)
  4. Remove liquidity (check tokens returned)

Test Scenarios

describe("defi-amm", () => {
  it("initializes pool with correct fee", async () => {
    await program.methods
      .initializePool(30) // 0.3% fee
      .accounts({ /* ... */ })
      .rpc();

    const pool = await program.account.pool.fetch(poolPda);
    expect(pool.feeRate).to.equal(30);
  });

  it("maintains constant product after swap", async () => {
    const beforeA = vaultA.amount;
    const beforeB = vaultB.amount;
    const kBefore = beforeA * beforeB;

    await program.methods
      .swap(100, 180, true) // Swap 100 A for B, expect at least 180
      .accounts({ /* ... */ })
      .rpc();

    await vaultA.reload();
    await vaultB.reload();
    const kAfter = vaultA.amount * vaultB.amount;

    // k should increase (fees stay in pool)
    expect(kAfter).to.be.at.least(kBefore);
  });

  it("fails on slippage exceeded", async () => {
    try {
      await program.methods
        .swap(100, 200, true) // Expect 200, but will get ~181
        .accounts({ /* ... */ })
        .rpc();
      expect.fail("Should have thrown");
    } catch (e) {
      expect(e.error.errorCode.code).to.equal("SlippageExceeded");
    }
  });
});

7.12 Security Considerations

Common AMM Vulnerabilities

Vulnerability Description Mitigation
Price manipulation Flash loan + swap to move price TWAP oracle, minimum delay
Reentrancy Re-enter during state change Reentrancy guard, checks-effects-interactions
Overflow Large amounts overflow Use checked math, u128 for intermediates
Front-running MEV bots sandwich trades Private transactions, slippage protection
Donation attack Donate to vault to skew LP ratio Track reserves in state, not vault balance

Best Practices

  1. Always use checked math for all calculations
  2. Use u128 for intermediate calculations to prevent overflow
  3. Validate all inputs (non-zero amounts, reasonable bounds)
  4. Implement slippage protection on all swaps
  5. Track reserves in state, don't rely on vault balance
  6. Use reentrancy guards for multi-step operations
  7. Consider TWAP for any price-dependent logic

Summary

In this module, you learned:

  • AMM Theory: Constant product formula x * y = k enables automated price discovery
  • LP Tokens: Represent proportional pool ownership, grow from fees
  • Swap Math: Output calculation with fee deduction and slippage
  • Liquidity Operations: Adding/removing liquidity with proportional shares
  • Fee Distribution: Fees accumulate in pool, benefiting LP holders
  • Impermanent Loss: Risk from price divergence, offset by trading fees
  • Security: Flash loan protection, reentrancy guards, TWAP oracles

Next Steps

Continue to Module 8: DAO Governance to learn:

  • Governance program architecture
  • Proposal and voting mechanisms
  • Treasury management
  • Execution of passed proposals

Back to Course Home