From dd5611d76507f15b9a977f30409e2bc6486879da Mon Sep 17 00:00:00 2001 From: shivam Date: Mon, 31 Jul 2023 20:07:48 +0530 Subject: [PATCH] add token swap example --- tokens/token-swap/README.md | 2 + tokens/token-swap/anchor/Anchor.toml | 15 ++ tokens/token-swap/anchor/Cargo.toml | 14 ++ tokens/token-swap/anchor/package.json | 20 ++ .../anchor/programs/token-swap/Cargo.toml | 27 +++ .../anchor/programs/token-swap/Xargo.toml | 2 + .../programs/token-swap/src/constants.rs | 10 + .../anchor/programs/token-swap/src/errors.rs | 19 ++ .../token-swap/src/instructions/create_amm.rs | 39 +++ .../src/instructions/create_pool.rs | 101 ++++++++ .../src/instructions/deposit_liquidity.rs | 220 +++++++++++++++++ .../token-swap/src/instructions/mod.rs | 11 + .../swap_exact_tokens_for_tokens.rs | 224 ++++++++++++++++++ .../src/instructions/withdraw_liquidity.rs | 187 +++++++++++++++ .../anchor/programs/token-swap/src/lib.rs | 45 ++++ .../anchor/programs/token-swap/src/state.rs | 35 +++ tokens/token-swap/anchor/tests/create-amm.ts | 44 ++++ tokens/token-swap/anchor/tests/create-pool.ts | 89 +++++++ .../anchor/tests/deposit-liquidity.ts | 83 +++++++ tokens/token-swap/anchor/tests/swap.ts | 100 ++++++++ tokens/token-swap/anchor/tests/utils.ts | 216 +++++++++++++++++ .../anchor/tests/withdraw-liquidity.ts | 107 +++++++++ tokens/token-swap/anchor/tsconfig.json | 11 + 23 files changed, 1621 insertions(+) create mode 100644 tokens/token-swap/README.md create mode 100644 tokens/token-swap/anchor/Anchor.toml create mode 100644 tokens/token-swap/anchor/Cargo.toml create mode 100644 tokens/token-swap/anchor/package.json create mode 100644 tokens/token-swap/anchor/programs/token-swap/Cargo.toml create mode 100644 tokens/token-swap/anchor/programs/token-swap/Xargo.toml create mode 100644 tokens/token-swap/anchor/programs/token-swap/src/constants.rs create mode 100644 tokens/token-swap/anchor/programs/token-swap/src/errors.rs create mode 100644 tokens/token-swap/anchor/programs/token-swap/src/instructions/create_amm.rs create mode 100644 tokens/token-swap/anchor/programs/token-swap/src/instructions/create_pool.rs create mode 100644 tokens/token-swap/anchor/programs/token-swap/src/instructions/deposit_liquidity.rs create mode 100644 tokens/token-swap/anchor/programs/token-swap/src/instructions/mod.rs create mode 100644 tokens/token-swap/anchor/programs/token-swap/src/instructions/swap_exact_tokens_for_tokens.rs create mode 100644 tokens/token-swap/anchor/programs/token-swap/src/instructions/withdraw_liquidity.rs create mode 100644 tokens/token-swap/anchor/programs/token-swap/src/lib.rs create mode 100644 tokens/token-swap/anchor/programs/token-swap/src/state.rs create mode 100644 tokens/token-swap/anchor/tests/create-amm.ts create mode 100644 tokens/token-swap/anchor/tests/create-pool.ts create mode 100644 tokens/token-swap/anchor/tests/deposit-liquidity.ts create mode 100644 tokens/token-swap/anchor/tests/swap.ts create mode 100644 tokens/token-swap/anchor/tests/utils.ts create mode 100644 tokens/token-swap/anchor/tests/withdraw-liquidity.ts create mode 100644 tokens/token-swap/anchor/tsconfig.json diff --git a/tokens/token-swap/README.md b/tokens/token-swap/README.md new file mode 100644 index 000000000..bdcafd3ad --- /dev/null +++ b/tokens/token-swap/README.md @@ -0,0 +1,2 @@ +## Token swap example amm in anchor rust +## coming soon \ No newline at end of file diff --git a/tokens/token-swap/anchor/Anchor.toml b/tokens/token-swap/anchor/Anchor.toml new file mode 100644 index 000000000..7786218c0 --- /dev/null +++ b/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" diff --git a/tokens/token-swap/anchor/Cargo.toml b/tokens/token-swap/anchor/Cargo.toml new file mode 100644 index 000000000..02254ce8f --- /dev/null +++ b/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 + diff --git a/tokens/token-swap/anchor/package.json b/tokens/token-swap/anchor/package.json new file mode 100644 index 000000000..bfab59d31 --- /dev/null +++ b/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" + } +} diff --git a/tokens/token-swap/anchor/programs/token-swap/Cargo.toml b/tokens/token-swap/anchor/programs/token-swap/Cargo.toml new file mode 100644 index 000000000..e48815d6c --- /dev/null +++ b/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" + diff --git a/tokens/token-swap/anchor/programs/token-swap/Xargo.toml b/tokens/token-swap/anchor/programs/token-swap/Xargo.toml new file mode 100644 index 000000000..475fb71ed --- /dev/null +++ b/tokens/token-swap/anchor/programs/token-swap/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/tokens/token-swap/anchor/programs/token-swap/src/constants.rs b/tokens/token-swap/anchor/programs/token-swap/src/constants.rs new file mode 100644 index 000000000..6af7ab48c --- /dev/null +++ b/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"; diff --git a/tokens/token-swap/anchor/programs/token-swap/src/errors.rs b/tokens/token-swap/anchor/programs/token-swap/src/errors.rs new file mode 100644 index 000000000..39b5457d7 --- /dev/null +++ b/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, +} diff --git a/tokens/token-swap/anchor/programs/token-swap/src/instructions/create_amm.rs b/tokens/token-swap/anchor/programs/token-swap/src/instructions/create_amm.rs new file mode 100644 index 000000000..94f89b0b8 --- /dev/null +++ b/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, 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>, +} diff --git a/tokens/token-swap/anchor/programs/token-swap/src/instructions/create_pool.rs b/tokens/token-swap/anchor/programs/token-swap/src/instructions/create_pool.rs new file mode 100644 index 000000000..846fe5903 --- /dev/null +++ b/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) -> 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>, + + pub mint_a: Box>, + + pub mint_b: Box>, + + #[account( + init, + payer = payer, + associated_token::mint = mint_a, + associated_token::authority = pool_authority, + )] + pub pool_account_a: Box>, + + #[account( + init, + payer = payer, + associated_token::mint = mint_b, + associated_token::authority = pool_authority, + )] + pub pool_account_b: Box>, + + /// 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>, +} diff --git a/tokens/token-swap/anchor/programs/token-swap/src/instructions/deposit_liquidity.rs b/tokens/token-swap/anchor/programs/token-swap/src/instructions/deposit_liquidity.rs new file mode 100644 index 000000000..38fa88143 --- /dev/null +++ b/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, + 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::(), + amount_b, + ) + } else { + ( + amount_a, + I64F64::from_num(amount_a) + .checked_div(ratio) + .unwrap() + .to_num::(), + ) + } + }; + + // 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::(); + + // 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>, + + pub mint_a: Box>, + + pub mint_b: Box>, + + #[account( + mut, + associated_token::mint = mint_a, + associated_token::authority = pool_authority, + )] + pub pool_account_a: Box>, + + #[account( + mut, + associated_token::mint = mint_b, + associated_token::authority = pool_authority, + )] + pub pool_account_b: Box>, + + #[account( + init_if_needed, + payer = payer, + associated_token::mint = mint_liquidity, + associated_token::authority = depositor, + )] + pub depositor_account_liquidity: Box>, + + #[account( + init_if_needed, + payer = payer, + associated_token::mint = mint_a, + associated_token::authority = depositor, + )] + pub depositor_account_a: Box>, + + #[account( + init_if_needed, + payer = payer, + associated_token::mint = mint_b, + associated_token::authority = depositor, + )] + pub depositor_account_b: Box>, + + /// 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>, +} diff --git a/tokens/token-swap/anchor/programs/token-swap/src/instructions/mod.rs b/tokens/token-swap/anchor/programs/token-swap/src/instructions/mod.rs new file mode 100644 index 000000000..3c822791b --- /dev/null +++ b/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::*; diff --git a/tokens/token-swap/anchor/programs/token-swap/src/instructions/swap_exact_tokens_for_tokens.rs b/tokens/token-swap/anchor/programs/token-swap/src/instructions/swap_exact_tokens_for_tokens.rs new file mode 100644 index 000000000..45dcefe3c --- /dev/null +++ b/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, + 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::(); + + 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>, + + pub mint_b: Box>, + + #[account( + mut, + associated_token::mint = mint_a, + associated_token::authority = pool_authority, + )] + pub pool_account_a: Box>, + + #[account( + mut, + associated_token::mint = mint_b, + associated_token::authority = pool_authority, + )] + pub pool_account_b: Box>, + + #[account( + init_if_needed, + payer = payer, + associated_token::mint = mint_a, + associated_token::authority = trader, + )] + pub trader_account_a: Box>, + + #[account( + init_if_needed, + payer = payer, + associated_token::mint = mint_b, + associated_token::authority = trader, + )] + pub trader_account_b: Box>, + + /// 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>, +} diff --git a/tokens/token-swap/anchor/programs/token-swap/src/instructions/withdraw_liquidity.rs b/tokens/token-swap/anchor/programs/token-swap/src/instructions/withdraw_liquidity.rs new file mode 100644 index 000000000..292bd1e24 --- /dev/null +++ b/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, 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::(); + 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::(); + 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(mut)] + pub mint_a: Box>, + + #[account(mut)] + pub mint_b: Box>, + + #[account( + mut, + associated_token::mint = mint_a, + associated_token::authority = pool_authority, + )] + pub pool_account_a: Box>, + + #[account( + mut, + associated_token::mint = mint_b, + associated_token::authority = pool_authority, + )] + pub pool_account_b: Box>, + + #[account( + init_if_needed, + payer = payer, + associated_token::mint = mint_liquidity, + associated_token::authority = depositor, + )] + pub depositor_account_liquidity: Box>, + + #[account( + init_if_needed, + payer = payer, + associated_token::mint = mint_a, + associated_token::authority = depositor, + )] + pub depositor_account_a: Box>, + + #[account( + init_if_needed, + payer = payer, + associated_token::mint = mint_b, + associated_token::authority = depositor, + )] + pub depositor_account_b: Box>, + + /// 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>, +} diff --git a/tokens/token-swap/anchor/programs/token-swap/src/lib.rs b/tokens/token-swap/anchor/programs/token-swap/src/lib.rs new file mode 100644 index 000000000..26e6799c8 --- /dev/null +++ b/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, id: Pubkey, fee: u16) -> Result<()> { + instructions::create_amm(ctx, id, fee) + } + + pub fn create_pool(ctx: Context) -> Result<()> { + instructions::create_pool(ctx) + } + + pub fn deposit_liquidity( + ctx: Context, + amount_a: u64, + amount_b: u64, + ) -> Result<()> { + instructions::deposit_liquidity(ctx, amount_a, amount_b) + } + + pub fn withdraw_liquidity(ctx: Context, amount: u64) -> Result<()> { + instructions::withdraw_liquidity(ctx, amount) + } + + pub fn swap_exact_tokens_for_tokens( + ctx: Context, + 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) + } +} diff --git a/tokens/token-swap/anchor/programs/token-swap/src/state.rs b/tokens/token-swap/anchor/programs/token-swap/src/state.rs new file mode 100644 index 000000000..743cdcde4 --- /dev/null +++ b/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; +} diff --git a/tokens/token-swap/anchor/tests/create-amm.ts b/tokens/token-swap/anchor/tests/create-amm.ts new file mode 100644 index 000000000..8e5da0047 --- /dev/null +++ b/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; + + 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() + ); + }); +}); diff --git a/tokens/token-swap/anchor/tests/create-pool.ts b/tokens/token-swap/anchor/tests/create-pool.ts new file mode 100644 index 000000000..598e28596 --- /dev/null +++ b/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; + + 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() + ); + }); +}); diff --git a/tokens/token-swap/anchor/tests/deposit-liquidity.ts b/tokens/token-swap/anchor/tests/deposit-liquidity.ts new file mode 100644 index 000000000..aee8f4784 --- /dev/null +++ b/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; + + 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() + ); + }); +}); diff --git a/tokens/token-swap/anchor/tests/swap.ts b/tokens/token-swap/anchor/tests/swap.ts new file mode 100644 index 000000000..9caff041c --- /dev/null +++ b/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; + + 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() + ); + }); +}); diff --git a/tokens/token-swap/anchor/tests/utils.ts b/tokens/token-swap/anchor/tests/utils.ts new file mode 100644 index 000000000..8e2df9107 --- /dev/null +++ b/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) => { + 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), + }; +} diff --git a/tokens/token-swap/anchor/tests/withdraw-liquidity.ts b/tokens/token-swap/anchor/tests/withdraw-liquidity.ts new file mode 100644 index 000000000..7c3af80c9 --- /dev/null +++ b/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; + + 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() + ); + }); +}); diff --git a/tokens/token-swap/anchor/tsconfig.json b/tokens/token-swap/anchor/tsconfig.json new file mode 100644 index 000000000..558b83e5e --- /dev/null +++ b/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 + } + } + \ No newline at end of file