A constant-product Automated Market Maker (AMM) built in Anchor for the
Turbine3 Solana cohort. The program lets anyone create an (A, B) liquidity
pool, deposit reserves in exchange for LP tokens, swap one side for the other
at the curve price (minus a configurable fee), withdraw a pro-rata share of
the pool, and — for the optional pool authority — lock and unlock trading.
The math, accounts, instructions, and tests were all written from scratch for this submission.
- Web app: https://amm-solana.vercel.app
- Program (devnet):
4DmfmgZHzg7aTC11qaZGc7WsbiA7hjtgLU4TpePrSB3v - Source: https://github.com/Hijanhv/AMM-Turbine3
programs/amm/
src/
lib.rs program entrypoint, declares each instruction
constants.rs PDA seeds and the fee denominator
error.rs AmmError variants
curve.rs constant-product math + unit tests
state/config.rs the Config account
instructions/
initialize.rs initialize the pool, vaults and the LP mint
deposit.rs add liquidity, mint LP tokens
withdraw.rs burn LP tokens, withdraw both sides
swap.rs constant-product swap with fee
lock.rs lock / unlock (authority gated)
tests/amm.ts integration tests against a local validator
docs/tests-passing.png screenshot of the full passing suite
app/ Next.js 16 frontend (deployable to Vercel)
| Instruction | What it does |
|---|---|
initialize(seed, fee_bps, authority) |
Create the Config PDA, the LP mint PDA, and both vault ATAs owned by the config. fee_bps must be < 10_000. |
deposit(amount_a, max_b, min_lp) |
Transfer amount_a of mint A and the matching amount of mint B (capped by max_b) into the vaults, mint LP. On the first deposit the LP supply is sqrt(a * b); subsequent deposits mint proportional to the share of reserves added. |
withdraw(lp_amount, min_a, min_b) |
Burn lp_amount of LP tokens, return the matching pro-rata share of each vault. Honors slippage floors. |
swap(amount_in, min_amount_out, a_to_b) |
Constant-product swap with the configured fee, honors min_amount_out as a slippage floor. |
lock() / unlock() |
Authority-gated; locking blocks all deposit/withdraw/swap calls. |
Given reserves (rA, rB) and an input amount_in of side A, the output
side B is:
amount_in_with_fee = amount_in * (FEE_DENOMINATOR - fee_bps)
amount_out = (amount_in_with_fee * rB)
/ (rA * FEE_DENOMINATOR + amount_in_with_fee)
That preserves the constant product k = rA * rB modulo the protocol fee,
which accrues to LPs. FEE_DENOMINATOR is 10_000, so a fee_bps of 30
gives the familiar 0.30% fee.
| PDA | Seeds |
|---|---|
Config |
[b"config", seed.to_le_bytes()] |
| LP mint | [b"lp", config.key()] |
vault_a, vault_b |
Associated token accounts of config for each mint |
Config keeps the two mints, the fee, an optional authority, the lock flag,
and both bumps so the on-chain instructions never recompute them.
All custom errors live in programs/amm/src/error.rs:
PoolLocked, InvalidFee, ZeroAmount, SlippageExceeded, Overflow,
EmptyReserves, Unauthorized, IdenticalMints.
Prerequisites:
- Rust + Cargo (toolchain pinned by
rust-toolchain.toml) - Solana CLI
>= 1.18 - Anchor
0.32.1 - Node 18+ and
yarn
Install JS deps and build the program:
yarn install
anchor buildanchor testanchor test boots a local validator, deploys the program, and runs the
TypeScript suite in tests/amm.ts. The suite covers every instruction —
both the happy path and the error paths (slippage, zero amounts, identical
mints, oversized fees, unauthorized lock, swaps against a locked pool, etc.).
Pure-Rust curve tests can be run on their own:
cargo test --manifest-path programs/amm/Cargo.toml --libanchor test — 17/17 integration tests
cargo test --lib — 3/3 Rust unit tests for the curve math
The raw mocha output is captured in docs/test-output.txt. A clean
rendered version of the anchor-test output is also available at
docs/tests-passing.png.
This is a regular Anchor program, so it deploys to any Solana cluster via
anchor deploy. The shipped program ID is the keypair in
target/deploy/amm-keypair.json (4DmfmgZHzg7aTC11qaZGc7WsbiA7hjtgLU4TpePrSB3v).
If you fork the repo, generate a fresh keypair and rewrite the ID in
programs/amm/src/lib.rs (declare_id!) and Anchor.toml before deploying.
The program is already deployed:
| Program ID | 4DmfmgZHzg7aTC11qaZGc7WsbiA7hjtgLU4TpePrSB3v |
| Cluster | Devnet |
| Upgrade authority | R4yWvKDEjbdzXEmn5sNpEwaHSHbvodeUXm5u2gwMAko |
| Deploy tx | 3A1bt6nHsRju81HDQCvW5ffCwiCtrptWvYg1sWqBhYtN8CqvAH6sUKzjBTKjCbrzShyCRdrShqBsHH5yGs6dNVo3 |
| IDL account | HGPLgFHhyHZbVQLSkJV9mR7c9JUhLTcwWyoGrFeNad9g |
anchor test already does this on every run — it spins up
solana-test-validator, deploys the program, then runs the TS suite. To keep
the validator alive after the tests so you can poke at the pool from a script
or the CLI:
anchor test --detach# 1. Point the Solana CLI at devnet and fund the deploy keypair
solana config set --url https://api.devnet.solana.com
solana airdrop 2
# 2. Build, then deploy
anchor build
anchor deploy --provider.cluster devnetThe deploy cost is roughly ~3 SOL of rent (the rent is refundable on
solana program close). Airdrops on devnet are capped at 2 SOL per request,
so you may need a couple of attempts or a faucet such as
https://faucet.solana.com to top up.
Same flow as devnet, but point at a mainnet RPC and pre-fund the deploy keypair with real SOL:
anchor deploy --provider.cluster mainnetOnce deployed, the same TypeScript client in tests/amm.ts works against any
cluster — set ANCHOR_PROVIDER_URL and ANCHOR_WALLET to match the target.
The on-chain program already lives on devnet — that part can't go on
Vercel (it's a BPF binary that runs on Solana validators, not Node/edge).
What does go on Vercel is the web UI in app/, a Next.js 16
app that:
- Connects Phantom or Solflare via
@solana/wallet-adapter-react. - Builds an Anchor
Programfromtarget/idl/amm.jsonagainst4DmfmgZHzg7aTC11qaZGc7WsbiA7hjtgLU4TpePrSB3v. - Exposes forms for
initialize,deposit,swap,withdraw, and the authority-onlylock/unlockflows, plus a live pool-state panel. - Has a one-click Create test mints button that mints two fresh devnet SPL tokens (1,000,000 each, 6 decimals) into the connected wallet and auto-fills the Mint A / Mint B inputs, so first-time users can spin up a pool without leaving the page.
Live deployment: https://amm-solana.vercel.app.
cd app
yarn install
yarn dev
# open http://localhost:3000You'll need a Solana wallet extension (Phantom or Solflare) set to devnet. You don't need to bring your own SPL mints — hit Create test mints in the initialize form and the app will create two fresh tokens for you and plug them into the mint inputs. If you'd rather use existing mints, the inputs accept any pair of devnet mint pubkeys.
From the dashboard:
- Push this repo to GitHub.
- On Vercel, New Project → Import the repo.
- Set Root Directory to
app. The framework is auto-detected as Next.js. - Optional: in Environment Variables add
NEXT_PUBLIC_RPC_URL— your preferred devnet RPC (defaults toclusterApiUrl("devnet"))NEXT_PUBLIC_PROGRAM_ID— only if you redeployed the program with a different ID
- Deploy.
From the CLI (what this repo was deployed with):
cd app
vercel link --project amm-solana --yes
vercel deploy --prod --yesThe frontend is a static + client-rendered Next.js app, so the Vercel free tier covers it.
GitHub auto-deploy is already wired up for this project — vercel git connect <repo> connected the production branch to main, so every push
triggers a fresh build. The CLI flow above is only needed for the very
first deployment (or for previewing a branch).
@types/bn.jsmust live inapp/'s devDependencies. Vercel doesn't see the repo-rootpackage.json, so types installed only at the root break the production typecheck.- TS target must be
ES2020or newer —bn.jsand Solana web3 shipBigIntliterals. The defaultES2017fromcreate-next-appfails to compile. - Tailwind v4 reserves
@utilityfor plain names —@utility input:focusis invalid. Define focus rules as regular CSS inglobals.css. - Monorepo Vercel deploys need
outputFileTracingRoot. Because the Anchor workspace sits one directory aboveapp/, Vercel'smodifyConfigstep crashes (path argument undefined) on builds where the option is unset. Pin it innext.config.ts:LeaveoutputFileTracingRoot: path.resolve(process.cwd(), "..")
distDirat its default — Vercel still expects the build output insideapp/.next/.
- The vault token accounts are created up-front in
initialize, so the rest of the instructions only needmutaccess — noinit_if_neededon hot paths. - Token transfers use
transfer_checkedviaanchor_spl::token_interface, which works for both Token and Token-2022 mints. - The LP mint authority is the
ConfigPDA itself; the program signsmint_to/transfer_checkedwith the config seeds. - Accounts that hold
InterfaceAccount/MintareBoxed inside each context so the BPF stack stays under the 4 KiB limit.

