Browse Source

add token swap example

shivam 2 years ago
parent
commit
dd5611d765
23 changed files with 1621 additions and 0 deletions
  1. 2 0
      tokens/token-swap/README.md
  2. 15 0
      tokens/token-swap/anchor/Anchor.toml
  3. 14 0
      tokens/token-swap/anchor/Cargo.toml
  4. 20 0
      tokens/token-swap/anchor/package.json
  5. 27 0
      tokens/token-swap/anchor/programs/token-swap/Cargo.toml
  6. 2 0
      tokens/token-swap/anchor/programs/token-swap/Xargo.toml
  7. 10 0
      tokens/token-swap/anchor/programs/token-swap/src/constants.rs
  8. 19 0
      tokens/token-swap/anchor/programs/token-swap/src/errors.rs
  9. 39 0
      tokens/token-swap/anchor/programs/token-swap/src/instructions/create_amm.rs
  10. 101 0
      tokens/token-swap/anchor/programs/token-swap/src/instructions/create_pool.rs
  11. 220 0
      tokens/token-swap/anchor/programs/token-swap/src/instructions/deposit_liquidity.rs
  12. 11 0
      tokens/token-swap/anchor/programs/token-swap/src/instructions/mod.rs
  13. 224 0
      tokens/token-swap/anchor/programs/token-swap/src/instructions/swap_exact_tokens_for_tokens.rs
  14. 187 0
      tokens/token-swap/anchor/programs/token-swap/src/instructions/withdraw_liquidity.rs
  15. 45 0
      tokens/token-swap/anchor/programs/token-swap/src/lib.rs
  16. 35 0
      tokens/token-swap/anchor/programs/token-swap/src/state.rs
  17. 44 0
      tokens/token-swap/anchor/tests/create-amm.ts
  18. 89 0
      tokens/token-swap/anchor/tests/create-pool.ts
  19. 83 0
      tokens/token-swap/anchor/tests/deposit-liquidity.ts
  20. 100 0
      tokens/token-swap/anchor/tests/swap.ts
  21. 216 0
      tokens/token-swap/anchor/tests/utils.ts
  22. 107 0
      tokens/token-swap/anchor/tests/withdraw-liquidity.ts
  23. 11 0
      tokens/token-swap/anchor/tsconfig.json

+ 2 - 0
tokens/token-swap/README.md

@@ -0,0 +1,2 @@
+## Token swap example amm in anchor rust
+## coming soon

+ 15 - 0
tokens/token-swap/anchor/Anchor.toml

@@ -0,0 +1,15 @@
+[features]
+seeds = false
+skip-lint = false
+[programs.devnet]
+amm_tutorial = "C3ti6PFK6PoYShRFx1BNNTQU3qeY1iVwjwCA6SjJhiuW"
+
+[registry]
+url = "https://api.apr.dev"
+
+[provider]
+cluster = "Devnet"
+wallet = "/Users/shivamsoni/.config/solana/id.json"
+
+[scripts]
+test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

+ 14 - 0
tokens/token-swap/anchor/Cargo.toml

@@ -0,0 +1,14 @@
+[workspace]
+members = [
+    "programs/*"
+]
+
+[profile.release]
+overflow-checks = true
+lto = "fat"
+codegen-units = 1
+[profile.release.build-override]
+opt-level = 3
+incremental = false
+codegen-units = 1
+

+ 20 - 0
tokens/token-swap/anchor/package.json

@@ -0,0 +1,20 @@
+{
+    "scripts": {
+        "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
+        "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
+    },
+    "dependencies": {
+        "@project-serum/anchor": "^0.26.0",
+        "@solana/spl-token": "^0.3.8"
+    },
+    "devDependencies": {
+        "@types/bn.js": "^5.1.0",
+        "@types/chai": "^4.3.0",
+        "@types/mocha": "^9.0.0",
+        "chai": "^4.3.4",
+        "mocha": "^9.0.3",
+        "prettier": "^2.6.2",
+        "ts-mocha": "^10.0.0",
+        "typescript": "^4.3.5"
+    }
+}

+ 27 - 0
tokens/token-swap/anchor/programs/token-swap/Cargo.toml

@@ -0,0 +1,27 @@
+[package]
+name = "amm-tutorial"
+version = "0.1.0"
+description = "Created with Anchor"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+name = "amm_tutorial"
+
+[features]
+no-entrypoint = []
+no-idl = []
+no-log-ix-name = []
+cpi = ["no-entrypoint"]
+default = []
+
+[dependencies]
+anchor-lang = { version = "0.26.0", features = ["init-if-needed"] }
+anchor-spl = { version = "0.26.0" }
+fixed = "1.23.1"
+half = "=2.2.1"
+fixed-sqrt = "0.2.5"
+solana-program = "~1.14.19"
+winnow = "=0.4.1"
+toml_datetime = "=0.6.1"
+

+ 2 - 0
tokens/token-swap/anchor/programs/token-swap/Xargo.toml

@@ -0,0 +1,2 @@
+[target.bpfel-unknown-unknown.dependencies.std]
+features = []

+ 10 - 0
tokens/token-swap/anchor/programs/token-swap/src/constants.rs

@@ -0,0 +1,10 @@
+use anchor_lang::prelude::*;
+
+#[constant]
+pub const MINIMUM_LIQUIDITY: u64 = 100;
+
+#[constant]
+pub const AUTHORITY_SEED: &str = "authority";
+
+#[constant]
+pub const LIQUIDITY_SEED: &str = "liquidity";

+ 19 - 0
tokens/token-swap/anchor/programs/token-swap/src/errors.rs

@@ -0,0 +1,19 @@
+use anchor_lang::prelude::*;
+
+#[error_code]
+pub enum TutorialError {
+    #[msg("Invalid fee value")]
+    InvalidFee,
+
+    #[msg("Invalid mint for the pool")]
+    InvalidMint,
+
+    #[msg("Depositing too little liquidity")]
+    DepositTooSmall,
+
+    #[msg("Output is below the minimum expected")]
+    OutputTooSmall,
+
+    #[msg("Invariant does not hold")]
+    InvariantViolated,
+}

+ 39 - 0
tokens/token-swap/anchor/programs/token-swap/src/instructions/create_amm.rs

@@ -0,0 +1,39 @@
+use anchor_lang::prelude::*;
+
+use crate::{errors::*, state::Amm};
+
+pub fn create_amm(ctx: Context<CreateAmm>, id: Pubkey, fee: u16) -> Result<()> {
+    let amm = &mut ctx.accounts.amm;
+    amm.id = id;
+    amm.admin = ctx.accounts.admin.key();
+    amm.fee = fee;
+
+    Ok(())
+}
+
+#[derive(Accounts)]
+#[instruction(id: Pubkey, fee: u16)]
+pub struct CreateAmm<'info> {
+    #[account(
+        init,
+        payer = payer,
+        space = Amm::LEN,
+        seeds = [
+            id.as_ref()
+        ],
+        bump,
+        constraint = fee < 10000 @ TutorialError::InvalidFee,
+    )]
+    pub amm: Account<'info, Amm>,
+
+    /// The admin of the AMM
+    /// CHECK: Read only, delegatable creation
+    pub admin: AccountInfo<'info>,
+
+    /// The account paying for all rents
+    #[account(mut)]
+    pub payer: Signer<'info>,
+
+    /// Solana ecosystem accounts
+    pub system_program: Program<'info, System>,
+}

+ 101 - 0
tokens/token-swap/anchor/programs/token-swap/src/instructions/create_pool.rs

@@ -0,0 +1,101 @@
+use anchor_lang::prelude::*;
+use anchor_spl::{
+    associated_token::AssociatedToken,
+    token::{Mint, Token, TokenAccount},
+};
+
+use crate::{
+    constants::{AUTHORITY_SEED, LIQUIDITY_SEED},
+    errors::*,
+    state::{Amm, Pool},
+};
+
+pub fn create_pool(ctx: Context<CreatePool>) -> Result<()> {
+    let pool = &mut ctx.accounts.pool;
+    pool.amm = ctx.accounts.amm.key();
+    pool.mint_a = ctx.accounts.mint_a.key();
+    pool.mint_b = ctx.accounts.mint_b.key();
+
+    Ok(())
+}
+
+#[derive(Accounts)]
+pub struct CreatePool<'info> {
+    #[account(
+        seeds = [
+            amm.id.as_ref()
+        ],
+        bump,
+    )]
+    pub amm: Account<'info, Amm>,
+
+    #[account(
+        init,
+        payer = payer,
+        space = Pool::LEN,
+        seeds = [
+            amm.key().as_ref(),
+            mint_a.key().as_ref(),
+            mint_b.key().as_ref(),
+        ],
+        bump,
+        constraint = mint_a.key() < mint_b.key() @ TutorialError::InvalidMint
+    )]
+    pub pool: Account<'info, Pool>,
+
+    /// CHECK: Read only authority
+    #[account(
+        seeds = [
+            amm.key().as_ref(),
+            mint_a.key().as_ref(),
+            mint_b.key().as_ref(),
+            AUTHORITY_SEED.as_ref(),
+        ],
+        bump,
+    )]
+    pub pool_authority: AccountInfo<'info>,
+
+    #[account(
+        init,
+        payer = payer,
+        seeds = [
+            amm.key().as_ref(),
+            mint_a.key().as_ref(),
+            mint_b.key().as_ref(),
+            LIQUIDITY_SEED.as_ref(),
+        ],
+        bump,
+        mint::decimals = 6,
+        mint::authority = pool_authority,
+    )]
+    pub mint_liquidity: Box<Account<'info, Mint>>,
+
+    pub mint_a: Box<Account<'info, Mint>>,
+
+    pub mint_b: Box<Account<'info, Mint>>,
+
+    #[account(
+        init,
+        payer = payer,
+        associated_token::mint = mint_a,
+        associated_token::authority = pool_authority,
+    )]
+    pub pool_account_a: Box<Account<'info, TokenAccount>>,
+
+    #[account(
+        init,
+        payer = payer,
+        associated_token::mint = mint_b,
+        associated_token::authority = pool_authority,
+    )]
+    pub pool_account_b: Box<Account<'info, TokenAccount>>,
+
+    /// The account paying for all rents
+    #[account(mut)]
+    pub payer: Signer<'info>,
+
+    /// Solana ecosystem accounts
+    pub token_program: Program<'info, Token>,
+    pub associated_token_program: Program<'info, AssociatedToken>,
+    pub system_program: Program<'info, System>,
+}

+ 220 - 0
tokens/token-swap/anchor/programs/token-swap/src/instructions/deposit_liquidity.rs

@@ -0,0 +1,220 @@
+use anchor_lang::prelude::*;
+use anchor_spl::{
+    associated_token::AssociatedToken,
+    token::{self, Mint, MintTo, Token, TokenAccount, Transfer},
+};
+use fixed::types::I64F64;
+use fixed_sqrt::FixedSqrt;
+
+use crate::{
+    constants::{AUTHORITY_SEED, LIQUIDITY_SEED, MINIMUM_LIQUIDITY},
+    errors::TutorialError,
+    state::Pool,
+};
+
+pub fn deposit_liquidity(
+    ctx: Context<DepositLiquidity>,
+    amount_a: u64,
+    amount_b: u64,
+) -> Result<()> {
+    // Prevent depositing assets the depositor does not own
+    let mut amount_a = if amount_a > ctx.accounts.depositor_account_a.amount {
+        ctx.accounts.depositor_account_a.amount
+    } else {
+        amount_a
+    };
+    let mut amount_b = if amount_b > ctx.accounts.depositor_account_b.amount {
+        ctx.accounts.depositor_account_b.amount
+    } else {
+        amount_b
+    };
+
+    // Making sure they are provided in the same proportion as existing liquidity
+    let pool_a = &ctx.accounts.pool_account_a;
+    let pool_b = &ctx.accounts.pool_account_b;
+    // Defining pool creation like this allows attackers to frontrun pool creation with bad ratios
+    let pool_creation = pool_a.amount == 0 && pool_b.amount == 0;
+    (amount_a, amount_b) = if pool_creation {
+        // Add as is if there is no liquidity
+        (amount_a, amount_b)
+    } else {
+        let ratio = I64F64::from_num(pool_a.amount)
+            .checked_mul(I64F64::from_num(pool_b.amount))
+            .unwrap();
+        if pool_a.amount > pool_b.amount {
+            (
+                I64F64::from_num(amount_b)
+                    .checked_mul(ratio)
+                    .unwrap()
+                    .to_num::<u64>(),
+                amount_b,
+            )
+        } else {
+            (
+                amount_a,
+                I64F64::from_num(amount_a)
+                    .checked_div(ratio)
+                    .unwrap()
+                    .to_num::<u64>(),
+            )
+        }
+    };
+
+    // Computing the amount of liquidity about to be deposited
+    let mut liquidity = I64F64::from_num(amount_a)
+        .checked_mul(I64F64::from_num(amount_b))
+        .unwrap()
+        .sqrt()
+        .to_num::<u64>();
+
+    // Lock some minimum liquidity on the first deposit
+    if pool_creation {
+        if liquidity < MINIMUM_LIQUIDITY {
+            return err!(TutorialError::DepositTooSmall);
+        }
+
+        liquidity -= MINIMUM_LIQUIDITY;
+    }
+
+    // Transfer tokens to the pool
+    token::transfer(
+        CpiContext::new(
+            ctx.accounts.token_program.to_account_info(),
+            Transfer {
+                from: ctx.accounts.depositor_account_a.to_account_info(),
+                to: ctx.accounts.pool_account_a.to_account_info(),
+                authority: ctx.accounts.depositor.to_account_info(),
+            },
+        ),
+        amount_a,
+    )?;
+    token::transfer(
+        CpiContext::new(
+            ctx.accounts.token_program.to_account_info(),
+            Transfer {
+                from: ctx.accounts.depositor_account_b.to_account_info(),
+                to: ctx.accounts.pool_account_b.to_account_info(),
+                authority: ctx.accounts.depositor.to_account_info(),
+            },
+        ),
+        amount_b,
+    )?;
+
+    // Mint the liquidity to user
+    let authority_bump = *ctx.bumps.get("pool_authority").unwrap();
+    let authority_seeds = &[
+        &ctx.accounts.pool.amm.to_bytes(),
+        &ctx.accounts.mint_a.key().to_bytes(),
+        &ctx.accounts.mint_b.key().to_bytes(),
+        AUTHORITY_SEED.as_bytes(),
+        &[authority_bump],
+    ];
+    let signer_seeds = &[&authority_seeds[..]];
+    token::mint_to(
+        CpiContext::new_with_signer(
+            ctx.accounts.token_program.to_account_info(),
+            MintTo {
+                mint: ctx.accounts.mint_liquidity.to_account_info(),
+                to: ctx.accounts.depositor_account_liquidity.to_account_info(),
+                authority: ctx.accounts.pool_authority.to_account_info(),
+            },
+            signer_seeds,
+        ),
+        liquidity,
+    )?;
+
+    Ok(())
+}
+
+#[derive(Accounts)]
+pub struct DepositLiquidity<'info> {
+    #[account(
+        seeds = [
+            pool.amm.as_ref(),
+            pool.mint_a.key().as_ref(),
+            pool.mint_b.key().as_ref(),
+        ],
+        bump,
+        has_one = mint_a,
+        has_one = mint_b,
+    )]
+    pub pool: Account<'info, Pool>,
+
+    /// CHECK: Read only authority
+    #[account(
+        seeds = [
+            pool.amm.as_ref(),
+            mint_a.key().as_ref(),
+            mint_b.key().as_ref(),
+            AUTHORITY_SEED.as_ref(),
+        ],
+        bump,
+    )]
+    pub pool_authority: AccountInfo<'info>,
+
+    /// The account paying for all rents
+    pub depositor: Signer<'info>,
+
+    #[account(
+        mut,
+        seeds = [
+            pool.amm.as_ref(),
+            mint_a.key().as_ref(),
+            mint_b.key().as_ref(),
+            LIQUIDITY_SEED.as_ref(),
+        ],
+        bump,
+    )]
+    pub mint_liquidity: Box<Account<'info, Mint>>,
+
+    pub mint_a: Box<Account<'info, Mint>>,
+
+    pub mint_b: Box<Account<'info, Mint>>,
+
+    #[account(
+        mut,
+        associated_token::mint = mint_a,
+        associated_token::authority = pool_authority,
+    )]
+    pub pool_account_a: Box<Account<'info, TokenAccount>>,
+
+    #[account(
+        mut,
+        associated_token::mint = mint_b,
+        associated_token::authority = pool_authority,
+    )]
+    pub pool_account_b: Box<Account<'info, TokenAccount>>,
+
+    #[account(
+        init_if_needed,
+        payer = payer,
+        associated_token::mint = mint_liquidity,
+        associated_token::authority = depositor,
+    )]
+    pub depositor_account_liquidity: Box<Account<'info, TokenAccount>>,
+
+    #[account(
+        init_if_needed,
+        payer = payer,
+        associated_token::mint = mint_a,
+        associated_token::authority = depositor,
+    )]
+    pub depositor_account_a: Box<Account<'info, TokenAccount>>,
+
+    #[account(
+        init_if_needed,
+        payer = payer,
+        associated_token::mint = mint_b,
+        associated_token::authority = depositor,
+    )]
+    pub depositor_account_b: Box<Account<'info, TokenAccount>>,
+
+    /// The account paying for all rents
+    #[account(mut)]
+    pub payer: Signer<'info>,
+
+    /// Solana ecosystem accounts
+    pub token_program: Program<'info, Token>,
+    pub associated_token_program: Program<'info, AssociatedToken>,
+    pub system_program: Program<'info, System>,
+}

+ 11 - 0
tokens/token-swap/anchor/programs/token-swap/src/instructions/mod.rs

@@ -0,0 +1,11 @@
+mod create_amm;
+mod create_pool;
+mod deposit_liquidity;
+mod swap_exact_tokens_for_tokens;
+mod withdraw_liquidity;
+
+pub use create_amm::*;
+pub use create_pool::*;
+pub use deposit_liquidity::*;
+pub use swap_exact_tokens_for_tokens::*;
+pub use withdraw_liquidity::*;

+ 224 - 0
tokens/token-swap/anchor/programs/token-swap/src/instructions/swap_exact_tokens_for_tokens.rs

@@ -0,0 +1,224 @@
+use anchor_lang::prelude::*;
+use anchor_spl::{
+    associated_token::AssociatedToken,
+    token::{self, Mint, Token, TokenAccount, Transfer},
+};
+use fixed::types::I64F64;
+
+use crate::{
+    constants::AUTHORITY_SEED,
+    errors::*,
+    state::{Amm, Pool},
+};
+
+pub fn swap_exact_tokens_for_tokens(
+    ctx: Context<SwapExactTokensForTokens>,
+    swap_a: bool,
+    input_amount: u64,
+    min_output_amount: u64,
+) -> Result<()> {
+    // Prevent depositing assets the depositor does not own
+    let input = if swap_a && input_amount > ctx.accounts.trader_account_a.amount {
+        ctx.accounts.trader_account_a.amount
+    } else if !swap_a && input_amount > ctx.accounts.trader_account_b.amount {
+        ctx.accounts.trader_account_b.amount
+    } else {
+        input_amount
+    };
+
+    // Apply trading fee, used to compute the output
+    let amm = &ctx.accounts.amm;
+    let taxed_input = input - input * amm.fee as u64 / 10000;
+
+    let pool_a = &ctx.accounts.pool_account_a;
+    let pool_b = &ctx.accounts.pool_account_b;
+    let output = if swap_a {
+        I64F64::from_num(taxed_input)
+            .checked_mul(I64F64::from_num(pool_b.amount))
+            .unwrap()
+            .checked_div(
+                I64F64::from_num(pool_a.amount)
+                    .checked_add(I64F64::from_num(taxed_input))
+                    .unwrap(),
+            )
+            .unwrap()
+    } else {
+        I64F64::from_num(taxed_input)
+            .checked_mul(I64F64::from_num(pool_a.amount))
+            .unwrap()
+            .checked_div(
+                I64F64::from_num(pool_b.amount)
+                    .checked_add(I64F64::from_num(taxed_input))
+                    .unwrap(),
+            )
+            .unwrap()
+    }
+    .to_num::<u64>();
+
+    if output < min_output_amount {
+        return err!(TutorialError::OutputTooSmall);
+    }
+
+    // Compute the invariant before the trade
+    let invariant = pool_a.amount * pool_b.amount;
+
+    // Transfer tokens to the pool
+    let authority_bump = *ctx.bumps.get("pool_authority").unwrap();
+    let authority_seeds = &[
+        &ctx.accounts.pool.amm.to_bytes(),
+        &ctx.accounts.mint_a.key().to_bytes(),
+        &ctx.accounts.mint_b.key().to_bytes(),
+        AUTHORITY_SEED.as_bytes(),
+        &[authority_bump],
+    ];
+    let signer_seeds = &[&authority_seeds[..]];
+    if swap_a {
+        token::transfer(
+            CpiContext::new(
+                ctx.accounts.token_program.to_account_info(),
+                Transfer {
+                    from: ctx.accounts.trader_account_a.to_account_info(),
+                    to: ctx.accounts.pool_account_a.to_account_info(),
+                    authority: ctx.accounts.trader.to_account_info(),
+                },
+            ),
+            input,
+        )?;
+        token::transfer(
+            CpiContext::new_with_signer(
+                ctx.accounts.token_program.to_account_info(),
+                Transfer {
+                    from: ctx.accounts.pool_account_b.to_account_info(),
+                    to: ctx.accounts.trader_account_b.to_account_info(),
+                    authority: ctx.accounts.pool_authority.to_account_info(),
+                },
+                signer_seeds,
+            ),
+            output,
+        )?;
+    } else {
+        token::transfer(
+            CpiContext::new_with_signer(
+                ctx.accounts.token_program.to_account_info(),
+                Transfer {
+                    from: ctx.accounts.pool_account_a.to_account_info(),
+                    to: ctx.accounts.trader_account_a.to_account_info(),
+                    authority: ctx.accounts.pool_authority.to_account_info(),
+                },
+                signer_seeds,
+            ),
+            input,
+        )?;
+        token::transfer(
+            CpiContext::new(
+                ctx.accounts.token_program.to_account_info(),
+                Transfer {
+                    from: ctx.accounts.trader_account_b.to_account_info(),
+                    to: ctx.accounts.pool_account_b.to_account_info(),
+                    authority: ctx.accounts.trader.to_account_info(),
+                },
+            ),
+            output,
+        )?;
+    }
+
+    msg!(
+        "Traded {} tokens ({} after fees) for {}",
+        input,
+        taxed_input,
+        output
+    );
+
+    // Verify the invariant still holds
+    // Reload accounts because of the CPIs
+    // We tolerate if the new invariant is higher because it means a rounding error for LPs
+    ctx.accounts.pool_account_a.reload()?;
+    ctx.accounts.pool_account_b.reload()?;
+    if invariant > ctx.accounts.pool_account_a.amount * ctx.accounts.pool_account_a.amount {
+        return err!(TutorialError::InvariantViolated);
+    }
+
+    Ok(())
+}
+
+#[derive(Accounts)]
+pub struct SwapExactTokensForTokens<'info> {
+    #[account(
+        seeds = [
+            amm.id.as_ref()
+        ],
+        bump,
+    )]
+    pub amm: Account<'info, Amm>,
+
+    #[account(
+        seeds = [
+            pool.amm.as_ref(),
+            pool.mint_a.key().as_ref(),
+            pool.mint_b.key().as_ref(),
+        ],
+        bump,
+        has_one = amm,
+        has_one = mint_a,
+        has_one = mint_b,
+    )]
+    pub pool: Account<'info, Pool>,
+
+    /// CHECK: Read only authority
+    #[account(
+        seeds = [
+            pool.amm.as_ref(),
+            mint_a.key().as_ref(),
+            mint_b.key().as_ref(),
+            AUTHORITY_SEED.as_ref(),
+        ],
+        bump,
+    )]
+    pub pool_authority: AccountInfo<'info>,
+
+    /// The account doing the swap
+    pub trader: Signer<'info>,
+
+    pub mint_a: Box<Account<'info, Mint>>,
+
+    pub mint_b: Box<Account<'info, Mint>>,
+
+    #[account(
+        mut,
+        associated_token::mint = mint_a,
+        associated_token::authority = pool_authority,
+    )]
+    pub pool_account_a: Box<Account<'info, TokenAccount>>,
+
+    #[account(
+        mut,
+        associated_token::mint = mint_b,
+        associated_token::authority = pool_authority,
+    )]
+    pub pool_account_b: Box<Account<'info, TokenAccount>>,
+
+    #[account(
+        init_if_needed,
+        payer = payer,
+        associated_token::mint = mint_a,
+        associated_token::authority = trader,
+    )]
+    pub trader_account_a: Box<Account<'info, TokenAccount>>,
+
+    #[account(
+        init_if_needed,
+        payer = payer,
+        associated_token::mint = mint_b,
+        associated_token::authority = trader,
+    )]
+    pub trader_account_b: Box<Account<'info, TokenAccount>>,
+
+    /// The account paying for all rents
+    #[account(mut)]
+    pub payer: Signer<'info>,
+
+    /// Solana ecosystem accounts
+    pub token_program: Program<'info, Token>,
+    pub associated_token_program: Program<'info, AssociatedToken>,
+    pub system_program: Program<'info, System>,
+}

+ 187 - 0
tokens/token-swap/anchor/programs/token-swap/src/instructions/withdraw_liquidity.rs

@@ -0,0 +1,187 @@
+use anchor_lang::prelude::*;
+use anchor_spl::{
+    associated_token::AssociatedToken,
+    token::{self, Burn, Mint, Token, TokenAccount, Transfer},
+};
+use fixed::types::I64F64;
+
+use crate::{
+    constants::{AUTHORITY_SEED, LIQUIDITY_SEED, MINIMUM_LIQUIDITY},
+    state::{Amm, Pool},
+};
+
+pub fn withdraw_liquidity(ctx: Context<WithdrawLiquidity>, amount: u64) -> Result<()> {
+    let authority_bump = *ctx.bumps.get("pool_authority").unwrap();
+    let authority_seeds = &[
+        &ctx.accounts.pool.amm.to_bytes(),
+        &ctx.accounts.mint_a.key().to_bytes(),
+        &ctx.accounts.mint_b.key().to_bytes(),
+        AUTHORITY_SEED.as_bytes(),
+        &[authority_bump],
+    ];
+    let signer_seeds = &[&authority_seeds[..]];
+
+    // Transfer tokens from the pool
+    let amount_a = I64F64::from_num(amount)
+        .checked_mul(I64F64::from_num(ctx.accounts.pool_account_a.amount))
+        .unwrap()
+        .checked_div(I64F64::from_num(
+            ctx.accounts.mint_liquidity.supply + MINIMUM_LIQUIDITY,
+        ))
+        .unwrap()
+        .floor()
+        .to_num::<u64>();
+    token::transfer(
+        CpiContext::new_with_signer(
+            ctx.accounts.token_program.to_account_info(),
+            Transfer {
+                from: ctx.accounts.pool_account_a.to_account_info(),
+                to: ctx.accounts.depositor_account_a.to_account_info(),
+                authority: ctx.accounts.pool_authority.to_account_info(),
+            },
+            signer_seeds,
+        ),
+        amount_a,
+    )?;
+
+    let amount_b = I64F64::from_num(amount)
+        .checked_mul(I64F64::from_num(ctx.accounts.pool_account_b.amount))
+        .unwrap()
+        .checked_div(I64F64::from_num(
+            ctx.accounts.mint_liquidity.supply + MINIMUM_LIQUIDITY,
+        ))
+        .unwrap()
+        .floor()
+        .to_num::<u64>();
+    token::transfer(
+        CpiContext::new_with_signer(
+            ctx.accounts.token_program.to_account_info(),
+            Transfer {
+                from: ctx.accounts.pool_account_b.to_account_info(),
+                to: ctx.accounts.depositor_account_b.to_account_info(),
+                authority: ctx.accounts.pool_authority.to_account_info(),
+            },
+            signer_seeds,
+        ),
+        amount_b,
+    )?;
+
+    // Burn the liquidity tokens
+    // It will fail if the amount is invalid
+    token::burn(
+        CpiContext::new(
+            ctx.accounts.token_program.to_account_info(),
+            Burn {
+                mint: ctx.accounts.mint_liquidity.to_account_info(),
+                from: ctx.accounts.depositor_account_liquidity.to_account_info(),
+                authority: ctx.accounts.depositor.to_account_info(),
+            },
+        ),
+        amount,
+    )?;
+
+    Ok(())
+}
+
+#[derive(Accounts)]
+pub struct WithdrawLiquidity<'info> {
+    #[account(
+        seeds = [
+            amm.id.as_ref()
+        ],
+        bump,
+    )]
+    pub amm: Account<'info, Amm>,
+
+    #[account(
+        seeds = [
+            pool.amm.as_ref(),
+            pool.mint_a.key().as_ref(),
+            pool.mint_b.key().as_ref(),
+        ],
+        bump,
+        has_one = mint_a,
+        has_one = mint_b,
+    )]
+    pub pool: Account<'info, Pool>,
+
+    /// CHECK: Read only authority
+    #[account(
+        seeds = [
+            pool.amm.as_ref(),
+            mint_a.key().as_ref(),
+            mint_b.key().as_ref(),
+            AUTHORITY_SEED.as_ref(),
+        ],
+        bump,
+    )]
+    pub pool_authority: AccountInfo<'info>,
+
+    /// The account paying for all rents
+    pub depositor: Signer<'info>,
+
+    #[account(
+        mut,
+        seeds = [
+            pool.amm.as_ref(),
+            mint_a.key().as_ref(),
+            mint_b.key().as_ref(),
+            LIQUIDITY_SEED.as_ref(),
+        ],
+        bump,
+    )]
+    pub mint_liquidity: Box<Account<'info, Mint>>,
+
+    #[account(mut)]
+    pub mint_a: Box<Account<'info, Mint>>,
+
+    #[account(mut)]
+    pub mint_b: Box<Account<'info, Mint>>,
+
+    #[account(
+        mut,
+        associated_token::mint = mint_a,
+        associated_token::authority = pool_authority,
+    )]
+    pub pool_account_a: Box<Account<'info, TokenAccount>>,
+
+    #[account(
+        mut,
+        associated_token::mint = mint_b,
+        associated_token::authority = pool_authority,
+    )]
+    pub pool_account_b: Box<Account<'info, TokenAccount>>,
+
+    #[account(
+        init_if_needed,
+        payer = payer,
+        associated_token::mint = mint_liquidity,
+        associated_token::authority = depositor,
+    )]
+    pub depositor_account_liquidity: Box<Account<'info, TokenAccount>>,
+
+    #[account(
+        init_if_needed,
+        payer = payer,
+        associated_token::mint = mint_a,
+        associated_token::authority = depositor,
+    )]
+    pub depositor_account_a: Box<Account<'info, TokenAccount>>,
+
+    #[account(
+        init_if_needed,
+        payer = payer,
+        associated_token::mint = mint_b,
+        associated_token::authority = depositor,
+    )]
+    pub depositor_account_b: Box<Account<'info, TokenAccount>>,
+
+    /// The account paying for all rents
+    #[account(mut)]
+    pub payer: Signer<'info>,
+
+    /// Solana ecosystem accounts
+    pub token_program: Program<'info, Token>,
+    pub associated_token_program: Program<'info, AssociatedToken>,
+    pub system_program: Program<'info, System>,
+}

+ 45 - 0
tokens/token-swap/anchor/programs/token-swap/src/lib.rs

@@ -0,0 +1,45 @@
+use anchor_lang::prelude::*;
+
+mod constants;
+mod errors;
+mod instructions;
+mod state;
+
+pub use instructions::*;
+
+// Set the correct key here
+declare_id!("C3ti6PFK6PoYShRFx1BNNTQU3qeY1iVwjwCA6SjJhiuW");
+
+#[program]
+pub mod swap_example {
+    use super::*;
+
+    pub fn create_amm(ctx: Context<CreateAmm>, id: Pubkey, fee: u16) -> Result<()> {
+        instructions::create_amm(ctx, id, fee)
+    }
+
+    pub fn create_pool(ctx: Context<CreatePool>) -> Result<()> {
+        instructions::create_pool(ctx)
+    }
+
+    pub fn deposit_liquidity(
+        ctx: Context<DepositLiquidity>,
+        amount_a: u64,
+        amount_b: u64,
+    ) -> Result<()> {
+        instructions::deposit_liquidity(ctx, amount_a, amount_b)
+    }
+
+    pub fn withdraw_liquidity(ctx: Context<WithdrawLiquidity>, amount: u64) -> Result<()> {
+        instructions::withdraw_liquidity(ctx, amount)
+    }
+
+    pub fn swap_exact_tokens_for_tokens(
+        ctx: Context<SwapExactTokensForTokens>,
+        swap_a: bool,
+        input_amount: u64,
+        min_output_amount: u64,
+    ) -> Result<()> {
+        instructions::swap_exact_tokens_for_tokens(ctx, swap_a, input_amount, min_output_amount)
+    }
+}

+ 35 - 0
tokens/token-swap/anchor/programs/token-swap/src/state.rs

@@ -0,0 +1,35 @@
+use anchor_lang::prelude::*;
+
+#[account]
+#[derive(Default)]
+pub struct Amm {
+    /// The primary key of the AMM
+    pub id: Pubkey,
+
+    /// Account that has admin authority over the AMM
+    pub admin: Pubkey,
+
+    /// The LP fee taken on each trade, in basis points
+    pub fee: u16,
+}
+
+impl Amm {
+    pub const LEN: usize = 8 + 32 + 32 + 2;
+}
+
+#[account]
+#[derive(Default)]
+pub struct Pool {
+    /// Primary key of the AMM
+    pub amm: Pubkey,
+
+    /// Mint of token A
+    pub mint_a: Pubkey,
+
+    /// Mint of token B
+    pub mint_b: Pubkey,
+}
+
+impl Pool {
+    pub const LEN: usize = 8 + 32 + 32 + 32;
+}

+ 44 - 0
tokens/token-swap/anchor/tests/create-amm.ts

@@ -0,0 +1,44 @@
+import * as anchor from "@project-serum/anchor";
+import { Program } from "@project-serum/anchor";
+import { AmmTutorial } from "../target/types/amm_tutorial";
+import { expect } from "chai";
+import { TestValues, createValues, expectRevert } from "./utils";
+
+describe("Create AMM", () => {
+  const provider = anchor.AnchorProvider.env();
+  const connection = provider.connection;
+  anchor.setProvider(provider);
+
+  const program = anchor.workspace.AmmTutorial as Program<AmmTutorial>;
+
+  let values: TestValues;
+
+  beforeEach(() => {
+    values = createValues();
+  });
+
+  it("Creation", async () => {
+    await program.methods
+      .createAmm(values.id, values.fee)
+      .accounts({ amm: values.ammKey, admin: values.admin.publicKey })
+      .rpc();
+
+    const ammAccount = await program.account.amm.fetch(values.ammKey);
+    expect(ammAccount.id.toString()).to.equal(values.id.toString());
+    expect(ammAccount.admin.toString()).to.equal(
+      values.admin.publicKey.toString()
+    );
+    expect(ammAccount.fee.toString()).to.equal(values.fee.toString());
+  });
+
+  it("Invalid fee", async () => {
+    values.fee = 10000;
+
+    await expectRevert(
+      program.methods
+        .createAmm(values.id, values.fee)
+        .accounts({ amm: values.ammKey, admin: values.admin.publicKey })
+        .rpc()
+    );
+  });
+});

+ 89 - 0
tokens/token-swap/anchor/tests/create-pool.ts

@@ -0,0 +1,89 @@
+import * as anchor from "@project-serum/anchor";
+import { Program } from "@project-serum/anchor";
+import { PublicKey } from "@solana/web3.js";
+import { AmmTutorial } from "../target/types/amm_tutorial";
+import { TestValues, createValues, expectRevert, mintingTokens } from "./utils";
+
+describe("Create pool", () => {
+  const provider = anchor.AnchorProvider.env();
+  const connection = provider.connection;
+  anchor.setProvider(provider);
+
+  const program = anchor.workspace.AmmTutorial as Program<AmmTutorial>;
+
+  let values: TestValues;
+
+
+
+  beforeEach(async () => {
+    values = createValues();
+    console.log("values",values)
+
+    await program.methods
+      .createAmm(values.id, values.fee)
+      .accounts({ amm: values.ammKey, admin: values.admin.publicKey })
+      .rpc();
+
+    await mintingTokens({
+      connection,
+      creator: values.admin,
+      mintAKeypair: values.mintAKeypair,
+      mintBKeypair: values.mintBKeypair,
+    });
+  });
+
+  it("Creation", async () => {
+    await program.methods
+      .createPool()
+      .accounts({
+        amm: values.ammKey,
+        pool: values.poolKey,
+        poolAuthority: values.poolAuthority,
+        mintLiquidity: values.mintLiquidity,
+        mintA: values.mintAKeypair.publicKey,
+        mintB: values.mintBKeypair.publicKey,
+        poolAccountA: values.poolAccountA,
+        poolAccountB: values.poolAccountB,
+      })
+      .rpc({ skipPreflight: true });
+  });
+
+  it("Invalid mints", async () => {
+    values = createValues({
+      mintBKeypair: values.mintAKeypair,
+      poolKey: PublicKey.findProgramAddressSync(
+        [
+          values.id.toBuffer(),
+          values.mintAKeypair.publicKey.toBuffer(),
+          values.mintBKeypair.publicKey.toBuffer(),
+        ],
+        program.programId
+      )[0],
+      poolAuthority: PublicKey.findProgramAddressSync(
+        [
+          values.id.toBuffer(),
+          values.mintAKeypair.publicKey.toBuffer(),
+          values.mintBKeypair.publicKey.toBuffer(),
+          Buffer.from("authority"),
+        ],
+        program.programId
+      )[0],
+    });
+
+    await expectRevert(
+      program.methods
+        .createPool()
+        .accounts({
+          amm: values.ammKey,
+          pool: values.poolKey,
+          poolAuthority: values.poolAuthority,
+          mintLiquidity: values.mintLiquidity,
+          mintA: values.mintAKeypair.publicKey,
+          mintB: values.mintBKeypair.publicKey,
+          poolAccountA: values.poolAccountA,
+          poolAccountB: values.poolAccountB,
+        })
+        .rpc()
+    );
+  });
+});

+ 83 - 0
tokens/token-swap/anchor/tests/deposit-liquidity.ts

@@ -0,0 +1,83 @@
+import * as anchor from "@project-serum/anchor";
+import { Program } from "@project-serum/anchor";
+import { AmmTutorial } from "../target/types/amm_tutorial";
+import { expect } from "chai";
+import { TestValues, createValues, mintingTokens } from "./utils";
+
+describe("Deposit liquidity", () => {
+  const provider = anchor.AnchorProvider.env();
+  const connection = provider.connection;
+  anchor.setProvider(provider);
+
+  const program = anchor.workspace.AmmTutorial as Program<AmmTutorial>;
+
+  let values: TestValues;
+
+  beforeEach(async () => {
+    values = createValues();
+
+    await program.methods
+      .createAmm(values.id, values.fee)
+      .accounts({ amm: values.ammKey, admin: values.admin.publicKey })
+      .rpc();
+
+    await mintingTokens({
+      connection,
+      creator: values.admin,
+      mintAKeypair: values.mintAKeypair,
+      mintBKeypair: values.mintBKeypair,
+    });
+
+    await program.methods
+      .createPool()
+      .accounts({
+        amm: values.ammKey,
+        pool: values.poolKey,
+        poolAuthority: values.poolAuthority,
+        mintLiquidity: values.mintLiquidity,
+        mintA: values.mintAKeypair.publicKey,
+        mintB: values.mintBKeypair.publicKey,
+        poolAccountA: values.poolAccountA,
+        poolAccountB: values.poolAccountB,
+      })
+      .rpc();
+  });
+
+  it("Deposit equal amounts", async () => {
+    await program.methods
+      .depositLiquidity(values.depositAmountA, values.depositAmountA)
+      .accounts({
+        pool: values.poolKey,
+        poolAuthority: values.poolAuthority,
+        depositor: values.admin.publicKey,
+        mintLiquidity: values.mintLiquidity,
+        mintA: values.mintAKeypair.publicKey,
+        mintB: values.mintBKeypair.publicKey,
+        poolAccountA: values.poolAccountA,
+        poolAccountB: values.poolAccountB,
+        depositorAccountLiquidity: values.liquidityAccount,
+        depositorAccountA: values.holderAccountA,
+        depositorAccountB: values.holderAccountB,
+      })
+      .signers([values.admin])
+      .rpc({ skipPreflight: true });
+
+    const depositTokenAccountLiquditiy =
+      await connection.getTokenAccountBalance(values.liquidityAccount);
+    expect(depositTokenAccountLiquditiy.value.amount).to.equal(
+      values.depositAmountA.sub(values.minimumLiquidity).toString()
+    );
+    const depositTokenAccountA = await connection.getTokenAccountBalance(
+      values.holderAccountA
+    );
+    expect(depositTokenAccountA.value.amount).to.equal(
+      values.defaultSupply.sub(values.depositAmountA).toString()
+    );
+    const depositTokenAccountB = await connection.getTokenAccountBalance(
+      values.holderAccountB
+    );
+    expect(depositTokenAccountB.value.amount).to.equal(
+      values.defaultSupply.sub(values.depositAmountA).toString()
+    );
+  });
+});

+ 100 - 0
tokens/token-swap/anchor/tests/swap.ts

@@ -0,0 +1,100 @@
+import * as anchor from "@project-serum/anchor";
+import { Program } from "@project-serum/anchor";
+import { AmmTutorial } from "../target/types/amm_tutorial";
+import { expect } from "chai";
+import { TestValues, createValues, mintingTokens } from "./utils";
+import { BN } from "bn.js";
+
+describe("Swap", () => {
+  const provider = anchor.AnchorProvider.env();
+  const connection = provider.connection;
+  anchor.setProvider(provider);
+
+  const program = anchor.workspace.AmmTutorial as Program<AmmTutorial>;
+
+  let values: TestValues;
+
+  beforeEach(async () => {
+    values = createValues();
+
+    await program.methods
+      .createAmm(values.id, values.fee)
+      .accounts({ amm: values.ammKey, admin: values.admin.publicKey })
+      .rpc();
+
+    await mintingTokens({
+      connection,
+      creator: values.admin,
+      mintAKeypair: values.mintAKeypair,
+      mintBKeypair: values.mintBKeypair,
+    });
+
+    await program.methods
+      .createPool()
+      .accounts({
+        amm: values.ammKey,
+        pool: values.poolKey,
+        poolAuthority: values.poolAuthority,
+        mintLiquidity: values.mintLiquidity,
+        mintA: values.mintAKeypair.publicKey,
+        mintB: values.mintBKeypair.publicKey,
+        poolAccountA: values.poolAccountA,
+        poolAccountB: values.poolAccountB,
+      })
+      .rpc();
+
+    await program.methods
+      .depositLiquidity(values.depositAmountA, values.depositAmountB)
+      .accounts({
+        pool: values.poolKey,
+        poolAuthority: values.poolAuthority,
+        depositor: values.admin.publicKey,
+        mintLiquidity: values.mintLiquidity,
+        mintA: values.mintAKeypair.publicKey,
+        mintB: values.mintBKeypair.publicKey,
+        poolAccountA: values.poolAccountA,
+        poolAccountB: values.poolAccountB,
+        depositorAccountLiquidity: values.liquidityAccount,
+        depositorAccountA: values.holderAccountA,
+        depositorAccountB: values.holderAccountB,
+      })
+      .signers([values.admin])
+      .rpc({ skipPreflight: true });
+  });
+
+  it("Swap from A to B", async () => {
+    const input = new BN(10 ** 6);
+    await program.methods
+      .swapExactTokensForTokens(true, input, new BN(100))
+      .accounts({
+        amm: values.ammKey,
+        pool: values.poolKey,
+        poolAuthority: values.poolAuthority,
+        trader: values.admin.publicKey,
+        mintA: values.mintAKeypair.publicKey,
+        mintB: values.mintBKeypair.publicKey,
+        poolAccountA: values.poolAccountA,
+        poolAccountB: values.poolAccountB,
+        traderAccountA: values.holderAccountA,
+        traderAccountB: values.holderAccountB,
+      })
+      .signers([values.admin])
+      .rpc({ skipPreflight: true });
+
+    const traderTokenAccountA = await connection.getTokenAccountBalance(
+      values.holderAccountA
+    );
+    const traderTokenAccountB = await connection.getTokenAccountBalance(
+      values.holderAccountB
+    );
+    expect(traderTokenAccountA.value.amount).to.equal(
+      values.defaultSupply.sub(values.depositAmountA).sub(input).toString()
+    );
+    expect(Number(traderTokenAccountB.value.amount)).to.be.greaterThan(
+      values.defaultSupply.sub(values.depositAmountB).toNumber()
+    );
+    expect(Number(traderTokenAccountB.value.amount)).to.be.lessThan(
+      values.defaultSupply.sub(values.depositAmountB).add(input).toNumber()
+    );
+  });
+});

+ 216 - 0
tokens/token-swap/anchor/tests/utils.ts

@@ -0,0 +1,216 @@
+import * as anchor from "@project-serum/anchor";
+import {
+  createMint,
+  getAssociatedTokenAddressSync,
+  getOrCreateAssociatedTokenAccount,
+  mintTo,
+} from "@solana/spl-token";
+import { Keypair, PublicKey, Connection, Signer } from "@solana/web3.js";
+import { BN } from "bn.js";
+
+export async function sleep(seconds: number) {
+  new Promise((resolve) => setTimeout(resolve, seconds * 1000));
+}
+
+export const generateSeededKeypair = (seed: string) => {
+  return Keypair.fromSeed(
+    anchor.utils.bytes.utf8.encode(anchor.utils.sha256.hash(seed)).slice(0, 32)
+  );
+};
+
+export const expectRevert = async (promise: Promise<any>) => {
+  try {
+    await promise;
+    throw new Error("Expected a revert");
+  } catch {
+    return;
+  }
+};
+
+export const mintingTokens = async ({
+  connection,
+  creator,
+  holder = creator,
+  mintAKeypair,
+  mintBKeypair,
+  mintedAmount = 100,
+  decimals = 6,
+}: {
+  connection: Connection;
+  creator: Signer;
+  holder?: Signer;
+  mintAKeypair: Keypair;
+  mintBKeypair: Keypair;
+  mintedAmount?: number;
+  decimals?: number;
+}) => {
+  // Mint tokens
+  await connection.confirmTransaction(
+    await connection.requestAirdrop(creator.publicKey, 10 ** 10)
+  );
+  await createMint(
+    connection,
+    creator,
+    creator.publicKey,
+    creator.publicKey,
+    decimals,
+    mintAKeypair
+  );
+  await createMint(
+    connection,
+    creator,
+    creator.publicKey,
+    creator.publicKey,
+    decimals,
+    mintBKeypair
+  );
+  await getOrCreateAssociatedTokenAccount(
+    connection,
+    holder,
+    mintAKeypair.publicKey,
+    holder.publicKey,
+    true
+  );
+  await getOrCreateAssociatedTokenAccount(
+    connection,
+    holder,
+    mintBKeypair.publicKey,
+    holder.publicKey,
+    true
+  );
+  await mintTo(
+    connection,
+    creator,
+    mintAKeypair.publicKey,
+    getAssociatedTokenAddressSync(
+      mintAKeypair.publicKey,
+      holder.publicKey,
+      true
+    ),
+    creator.publicKey,
+    mintedAmount * 10 ** decimals
+  );
+  await mintTo(
+    connection,
+    creator,
+    mintBKeypair.publicKey,
+    getAssociatedTokenAddressSync(
+      mintBKeypair.publicKey,
+      holder.publicKey,
+      true
+    ),
+    creator.publicKey,
+    mintedAmount * 10 ** decimals
+  );
+};
+
+export interface TestValues {
+  id: PublicKey;
+  fee: number;
+  admin: Keypair;
+  mintAKeypair: Keypair;
+  mintBKeypair: Keypair;
+  defaultSupply: anchor.BN;
+  ammKey: PublicKey;
+  minimumLiquidity: anchor.BN;
+  poolKey: PublicKey;
+  poolAuthority: PublicKey;
+  mintLiquidity: PublicKey;
+  depositAmountA: anchor.BN;
+  depositAmountB: anchor.BN;
+  liquidityAccount: PublicKey;
+  poolAccountA: PublicKey;
+  poolAccountB: PublicKey;
+  holderAccountA: PublicKey;
+  holderAccountB: PublicKey;
+}
+
+type TestValuesDefaults = {
+  [K in keyof TestValues]+?: TestValues[K];
+};
+export function createValues(defaults?: TestValuesDefaults): TestValues {
+  const id = defaults?.id || Keypair.generate().publicKey;
+  const admin = Keypair.generate();
+  const ammKey = PublicKey.findProgramAddressSync(
+    [id.toBuffer()],
+    anchor.workspace.AmmTutorial.programId
+  )[0];
+
+  // Making sure tokens are in the right order
+  const mintAKeypair = Keypair.generate();
+  let mintBKeypair = Keypair.generate();
+  while (
+    new BN(mintBKeypair.publicKey.toBytes()).lt(
+      new BN(mintAKeypair.publicKey.toBytes())
+    )
+  ) {
+    mintBKeypair = Keypair.generate();
+  }
+
+  const poolAuthority = PublicKey.findProgramAddressSync(
+    [
+      ammKey.toBuffer(),
+      mintAKeypair.publicKey.toBuffer(),
+      mintBKeypair.publicKey.toBuffer(),
+      Buffer.from("authority"),
+    ],
+    anchor.workspace.AmmTutorial.programId
+  )[0];
+  const mintLiquidity = PublicKey.findProgramAddressSync(
+    [
+      ammKey.toBuffer(),
+      mintAKeypair.publicKey.toBuffer(),
+      mintBKeypair.publicKey.toBuffer(),
+      Buffer.from("liquidity"),
+    ],
+    anchor.workspace.AmmTutorial.programId
+  )[0];
+  const poolKey = PublicKey.findProgramAddressSync(
+    [
+      ammKey.toBuffer(),
+      mintAKeypair.publicKey.toBuffer(),
+      mintBKeypair.publicKey.toBuffer(),
+    ],
+    anchor.workspace.AmmTutorial.programId
+  )[0];
+  return {
+    id,
+    fee: 500,
+    admin,
+    ammKey,
+    mintAKeypair,
+    mintBKeypair,
+    mintLiquidity,
+    poolKey,
+    poolAuthority,
+    poolAccountA: getAssociatedTokenAddressSync(
+      mintAKeypair.publicKey,
+      poolAuthority,
+      true
+    ),
+    poolAccountB: getAssociatedTokenAddressSync(
+      mintBKeypair.publicKey,
+      poolAuthority,
+      true
+    ),
+    liquidityAccount: getAssociatedTokenAddressSync(
+      mintLiquidity,
+      admin.publicKey,
+      true
+    ),
+    holderAccountA: getAssociatedTokenAddressSync(
+      mintAKeypair.publicKey,
+      admin.publicKey,
+      true
+    ),
+    holderAccountB: getAssociatedTokenAddressSync(
+      mintBKeypair.publicKey,
+      admin.publicKey,
+      true
+    ),
+    depositAmountA: new BN(4 * 10 ** 6),
+    depositAmountB: new BN(1 * 10 ** 6),
+    minimumLiquidity: new BN(100),
+    defaultSupply: new BN(100 * 10 ** 6),
+  };
+}

+ 107 - 0
tokens/token-swap/anchor/tests/withdraw-liquidity.ts

@@ -0,0 +1,107 @@
+import * as anchor from "@project-serum/anchor";
+import { Program } from "@project-serum/anchor";
+import { AmmTutorial } from "../target/types/amm_tutorial";
+import { expect } from "chai";
+import { TestValues, createValues, mintingTokens } from "./utils";
+
+describe("Withdraw liquidity", () => {
+  const provider = anchor.AnchorProvider.env();
+  const connection = provider.connection;
+  anchor.setProvider(provider);
+
+  const program = anchor.workspace.AmmTutorial as Program<AmmTutorial>;
+
+  let values: TestValues;
+
+  beforeEach(async () => {
+    values = createValues();
+
+    await program.methods
+      .createAmm(values.id, values.fee)
+      .accounts({ amm: values.ammKey, admin: values.admin.publicKey })
+      .rpc();
+
+    await mintingTokens({
+      connection,
+      creator: values.admin,
+      mintAKeypair: values.mintAKeypair,
+      mintBKeypair: values.mintBKeypair,
+    });
+
+    await program.methods
+      .createPool()
+      .accounts({
+        amm: values.ammKey,
+        pool: values.poolKey,
+        poolAuthority: values.poolAuthority,
+        mintLiquidity: values.mintLiquidity,
+        mintA: values.mintAKeypair.publicKey,
+        mintB: values.mintBKeypair.publicKey,
+        poolAccountA: values.poolAccountA,
+        poolAccountB: values.poolAccountB,
+      })
+      .rpc();
+
+    await program.methods
+      .depositLiquidity(values.depositAmountA, values.depositAmountA)
+      .accounts({
+        pool: values.poolKey,
+        poolAuthority: values.poolAuthority,
+        depositor: values.admin.publicKey,
+        mintLiquidity: values.mintLiquidity,
+        mintA: values.mintAKeypair.publicKey,
+        mintB: values.mintBKeypair.publicKey,
+        poolAccountA: values.poolAccountA,
+        poolAccountB: values.poolAccountB,
+        depositorAccountLiquidity: values.liquidityAccount,
+        depositorAccountA: values.holderAccountA,
+        depositorAccountB: values.holderAccountB,
+      })
+      .signers([values.admin])
+      .rpc({ skipPreflight: true });
+  });
+
+  it("Withdraw everything", async () => {
+    await program.methods
+      .withdrawLiquidity(values.depositAmountA.sub(values.minimumLiquidity))
+      .accounts({
+        amm: values.ammKey,
+        pool: values.poolKey,
+        poolAuthority: values.poolAuthority,
+        depositor: values.admin.publicKey,
+        mintLiquidity: values.mintLiquidity,
+        mintA: values.mintAKeypair.publicKey,
+        mintB: values.mintBKeypair.publicKey,
+        poolAccountA: values.poolAccountA,
+        poolAccountB: values.poolAccountB,
+        depositorAccountLiquidity: values.liquidityAccount,
+        depositorAccountA: values.holderAccountA,
+        depositorAccountB: values.holderAccountB,
+      })
+      .signers([values.admin])
+      .rpc({ skipPreflight: true });
+
+    const liquidityTokenAccount = await connection.getTokenAccountBalance(
+      values.liquidityAccount
+    );
+    const depositTokenAccountA = await connection.getTokenAccountBalance(
+      values.holderAccountA
+    );
+    const depositTokenAccountB = await connection.getTokenAccountBalance(
+      values.holderAccountB
+    );
+    expect(liquidityTokenAccount.value.amount).to.equal("0");
+    expect(Number(depositTokenAccountA.value.amount)).to.be.lessThan(
+      values.defaultSupply.toNumber()
+    );
+    expect(Number(depositTokenAccountA.value.amount)).to.be.greaterThan(
+      values.defaultSupply.sub(values.depositAmountA).toNumber()
+    );
+    expect(Number(depositTokenAccountB.value.amount)).to.be.lessThan(
+      values.defaultSupply.toNumber()
+    );
+    expect(Number(depositTokenAccountB.value.amount)).to.be.greaterThan(
+      values.defaultSupply.sub(values.depositAmountA).toNumber()
+    );
+  });
+});

+ 11 - 0
tokens/token-swap/anchor/tsconfig.json

@@ -0,0 +1,11 @@
+{
+            "compilerOptions": {
+              "types": ["mocha", "chai"],
+              "typeRoots": ["./node_modules/@types"],
+              "lib": ["es2015"],
+              "module": "commonjs",
+              "target": "es6",
+              "esModuleInterop": true
+            }
+          }
+