Browse Source

Merge pull request #38 from shivamsoni00/tokenswap

Valentin Madrid 2 years ago
parent
commit
419cb6b6c2
23 changed files with 1948 additions and 0 deletions
  1. 332 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. 86 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

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

@@ -0,0 +1,332 @@
+## Token swap example amm in anchor rust
+
+**Automated Market Makers (AMM)** - Your Gateway to Effortless Trading!
+Welcome to the world of Automated Market Makers (AMM), where seamless trading is made possible with the power of automation. The primary goal of AMMs is to act as automatic buyers and sellers, readily available whenever users wish to trade their assets.
+
+**Advantages of AMMs:**
+
+- Always Available Trading: Thanks to the algorithmic trading, AMMs are operational round-the-clock, ensuring you never miss a trading opportunity.
+
+- Low Operational Costs: Embrace cheaper trades as AMMs eliminate the need for a market-making firm. Say goodbye to hefty fees! (In practice, MEV bots handle this role.)
+
+Selecting the right algorithm for the AMM becomes the essential task. One fascinating development in blockchain AMMs is the Constant Function AMM (CFAMM), which permits trades that preserve a predefined condition on a constant function of the AMM's reserves, known as the Invariant. This enforcement compels the reserves to evolve along a remarkable Bonding Curve.
+
+Meet the Constant Product AMM (CPAMM): Among the simplest CFAMMs and made popular by Uniswap V2, the CPAMM ensures the product of both reserves (xy) remains constant (K) for a given liquidity quantity. Simply put, if x denotes the reserve of token A and y denotes the reserve of token B, then xy = K, with K depending on the liquidity.
+
+*Discover Diverse Bonding Curves:*
+
+- Constant Sum AMM (CSAMM): The pool's invariant, x + y = K, maintains a constant price, but reserves for each asset can be emptied.
+
+- Curve's Stableswap: A clever mix of CSAMM and CPAMM, the Stableswap brings unique properties to the AMM, depending on the token balance.
+
+- Uniswap V3 Concentrated Liquidity AMM (CLAMM): Utilizing CPAMM, this model splits the curve into independent buckets, allowing liquidity provision to specific price buckets for efficient trading.
+
+- Trader Joe CLAMM: Similar to UniV3 CLAMM, it divides the price range into buckets, where each bucket operates as a CSAMM instead of a CPAMM.
+
+*The Undeniable Perks of CPAMMs:*
+
+- Easier to Understand and Use: Unlike complex liquidity buckets, CPAMMs offer a single, user-friendly pool for straightforward trading.
+
+- Memory Efficiency: With just one pool to maintain instead of multiple buckets, CPAMMs are incredibly memory-efficient, leading to lower memory usage and reduced costs.
+
+For these reasons, we focus on implementing the CPAMM.
+
+## Program Implementation
+
+### Design
+
+Let's go over the essential requirements for our smart contract design:
+
+- Fee Distribution: Every pool must have a fee to reward Liquidity Providers (LPs). This fee is charged on trades and paid directly in the traded token. To maintain consistency across all pools, the fees will be shared.
+
+- Single Pool per Asset Pair: Each asset pair will have precisely one pool. This approach avoids liquidity fragmentation and simplifies the process for developers to locate the appropriate pool.
+
+- LPs Deposit Accounting: We need to keep track of LPs deposits in the smart contract.
+
+To achieve an efficient and organized design, we can implement the following strategies:
+
+- Shared Parameters: As pools can share certain parameters like the trading fee, we can create a single account to store these shared parameters for all pools. Additionally, each pool will have its separate account. This approach saves storage space, except when the configuration is smaller than 32 bytes due to the need to store the public key. In our case, we'll include an admin for the AMM to control fees, which exceeds the limit.
+
+- Unique Pool Identification: To ensure each pool remains unique, we'll utilize seeds to generate a Program Derived Account (PDA). This helps avoid any ambiguity or confusion.
+
+- SPL Token for Liquidity Accounting: We'll utilize the SPL token standard for liquidity accounting. This choice ensures easy composability and simplifies the handling of liquidity in the contract.
+
+By implementing these strategies, we are creating a solana program that efficiently manages liquidity pools, rewards LPs, and maintains a seamless trading experience across various asset pairs.
+
+## Principals
+
+Here are some essential principles to consider when building on-chain programs in Solana:
+
+- Store Keys in the Account: It's beneficial to store keys in the account when creating Program Derived Accounts (PDAs) using seeds. While this may increase account rent slightly, it offers significant advantages. By having all the necessary keys in the account, it becomes effortless to locate the account (since you can recreate its public key). Additionally, this approach works seamlessly with Anchor's has_one clause, streamlining the process.
+
+- Simplicity in Seeds: When creating PDA seeds, prioritize simplicity. Using a straightforward logic for seeds makes it easier to remember and clarifies the relationship between accounts. A logical approach is to first include the seeds of the parent account and then use the current object's identifiers, preferably in alphabetical order. For example, in an AMM account storing configuration (with no parent), adding an identifier attribute, usually a pubkey, becomes necessary since the admin can change. For pools, which have the AMM as a parent and are uniquely defined by the tokens they facilitate trades for, it's advisable to use the AMM's pubkey as the seed, followed by token A's pubkey and then token B's.
+
+- Minimize Instruction's Scope: Keeping each instruction's scope as small as possible is crucial for several reasons. It helps reduce transaction size by limiting the number of accounts touched simultaneously. Moreover, it enhances composability, readability, and security. However, a trade-off to consider is that it may lead to an increase in Lines Of Code (LOC).
+
+- By following these principles, you can build on-chain programs in Solana that are efficient, well-organized, and conducive to seamless interactions, ensuring a robust foundation for your blockchain projects.
+
+## Code Examples
+
+```file structure
+programs/token-swap/src/
+├── constants.rs
+├── errors.rs
+├── instructions
+│   ├── create_amm.rs
+│   ├── create_pool.rs
+│   ├── deposit_liquidity.rs
+│   ├── mod.rs
+│   ├── swap_exact_tokens_for_tokens.rs
+│   └── withdraw_liquidity.rs
+├── lib.rs
+└── state.rs
+```
+
+
+1. **Entrypoint**
+
+This code is entrypoint for a swap example using the **`anchor_lang`** library. The **`anchor_lang`** library provides tools for creating Solana programs using the Anchor framework. The code defines several functions:
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/lib.rs#L1-L8
+
+The above section contains the necessary imports and module declarations for the program. It imports modules from the anchor_lang library and declares local modules for the crate. The pub use instructions::*; re-exports all items from the instructions module so that they can be accessed from outside this module.
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/lib.rs#L11
+
+This macro declares the program ID and associates it with the given string. This ID should match the deployed Solana program's ID to ensure the correct program is invoked when interacting with the smart contract.
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/lib.rs#L13-L45
+
+This section defines the program module using the **`#[program]`** attribute. Each function in this module represents an entry point to the smart contract. Each entry point function takes a **`Context`** parameter, which provides essential information for executing the function, such as the accounts involved and the transaction context.
+
+The entry point functions call their respective functions from the **`instructions`** module, passing the required arguments.
+
+Overall, this code defines a Rust module for a Solana program using the Anchor framework. The program supports functions related to creating an Automated Market Maker (AMM) and interacting with it, such as creating a pool, depositing liquidity, withdrawing liquidity, and swapping tokens using an AMM mechanism.
+
+2. **Account Definitions**
+
+Let's embark on our exploration by charting the course for our accounts. Each account will be thoughtfully defined, beginning with their keys arranged in the precise order they will appear in the seeds. Following the keys, we'll list the attributes that are utilized for each account. As we journey through this process, we'll unravel the intricate web of connections and forge a path towards a cohesive and well-structured design. Let the exploration begin!
+
+The above code declares an account structure called **`Amm`**. The **`#[account]`** attribute indicates that this structure will be used as an account on the Solana blockchain. The **`#[derive(Default)]`** attribute automatically generates a default implementation of the struct with all fields set to their default values.
+
+The **`Amm`** struct has three fields:
+
+1. **`id`**: The primary key of the AMM, represented as a **`Pubkey`**.
+2. **`admin`**: The account that has admin authority over the AMM, represented as a **`Pubkey`**.
+3. **`fee`**: The LP fee taken on each trade, represented as a **`u16`** (unsigned 16-bit integer) in basis points.
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/state.rs#L1-L14
+
+
+The above code declares an account structure called Amm. The #[account] attribute indicates that this structure will be used as an account on the Solana blockchain. The #[derive(Default)] attribute automatically generates a default implementation of the struct with all fields set to their default values.
+
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/state.rs#L16-L18
+
+This code implements a constant LEN for the Amm struct, which represents the size of the Amm account in bytes. The size is calculated by adding the sizes of the individual fields (id, admin, and fee). For example, Pubkey has a fixed size of 32 bytes, and u16 has a size of 2 bytes.
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/state.rs#L20-L31
+
+The code declares another account structure called **`Pool`**. As before, the **`#[account]`** attribute indicates that this struct will be used as an account on the Solana blockchain, and the **`#[derive(Default)]`** attribute generates a default implementation with all fields set to their default values.
+
+The **`Pool`** struct has three fields:
+
+1. **`amm`**: The primary key of the AMM (Automated Market Maker) that this pool belongs to, represented as a **`Pubkey`**.
+2. **`mint_a`**: The mint of token A associated with this pool, represented as a **`Pubkey`**.
+3. **`mint_b`**: The mint of token B associated with this pool, represented as a **`Pubkey`**.
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/state.rs#L33-L35
+
+This code implements a constant LEN for the Pool struct, which represents the size of the Pool account in bytes. Similar to the Amm struct, the size is calculated by adding the sizes of the individual fields (amm, mint_a, and mint_b). Each Pubkey has a size of 32 bytes, and the total size is 8 bytes (for padding) + 32 bytes (amm) + 32 bytes (mint_a) + 32 bytes (mint_b) = 104 bytes.
+
+3. **Instructions**
+   
+   3.1 **create amm**
+
+   https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/instructions/create_amm.rs#L1-L12
+
+   The above code defines a function named **`create_amm`** that is used to create an AMM account. It takes four parameters:
+
+1. **`ctx`**: The **`Context<CreateAmm>`** parameter contains the context data required to execute the function.
+2. **`id`**: The **`Pubkey`** parameter represents the ID for the new AMM account.
+3. **`fee`**: The **`u16`** parameter represents the LP fee (in basis points) to be set for the new AMM account.
+
+The function does the following:
+
+- It gets a mutable reference to the AMM account from the context using **`let amm = &mut ctx.accounts.amm;`**.
+- It sets the fields of the AMM account with the provided values using **`amm.id = id;`**, **`amm.admin = ctx.accounts.admin.key();`**, and **`amm.fee = fee;`**.
+- It returns **`Ok(())`** to indicate the success of the operation.
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/instructions/create_amm.rs#L14-L39
+
+This code defines a struct **`CreateAmm`** using the **`Accounts`** attribute, which serves as the accounts instruction for the **`create_amm`** function.
+
+The **`CreateAmm`** struct has four fields:
+
+1. **`amm`**: An account field marked with **`init`** attribute, which represents the AMM account to be created. It uses the provided **`id`** as a seed to derive the account address, sets the required space for the account using **`Amm::LEN`**, and uses the **`payer`** account for paying rent. Additionally, it specifies a constraint to ensure that the fee is less than 10000 basis points; otherwise, it will raise the error **`TutorialError::InvalidFee`**.
+2. **`admin`**: An **`AccountInfo`** field representing the admin account for the AMM. It is read-only and not mutable.
+3. **`payer`**: A **`Signer`** field representing the account that pays for the rent of the AMM account. It is marked as mutable.
+4. **`system_program`**: A **`Program`** field representing the Solana system program, used for certain system operations.
+
+TLDR-, this code sets up the instruction structure for the **`create_amm`** function, defining how the accounts should be initialized, accessed, and used when calling the function.
+
+  3.2 **create pool**
+
+  https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/instructions/create_pool.rs#L1-L20
+
+  The above code defines a function named **`create_pool`** that creates a liquidity pool. It takes a single parameter, **`ctx`**, which represents the **`Context<CreatePool>`** used to execute the function.
+
+The function does the following:
+
+- It gets a mutable reference to the **`Pool`** account from the context using **`let pool = &mut ctx.accounts.pool;`**.
+- It sets the fields of the **`Pool`** account with the keys of the associated accounts using **`pool.amm = ctx.accounts.amm.key();`**, **`pool.mint_a = ctx.accounts.mint_a.key();`**, and **`pool.mint_b = ctx.accounts.mint_b.key();`**.
+- It returns **`Ok(())`** to indicate the success of the operation.
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/instructions/create_pool.rs#L22-L101
+
+This code defines a struct named **`CreatePool`**, which serves as the accounts instruction for the **`create_pool`** function.
+
+The **`CreatePool`** struct has several fields, each representing an account that the **`create_pool`** function needs to access during its execution. The attributes applied to each field define the behavior of how the accounts are accessed and handled.
+
+Here's an explanation of each field:
+
+1. **`amm`**: An account field representing the AMM (Automated Market Maker) associated with the pool. It derives the address of the account using the seed of the AMM account.
+2. **`pool`**: An account field that will be initialized as the new liquidity pool account. It specifies the required space for the account, derives the address using seeds derived from the AMM, and ensures that **`mint_a`**'s key is less than **`mint_b`**'s key (assumes lexicographic order) to prevent invalid creation of the pool.
+3. **`pool_authority`**: An account info field representing the read-only authority account for the pool. It is used as a seed to derive the address of the **`mint_liquidity`** account.
+4. **`mint_liquidity`**: A boxed account field representing the mint for the liquidity tokens (LP tokens) of the pool. It is initialized with the provided authority and has a fixed decimal precision of 6.
+5. **`mint_a`** and **`mint_b`**: Boxed account fields representing the mints for token A and token B, respectively.
+6. **`pool_account_a`** and **`pool_account_b`**: Boxed account fields representing the associated token accounts for token A and token B, respectively, for the pool. These accounts are associated with their respective mints and have **`pool_authority`** as their authority.
+7. **`payer`**: A signer field representing the account that pays for the rent of the new accounts.
+8. **`token_program`**, **`associated_token_program`**, and **`system_program`**: Program fields representing the Solana token program, associated token program, and system program, respectively.
+
+TLDR, this code defines the accounts instruction structure for the **`create_pool`** function, specifying how the accounts should be initialized, accessed, and used when calling the function.
+
+  3.3 **deposite liquidity**
+
+  https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/instructions/deposit_liquidity.rs#L1-L30
+
+The above code defines a function named **`deposit_liquidity`** that allows depositing liquidity into the pool. It takes three parameters:
+
+1. **`ctx`**: The **`Context<DepositLiquidity>`** parameter contains the context data required to execute the function.
+2. **`amount_a`**: The **`u64`** parameter represents the amount of token A to be deposited.
+3. **`amount_b`**: The **`u64`** parameter represents the amount of token B to be deposited.
+
+The function does the following:
+
+- It checks if the depositor has enough tokens for each type (A and B) before depositing and restricts the amounts to the available balances using the **`if`** conditions.
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/instructions/deposit_liquidity.rs#L32-L61
+
+This code ensures that the amounts of tokens A and B being deposited are provided in the same proportion as the existing liquidity in the pool. If this is the first deposit (pool creation), the amounts are added as is. Otherwise, the function calculates the ratio of the existing liquidity (pool_a.amount and pool_b.amount) and adjusts the amounts being deposited accordingly.
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/instructions/deposit_liquidity.rs#L63-L77
+
+This code calculates the amount of liquidity that is about to be deposited into the pool. It calculates the square root of the product of **`amount_a`** and **`amount_b`**, using fixed-point arithmetic to ensure precision.
+
+If this is the first deposit (pool creation), the function checks if the calculated liquidity is greater than the **`MINIMUM_LIQUIDITY`** constant (a minimum liquidity required for the pool). If it's not, the function returns an error to indicate that the deposit is too small. Additionally, it subtracts the **`MINIMUM_LIQUIDITY`** from the calculated liquidity to lock it as the initial liquidity.
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/instructions/deposit_liquidity.rs#L79-L101
+
+This code uses the token::transfer function from the Anchor SPL token crate to transfer the deposited amounts of tokens A and B from the depositor's accounts (depositor_account_a and depositor_account_b, respectively) to the pool's accounts (pool_account_a and pool_account_b, respectively). It does this through cross-program invocation (CPI) using the token program, and the authority for the transfer is the depositor.
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/instructions/deposit_liquidity.rs#L102-L124
+
+This code uses the **`token::mint_to`** function from the Anchor SPL token crate to mint the liquidity tokens to the depositor. It does this through cross-program invocation (CPI) using the token program. The minting is authorized by the pool authority (**`pool_authority`**).
+
+The function calculates the correct authority bump, as required by the SPL token program, and creates the necessary seeds for the authority. It then uses the **`CpiContext::new_with_signer`** function to set up the context for the CPI with the correct authority.
+
+TRDR, this code implements the logic to deposit liquidity into the pool, ensuring correct proportions, handling the initial pool creation, and minting the corresponding liquidity tokens to the depositor.
+
+ 3.4 **swap exact tokens**
+
+ https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/instructions/swap_exact_tokens_for_tokens.rs#L1-L27
+
+ This code defines a function named **`swap_exact_tokens_for_tokens`** that allows swapping tokens A for tokens B (and vice versa) in the AMM pool. It takes five parameters:
+
+1. **`ctx`**: The **`Context<SwapExactTokensForTokens>`** parameter contains the context data required to execute the function.
+2. **`swap_a`**: The **`bool`** parameter indicates whether tokens A should be swapped for tokens B (**`true`**) or tokens B should be swapped for tokens A (**`false`**).
+3. **`input_amount`**: The **`u64`** parameter represents the amount of tokens to be swapped.
+4. **`min_output_amount`**: The **`u64`** parameter represents the minimum expected output amount after the swap.
+
+The function does the following:
+
+- It checks if the trader has enough tokens for the input amount of the specified token (**`swap_a`**) before proceeding with the swap. If the trader doesn't have enough tokens, it uses the available amount for the swap.
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/instructions/swap_exact_tokens_for_tokens.rs#L29-L31
+
+This code applies the trading fee to the input amount (input) based on the amm (AMM) account's fee value. The trading fee is subtracted from the input amount to calculate the taxed_input, which is the actual amount of tokens available for the swap after deducting the fee.
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/instructions/swap_exact_tokens_for_tokens.rs#L33-L56
+
+This code calculates the output amount of the swapped token based on the taxed_input, current pool balances (pool_a.amount and pool_b.amount), and whether the swap is from token A to token B or vice versa. It uses fixed-point arithmetic to ensure precise calculations. The resulting output represents the amount of tokens the trader will receive after the swap.
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/instructions/swap_exact_tokens_for_tokens.rs#L58-L60
+
+This code checks if the calculated **`output`** is less than the specified **`min_output_amount`**. If so, it returns an error, indicating that the output amount is too small.
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/instructions/swap_exact_tokens_for_tokens.rs#L62-L63
+
+This code calculates the invariant of the pool, which is the product of the current balances of token A (**`pool_a.amount`**) and token B (**`pool_b.amount`**).
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/instructions/swap_exact_tokens_for_tokens.rs#L65-L123
+
+This code transfers the input and output amounts of tokens between the trader and the pool, performing the token swap. It uses the **`token::transfer`** function from the Anchor SPL token crate to transfer tokens from one account to another. The **`CpiContext`** is used for Cross-Program Invocation (CPI) to interact with the SPL token program.
+
+The code chooses the appropriate token accounts to perform the transfer based on whether the swap is from token A to token B or vice versa (**`swap_a`**). The transfer authority is specified as either **`trader`** or **`pool_authority`** based on the situation.
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/instructions/swap_exact_tokens_for_tokens.rs#L124-L130
+
+This code logs a message indicating the details of the trade, including the input amount, the taxed input amount, and the output amount.
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/instructions/swap_exact_tokens_for_tokens.rs#L132-L141
+
+This code reloads the pool token accounts (pool_account_a and pool_account_b) to get the updated balances after the swap. It then checks if the invariant still holds, ensuring that the product of the balances remains constant. If the invariant is violated, it returns an error.
+Finally, this code returns Ok(()) if all operations in the function executed successfully.
+
+ 3.5 **withdraw liquidity**
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/instructions/withdraw_liquidity.rs#L1-L11
+
+The use statements import required modules and types for the function.
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/instructions/withdraw_liquidity.rs#L13
+
+This code defines a function named **`withdraw_liquidity`** that allows a liquidity provider to withdraw their liquidity from the AMM pool. It takes two parameters:
+
+1. **`ctx`**: The **`Context<WithdrawLiquidity>`** parameter contains the context data required to execute the function.
+2. **`amount`**: The **`u64`** parameter represents the amount of liquidity tokens the provider wants to withdraw.
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/instructions/withdraw_liquidity.rs#L14-L22
+
+This code sets up the authority seeds and signer seeds required for performing token transfers and burning the liquidity tokens. The authority seeds include the AMM ID, mint keys of tokens A and B, the authority seed constant, and the authority bump seed. The signer seeds are derived from the authority seeds.
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/instructions/withdraw_liquidity.rs#L24-L45
+
+This code calculates the amount of token A to be transferred to the liquidity provider by performing the following steps:
+
+1. Calculate the ratio of the amount of liquidity tokens being withdrawn (**`amount`**) to the total supply of liquidity tokens (**`ctx.accounts.mint_liquidity.supply + MINIMUM_LIQUIDITY`**).
+2. Calculate the proportional amount of token A based on the pool's token A balance (**`ctx.accounts.pool_account_a.amount`**).
+3. Transfer the calculated amount of token A from the pool account to the liquidity provider's account using the **`token::transfer`** function.
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/instructions/withdraw_liquidity.rs#L47-L67
+
+This code follows the same steps as above but for token B, transferring the calculated amount of token B from the pool account to the liquidity provider's account.
+
+https://github.com/shivamsoni00/program-examples/blob/3cc5dee4db6501365fb96d781ad3461392cf34e1/tokens/token-swap/anchor/programs/token-swap/src/instructions/withdraw_liquidity.rs#L69-L83
+
+This code burns the specified amount of liquidity tokens (amount) by calling the token::burn function. The liquidity tokens are destroyed, reducing the total supply.
+Finally, this code returns Ok(()) if all operations in the function executed successfully. This indicates that the liquidity withdrawal was completed without any errors.
+
+
+
+
+  
+
+
+
+
+
+
+
+
+

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

@@ -0,0 +1,15 @@
+[features]
+seeds = false
+skip-lint = false
+[programs.devnet]
+swap_example = "C3ti6PFK6PoYShRFx1BNNTQU3qeY1iVwjwCA6SjJhiuW"
+
+[registry]
+url = "https://api.apr.dev"
+
+[provider]
+cluster = "Devnet"
+wallet = "~/.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": {
+        "@coral-xyz/anchor": "^0.27.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 "@coral-xyz/anchor";
+import { Program } from "@coral-xyz/anchor";
+import { SwapExample } from "../target/types/swap_example";
+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.SwapExample as Program<SwapExample>;
+
+  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()
+    );
+  });
+});

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

@@ -0,0 +1,86 @@
+import * as anchor from "@coral-xyz/anchor";
+import { Program } from "@coral-xyz/anchor";
+import { PublicKey } from "@solana/web3.js";
+import { SwapExample } from "../target/types/swap_example";
+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.SwapExample as Program<SwapExample>;
+
+  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,
+    });
+  });
+
+  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 "@coral-xyz/anchor";
+import { Program } from "@coral-xyz/anchor";
+import { SwapExample } from "../target/types/swap_example";
+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.SwapExample as Program<SwapExample>;
+
+  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 "@coral-xyz/anchor";
+import { Program } from "@coral-xyz/anchor";
+import { SwapExample } from "../target/types/swap_example";
+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 "@coral-xyz/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 "@coral-xyz/anchor";
+import { Program } from "@coral-xyz/anchor";
+import { SwapExample } from "../target/types/swap_example";
+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.SwapExample as Program<SwapExample>;
+
+  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
+            }
+          }
+