Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ All notable changes to this repository are documented here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [2026-10-02] - Token Swap (Quasar): reserves live at PDAs of the pool

### Changed

- `finance/token-swap/quasar` creates `pool_a` and `pool_b` at PDAs of the
pool (`[b"pool_a", pool_config]`, `[b"pool_b", pool_config]`) instead of at
addresses the client chose, as the order book's Quasar version does for its
vaults. Clients no longer pass the reserves to `initialize_pool`.

## [2026-10-02] - Token Swap (Quasar): reserves are bound to the pool

### Fixed
Expand Down
12 changes: 12 additions & 0 deletions finance/token-swap/quasar/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## [2026-10-02] - Reserves at PDAs

### Changed

- `pool_a` and `pool_b` are PDAs of the pool, at seeds `[b"pool_a",
pool_config]` and `[b"pool_b", pool_config]` (`PoolAPda`, `PoolBPda`), where
before they were token accounts at addresses the client chose.
`initialize_pool` creates them there with `init` rather than
`init(idempotent)`, so a client finds a pool's reserves from the pool's
address alone, and the instruction builder derives them. `PoolConfig` still
records both addresses and every handler still checks them with `has_one`.

## [2026-10-02]

### Fixed
Expand Down
13 changes: 8 additions & 5 deletions finance/token-swap/quasar/src/instructions/initialize_pool.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use {
crate::{
state::{Config, PoolConfig, PoolConfigInner},
ConfigPda, LiquidityMintPda, PoolPda,
ConfigPda, LiquidityMintPda, PoolAPda, PoolBPda, PoolPda,
},
quasar_lang::prelude::*,
quasar_spl::prelude::*,
Expand All @@ -10,6 +10,7 @@ use {
/// Seeds:
/// - `pool_config = [config, mint_a, mint_b]`
/// - `liquidity_provider_mint = [b"liquidity", config, mint_a, mint_b]`
/// - `pool_a = [b"pool_a", pool_config]`, `pool_b = [b"pool_b", pool_config]`
///
/// `pool_config` owns both reserves and is the LP mint's mint authority; it
/// signs for them with its own seeds. `liquidity_provider_mint` derives at a
Expand Down Expand Up @@ -39,19 +40,21 @@ pub struct InitializePoolAccountConstraints {
pub liquidity_provider_mint: Account<Mint>,
pub mint_a: Account<Mint>,
pub mint_b: Account<Mint>,
/// Pool's token A reserve, owned by `pool_config`.
/// Pool's token A reserve, owned by `pool_config`, at a PDA of the pool.
#[account(
mut,
init(idempotent),
init,
payer = payer,
address = PoolAPda::seeds(pool_config.address()),
token(mint = mint_a, authority = pool_config, token_program = token_program),
)]
pub pool_a: Account<Token>,
/// Pool's token B reserve, owned by `pool_config`.
/// Pool's token B reserve, owned by `pool_config`, at a PDA of the pool.
#[account(
mut,
init(idempotent),
init,
payer = payer,
address = PoolBPda::seeds(pool_config.address()),
token(mint = mint_b, authority = pool_config, token_program = token_program),
)]
pub pool_b: Account<Token>,
Expand Down
14 changes: 14 additions & 0 deletions finance/token-swap/quasar/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ pub struct PoolPda;
#[seeds(b"liquidity", config: Address, mint_a: Address, mint_b: Address)]
pub struct LiquidityMintPda;

/// The pool's token A reserve, a PDA of the pool at seeds = [b"pool_a",
/// pool_config], so any client can derive where a pool keeps its tokens
/// without being told. `PoolConfig` also records the address, and every
/// handler that touches the reserve checks it with `has_one(pool_a)`.
#[derive(Seeds)]
#[seeds(b"pool_a", pool_config: Address)]
pub struct PoolAPda;

/// The pool's token B reserve, at seeds = [b"pool_b", pool_config], recorded
/// and checked as `pool_a` is.
#[derive(Seeds)]
#[seeds(b"pool_b", pool_config: Address)]
pub struct PoolBPda;

/// Simple constant-product AMM (token swap).
///
/// Six instructions:
Expand Down
85 changes: 48 additions & 37 deletions finance/token-swap/quasar/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use {
},
error::AmmError,
state::Config,
ConfigPda, LiquidityMintPda, PoolPda,
ConfigPda, LiquidityMintPda, PoolAPda, PoolBPda, PoolPda,
},
quasar_test::prelude::*,
};
Expand Down Expand Up @@ -48,8 +48,6 @@ const ADMIN: Pubkey = Pubkey::new_from_array([1; 32]);
const PAYER: Pubkey = Pubkey::new_from_array([2; 32]);
const MINT_A: Pubkey = Pubkey::new_from_array([3; 32]);
const MINT_B: Pubkey = Pubkey::new_from_array([4; 32]);
const POOL_A: Pubkey = Pubkey::new_from_array([5; 32]);
const POOL_B: Pubkey = Pubkey::new_from_array([6; 32]);
// The pool-seeding depositor.
const SEEDER: Pubkey = Pubkey::new_from_array([7; 32]);
const SEEDER_TOKEN_A: Pubkey = Pubkey::new_from_array([8; 32]);
Expand All @@ -72,6 +70,22 @@ const BAD_ACTOR: Pubkey = Pubkey::new_from_array([22; 32]);
const BAD_TOKEN_A: Pubkey = Pubkey::new_from_array([23; 32]);
const BAD_TOKEN_B: Pubkey = Pubkey::new_from_array([24; 32]);

/// The pool's address, derived as the program derives it.
fn pool_config_address(test: &Test) -> Pubkey {
let config = test.derive_pda(ConfigPda::seeds());
test.derive_pda(PoolPda::seeds(&config, &MINT_A, &MINT_B))
}

/// The pool's token A reserve, a PDA of the pool.
fn pool_a(test: &Test) -> Pubkey {
test.derive_pda(PoolAPda::seeds(&pool_config_address(test)))
}

/// The pool's token B reserve, a PDA of the pool.
fn pool_b(test: &Test) -> Pubkey {
test.derive_pda(PoolBPda::seeds(&pool_config_address(test)))
}

struct PoolEnv {
pool_config: Pubkey,
lp_mint: Pubkey,
Expand All @@ -95,14 +109,11 @@ fn setup_pool(test: &mut Test) -> PoolEnv {
test.add(Mint::new(PAYER).at(MINT_A).decimals(6));
test.add(Mint::new(PAYER).at(MINT_B).decimals(6));

// initialize_pool: the pool_config and LP-mint PDAs are derived by the
// builder; pool_a/pool_b are non-PDA token accounts the program creates
// at the given addresses, owned by pool_config.
// initialize_pool: the pool_config, LP-mint and reserve PDAs are all
// derived by the builder; the reserves are owned by pool_config.
test.send(InitializePoolInstruction {
mint_a: MINT_A,
mint_b: MINT_B,
pool_a: POOL_A,
pool_b: POOL_B,
payer: PAYER,
})
.succeeds();
Expand Down Expand Up @@ -151,8 +162,8 @@ fn deposit(
depositor,
mint_a: MINT_A,
mint_b: MINT_B,
pool_a: POOL_A,
pool_b: POOL_B,
pool_a: pool_a(test),
pool_b: pool_b(test),
liquidity_provider_token: lp_token,
token_a,
token_b,
Expand Down Expand Up @@ -201,8 +212,8 @@ fn swap(
trader,
mint_a: MINT_A,
mint_b: MINT_B,
pool_a: POOL_A,
pool_b: POOL_B,
pool_a: pool_a(test),
pool_b: pool_b(test),
token_a,
token_b,
payer: PAYER,
Expand All @@ -221,8 +232,8 @@ fn claim_fees(
test.send(ClaimAdminFeesInstruction {
mint_a: MINT_A,
mint_b: MINT_B,
pool_a: POOL_A,
pool_b: POOL_B,
pool_a: pool_a(test),
pool_b: pool_b(test),
admin,
admin_token_a,
admin_token_b,
Expand Down Expand Up @@ -291,7 +302,7 @@ fn initialize_pool_creates_pool_config_and_lp_mint(test: &mut Test) {
expected,
"LP mint authority must be pool_config"
);
for reserve in [POOL_A, POOL_B] {
for reserve in [pool_a(test), pool_b(test)] {
let account = test.account(reserve).expect("reserve missing");
assert_eq!(
&account.data[32..64],
Expand All @@ -314,8 +325,8 @@ fn deposit_liquidity_initial(test: &mut Test) {
// LP token account must exist with a non-zero balance, and the pool
// reserves must have received the tokens.
assert!(lp_balance > 0, "expected LP tokens, got 0");
assert_eq!(test.tokens(POOL_A), amount_a);
assert_eq!(test.tokens(POOL_B), amount_b);
assert_eq!(test.tokens(pool_a(test)), amount_a);
assert_eq!(test.tokens(pool_b(test)), amount_b);
}

#[quasar_test]
Expand Down Expand Up @@ -367,8 +378,8 @@ fn deposit_after_every_lp_token_is_burned(test: &mut Test) {
// of each side stays.
withdraw(test, SEEDER, SEEDER_LP, RECV_A, RECV_B, lp_balance, 0, 0)
.succeeds()
.has_tokens(POOL_A, crate::MINIMUM_LIQUIDITY)
.has_tokens(POOL_B, crate::MINIMUM_LIQUIDITY);
.has_tokens(pool_a(test), crate::MINIMUM_LIQUIDITY)
.has_tokens(pool_b(test), crate::MINIMUM_LIQUIDITY);

// 1_000_000 * (0 + 100) / 100 = 1_000_000 LP tokens.
fund(
Expand Down Expand Up @@ -463,8 +474,8 @@ fn deposit_clamps_down_never_up(test: &mut Test) {
// Exact amounts pulled: all of A, ratio-clamped B, nothing more.
.has_tokens(DEPOSITOR_TOKEN_A, 0)
.has_tokens(DEPOSITOR_TOKEN_B, stated_b - expected_b_pulled)
.has_tokens(POOL_A, pool_seed_a + stated_a)
.has_tokens(POOL_B, pool_seed_b + expected_b_pulled)
.has_tokens(pool_a(test), pool_seed_a + stated_a)
.has_tokens(pool_b(test), pool_seed_b + expected_b_pulled)
// LP mint must be proportional.
.has_tokens(DEPOSITOR_LP, expected_lp);
}
Expand Down Expand Up @@ -509,8 +520,8 @@ fn deposit_clamps_down_other_side(test: &mut Test) {
.succeeds()
.has_tokens(DEPOSITOR_TOKEN_A, stated_a - expected_a_pulled)
.has_tokens(DEPOSITOR_TOKEN_B, 0)
.has_tokens(POOL_A, pool_seed_a + expected_a_pulled)
.has_tokens(POOL_B, pool_seed_b + stated_b)
.has_tokens(pool_a(test), pool_seed_a + expected_a_pulled)
.has_tokens(pool_b(test), pool_seed_b + stated_b)
// LP mint must be proportional.
.has_tokens(DEPOSITOR_LP, expected_lp);
}
Expand Down Expand Up @@ -559,12 +570,12 @@ fn deposit_slippage_rejected(test: &mut Test) {
"token B must be untouched after revert"
);
assert_eq!(
test.tokens(POOL_A),
test.tokens(pool_a(test)),
pool_seed_a,
"pool_a must be untouched after revert"
);
assert_eq!(
test.tokens(POOL_B),
test.tokens(pool_b(test)),
pool_seed_b,
"pool_b must be untouched after revert"
);
Expand All @@ -587,8 +598,8 @@ fn withdraw(
depositor,
mint_a: MINT_A,
mint_b: MINT_B,
pool_a: POOL_A,
pool_b: POOL_B,
pool_a: pool_a(test),
pool_b: pool_b(test),
liquidity_provider_token: lp_token,
token_a: recv_a,
token_b: recv_b,
Expand Down Expand Up @@ -666,12 +677,12 @@ fn withdraw_slippage_rejected(test: &mut Test) {

// Nothing moved: pool reserves and the LP balance are unchanged.
assert_eq!(
test.tokens(POOL_A),
test.tokens(pool_a(test)),
2_000_000,
"pool_a must be untouched after revert"
);
assert_eq!(
test.tokens(POOL_B),
test.tokens(pool_b(test)),
2_000_000,
"pool_b must be untouched after revert"
);
Expand Down Expand Up @@ -719,8 +730,8 @@ fn swap_a_to_b_conserves_balances(test: &mut Test) {
// what the pool sent; nothing is minted or lost in transit.
.has_tokens(TRADER_TOKEN_A, trader_funding - input)
.has_tokens(TRADER_TOKEN_B, expected_output)
.has_tokens(POOL_A, pool_seed_a + input)
.has_tokens(POOL_B, pool_seed_b - expected_output);
.has_tokens(pool_a(test), pool_seed_a + input)
.has_tokens(pool_b(test), pool_seed_b - expected_output);
}

#[quasar_test]
Expand Down Expand Up @@ -752,8 +763,8 @@ fn swap_b_to_a_conserves_balances(test: &mut Test) {
.succeeds()
.has_tokens(TRADER_TOKEN_B, trader_funding - input)
.has_tokens(TRADER_TOKEN_A, expected_output)
.has_tokens(POOL_B, pool_seed_b + input)
.has_tokens(POOL_A, pool_seed_a - expected_output);
.has_tokens(pool_b(test), pool_seed_b + input)
.has_tokens(pool_a(test), pool_seed_a - expected_output);
}

#[quasar_test]
Expand Down Expand Up @@ -789,12 +800,12 @@ fn swap_slippage_rejected(test: &mut Test) {
"trader balance must be untouched after revert"
);
assert_eq!(
test.tokens(POOL_A),
test.tokens(pool_a(test)),
10_000_000,
"pool_a must be untouched after revert"
);
assert_eq!(
test.tokens(POOL_B),
test.tokens(pool_b(test)),
10_000_000,
"pool_b must be untouched after revert"
);
Expand Down Expand Up @@ -829,7 +840,7 @@ fn swap_rejects_substituted_pool_vault(test: &mut Test) {
mint_a: MINT_A,
mint_b: MINT_B,
pool_a: fake_pool_a,
pool_b: POOL_B,
pool_b: pool_b(test),
token_a: BAD_TOKEN_A,
token_b: BAD_TOKEN_B,
payer: PAYER,
Expand All @@ -840,7 +851,7 @@ fn swap_rejects_substituted_pool_vault(test: &mut Test) {
.fails_with(AmmError::InvalidPoolVault);

assert_eq!(
test.tokens(POOL_B),
test.tokens(pool_b(test)),
10_000_000,
"pool_b must be untouched after the refused swap"
);
Expand Down
Loading