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:
- Provide Liquidity: Deposit token pairs into pools
- Trade: Swap tokens against the pool reserves
- 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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
- Fee is deducted from input amount before swap calculation
- Fee tokens stay in the vault, increasing reserves
- LP token holders automatically own more underlying tokens
- 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¶
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¶
- Price Manipulation: Inflate price with flash loan, profit, repay
- Reentrancy: Re-enter during vulnerable state
- 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:
- Create a pool with 1000 Token A and 2000 Token B
- Add liquidity (check LP tokens received)
- Execute a swap (verify constant product maintained)
- 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¶
- Always use checked math for all calculations
- Use u128 for intermediate calculations to prevent overflow
- Validate all inputs (non-zero amounts, reasonable bounds)
- Implement slippage protection on all swaps
- Track reserves in state, don't rely on vault balance
- Use reentrancy guards for multi-step operations
- Consider TWAP for any price-dependent logic
Summary¶
In this module, you learned:
- AMM Theory: Constant product formula
x * y = kenables 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