Skip to content

Bind pool reserves to PoolConfig and validate them in handlers - #180

Merged
mikemaccana merged 1 commit into
mainfrom
claude/eager-edison-p09qzu
Oct 2, 2026
Merged

mikemaccana merged 1 commit into
mainfrom
claude/eager-edison-p09qzu

Conversation

@mikemaccana

Copy link
Copy Markdown
Collaborator

Summary

This PR fixes a critical vulnerability in the Quasar token swap program where handlers accepted arbitrary token accounts as pool reserves without validation. An attacker could supply their own token account as pool_a or pool_b, causing the handler to price trades from the attacker's balance while draining the real reserve on the other side.

Key Changes

  • Store reserve addresses in PoolConfig: Added pool_a and pool_b fields to PoolConfig struct, recorded during pool initialization. This grows PoolConfig by 64 bytes, making pools created before this change incompatible.

  • Add validation constraints: Added has_one(pool_a) and has_one(pool_b) checks to all four handlers that touch reserves:

    • swap_tokens
    • deposit_liquidity
    • withdraw_liquidity
    • claim_admin_fees

    These checks fail with the new InvalidPoolVault error if the provided accounts don't match the stored addresses.

  • Improve invariant verification in swap_tokens: Changed the constant-product invariant check to read vault balances directly after transfers complete, rather than computing them from the amounts the handler intended to move. This catches cases where transfers moved different amounts than expected.

  • Add security tests: Included swap_rejects_substituted_pool_vault and deposit_rejects_substituted_pool_vaults tests that demonstrate the attack and verify it is now prevented.

Implementation Details

  • The PoolConfig struct now records both reserve addresses at initialization time, since a token account's mint and authority alone don't uniquely identify it.
  • All handlers that modify PoolConfig now preserve the pool_a and pool_b fields when updating other fields.
  • The swap handler now reads actual vault balances using zero-copy token account access, which reads the runtime buffer that CPIs just wrote without requiring a reload.
  • Updated CHANGELOG entries document the fix and note that the Anchor v1 and v2 versions already bound reserves as associated token accounts.

https://claude.ai/code/session_01BvopS6WYzsT9Qf32NTu29V

…r a swap

deposit_liquidity, withdraw_liquidity, swap_tokens and claim_admin_fees took
pool_a and pool_b with no check that they were the pool's reserves. A trader
could name a mint-A token account of their own as pool_a: the swap priced
itself from that account's one-unit balance, sent the input back into it, and
paid out 9,999,989 of a 10,000,000 pool_b. PoolConfig now records both reserve
addresses at initialize_pool, and the four handlers check them with has_one,
failing with InvalidPoolVault. swap_rejects_substituted_pool_vault and
deposit_rejects_substituted_pool_vaults run the attack. The Anchor v1 and v2
versions already bound the reserves as the pool's associated token accounts.

swap_tokens also re-checked the constant-product invariant against reserves
computed from the amounts it meant to move. It now reads both vaults after
the transfers land, as the Anchor versions do.

Claude-Session: https://claude.ai/code/session_01BvopS6WYzsT9Qf32NTu29V
@mikemaccana
mikemaccana merged commit 7ed1e66 into main Oct 2, 2026
32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant