Root + Impact
Description
In the current implementation, the swap fee (0.3%) is deducted from the output amount but not transferred to any designated fee recipient. Instead, the deducted amount remains in the pool’s vault.
This leads to invisible value accumulation in the pool, which can distort the proportional accounting of LP token shares over time.
pub fn swap_exact_in(context: Context<SwapContext>, amount_in: u64, min_out: u64, zero_for_one: bool) -> Result<()> {
context.accounts.token_vault_a.reload()?;
context.accounts.token_vault_b.reload()?;
let reserve_a: u64 = context.accounts.token_vault_a.amount;
let reserve_b: u64 = context.accounts.token_vault_b.amount;
if reserve_a == 0 || reserve_b == 0 {
return err!(AmmError::PoolIsEmpty);
}
if amount_in == 0 {
return err!(AmmError::NoZeroAmount);
}
if zero_for_one {
let numerator: u128 = (reserve_b as u128).checked_mul(amount_in as u128).ok_or(AmmError::Overflow)?;
let denominator: u128 = (reserve_a as u128).checked_add(amount_in as u128).ok_or(AmmError::Overflow)?;
if denominator == 0 {
return err!(AmmError::DivisionByZero);
}
let mut amount_out: u64 = numerator.div_floor(&denominator) as u64;
let lp_fees = (amount_out as u128 * 3).div_floor(&1000) as u64;
amount_out = amount_out - lp_fees;
if amount_out == 0 || amount_out < min_out {
return err!(AmmError::Slippage);
}
....
Risk
Likelihood:
Impact:
Proof of Concept
Creates a pool say SOLUSDC, next simply execute the swap and makes the 0.3% fee stays in the pool
Recommended Mitigation
Explicitly route collected fees to a designated recipient (e.g., fee_receiver account or protocol-owned reserve)
Alternatively, record the fee in a fee_accumulator field in the pool state for later distribution.
// Example: transfer lp_fees to fee_receiver
token::transfer(
ctx.accounts
.transfer_fee_context()
.with_signer(&[&pool_signer_seeds]),
lp_fees,
)?;