DeFi App
Build a decentralized exchange (DEX) with token swaps and liquidity pools. This project brings together smart contract development, DeFi concepts, and frontend integration.
Project Overview
Text
┌─────────────────────────────────────────────────────────────────┐
│ DeFi Exchange │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ [Swap] [Pool] [Portfolio] [Connect Wallet] │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ SWAP TAB: │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ From: ┌────────────────────────────────────────┐ │ │
│ │ │ 100.00 [SOL ▼]│ │ │
│ │ └────────────────────────────────────────┘ │ │
│ │ ↓↑ │ │
│ │ To: ┌────────────────────────────────────────┐ │ │
│ │ │ 1,985.00 [USDC ▼]│ │ │
│ │ └────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Rate: 1 SOL = 19.85 USDC │ │
│ │ Price Impact: 0.15% │ │
│ │ Minimum Received: 1,975.08 USDC │ │
│ │ │ │
│ │ [ Swap Tokens ] │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ POOL TAB: │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Your Positions │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ SOL/USDC $5,420.00 [Manage] │ │ │
│ │ │ 12.5 LP Tokens Share: 0.05% │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ [ Add Liquidity ] [ Create Pool ] │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Architecture
Text
┌─────────────────────────────────────────────────────────────────┐
│ DeFi App Architecture │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Frontend (Next.js) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ • Swap Interface • Pool Management │ │
│ │ • Token Selection • Transaction History │ │
│ │ • Price Charts • Portfolio View │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ On-Chain Programs │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ AMM Program │ │ Token Vaults │ │
│ │ │ │ │ │
│ │ • create_pool │ │ • deposit │ │
│ │ • swap │ │ • withdraw │ │
│ │ • add_liquidity │ │ • LP tokens │ │
│ │ • remove_liquid. │ │ │ │
│ └──────────────────┘ └──────────────────┘ │
│ │ │
│ ▼ │
│ Token Accounts (SPL) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Pool Reserve A │ Pool Reserve B │ LP Mint │ User Tokens │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Project Setup
Bash
# Create Anchor project
anchor init dex
cd dex
# Get program ID and update
anchor build
solana address -k target/deploy/dex-keypair.json
# Update declare_id! in lib.rs
anchor build
Anchor Program
Rust
// programs/dex/src/lib.rs
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Mint, Token, TokenAccount, Transfer};
declare_id!("YOUR_PROGRAM_ID");
#[program]
pub mod dex {
use super::*;
/// Initialize a new liquidity pool
pub fn initialize_pool(
ctx: Context<InitializePool>,
fee_numerator: u64,
fee_denominator: u64,
) -> Result<()> {
require!(
fee_numerator <= fee_denominator,
DexError::InvalidFee
);
let pool = &mut ctx.accounts.pool;
pool.token_a_mint = ctx.accounts.token_a_mint.key();
pool.token_b_mint = ctx.accounts.token_b_mint.key();
pool.token_a_vault = ctx.accounts.token_a_vault.key();
pool.token_b_vault = ctx.accounts.token_b_vault.key();
pool.lp_mint = ctx.accounts.lp_mint.key();
pool.fee_numerator = fee_numerator;
pool.fee_denominator = fee_denominator;
pool.authority = ctx.accounts.authority.key();
pool.bump = ctx.bumps.pool;
msg!("Pool initialized: {} / {}",
ctx.accounts.token_a_mint.key(),
ctx.accounts.token_b_mint.key()
);
Ok(())
}
/// Add liquidity to the pool
pub fn add_liquidity(
ctx: Context<AddLiquidity>,
amount_a: u64,
amount_b: u64,
min_lp_tokens: u64,
) -> Result<()> {
let pool = &ctx.accounts.pool;
let reserve_a = ctx.accounts.token_a_vault.amount;
let reserve_b = ctx.accounts.token_b_vault.amount;
let lp_supply = ctx.accounts.lp_mint.supply;
// Calculate LP tokens to mint
let lp_tokens = if lp_supply == 0 {
// Initial liquidity: sqrt(amount_a * amount_b)
(amount_a as u128)
.checked_mul(amount_b as u128)
.ok_or(DexError::MathOverflow)?
.integer_sqrt() as u64
} else {
// Proportional to existing reserves
let lp_a = (amount_a as u128)
.checked_mul(lp_supply as u128)
.ok_or(DexError::MathOverflow)?
.checked_div(reserve_a as u128)
.ok_or(DexError::MathOverflow)? as u64;
let lp_b = (amount_b as u128)
.checked_mul(lp_supply as u128)
.ok_or(DexError::MathOverflow)?
.checked_div(reserve_b as u128)
.ok_or(DexError::MathOverflow)? as u64;
std::cmp::min(lp_a, lp_b)
};
require!(lp_tokens >= min_lp_tokens, DexError::SlippageExceeded);
// Transfer tokens to vault
token::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.user_token_a.to_account_info(),
to: ctx.accounts.token_a_vault.to_account_info(),
authority: ctx.accounts.user.to_account_info(),
},
),
amount_a,
)?;
token::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.user_token_b.to_account_info(),
to: ctx.accounts.token_b_vault.to_account_info(),
authority: ctx.accounts.user.to_account_info(),
},
),
amount_b,
)?;
// Mint LP tokens
let seeds = &[
b"pool",
pool.token_a_mint.as_ref(),
pool.token_b_mint.as_ref(),
&[pool.bump],
];
let signer = &[&seeds[..]];
token::mint_to(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
token::MintTo {
mint: ctx.accounts.lp_mint.to_account_info(),
to: ctx.accounts.user_lp.to_account_info(),
authority: ctx.accounts.pool.to_account_info(),
},
signer,
),
lp_tokens,
)?;
msg!("Added liquidity: {} A, {} B -> {} LP", amount_a, amount_b, lp_tokens);
Ok(())
}
/// Remove liquidity from the pool
pub fn remove_liquidity(
ctx: Context<RemoveLiquidity>,
lp_amount: u64,
min_amount_a: u64,
min_amount_b: u64,
) -> Result<()> {
let pool = &ctx.accounts.pool;
let reserve_a = ctx.accounts.token_a_vault.amount;
let reserve_b = ctx.accounts.token_b_vault.amount;
let lp_supply = ctx.accounts.lp_mint.supply;
// Calculate tokens to return
let amount_a = (lp_amount as u128)
.checked_mul(reserve_a as u128)
.ok_or(DexError::MathOverflow)?
.checked_div(lp_supply as u128)
.ok_or(DexError::MathOverflow)? as u64;
let amount_b = (lp_amount as u128)
.checked_mul(reserve_b as u128)
.ok_or(DexError::MathOverflow)?
.checked_div(lp_supply as u128)
.ok_or(DexError::MathOverflow)? as u64;
require!(amount_a >= min_amount_a, DexError::SlippageExceeded);
require!(amount_b >= min_amount_b, DexError::SlippageExceeded);
// Burn LP tokens
token::burn(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
token::Burn {
mint: ctx.accounts.lp_mint.to_account_info(),
from: ctx.accounts.user_lp.to_account_info(),
authority: ctx.accounts.user.to_account_info(),
},
),
lp_amount,
)?;
// Transfer tokens from vault
let seeds = &[
b"pool",
pool.token_a_mint.as_ref(),
pool.token_b_mint.as_ref(),
&[pool.bump],
];
let signer = &[&seeds[..]];
token::transfer(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.token_a_vault.to_account_info(),
to: ctx.accounts.user_token_a.to_account_info(),
authority: ctx.accounts.pool.to_account_info(),
},
signer,
),
amount_a,
)?;
token::transfer(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.token_b_vault.to_account_info(),
to: ctx.accounts.user_token_b.to_account_info(),
authority: ctx.accounts.pool.to_account_info(),
},
signer,
),
amount_b,
)?;
msg!("Removed liquidity: {} LP -> {} A, {} B", lp_amount, amount_a, amount_b);
Ok(())
}
/// Swap token A for token B
pub fn swap(
ctx: Context<Swap>,
amount_in: u64,
minimum_amount_out: u64,
) -> Result<()> {
let pool = &ctx.accounts.pool;
// Determine swap direction
let (
reserve_in,
reserve_out,
vault_in,
vault_out,
user_in,
user_out,
) = if ctx.accounts.user_token_in.mint == pool.token_a_mint {
(
ctx.accounts.token_a_vault.amount,
ctx.accounts.token_b_vault.amount,
ctx.accounts.token_a_vault.to_account_info(),
ctx.accounts.token_b_vault.to_account_info(),
ctx.accounts.user_token_in.to_account_info(),
ctx.accounts.user_token_out.to_account_info(),
)
} else {
(
ctx.accounts.token_b_vault.amount,
ctx.accounts.token_a_vault.amount,
ctx.accounts.token_b_vault.to_account_info(),
ctx.accounts.token_a_vault.to_account_info(),
ctx.accounts.user_token_in.to_account_info(),
ctx.accounts.user_token_out.to_account_info(),
)
};
// Calculate output with fee
// amount_out = reserve_out * amount_in * (1 - fee) / (reserve_in + amount_in * (1 - fee))
let fee_amount = (amount_in as u128)
.checked_mul(pool.fee_numerator as u128)
.ok_or(DexError::MathOverflow)?
.checked_div(pool.fee_denominator as u128)
.ok_or(DexError::MathOverflow)? as u64;
let amount_in_after_fee = amount_in.checked_sub(fee_amount)
.ok_or(DexError::MathOverflow)?;
let amount_out = (reserve_out as u128)
.checked_mul(amount_in_after_fee as u128)
.ok_or(DexError::MathOverflow)?
.checked_div(
(reserve_in as u128)
.checked_add(amount_in_after_fee as u128)
.ok_or(DexError::MathOverflow)?
)
.ok_or(DexError::MathOverflow)? as u64;
require!(amount_out >= minimum_amount_out, DexError::SlippageExceeded);
// Transfer tokens
token::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: user_in,
to: vault_in,
authority: ctx.accounts.user.to_account_info(),
},
),
amount_in,
)?;
let seeds = &[
b"pool",
pool.token_a_mint.as_ref(),
pool.token_b_mint.as_ref(),
&[pool.bump],
];
let signer = &[&seeds[..]];
token::transfer(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: vault_out,
to: user_out,
authority: ctx.accounts.pool.to_account_info(),
},
signer,
),
amount_out,
)?;
msg!("Swapped {} -> {} (fee: {})", amount_in, amount_out, fee_amount);
Ok(())
}
}
// ========== Accounts ==========
#[derive(Accounts)]
pub struct InitializePool<'info> {
#[account(mut)]
pub authority: Signer<'info>,
#[account(
init,
payer = authority,
space = Pool::SIZE,
seeds = [b"pool", token_a_mint.key().as_ref(), token_b_mint.key().as_ref()],
bump
)]
pub pool: Account<'info, Pool>,
pub token_a_mint: Account<'info, Mint>,
pub token_b_mint: Account<'info, Mint>,
#[account(
init,
payer = authority,
token::mint = token_a_mint,
token::authority = pool
)]
pub token_a_vault: Account<'info, TokenAccount>,
#[account(
init,
payer = authority,
token::mint = token_b_mint,
token::authority = pool
)]
pub token_b_vault: Account<'info, TokenAccount>,
#[account(
init,
payer = authority,
mint::decimals = 9,
mint::authority = pool
)]
pub lp_mint: Account<'info, Mint>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>,
}
#[derive(Accounts)]
pub struct AddLiquidity<'info> {
#[account(mut)]
pub user: Signer<'info>,
#[account(
mut,
seeds = [b"pool", pool.token_a_mint.as_ref(), pool.token_b_mint.as_ref()],
bump = pool.bump
)]
pub pool: Account<'info, Pool>,
#[account(mut, constraint = token_a_vault.key() == pool.token_a_vault)]
pub token_a_vault: Account<'info, TokenAccount>,
#[account(mut, constraint = token_b_vault.key() == pool.token_b_vault)]
pub token_b_vault: Account<'info, TokenAccount>,
#[account(mut, constraint = lp_mint.key() == pool.lp_mint)]
pub lp_mint: Account<'info, Mint>,
#[account(mut, constraint = user_token_a.mint == pool.token_a_mint)]
pub user_token_a: Account<'info, TokenAccount>,
#[account(mut, constraint = user_token_b.mint == pool.token_b_mint)]
pub user_token_b: Account<'info, TokenAccount>,
#[account(mut, constraint = user_lp.mint == pool.lp_mint)]
pub user_lp: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
}
#[derive(Accounts)]
pub struct RemoveLiquidity<'info> {
#[account(mut)]
pub user: Signer<'info>,
#[account(
mut,
seeds = [b"pool", pool.token_a_mint.as_ref(), pool.token_b_mint.as_ref()],
bump = pool.bump
)]
pub pool: Account<'info, Pool>,
#[account(mut)]
pub token_a_vault: Account<'info, TokenAccount>,
#[account(mut)]
pub token_b_vault: Account<'info, TokenAccount>,
#[account(mut)]
pub lp_mint: Account<'info, Mint>,
#[account(mut)]
pub user_token_a: Account<'info, TokenAccount>,
#[account(mut)]
pub user_token_b: Account<'info, TokenAccount>,
#[account(mut)]
pub user_lp: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
}
#[derive(Accounts)]
pub struct Swap<'info> {
#[account(mut)]
pub user: Signer<'info>,
#[account(
seeds = [b"pool", pool.token_a_mint.as_ref(), pool.token_b_mint.as_ref()],
bump = pool.bump
)]
pub pool: Account<'info, Pool>,
#[account(mut)]
pub token_a_vault: Account<'info, TokenAccount>,
#[account(mut)]
pub token_b_vault: Account<'info, TokenAccount>,
#[account(mut)]
pub user_token_in: Account<'info, TokenAccount>,
#[account(mut)]
pub user_token_out: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
}
// ========== State ==========
#[account]
pub struct Pool {
pub token_a_mint: Pubkey,
pub token_b_mint: Pubkey,
pub token_a_vault: Pubkey,
pub token_b_vault: Pubkey,
pub lp_mint: Pubkey,
pub fee_numerator: u64,
pub fee_denominator: u64,
pub authority: Pubkey,
pub bump: u8,
}
impl Pool {
pub const SIZE: usize = 8 + // discriminator
32 * 5 + // pubkeys
8 * 2 + // fee
32 + // authority
1; // bump
}
// ========== Errors ==========
#[error_code]
pub enum DexError {
#[msg("Invalid fee configuration")]
InvalidFee,
#[msg("Math overflow")]
MathOverflow,
#[msg("Slippage tolerance exceeded")]
SlippageExceeded,
}
// ========== Helpers ==========
trait IntegerSqrt {
fn integer_sqrt(self) -> Self;
}
impl IntegerSqrt for u128 {
fn integer_sqrt(self) -> Self {
if self == 0 {
return 0;
}
let mut x = self;
let mut y = (x + 1) / 2;
while y < x {
x = y;
y = (x + self / x) / 2;
}
x
}
}
Frontend Swap Component
TypeScript
// components/SwapInterface.tsx
"use client";
import { useState, useMemo, useEffect } from "react";
import { useWallet } from "@solana/wallet-adapter-react";
import { ArrowDownUp, Settings } from "lucide-react";
import { useDex } from "@/hooks/useDex";
const TOKENS = [
{ symbol: "SOL", mint: "So11111111111111111111111111111111111111112", decimals: 9 },
{ symbol: "USDC", mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", decimals: 6 },
// Add more tokens
];
export function SwapInterface() {
const { connected } = useWallet();
const { pools, swap, isSwapping, getQuote } = useDex();
const [fromToken, setFromToken] = useState(TOKENS[0]);
const [toToken, setToToken] = useState(TOKENS[1]);
const [amountIn, setAmountIn] = useState("");
const [slippage, setSlippage] = useState(0.5);
const quote = useMemo(() => {
const amount = parseFloat(amountIn);
if (isNaN(amount) || amount <= 0) return null;
return getQuote(fromToken.mint, toToken.mint, amount);
}, [fromToken, toToken, amountIn, getQuote]);
const handleSwap = async () => {
if (!quote) return;
const minOut = quote.amountOut * (1 - slippage / 100);
await swap({
tokenIn: fromToken.mint,
tokenOut: toToken.mint,
amountIn: parseFloat(amountIn),
minimumAmountOut: minOut,
});
setAmountIn("");
};
const toggleTokens = () => {
setFromToken(toToken);
setToToken(fromToken);
setAmountIn("");
};
return (
<div className="bg-white dark:bg-gray-900 rounded-2xl p-6 shadow-lg max-w-md mx-auto">
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-bold">Swap</h2>
<button className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg">
<Settings className="w-5 h-5" />
</button>
</div>
{/* From Token */}
<div className="bg-gray-50 dark:bg-gray-800 rounded-xl p-4 mb-2">
<div className="flex justify-between mb-2">
<span className="text-sm text-gray-500">From</span>
<span className="text-sm text-gray-500">
Balance: 0.00
</span>
</div>
<div className="flex items-center gap-4">
<input
type="number"
value={amountIn}
onChange={(e) => setAmountIn(e.target.value)}
placeholder="0.00"
className="flex-1 bg-transparent text-2xl font-medium outline-none"
/>
<select
value={fromToken.symbol}
onChange={(e) => {
const token = TOKENS.find((t) => t.symbol === e.target.value);
if (token) setFromToken(token);
}}
className="px-3 py-2 bg-white dark:bg-gray-700 rounded-lg font-medium border dark:border-gray-600"
>
{TOKENS.map((t) => (
<option key={t.mint} value={t.symbol}>
{t.symbol}
</option>
))}
</select>
</div>
</div>
{/* Swap Button */}
<div className="flex justify-center -my-4 z-10 relative">
<button
onClick={toggleTokens}
className="p-3 bg-gray-100 dark:bg-gray-800 rounded-xl border-4 border-white dark:border-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"
>
<ArrowDownUp className="w-5 h-5" />
</button>
</div>
{/* To Token */}
<div className="bg-gray-50 dark:bg-gray-800 rounded-xl p-4 mt-2">
<div className="flex justify-between mb-2">
<span className="text-sm text-gray-500">To (estimated)</span>
</div>
<div className="flex items-center gap-4">
<input
type="text"
value={quote ? quote.amountOut.toFixed(6) : "0.00"}
readOnly
className="flex-1 bg-transparent text-2xl font-medium outline-none"
/>
<select
value={toToken.symbol}
onChange={(e) => {
const token = TOKENS.find((t) => t.symbol === e.target.value);
if (token) setToToken(token);
}}
className="px-3 py-2 bg-white dark:bg-gray-700 rounded-lg font-medium border dark:border-gray-600"
>
{TOKENS.map((t) => (
<option key={t.mint} value={t.symbol}>
{t.symbol}
</option>
))}
</select>
</div>
</div>
{/* Swap Details */}
{quote && (
<div className="mt-4 p-4 bg-gray-50 dark:bg-gray-800 rounded-xl space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-500">Rate</span>
<span>
1 {fromToken.symbol} = {quote.rate.toFixed(4)} {toToken.symbol}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Price Impact</span>
<span className={quote.priceImpact > 3 ? "text-red-500" : "text-green-500"}>
{quote.priceImpact.toFixed(2)}%
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Min. Received</span>
<span>
{(quote.amountOut * (1 - slippage / 100)).toFixed(4)} {toToken.symbol}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Fee</span>
<span>{quote.fee.toFixed(4)} {fromToken.symbol}</span>
</div>
</div>
)}
{/* Slippage Selector */}
<div className="mt-4 flex items-center gap-2">
<span className="text-sm text-gray-500">Slippage:</span>
{[0.1, 0.5, 1.0].map((s) => (
<button
key={s}
onClick={() => setSlippage(s)}
className={`px-3 py-1 rounded-lg text-sm ${
slippage === s
? "bg-blue-600 text-white"
: "bg-gray-100 dark:bg-gray-800"
}`}
>
{s}%
</button>
))}
</div>
{/* Swap Button */}
<button
onClick={handleSwap}
disabled={!connected || !quote || isSwapping}
className="w-full mt-6 py-4 bg-blue-600 text-white rounded-xl font-medium text-lg
hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed
transition-colors"
>
{!connected
? "Connect Wallet"
: !amountIn
? "Enter Amount"
: isSwapping
? "Swapping..."
: "Swap"}
</button>
</div>
);
}
Liquidity Component
TypeScript
// components/LiquidityPanel.tsx
"use client";
import { useState } from "react";
import { useDex } from "@/hooks/useDex";
import { useWallet } from "@solana/wallet-adapter-react";
export function LiquidityPanel() {
const { connected } = useWallet();
const { pools, positions, addLiquidity, removeLiquidity, isAdding, isRemoving } = useDex();
const [selectedPool, setSelectedPool] = useState<string | null>(null);
const [amountA, setAmountA] = useState("");
const [amountB, setAmountB] = useState("");
const [lpAmount, setLpAmount] = useState("");
const [mode, setMode] = useState<"add" | "remove">("add");
const handleAddLiquidity = async () => {
if (!selectedPool) return;
await addLiquidity({
pool: selectedPool,
amountA: parseFloat(amountA),
amountB: parseFloat(amountB),
minLpTokens: 0, // Calculate based on slippage
});
setAmountA("");
setAmountB("");
};
const handleRemoveLiquidity = async () => {
if (!selectedPool) return;
await removeLiquidity({
pool: selectedPool,
lpAmount: parseFloat(lpAmount),
minAmountA: 0,
minAmountB: 0,
});
setLpAmount("");
};
return (
<div className="bg-white dark:bg-gray-900 rounded-2xl p-6 shadow-lg">
<h2 className="text-xl font-bold mb-6">Liquidity</h2>
{/* Mode Toggle */}
<div className="flex gap-2 mb-6">
<button
onClick={() => setMode("add")}
className={`flex-1 py-2 rounded-lg font-medium ${
mode === "add"
? "bg-green-600 text-white"
: "bg-gray-100 dark:bg-gray-800"
}`}
>
Add
</button>
<button
onClick={() => setMode("remove")}
className={`flex-1 py-2 rounded-lg font-medium ${
mode === "remove"
? "bg-red-600 text-white"
: "bg-gray-100 dark:bg-gray-800"
}`}
>
Remove
</button>
</div>
{/* Pool Selection */}
<select
value={selectedPool || ""}
onChange={(e) => setSelectedPool(e.target.value || null)}
className="w-full p-3 bg-gray-50 dark:bg-gray-800 rounded-lg mb-4"
>
<option value="">Select Pool</option>
{pools.map((pool) => (
<option key={pool.address} value={pool.address}>
{pool.tokenA.symbol} / {pool.tokenB.symbol}
</option>
))}
</select>
{mode === "add" ? (
/* Add Liquidity Form */
<div className="space-y-4">
<div>
<label className="text-sm text-gray-500 block mb-1">Token A</label>
<input
type="number"
value={amountA}
onChange={(e) => setAmountA(e.target.value)}
placeholder="0.00"
className="w-full p-3 bg-gray-50 dark:bg-gray-800 rounded-lg"
/>
</div>
<div>
<label className="text-sm text-gray-500 block mb-1">Token B</label>
<input
type="number"
value={amountB}
onChange={(e) => setAmountB(e.target.value)}
placeholder="0.00"
className="w-full p-3 bg-gray-50 dark:bg-gray-800 rounded-lg"
/>
</div>
<button
onClick={handleAddLiquidity}
disabled={!connected || !selectedPool || isAdding}
className="w-full py-3 bg-green-600 text-white rounded-lg font-medium
hover:bg-green-700 disabled:bg-gray-400"
>
{isAdding ? "Adding..." : "Add Liquidity"}
</button>
</div>
) : (
/* Remove Liquidity Form */
<div className="space-y-4">
<div>
<label className="text-sm text-gray-500 block mb-1">LP Tokens</label>
<input
type="number"
value={lpAmount}
onChange={(e) => setLpAmount(e.target.value)}
placeholder="0.00"
className="w-full p-3 bg-gray-50 dark:bg-gray-800 rounded-lg"
/>
</div>
<button
onClick={handleRemoveLiquidity}
disabled={!connected || !selectedPool || isRemoving}
className="w-full py-3 bg-red-600 text-white rounded-lg font-medium
hover:bg-red-700 disabled:bg-gray-400"
>
{isRemoving ? "Removing..." : "Remove Liquidity"}
</button>
</div>
)}
{/* User Positions */}
{positions.length > 0 && (
<div className="mt-8">
<h3 className="font-semibold mb-4">Your Positions</h3>
<div className="space-y-2">
{positions.map((pos) => (
<div
key={pos.pool}
className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg"
>
<div className="flex justify-between">
<span className="font-medium">{pos.name}</span>
<span>${pos.valueUsd.toFixed(2)}</span>
</div>
<div className="text-sm text-gray-500">
{pos.lpTokens.toFixed(4)} LP • {pos.share.toFixed(2)}% share
</div>
</div>
))}
</div>
</div>
)}
</div>
);
}
Key Learning Outcomes
| Skill | Description |
|---|---|
| AMM Math | Constant product formula, fees, slippage |
| Token Vaults | Secure token storage with PDAs |
| LP Tokens | Mint/burn for liquidity tracking |
| CPI | Cross-program invocations for SPL Token |
| Frontend | Real-time quotes, transaction building |
Security Considerations
Text
┌─────────────────────────────────────────────────────────────────┐
│ Security Checklist │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ☐ Verify all account constraints │
│ ☐ Check for integer overflow/underflow │
│ ☐ Validate slippage limits │
│ ☐ Prevent price manipulation attacks │
│ ☐ Secure PDA derivation │
│ ☐ Rate limiting for swaps │
│ ☐ Emergency pause mechanism │
│ ☐ Admin key management │
│ │
└─────────────────────────────────────────────────────────────────┘
Next: DAO Governance - Build a decentralized governance system.