diff --git a/amm/programs/amm/src/instructions/deposit.rs b/amm/programs/amm/src/instructions/deposit.rs index 0ed7c82..7e8f4a7 100644 --- a/amm/programs/amm/src/instructions/deposit.rs +++ b/amm/programs/amm/src/instructions/deposit.rs @@ -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( @@ -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), @@ -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(()) } diff --git a/amm/programs/amm/src/instructions/swap.rs b/amm/programs/amm/src/instructions/swap.rs index 9026d31..23d48f0 100644 --- a/amm/programs/amm/src/instructions/swap.rs +++ b/amm/programs/amm/src/instructions/swap.rs @@ -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, @@ -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(), }, ); @@ -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(()) } diff --git a/amm/programs/amm/src/states/error.rs b/amm/programs/amm/src/states/error.rs index 6fc251d..2dadf92 100644 --- a/amm/programs/amm/src/states/error.rs +++ b/amm/programs/amm/src/states/error.rs @@ -10,4 +10,6 @@ pub enum AMMError { InvalidQuantity, #[msg("Invalid Liquidity specified")] InvalidLiquidity, + #[msg("Insufficient initial liquidity - must exceed minimum")] + InsufficientInitialLiquidity, }