SSSwap

First Flight #41
Beginner FriendlyRust
100 EXP
View results
Submission Details
Severity: high
Valid

On-Chain Vaults Updated, Internal Reserves Left Stale

Summary

The swap_exact_in and swap_exact_out functions update the token vault balances on-chain but fail to synchronize these values back to the LiquidityPool account’s reserve_a and reserve_b fields. This results in internal reserve values becoming stale and desynchronized from the real token balances held in the vaults. The inconsistency introduces downstream errors in liquidity operations and compromises the integrity of the pool’s accounting logic.

Vulnerability Details

The AMM stores two types of reserve data:

  • Vault state: real-time token balances in token_vault_a.amount and token_vault_b.amount.

  • Logical state: on-chain fields liquidity_pool.reserve_a and liquidity_pool.reserve_b.

While swap functions read the correct values from the vaults during swap calculations:

let reserve_a: u64 = context.accounts.token_vault_a.amount;
let reserve_b: u64 = context.accounts.token_vault_b.amount;

They fail to update the persistent LiquidityPool fields post-transaction. As a result, these fields retain outdated data.

Example scenario:

  • Initial state: vaults hold 100 A and 100 B; LiquidityPool.reserve_a = 100, reserve_b = 100.

  • After a swap of 10 A to B: vaults now hold 110 A and 91 B.

  • But LiquidityPool.reserve_a and reserve_b still report 100 A and 100 B.

This leads to inconsistencies in:

  • Liquidity withdrawals: LPs may request amounts based on outdated reserves.

  • Liquidity additions: skewed calculations of pool ratios and shares.

  • Off-chain metrics: external indexers or frontends reading LiquidityPool report false data.

Impact

Severity: High

  • Withdrawal logic may malfunction, returning excess tokens or reverting due to insufficient reserves.

  • Deposit/share calculations become inaccurate, breaking assumptions about proportional ownership.

  • Users and aggregators observing LiquidityPool fields will be misled about actual pool state.

  • Over time, desynchronization can cascade into severe accounting and trust failures.

Proof of Concept

Initial state:

- LiquidityPool.reserve_a = 100 - LiquidityPool.reserve_b = 100

- Vaults: 100 A, 100 B


Swap: 10 A → B

- New vaults:

110 A, 91 B

- LiquidityPool fields unchanged (still 100/100)


An LP withdraws assuming 100/100:

- Expects 100 A, 100 B - Only 91 B in vault → underflow or partial withdrawal

Steps to Reproduce

  1. Deploy contract as-is.

  2. Initialize a pool with 100 A and 100 B.

  3. Perform swap_exact_in(10 A).

  4. Read LiquidityPool.reserve_a/reserve_b → still 100/100.

  5. Attempt full LP withdrawal or add liquidity based on pool state.

  6. Observe incorrect results or transaction failure.

Recommendations

After each swap, immediately resynchronize the internal reserves:

+ context.accounts.liquidity_pool.reserve_a = context.accounts.token_vault_a.amount;
+ context.accounts.liquidity_pool.reserve_b = context.accounts.token_vault_b.amount;

This ensures that when Anchor serializes LiquidityPool, it reflects the actual vault balances and maintains accounting consistency across the protocol.

Updates

Lead Judging Commences

0xtimefliez Lead Judge 12 days ago
Submission Judgement Published
Validated
Assigned finding tags:

lack of account reload causes liquidity calculations to be outdated

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.