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.
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:
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.
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.
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
Deploy contract as-is.
Initialize a pool with 100 A and 100 B.
Perform swap_exact_in(10 A)
.
Read LiquidityPool.reserve_a/reserve_b
→ still 100/100.
Attempt full LP withdrawal or add liquidity based on pool state.
Observe incorrect results or transaction failure.
After each swap, immediately resynchronize the internal reserves:
This ensures that when Anchor serializes LiquidityPool
, it reflects the actual vault balances and maintains accounting consistency across the protocol.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.