Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 29 additions & 8 deletions amm/programs/amm/src/instructions/deposit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ use anchor_spl::token::{mint_to, transfer, Mint, MintTo, Token, TokenAccount, Tr

use crate::states::{AMMError, AMM};

/// Minimum liquidity locked forever on first deposit to prevent inflation attacks.
/// Similar to Uniswap V2's MINIMUM_LIQUIDITY = 1000.
pub const MINIMUM_LIQUIDITY: u64 = 1_000;

#[derive(Accounts)]
pub struct Deposit<'info> {
#[account(
Expand Down Expand Up @@ -87,15 +91,27 @@ impl<'info> Deposit<'info> {
if amm.lp_supply == 0 {
// sqrt mean of token deposits
// first LP sets constant product
// LP[minted] = Sqrt(qA X qB)

// Example
// Deposit - 100 A and 400 B
// LP[minted] = Sqrt(100 X 400) = 200
// First LP gets 200 tokens
// LP[minted] = Sqrt(qA X qB) - MINIMUM_LIQUIDITY
//
// MINIMUM_LIQUIDITY is permanently locked (never redeemable) to prevent
// the first-depositor inflation attack where an attacker:
// 1. Deposits tiny amounts (e.g., 1 wei of each token) → gets 1 LP token
// 2. Donates large amounts directly to reserves
// 3. Subsequent depositors' LP calculations round down to 0
// 4. Attacker redeems their 1 LP token for all reserves
//
// By locking MINIMUM_LIQUIDITY, the cost of this attack becomes prohibitive.

let value = (quantity_a as u128) * (quantity_b as u128);
tokens_to_issue = binary_search_sqrt(value);
let total_lp = binary_search_sqrt(value);

require!(
total_lp > MINIMUM_LIQUIDITY,
AMMError::InsufficientInitialLiquidity
);

// Lock MINIMUM_LIQUIDITY permanently (it stays in lp_supply but is never held by anyone)
tokens_to_issue = total_lp - MINIMUM_LIQUIDITY;
} else {
require!(
(quantity_a * self.reserve_b.amount == quantity_b * self.reserve_a.amount),
Expand Down Expand Up @@ -172,7 +188,12 @@ impl<'info> Deposit<'info> {
mint_to(mint_lp_token_ctx, tokens_to_issue)?;

let mut amm = self.amm.load_mut()?;
amm.lp_supply += tokens_to_issue;
if amm.lp_supply == 0 {
// First deposit: account for both issued tokens AND permanently locked minimum liquidity
amm.lp_supply = tokens_to_issue + MINIMUM_LIQUIDITY;
} else {
amm.lp_supply += tokens_to_issue;
}

Ok(())
}
Expand Down
41 changes: 21 additions & 20 deletions amm/programs/amm/src/instructions/swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ impl<'info> Swap<'info> {
pub fn swap(&mut self, quantity: u64, is_a: bool, bumps: &SwapBumps) -> Result<()> {
require!(quantity > 0, AMMError::InvalidQuantity);

let (from_reserve, to_reserve, from_token_account, to_token_account) = if is_a {
// input_reserve: reserve of the token being sent IN by the user
// output_reserve: reserve of the token being sent OUT to the user
// user_input_account: user's token account for the input token
// user_output_account: user's token account for the output token
let (input_reserve, output_reserve, user_input_account, user_output_account) = if is_a {
(
&self.reserve_a,
&self.reserve_b,
Expand All @@ -82,31 +86,27 @@ impl<'info> Swap<'info> {
// CONSTANT PRODUCT AMM
// xy = k
// (x + dx)(y - dy) = k
// y - dy = k / (x + dx)
// y - k/(x + dx) = dy
// y - xy(x + dx) = dy
// (yx + ydx - xy)/(x + dx) = dy
// ydx /(x + dx) = dy

// here,
// dx = quantity
// dy = other token quantity
// x = to_reserve_amount
// y = from_reserve_amount

// dy = y * dx / (x + dx)
//
// x = input_reserve (reserveIn)
// y = output_reserve (reserveOut)
// dx = quantity (amountIn)
// dy = amount_out
//
// amountOut = (reserveOut * amountIn) / (reserveIn + amountIn)

let other_token_quantity = from_reserve
let amount_out = output_reserve
.amount
.checked_mul(quantity)
.and_then(|p| p.checked_div(to_reserve.amount.checked_add(quantity)?))
.and_then(|p| p.checked_div(input_reserve.amount.checked_add(quantity)?))
.ok_or(AMMError::ArithmeticOverflow)?;

// Transfer input tokens from user to the correct input reserve
let transfer_to_reserve = CpiContext::new(
self.token_program.to_account_info(),
Transfer {
from: from_token_account.to_account_info(),
to: to_reserve.to_account_info(),
from: user_input_account.to_account_info(),
to: input_reserve.to_account_info(),
authority: self.signer.to_account_info(),
},
);
Expand All @@ -125,17 +125,18 @@ impl<'info> Swap<'info> {

let signer_seeds = &[&seeds[..]];

// Transfer output tokens from the correct output reserve to user
let transfer_to_user = CpiContext::new_with_signer(
self.token_program.to_account_info(),
Transfer {
from: from_reserve.to_account_info(),
to: to_token_account.to_account_info(),
from: output_reserve.to_account_info(),
to: user_output_account.to_account_info(),
authority: self.pool_authority.to_account_info(),
},
signer_seeds,
);

transfer(transfer_to_user, other_token_quantity)?;
transfer(transfer_to_user, amount_out)?;

Ok(())
}
Expand Down
2 changes: 2 additions & 0 deletions amm/programs/amm/src/states/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ pub enum AMMError {
InvalidQuantity,
#[msg("Invalid Liquidity specified")]
InvalidLiquidity,
#[msg("Insufficient initial liquidity - must exceed minimum")]
InsufficientInitialLiquidity,
}