What is an AMM?
An Automated Market Maker is a smart contract that holds reserves of two (or more) tokens and allows users to:
- Swap one token for another at a price determined by a formula (no order book).
- Add liquidity by depositing both tokens and receiving LP (Liquidity Provider) tokens that represent their share of the pool.
- Remove liquidity by burning LP tokens and receiving back a proportional amount of both tokens.
This project implements a constant-product AMM (like Uniswap v2): the invariant is x * y = k. After every swap, reserve_a * reserve_b stays (approximately) constant, which defines the exchange rate.
Prerequisites
- Rust (latest stable): rustup.rs
- Solana CLI (1.18+): Solana install
- Anchor (0.32.x): Anchor install
- Node.js and Yarn (for tests and IDL consumption)
Verify:
rustc --version
solana --version
anchor --versionProject Setup
This repo is an Anchor workspace. Top-level layout:
amm_demo/
├── Anchor.toml # Anchor config (program ID, cluster, tests)
├── Cargo.toml # Workspace with programs/*
├── programs/
│ └── amm_demo/
│ ├── Cargo.toml
│ └── src/
│ ├── lib.rs # Program entrypoint & declare_id!
│ ├── state.rs # Amm account struct
│ ├── errors.rs # Custom error codes
│ ├── math.rs # LP & swap math
│ └── instructions/
│ ├── mod.rs
│ ├── initialize.rs
│ ├── deposit.rs
│ ├── withdraw.rs
│ └── swap.rs
├── tests/
│ └── amm_demo.ts # TypeScript integration tests
└── target/
├── idl/amm_demo.json
└── types/amm_demo.ts
After cloning, install JS deps and build:
yarn install
anchor buildArchitecture Overview
The AMM program exposes four instructions:
| Instruction | Purpose |
|---|---|
initialize | Create a new pool: Amm PDA, two reserve token accounts, LP mint. |
deposit | User sends token A + token B; receives LP tokens (proportional to share). |
withdraw | User burns LP tokens; receives back token A and token B. |
swap | User sends one token; receives the other (constant-product + fee). |
All pool state lives in a single Amm account. Reserves and LP mint are PDAs owned by the program so only the program can move funds.
State: The AMM Account
The pool is represented by one account of type Amm:
// programs/amm_demo/src/state.rs
#[account]
pub struct Amm {
pub amm_id: Pubkey, // Unique pool id (e.g. keypair.publicKey)
pub pool_creator: Pubkey,
pub mint_a: Pubkey,
pub mint_b: Pubkey,
pub lp_mint: Pubkey,
pub reserve_a: Pubkey,
pub reserve_b: Pubkey,
pub pool_fee: u16, // Basis points (e.g. 30 = 0.3%)
pub amm_bump: u8,
}- amm_id: Used in PDA seeds so one program can host many pools.
- mint_a / mint_b: The two tokens in the pair.
- lp_mint: Mint for LP tokens; supply grows on deposit and shrinks on withdraw.
- reserve_a / reserve_b: Token accounts holding the pool's tokens (authority = Amm PDA).
- pool_fee: Swap fee in basis points (1 bps = 0.01%); must be < 10000.
- amm_bump: Stored for PDA signing (initialize, deposit, withdraw, swap).
Amm::LEN is the sum of discriminator (8) and all fields so Anchor can compute space for init.
Core Math
All math is in programs/amm_demo/src/math.rs and uses u128 internally to avoid overflows.
Constant-product swap
For a swap of amount_in from the "in" reserve:
amount_out = (reserve_out * amount_in_with_fee) / (reserve_in + amount_in_with_fee)
Fee is applied to the input: amount_in_with_fee = amount_in * (10000 - fee_bps) / 10000. So the user effectively swaps a slightly smaller amount, and the pool keeps the rest.
LP tokens on deposit
- First deposit (lp_supply == 0):
lp_tokens = sqrt(amount_a * amount_b)(geometric mean). - Later deposits:
lp_tokens = min(amount_a * lp_supply / reserve_a, amount_b * lp_supply / reserve_b)
so that the new LP share is proportional to the smaller side and the ratio is preserved.
Withdraw
For a given lp_amount and lp_supply:
amount_a = (lp_amount * reserve_a) / lp_supply
amount_b = (lp_amount * reserve_b) / lp_supply
All division is integer; rounding favors the pool slightly.
Using u128 for intermediate calculations prevents overflow when dealing with large token amounts and fees.
Instructions
Initialize
- Accounts: Amm (init), pool_creator (signer), mint_a, mint_b, reserve_a (init), reserve_b (init), lp_mint (init), token_program, system_program, rent.
- Parameters:
amm_id: Pubkey,fee: u16(must be < 10000). - Seeds: Amm =
["amm", amm_id]; reserve_a =["reserve_a", amm.key()]; reserve_b =["reserve_b", amm.key()]; lp_mint =["lp_mint", amm.key()]. - Logic: Set Amm fields (including
amm_bumpfromctx.bumps.amm). No tokens are transferred; reserves start empty.
Deposit
- Accounts: Amm, depositor (signer), mint_a, mint_b, reserve_a, reserve_b, lp_mint, depositor's token A/B accounts, depositor's LP account, token_program.
- Parameters:
amount_a,amount_b,min_lp_tokens(slippage guard). - Logic:
- Compute
lp_tokens_to_mintwithcalculate_lp_tokens_to_mint. - Require
lp_tokens_to_mint >= min_lp_tokens. - Transfer
amount_afrom user toreserve_a,amount_bfrom user toreserve_b. - Mint
lp_tokens_to_mintto depositor's LP account (authority = Amm PDA).
- Compute
Withdraw
- Accounts: Amm, withdrawer (signer), mints and reserves, lp_mint, withdrawer's A/B/LP token accounts, token_program.
- Parameters:
amount_lp,min_amount_a,min_amount_b(slippage). - Logic:
- Compute
amount_aandamount_bwithcalculate_withdraw_amount. - Slippage checks:
amount_a >= min_amount_a,amount_b >= min_amount_b. - Burn
amount_lpfrom withdrawer's LP account. - Transfer
amount_afromreserve_ato user,amount_bfromreserve_bto user (Amm PDA as authority).
- Compute
Swap
- Accounts: Amm, swapper (signer), mint_in, mint_out, reserve_in, reserve_out, swapper's in/out token accounts, token_program.
- Parameters:
amount_in,min_amount_out(slippage). - Logic:
- Compute
amount_outwithcalculate_swap_output(amount_in, reserve_in, reserve_out, pool_fee). - Require
amount_out >= min_amount_outandamount_out <= reserve_out.amount. - Transfer
amount_infrom swapper toreserve_in. - Transfer
amount_outfromreserve_outto swapper (Amm PDA as authority).
- Compute
Always include slippage parameters (min_lp_tokens, min_amount_a, min_amount_b, min_amount_out) in production to protect users from front-running and price impact.
Program-Derived Addresses (PDAs)
PDAs make the pool and its token accounts deterministic and owned by the program:
| Account | Seeds | Purpose |
|---|---|---|
| Amm | ["amm", amm_id] | One pool per amm_id. |
| reserve_a | ["reserve_a", amm.key()] | Token account for mint_a. |
| reserve_b | ["reserve_b", amm.key()] | Token account for mint_b. |
| lp_mint | ["lp_mint", amm.key()] | LP token mint; authority = Amm. |
The Amm PDA is the authority for reserve_a, reserve_b, and lp_mint. Only the program can sign with it (using the stored bump). Clients derive the same addresses with:
const [ammPda] = PublicKey.findProgramAddressSync(
[Buffer.from("amm"), ammId.toBuffer()],
program.programId
);Same pattern for reserve_a, reserve_b, and lp_mint using ammPda.
Testing
Tests are in tests/amm_demo.ts and use Anchor's local validator.
- before: Create mint A and B, user ATA for A and B, mint 100 of each to user, derive Amm/reserve_a/reserve_b/lp_mint PDAs.
- Initialize: Call
initialize(ammId, 30), assert Amm data (ammId, fee, mints). - Deposit: Create user LP ATA, deposit 10 A + 10 B with
minLpTokens = 10, assert LP balance. - Swap: Swap 1 A for B with
minAmountOut, assert B received ≥ minAmountOut. - Withdraw: Burn half of LP, assert LP balance and that user received more A (and B).
Run tests:
anchor testThis starts a local validator, deploys the program, and runs the TypeScript tests.
Build and Deploy
-
Build:
anchor build
Produces the program binary andtarget/idl/amm_demo.jsonandtarget/types/amm_demo.ts. -
Localnet: In
Anchor.toml,cluster = "localnet". Runsolana-test-validator(or letanchor testdo it), then:anchor deploy --provider.cluster localnet -
Devnet / Mainnet: Change
[provider]inAnchor.toml(or override with env). Ensure wallet has SOL. Runanchor deployafter building.
Program ID is in Anchor.toml under [programs.localnet] and in programs/amm_demo/src/lib.rs via declare_id!(...). If you generate a new keypair for the program, update both and rebuild.
Next Steps
- Fee collection: Redirect swap fee to a separate account or treasury.
- Admin controls: Optional admin key to change fee or pause.
- Multiple pools: Reuse the same program; each pool is a different
amm_idand Amm PDA. - Oracles: Expose reserve amounts (or TWAP) for external use.
- Token-2022: Support for mint extensions (e.g. transfer fees) by using SPL Token-2022 and checking mint types.
This AMM is minimal and educational; for production you'd add more checks, events, and possibly a different curve or fee model.
Summary
| Topic | In this project |
|---|---|
| Model | Constant-product x * y = k with configurable fee (bps). |
| State | One Amm account per pool; reserves and LP mint as PDAs. |
| Instructions | initialize, deposit, withdraw, swap. |
| Math | math.rs: swap output, LP mint, withdraw amounts; u128 for safety. |
| Testing | Anchor + TypeScript: full flow init → deposit → swap → withdraw. |
By walking through the code in programs/amm_demo/src and tests/amm_demo.ts, you can see how an AMM is implemented end-to-end on Solana with Anchor.