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.
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.
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.
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
- 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:
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.
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.
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.
- 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:
id
: The primary key of the AMM, represented as aPubkey
.admin
: The account that has admin authority over the AMM, represented as aPubkey
.fee
: The LP fee taken on each trade, represented as au16
(unsigned 16-bit integer) in basis points.
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.
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.
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:
amm
: The primary key of the AMM (Automated Market Maker) that this pool belongs to, represented as aPubkey
.mint_a
: The mint of token A associated with this pool, represented as aPubkey
.mint_b
: The mint of token B associated with this pool, represented as aPubkey
.
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.
-
Instructions
3.1 create amm
The above code defines a function named
create_amm
that is used to create an AMM account. It takes four parameters: -
ctx
: TheContext<CreateAmm>
parameter contains the context data required to execute the function. -
id
: ThePubkey
parameter represents the ID for the new AMM account. -
fee
: Theu16
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();
, andamm.fee = fee;
. - It returns
Ok(())
to indicate the success of the operation.
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:
amm
: An account field marked withinit
attribute, which represents the AMM account to be created. It uses the providedid
as a seed to derive the account address, sets the required space for the account usingAmm::LEN
, and uses thepayer
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 errorTutorialError::InvalidFee
.admin
: AnAccountInfo
field representing the admin account for the AMM. It is read-only and not mutable.payer
: ASigner
field representing the account that pays for the rent of the AMM account. It is marked as mutable.system_program
: AProgram
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
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 usinglet pool = &mut ctx.accounts.pool;
. - It sets the fields of the
Pool
account with the keys of the associated accounts usingpool.amm = ctx.accounts.amm.key();
,pool.mint_a = ctx.accounts.mint_a.key();
, andpool.mint_b = ctx.accounts.mint_b.key();
. - It returns
Ok(())
to indicate the success of the operation.
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:
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.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 thatmint_a
's key is less thanmint_b
's key (assumes lexicographic order) to prevent invalid creation of the pool.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 themint_liquidity
account.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.mint_a
andmint_b
: Boxed account fields representing the mints for token A and token B, respectively.pool_account_a
andpool_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 havepool_authority
as their authority.payer
: A signer field representing the account that pays for the rent of the new accounts.token_program
,associated_token_program
, andsystem_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
The above code defines a function named deposit_liquidity
that allows depositing liquidity into the pool. It takes three parameters:
ctx
: TheContext<DepositLiquidity>
parameter contains the context data required to execute the function.amount_a
: Theu64
parameter represents the amount of token A to be deposited.amount_b
: Theu64
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.
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.
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.
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.
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
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:
ctx
: TheContext<SwapExactTokensForTokens>
parameter contains the context data required to execute the function.swap_a
: Thebool
parameter indicates whether tokens A should be swapped for tokens B (true
) or tokens B should be swapped for tokens A (false
).input_amount
: Theu64
parameter represents the amount of tokens to be swapped.min_output_amount
: Theu64
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.
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.
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.
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.
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
).
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.
This code logs a message indicating the details of the trade, including the input amount, the taxed input amount, and the output amount.
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
The use statements import required modules and types for the function.
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:
ctx
: TheContext<WithdrawLiquidity>
parameter contains the context data required to execute the function.amount
: Theu64
parameter represents the amount of liquidity tokens the provider wants to withdraw.
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.
This code calculates the amount of token A to be transferred to the liquidity provider by performing the following steps:
- 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
). - Calculate the proportional amount of token A based on the pool's token A balance (
ctx.accounts.pool_account_a.amount
). - Transfer the calculated amount of token A from the pool account to the liquidity provider's account using the
token::transfer
function.
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.
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.