lez-programs/amm/src/tests.rs
Ricardo Guilherme Schmidt 8259c1838d
refactor(amm)!: derive pool active state from LP supply instead of explicit flag
Remove the `active: bool` field from `PoolDefinition` and replace it with an
implicit invariant: a pool is considered active when
`liquidity_pool_supply >= MINIMUM_LIQUIDITY`.

BREAKING CHANGE: `PoolDefinition` Borsh serialization format has changed.
Existing on-chain pool accounts encoded with the `active` field are
incompatible with this version.

Closes #25
2026-04-14 11:48:11 +02:00

3074 lines
114 KiB
Rust

#![cfg(test)]
use std::num::NonZero;
use amm_core::{
compute_liquidity_token_pda, compute_liquidity_token_pda_seed, compute_lp_lock_holding_pda,
compute_pool_pda, compute_vault_pda, compute_vault_pda_seed, PoolDefinition, FEE_TIER_BPS_1,
FEE_TIER_BPS_100, FEE_TIER_BPS_30, FEE_TIER_BPS_5, MINIMUM_LIQUIDITY,
};
use nssa_core::{
account::{Account, AccountId, AccountWithMetadata, Data, Nonce},
program::{ChainedCall, ProgramId},
};
use token_core::{TokenDefinition, TokenHolding};
use crate::{
add::add_liquidity,
new_definition::new_definition,
remove::remove_liquidity,
swap::{swap_exact_input, swap_exact_output},
sync::sync_reserves,
};
const TOKEN_PROGRAM_ID: ProgramId = [15; 8];
const AMM_PROGRAM_ID: ProgramId = [42; 8];
struct BalanceForTests;
struct ChainedCallForTests;
struct IdForTests;
struct AccountWithMetadataForTests;
type AccountForTests = AccountWithMetadataForTests;
impl BalanceForTests {
fn fee_tier() -> u128 {
FEE_TIER_BPS_30
}
fn vault_a_reserve_init() -> u128 {
5_000
}
fn vault_b_reserve_init() -> u128 {
2_500
}
fn vault_a_reserve_low() -> u128 {
10
}
fn vault_b_reserve_low() -> u128 {
10
}
fn vault_a_reserve_high() -> u128 {
500_000
}
fn vault_b_reserve_high() -> u128 {
500_000
}
fn user_token_a_balance() -> u128 {
10_000
}
fn user_token_b_balance() -> u128 {
10_000
}
fn user_token_lp_balance() -> u128 {
100
}
fn remove_min_amount_a() -> u128 {
50
}
fn remove_min_amount_b() -> u128 {
100
}
fn remove_actual_a_successful() -> u128 {
141
}
fn remove_min_amount_b_low() -> u128 {
50
}
fn remove_amount_lp() -> u128 {
100
}
fn remove_amount_lp_1() -> u128 {
30
}
fn add_max_amount_a() -> u128 {
500
}
fn add_max_amount_b() -> u128 {
200
}
fn add_max_amount_a_low() -> u128 {
10
}
fn add_max_amount_b_low() -> u128 {
10
}
fn add_min_amount_lp() -> u128 {
20
}
fn lp_supply_init() -> u128 {
// sqrt(vault_a_reserve_init * vault_b_reserve_init) = sqrt(5000 * 2500) = 3535
(BalanceForTests::vault_a_reserve_init() * BalanceForTests::vault_b_reserve_init()).isqrt()
}
fn lp_user_init() -> u128 {
BalanceForTests::lp_supply_init() - MINIMUM_LIQUIDITY
}
fn vault_a_swap_test_1() -> u128 {
BalanceForTests::vault_a_reserve_init() + BalanceForTests::add_max_amount_a()
}
fn vault_a_swap_test_2() -> u128 {
BalanceForTests::vault_a_reserve_init() - BalanceForTests::swap_amount_out_a()
}
fn vault_b_swap_test_1() -> u128 {
BalanceForTests::vault_b_reserve_init() - BalanceForTests::swap_amount_out_b()
}
fn vault_b_swap_test_2() -> u128 {
BalanceForTests::vault_b_reserve_init() + BalanceForTests::add_max_amount_b()
}
fn min_amount_out() -> u128 {
200
}
fn min_amount_out_too_high() -> u128 {
BalanceForTests::swap_amount_out_b() + 1
}
fn vault_a_add_successful() -> u128 {
BalanceForTests::vault_a_reserve_init() + BalanceForTests::add_successful_amount_a()
}
fn vault_b_add_successful() -> u128 {
BalanceForTests::vault_b_reserve_init() + BalanceForTests::add_successful_amount_b()
}
fn add_successful_amount_a() -> u128 {
(BalanceForTests::vault_a_reserve_init() * BalanceForTests::add_max_amount_b())
/ BalanceForTests::vault_b_reserve_init()
}
fn add_successful_amount_b() -> u128 {
BalanceForTests::add_max_amount_b()
}
fn max_amount_in() -> u128 {
166
}
fn vault_a_remove_successful() -> u128 {
BalanceForTests::vault_a_reserve_init() - BalanceForTests::remove_actual_a_successful()
}
fn vault_b_remove_successful() -> u128 {
BalanceForTests::vault_b_reserve_init() - BalanceForTests::remove_actual_b_successful()
}
fn swap_amount_out_b() -> u128 {
(BalanceForTests::vault_b_reserve_init() * BalanceForTests::add_max_amount_a())
/ (BalanceForTests::vault_a_reserve_init() + BalanceForTests::add_max_amount_a())
}
fn swap_amount_out_a() -> u128 {
(BalanceForTests::vault_a_reserve_init() * BalanceForTests::add_max_amount_b())
/ (BalanceForTests::vault_b_reserve_init() + BalanceForTests::add_max_amount_b())
}
fn add_delta_lp_successful() -> u128 {
std::cmp::min(
BalanceForTests::lp_supply_init() * BalanceForTests::add_successful_amount_a()
/ BalanceForTests::vault_a_reserve_init(),
BalanceForTests::lp_supply_init() * BalanceForTests::add_successful_amount_b()
/ BalanceForTests::vault_b_reserve_init(),
)
}
fn remove_actual_b_successful() -> u128 {
(BalanceForTests::vault_b_reserve_init() * BalanceForTests::remove_amount_lp())
/ BalanceForTests::lp_supply_init()
}
fn add_lp_supply_successful() -> u128 {
BalanceForTests::lp_supply_init() + BalanceForTests::add_delta_lp_successful()
}
fn remove_lp_supply_successful() -> u128 {
BalanceForTests::lp_supply_init() - BalanceForTests::remove_amount_lp()
}
}
impl ChainedCallForTests {
fn cc_swap_token_a_test_1() -> ChainedCall {
ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::vault_a_init(),
],
&token_core::Instruction::Transfer {
amount_to_transfer: BalanceForTests::add_max_amount_a(),
},
)
}
fn cc_swap_token_b_test_1() -> ChainedCall {
let swap_amount = BalanceForTests::swap_amount_out_b();
let mut vault_b_auth = AccountWithMetadataForTests::vault_b_init();
vault_b_auth.is_authorized = true;
ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![vault_b_auth, AccountWithMetadataForTests::user_holding_b()],
&token_core::Instruction::Transfer {
amount_to_transfer: swap_amount,
},
)
.with_pda_seeds(vec![compute_vault_pda_seed(
IdForTests::pool_definition_id(),
IdForTests::token_b_definition_id(),
)])
}
fn cc_swap_token_a_test_2() -> ChainedCall {
let swap_amount = BalanceForTests::swap_amount_out_a();
let mut vault_a_auth = AccountWithMetadataForTests::vault_a_init();
vault_a_auth.is_authorized = true;
ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![vault_a_auth, AccountWithMetadataForTests::user_holding_a()],
&token_core::Instruction::Transfer {
amount_to_transfer: swap_amount,
},
)
.with_pda_seeds(vec![compute_vault_pda_seed(
IdForTests::pool_definition_id(),
IdForTests::token_a_definition_id(),
)])
}
fn cc_swap_token_b_test_2() -> ChainedCall {
ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::vault_b_init(),
],
&token_core::Instruction::Transfer {
amount_to_transfer: BalanceForTests::add_max_amount_b(),
},
)
}
fn cc_swap_exact_output_token_a_test_1() -> ChainedCall {
let swap_amount: u128 = 498;
ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::vault_a_init(),
],
&token_core::Instruction::Transfer {
amount_to_transfer: swap_amount,
},
)
}
fn cc_swap_exact_output_token_b_test_1() -> ChainedCall {
let swap_amount: u128 = 166;
let mut vault_b_auth = AccountWithMetadataForTests::vault_b_init();
vault_b_auth.is_authorized = true;
ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![vault_b_auth, AccountWithMetadataForTests::user_holding_b()],
&token_core::Instruction::Transfer {
amount_to_transfer: swap_amount,
},
)
.with_pda_seeds(vec![compute_vault_pda_seed(
IdForTests::pool_definition_id(),
IdForTests::token_b_definition_id(),
)])
}
fn cc_swap_exact_output_token_a_test_2() -> ChainedCall {
let swap_amount: u128 = 285;
let mut vault_a_auth = AccountWithMetadataForTests::vault_a_init();
vault_a_auth.is_authorized = true;
ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![vault_a_auth, AccountWithMetadataForTests::user_holding_a()],
&token_core::Instruction::Transfer {
amount_to_transfer: swap_amount,
},
)
.with_pda_seeds(vec![compute_vault_pda_seed(
IdForTests::pool_definition_id(),
IdForTests::token_a_definition_id(),
)])
}
fn cc_swap_exact_output_token_b_test_2() -> ChainedCall {
let swap_amount: u128 = 200;
ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::vault_b_init(),
],
&token_core::Instruction::Transfer {
amount_to_transfer: swap_amount,
},
)
}
fn cc_add_token_a() -> ChainedCall {
ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::vault_a_init(),
],
&token_core::Instruction::Transfer {
amount_to_transfer: BalanceForTests::add_successful_amount_a(),
},
)
}
fn cc_add_token_b() -> ChainedCall {
ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::vault_b_init(),
],
&token_core::Instruction::Transfer {
amount_to_transfer: BalanceForTests::add_successful_amount_b(),
},
)
}
fn cc_add_pool_lp() -> ChainedCall {
let mut pool_lp_auth = AccountWithMetadataForTests::pool_lp_init();
pool_lp_auth.is_authorized = true;
ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![
pool_lp_auth,
AccountWithMetadataForTests::user_holding_lp_init(),
],
&token_core::Instruction::Mint {
amount_to_mint: BalanceForTests::add_delta_lp_successful(),
},
)
.with_pda_seeds(vec![compute_liquidity_token_pda_seed(
IdForTests::pool_definition_id(),
)])
}
fn cc_remove_token_a() -> ChainedCall {
let mut vault_a_auth = AccountWithMetadataForTests::vault_a_init();
vault_a_auth.is_authorized = true;
ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![vault_a_auth, AccountWithMetadataForTests::user_holding_a()],
&token_core::Instruction::Transfer {
amount_to_transfer: BalanceForTests::remove_actual_a_successful(),
},
)
.with_pda_seeds(vec![compute_vault_pda_seed(
IdForTests::pool_definition_id(),
IdForTests::token_a_definition_id(),
)])
}
fn cc_remove_token_b() -> ChainedCall {
let mut vault_b_auth = AccountWithMetadataForTests::vault_b_init();
vault_b_auth.is_authorized = true;
ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![vault_b_auth, AccountWithMetadataForTests::user_holding_b()],
&token_core::Instruction::Transfer {
amount_to_transfer: BalanceForTests::remove_actual_b_successful(),
},
)
.with_pda_seeds(vec![compute_vault_pda_seed(
IdForTests::pool_definition_id(),
IdForTests::token_b_definition_id(),
)])
}
fn cc_remove_pool_lp() -> ChainedCall {
let mut pool_lp_auth = AccountWithMetadataForTests::pool_lp_init();
pool_lp_auth.is_authorized = true;
ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![
pool_lp_auth,
AccountWithMetadataForTests::user_holding_lp_init(),
],
&token_core::Instruction::Burn {
amount_to_burn: BalanceForTests::remove_amount_lp(),
},
)
.with_pda_seeds(vec![compute_liquidity_token_pda_seed(
IdForTests::pool_definition_id(),
)])
}
fn cc_new_definition_token_a() -> ChainedCall {
ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::vault_a_init(),
],
&token_core::Instruction::Transfer {
amount_to_transfer: BalanceForTests::vault_a_reserve_init(),
},
)
}
fn cc_new_definition_token_b() -> ChainedCall {
ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::vault_b_init(),
],
&token_core::Instruction::Transfer {
amount_to_transfer: BalanceForTests::vault_b_reserve_init(),
},
)
}
fn cc_new_definition_token_lp_lock() -> ChainedCall {
let mut pool_lp_auth = AccountForTests::pool_lp_reinitializable();
pool_lp_auth.is_authorized = true;
ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![pool_lp_auth, AccountForTests::lp_lock_holding_uninit()],
&token_core::Instruction::Mint {
amount_to_mint: MINIMUM_LIQUIDITY,
},
)
.with_pda_seeds(vec![compute_liquidity_token_pda_seed(
IdForTests::pool_definition_id(),
)])
}
fn cc_new_definition_token_lp_user() -> ChainedCall {
ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![
AccountForTests::pool_lp_reinitialized_after_lock(),
AccountForTests::user_holding_lp_uninit(),
],
&token_core::Instruction::Mint {
amount_to_mint: BalanceForTests::lp_user_init(),
},
)
.with_pda_seeds(vec![compute_liquidity_token_pda_seed(
IdForTests::pool_definition_id(),
)])
}
}
impl IdForTests {
fn token_a_definition_id() -> AccountId {
AccountId::new([42; 32])
}
fn token_b_definition_id() -> AccountId {
AccountId::new([43; 32])
}
fn token_lp_definition_id() -> AccountId {
compute_liquidity_token_pda(AMM_PROGRAM_ID, IdForTests::pool_definition_id())
}
fn lp_lock_holding_id() -> AccountId {
compute_lp_lock_holding_pda(AMM_PROGRAM_ID, IdForTests::pool_definition_id())
}
fn user_token_a_id() -> AccountId {
AccountId::new([45; 32])
}
fn user_token_b_id() -> AccountId {
AccountId::new([46; 32])
}
fn user_token_lp_id() -> AccountId {
AccountId::new([47; 32])
}
fn pool_definition_id() -> AccountId {
compute_pool_pda(
AMM_PROGRAM_ID,
IdForTests::token_a_definition_id(),
IdForTests::token_b_definition_id(),
)
}
fn vault_a_id() -> AccountId {
compute_vault_pda(
AMM_PROGRAM_ID,
IdForTests::pool_definition_id(),
IdForTests::token_a_definition_id(),
)
}
fn vault_b_id() -> AccountId {
compute_vault_pda(
AMM_PROGRAM_ID,
IdForTests::pool_definition_id(),
IdForTests::token_b_definition_id(),
)
}
}
impl AccountWithMetadataForTests {
fn user_holding_a() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_a_definition_id(),
balance: BalanceForTests::user_token_a_balance(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::user_token_a_id(),
}
}
fn user_holding_b() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_b_definition_id(),
balance: BalanceForTests::user_token_b_balance(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::user_token_b_id(),
}
}
fn vault_a_init() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_a_definition_id(),
balance: BalanceForTests::vault_a_reserve_init(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::vault_a_id(),
}
}
fn vault_b_init() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_b_definition_id(),
balance: BalanceForTests::vault_b_reserve_init(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::vault_b_id(),
}
}
fn vault_a_init_high() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_a_definition_id(),
balance: BalanceForTests::vault_a_reserve_high(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::vault_a_id(),
}
}
fn vault_b_init_high() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_b_definition_id(),
balance: BalanceForTests::vault_b_reserve_high(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::vault_b_id(),
}
}
fn vault_a_init_low() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_a_definition_id(),
balance: BalanceForTests::vault_a_reserve_low(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::vault_a_id(),
}
}
fn vault_b_init_low() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_b_definition_id(),
balance: BalanceForTests::vault_b_reserve_low(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::vault_b_id(),
}
}
fn vault_a_init_zero() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_a_definition_id(),
balance: 0,
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::vault_a_id(),
}
}
fn vault_b_init_zero() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_b_definition_id(),
balance: 0,
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::vault_b_id(),
}
}
fn pool_lp_init() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0u128,
data: Data::from(&TokenDefinition::Fungible {
name: String::from("test"),
total_supply: BalanceForTests::lp_supply_init(),
metadata_id: None,
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::token_lp_definition_id(),
}
}
fn pool_lp_reinitializable() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0u128,
data: Data::from(&TokenDefinition::Fungible {
name: String::from("test"),
total_supply: 0,
metadata_id: None,
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::token_lp_definition_id(),
}
}
fn pool_lp_reinitialized_after_lock() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0u128,
data: Data::from(&TokenDefinition::Fungible {
name: String::from("test"),
total_supply: MINIMUM_LIQUIDITY,
metadata_id: None,
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::token_lp_definition_id(),
}
}
fn pool_lp_uninit() -> AccountWithMetadata {
AccountWithMetadata {
account: Account::default(),
is_authorized: true,
account_id: IdForTests::token_lp_definition_id(),
}
}
fn pool_lp_created_after_lock() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0u128,
data: Data::from(&TokenDefinition::Fungible {
name: String::from("LP Token"),
total_supply: MINIMUM_LIQUIDITY,
metadata_id: None,
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::token_lp_definition_id(),
}
}
fn pool_lp_with_wrong_id() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0u128,
data: Data::from(&TokenDefinition::Fungible {
name: String::from("test"),
total_supply: BalanceForTests::lp_supply_init(),
metadata_id: None,
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::vault_a_id(),
}
}
fn user_holding_lp_uninit() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_lp_definition_id(),
balance: 0,
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::user_token_lp_id(),
}
}
fn user_holding_lp_init() -> AccountWithMetadata {
AccountForTests::user_holding_lp_with_balance(BalanceForTests::user_token_lp_balance())
}
fn user_holding_lp_with_balance(balance: u128) -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_lp_definition_id(),
balance,
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::user_token_lp_id(),
}
}
fn lp_lock_holding_uninit() -> AccountWithMetadata {
AccountWithMetadata {
account: Account::default(),
is_authorized: false,
account_id: IdForTests::lp_lock_holding_id(),
}
}
fn lp_lock_holding_with_wrong_id() -> AccountWithMetadata {
AccountWithMetadata {
account: Account::default(),
is_authorized: false,
account_id: IdForTests::vault_a_id(),
}
}
fn pool_definition_init() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: ProgramId::default(),
balance: 0u128,
data: Data::from(&PoolDefinition {
definition_token_a_id: IdForTests::token_a_definition_id(),
definition_token_b_id: IdForTests::token_b_definition_id(),
vault_a_id: IdForTests::vault_a_id(),
vault_b_id: IdForTests::vault_b_id(),
liquidity_pool_id: IdForTests::token_lp_definition_id(),
liquidity_pool_supply: BalanceForTests::lp_supply_init(),
reserve_a: BalanceForTests::vault_a_reserve_init(),
reserve_b: BalanceForTests::vault_b_reserve_init(),
fees: BalanceForTests::fee_tier(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
}
}
/// A smaller pool (reserve_a=1_000, reserve_b=500) used exclusively by
/// swap-exact-output tests, whose hardcoded expected values were computed
/// against these reserves. vault_a_init/vault_b_init still satisfy the
/// balance ≥ reserve check (5_000 ≥ 1_000, 2_500 ≥ 500).
fn pool_definition_swap_exact_output_init() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: ProgramId::default(),
balance: 0u128,
data: Data::from(&PoolDefinition {
definition_token_a_id: IdForTests::token_a_definition_id(),
definition_token_b_id: IdForTests::token_b_definition_id(),
vault_a_id: IdForTests::vault_a_id(),
vault_b_id: IdForTests::vault_b_id(),
liquidity_pool_id: IdForTests::token_lp_definition_id(),
liquidity_pool_supply: BalanceForTests::lp_supply_init(),
reserve_a: 1_000,
reserve_b: 500,
fees: BalanceForTests::fee_tier(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
}
}
fn pool_definition_init_reserve_a_zero() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: ProgramId::default(),
balance: 0u128,
data: Data::from(&PoolDefinition {
definition_token_a_id: IdForTests::token_a_definition_id(),
definition_token_b_id: IdForTests::token_b_definition_id(),
vault_a_id: IdForTests::vault_a_id(),
vault_b_id: IdForTests::vault_b_id(),
liquidity_pool_id: IdForTests::token_lp_definition_id(),
liquidity_pool_supply: BalanceForTests::lp_supply_init(),
reserve_a: 0,
reserve_b: BalanceForTests::vault_b_reserve_init(),
fees: BalanceForTests::fee_tier(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
}
}
fn pool_definition_init_reserve_b_zero() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: ProgramId::default(),
balance: 0u128,
data: Data::from(&PoolDefinition {
definition_token_a_id: IdForTests::token_a_definition_id(),
definition_token_b_id: IdForTests::token_b_definition_id(),
vault_a_id: IdForTests::vault_a_id(),
vault_b_id: IdForTests::vault_b_id(),
liquidity_pool_id: IdForTests::token_lp_definition_id(),
liquidity_pool_supply: BalanceForTests::lp_supply_init(),
reserve_a: BalanceForTests::vault_a_reserve_init(),
reserve_b: 0,
fees: BalanceForTests::fee_tier(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
}
}
fn pool_definition_init_reserve_a_low() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: ProgramId::default(),
balance: 0u128,
data: Data::from(&PoolDefinition {
definition_token_a_id: IdForTests::token_a_definition_id(),
definition_token_b_id: IdForTests::token_b_definition_id(),
vault_a_id: IdForTests::vault_a_id(),
vault_b_id: IdForTests::vault_b_id(),
liquidity_pool_id: IdForTests::token_lp_definition_id(),
liquidity_pool_supply: BalanceForTests::vault_a_reserve_low(),
reserve_a: BalanceForTests::vault_a_reserve_low(),
reserve_b: BalanceForTests::vault_b_reserve_high(),
fees: BalanceForTests::fee_tier(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
}
}
fn pool_definition_init_reserve_b_low() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: ProgramId::default(),
balance: 0u128,
data: Data::from(&PoolDefinition {
definition_token_a_id: IdForTests::token_a_definition_id(),
definition_token_b_id: IdForTests::token_b_definition_id(),
vault_a_id: IdForTests::vault_a_id(),
vault_b_id: IdForTests::vault_b_id(),
liquidity_pool_id: IdForTests::token_lp_definition_id(),
liquidity_pool_supply: BalanceForTests::vault_a_reserve_high(),
reserve_a: BalanceForTests::vault_a_reserve_high(),
reserve_b: BalanceForTests::vault_b_reserve_low(),
fees: BalanceForTests::fee_tier(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
}
}
fn pool_definition_swap_test_1() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: ProgramId::default(),
balance: 0u128,
data: Data::from(&PoolDefinition {
definition_token_a_id: IdForTests::token_a_definition_id(),
definition_token_b_id: IdForTests::token_b_definition_id(),
vault_a_id: IdForTests::vault_a_id(),
vault_b_id: IdForTests::vault_b_id(),
liquidity_pool_id: IdForTests::token_lp_definition_id(),
liquidity_pool_supply: BalanceForTests::lp_supply_init(),
reserve_a: BalanceForTests::vault_a_swap_test_1(),
reserve_b: BalanceForTests::vault_b_swap_test_1(),
fees: BalanceForTests::fee_tier(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
}
}
fn pool_definition_swap_test_2() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: ProgramId::default(),
balance: 0u128,
data: Data::from(&PoolDefinition {
definition_token_a_id: IdForTests::token_a_definition_id(),
definition_token_b_id: IdForTests::token_b_definition_id(),
vault_a_id: IdForTests::vault_a_id(),
vault_b_id: IdForTests::vault_b_id(),
liquidity_pool_id: IdForTests::token_lp_definition_id(),
liquidity_pool_supply: BalanceForTests::lp_supply_init(),
reserve_a: BalanceForTests::vault_a_swap_test_2(),
reserve_b: BalanceForTests::vault_b_swap_test_2(),
fees: BalanceForTests::fee_tier(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
}
}
fn pool_definition_swap_exact_output_test_1() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: ProgramId::default(),
balance: 0_u128,
data: Data::from(&PoolDefinition {
definition_token_a_id: IdForTests::token_a_definition_id(),
definition_token_b_id: IdForTests::token_b_definition_id(),
vault_a_id: IdForTests::vault_a_id(),
vault_b_id: IdForTests::vault_b_id(),
liquidity_pool_id: IdForTests::token_lp_definition_id(),
liquidity_pool_supply: BalanceForTests::lp_supply_init(),
reserve_a: 1498_u128,
reserve_b: 334_u128,
fees: BalanceForTests::fee_tier(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
}
}
fn pool_definition_swap_exact_output_test_2() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: ProgramId::default(),
balance: 0_u128,
data: Data::from(&PoolDefinition {
definition_token_a_id: IdForTests::token_a_definition_id(),
definition_token_b_id: IdForTests::token_b_definition_id(),
vault_a_id: IdForTests::vault_a_id(),
vault_b_id: IdForTests::vault_b_id(),
liquidity_pool_id: IdForTests::token_lp_definition_id(),
liquidity_pool_supply: BalanceForTests::lp_supply_init(),
reserve_a: 715_u128,
reserve_b: 700_u128,
fees: BalanceForTests::fee_tier(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
}
}
fn pool_definition_add_zero_lp() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: ProgramId::default(),
balance: 0u128,
data: Data::from(&PoolDefinition {
definition_token_a_id: IdForTests::token_a_definition_id(),
definition_token_b_id: IdForTests::token_b_definition_id(),
vault_a_id: IdForTests::vault_a_id(),
vault_b_id: IdForTests::vault_b_id(),
liquidity_pool_id: IdForTests::token_lp_definition_id(),
liquidity_pool_supply: BalanceForTests::vault_a_reserve_low(),
reserve_a: BalanceForTests::vault_a_reserve_init(),
reserve_b: BalanceForTests::vault_b_reserve_init(),
fees: BalanceForTests::fee_tier(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
}
}
fn pool_definition_add_successful() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: ProgramId::default(),
balance: 0u128,
data: Data::from(&PoolDefinition {
definition_token_a_id: IdForTests::token_a_definition_id(),
definition_token_b_id: IdForTests::token_b_definition_id(),
vault_a_id: IdForTests::vault_a_id(),
vault_b_id: IdForTests::vault_b_id(),
liquidity_pool_id: IdForTests::token_lp_definition_id(),
liquidity_pool_supply: BalanceForTests::add_lp_supply_successful(),
reserve_a: BalanceForTests::vault_a_add_successful(),
reserve_b: BalanceForTests::vault_b_add_successful(),
fees: BalanceForTests::fee_tier(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
}
}
fn pool_definition_remove_successful() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: ProgramId::default(),
balance: 0u128,
data: Data::from(&PoolDefinition {
definition_token_a_id: IdForTests::token_a_definition_id(),
definition_token_b_id: IdForTests::token_b_definition_id(),
vault_a_id: IdForTests::vault_a_id(),
vault_b_id: IdForTests::vault_b_id(),
liquidity_pool_id: IdForTests::token_lp_definition_id(),
liquidity_pool_supply: BalanceForTests::remove_lp_supply_successful(),
reserve_a: BalanceForTests::vault_a_remove_successful(),
reserve_b: BalanceForTests::vault_b_remove_successful(),
fees: BalanceForTests::fee_tier(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
}
}
fn pool_definition_below_minimum_liquidity() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: ProgramId::default(),
balance: 0u128,
data: Data::from(&PoolDefinition {
definition_token_a_id: IdForTests::token_a_definition_id(),
definition_token_b_id: IdForTests::token_b_definition_id(),
vault_a_id: IdForTests::vault_a_id(),
vault_b_id: IdForTests::vault_b_id(),
liquidity_pool_id: IdForTests::token_lp_definition_id(),
liquidity_pool_supply: MINIMUM_LIQUIDITY - 1,
reserve_a: BalanceForTests::vault_a_reserve_init(),
reserve_b: BalanceForTests::vault_b_reserve_init(),
fees: BalanceForTests::fee_tier(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
}
}
fn pool_definition_zero_supply_reinitializable() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: ProgramId::default(),
balance: 0u128,
data: Data::from(&PoolDefinition {
definition_token_a_id: IdForTests::token_a_definition_id(),
definition_token_b_id: IdForTests::token_b_definition_id(),
vault_a_id: IdForTests::vault_a_id(),
vault_b_id: IdForTests::vault_b_id(),
liquidity_pool_id: IdForTests::token_lp_definition_id(),
liquidity_pool_supply: 0,
reserve_a: 0,
reserve_b: 0,
fees: 0u128,
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
}
}
fn pool_definition_with_wrong_id() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: ProgramId::default(),
balance: 0u128,
data: Data::from(&PoolDefinition {
definition_token_a_id: IdForTests::token_a_definition_id(),
definition_token_b_id: IdForTests::token_b_definition_id(),
vault_a_id: IdForTests::vault_a_id(),
vault_b_id: IdForTests::vault_b_id(),
liquidity_pool_id: IdForTests::token_lp_definition_id(),
liquidity_pool_supply: BalanceForTests::lp_supply_init(),
reserve_a: BalanceForTests::vault_a_reserve_init(),
reserve_b: BalanceForTests::vault_b_reserve_init(),
fees: BalanceForTests::fee_tier(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: AccountId::new([4; 32]),
}
}
fn vault_a_with_wrong_id() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_a_definition_id(),
balance: BalanceForTests::vault_a_reserve_init(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: AccountId::new([4; 32]),
}
}
fn vault_b_with_wrong_id() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_b_definition_id(),
balance: BalanceForTests::vault_b_reserve_init(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: AccountId::new([4; 32]),
}
}
/// Legacy/corrupted pool state whose reported supply has already been drained down to the
/// permanent lock (liquidity_pool_supply == MINIMUM_LIQUIDITY).
fn pool_definition_at_minimum_liquidity() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: ProgramId::default(),
balance: 0u128,
data: Data::from(&PoolDefinition {
definition_token_a_id: IdForTests::token_a_definition_id(),
definition_token_b_id: IdForTests::token_b_definition_id(),
vault_a_id: IdForTests::vault_a_id(),
vault_b_id: IdForTests::vault_b_id(),
liquidity_pool_id: IdForTests::token_lp_definition_id(),
liquidity_pool_supply: MINIMUM_LIQUIDITY,
reserve_a: BalanceForTests::vault_a_reserve_init(),
reserve_b: BalanceForTests::vault_b_reserve_init(),
fees: BalanceForTests::fee_tier(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
}
}
}
#[test]
fn test_pool_pda_produces_unique_id_for_token_pair() {
assert!(
amm_core::compute_pool_pda(
AMM_PROGRAM_ID,
IdForTests::token_a_definition_id(),
IdForTests::token_b_definition_id()
) == compute_pool_pda(
AMM_PROGRAM_ID,
IdForTests::token_b_definition_id(),
IdForTests::token_a_definition_id()
)
);
}
#[should_panic(expected = "Vault A was not provided")]
#[test]
fn test_call_add_liquidity_vault_a_omitted() {
let _post_states = add_liquidity(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_with_wrong_id(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_init(),
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::add_max_amount_b(),
);
}
#[should_panic(expected = "Vault B was not provided")]
#[test]
fn test_call_add_liquidity_vault_b_omitted() {
let _post_states = add_liquidity(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_with_wrong_id(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_init(),
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::add_max_amount_b(),
);
}
#[should_panic(expected = "LP definition mismatch")]
#[test]
fn test_call_add_liquidity_lp_definition_mismatch() {
let _post_states = add_liquidity(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_with_wrong_id(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_init(),
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::add_max_amount_b(),
);
}
#[should_panic(expected = "Both max-balances must be nonzero")]
#[test]
fn test_call_add_liquidity_zero_balance_1() {
let _post_states = add_liquidity(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_init(),
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
0,
BalanceForTests::add_max_amount_b(),
);
}
#[should_panic(expected = "Both max-balances must be nonzero")]
#[test]
fn test_call_add_liquidity_zero_balance_2() {
let _post_states = add_liquidity(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_init(),
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
0,
BalanceForTests::add_max_amount_a(),
);
}
#[should_panic(expected = "Vaults' balances must be at least the reserve amounts")]
#[test]
fn test_call_add_liquidity_vault_insufficient_balance_1() {
let _post_states = add_liquidity(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init_zero(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_init(),
NonZero::new(BalanceForTests::add_max_amount_a()).unwrap(),
BalanceForTests::add_max_amount_b(),
BalanceForTests::add_min_amount_lp(),
);
}
#[should_panic(expected = "Vaults' balances must be at least the reserve amounts")]
#[test]
fn test_call_add_liquidity_vault_insufficient_balance_2() {
let _post_states = add_liquidity(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init_zero(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_init(),
NonZero::new(BalanceForTests::add_max_amount_a()).unwrap(),
BalanceForTests::add_max_amount_b(),
BalanceForTests::add_min_amount_lp(),
);
}
#[should_panic(expected = "A trade amount is 0")]
#[test]
fn test_call_add_liquidity_actual_amount_zero_1() {
let _post_states = add_liquidity(
AccountWithMetadataForTests::pool_definition_init_reserve_a_low(),
AccountWithMetadataForTests::vault_a_init_low(),
AccountWithMetadataForTests::vault_b_init_high(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_init(),
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::add_max_amount_b(),
);
}
#[should_panic(expected = "A trade amount is 0")]
#[test]
fn test_call_add_liquidity_actual_amount_zero_2() {
let _post_states = add_liquidity(
AccountWithMetadataForTests::pool_definition_init_reserve_b_low(),
AccountWithMetadataForTests::vault_a_init_high(),
AccountWithMetadataForTests::vault_b_init_low(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_init(),
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
BalanceForTests::add_max_amount_a_low(),
BalanceForTests::add_max_amount_b_low(),
);
}
#[should_panic(expected = "Reserves must be nonzero")]
#[test]
fn test_call_add_liquidity_reserves_zero_1() {
let _post_states = add_liquidity(
AccountWithMetadataForTests::pool_definition_init_reserve_a_zero(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_init(),
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::add_max_amount_b(),
);
}
#[should_panic(expected = "Reserves must be nonzero")]
#[test]
fn test_call_add_liquidity_reserves_zero_2() {
let _post_states = add_liquidity(
AccountWithMetadataForTests::pool_definition_init_reserve_b_zero(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_init(),
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::add_max_amount_b(),
);
}
#[should_panic(expected = "Payable LP must be nonzero")]
#[test]
fn test_call_add_liquidity_payable_lp_zero() {
let _post_states = add_liquidity(
AccountWithMetadataForTests::pool_definition_add_zero_lp(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_init(),
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
BalanceForTests::add_max_amount_a_low(),
BalanceForTests::add_max_amount_b_low(),
);
}
#[test]
fn test_call_add_liquidity_chained_call_successsful() {
let (post_states, chained_calls) = add_liquidity(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_init(),
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::add_max_amount_b(),
);
let pool_post = post_states[0].clone();
assert!(
AccountWithMetadataForTests::pool_definition_add_successful().account
== *pool_post.account()
);
let chained_call_lp = chained_calls[0].clone();
let chained_call_b = chained_calls[1].clone();
let chained_call_a = chained_calls[2].clone();
assert!(chained_call_a == ChainedCallForTests::cc_add_token_a());
assert!(chained_call_b == ChainedCallForTests::cc_add_token_b());
assert!(chained_call_lp == ChainedCallForTests::cc_add_pool_lp());
}
#[should_panic(expected = "Vault A was not provided")]
#[test]
fn test_call_remove_liquidity_vault_a_omitted() {
let _post_states = remove_liquidity(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_with_wrong_id(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_init(),
NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(),
BalanceForTests::remove_min_amount_a(),
BalanceForTests::remove_min_amount_b(),
);
}
#[should_panic(expected = "Vault B was not provided")]
#[test]
fn test_call_remove_liquidity_vault_b_omitted() {
let _post_states = remove_liquidity(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_with_wrong_id(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_init(),
NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(),
BalanceForTests::remove_min_amount_a(),
BalanceForTests::remove_min_amount_b(),
);
}
#[should_panic(expected = "LP definition mismatch")]
#[test]
fn test_call_remove_liquidity_lp_def_mismatch() {
let _post_states = remove_liquidity(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_with_wrong_id(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_init(),
NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(),
BalanceForTests::remove_min_amount_a(),
BalanceForTests::remove_min_amount_b(),
);
}
#[should_panic(expected = "Invalid liquidity account provided")]
#[test]
fn test_call_remove_liquidity_insufficient_liquidity_amount() {
let _post_states = remove_liquidity(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_a(), /* different token account than lp to
* create desired
* error */
NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(),
BalanceForTests::remove_min_amount_a(),
BalanceForTests::remove_min_amount_b(),
);
}
#[should_panic(
expected = "Insufficient minimal withdraw amount (Token A) provided for liquidity amount"
)]
#[test]
fn test_call_remove_liquidity_insufficient_balance_1() {
let _post_states = remove_liquidity(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_init(),
NonZero::new(BalanceForTests::remove_amount_lp_1()).unwrap(),
BalanceForTests::remove_min_amount_a(),
BalanceForTests::remove_min_amount_b(),
);
}
#[should_panic(expected = "Pool only contains locked liquidity")]
#[test]
fn test_call_remove_liquidity_pool_at_minimum_liquidity() {
// Removing from a legacy/corrupted pool that is already at the locked floor must be rejected.
let _post_states = remove_liquidity(
AccountWithMetadataForTests::pool_definition_at_minimum_liquidity(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_with_balance(MINIMUM_LIQUIDITY),
NonZero::new(MINIMUM_LIQUIDITY).unwrap(),
1,
1,
);
}
#[should_panic(expected = "Cannot remove locked minimum liquidity")]
#[test]
fn test_call_remove_liquidity_exceeds_unlocked_supply() {
// Model corrupted ownership by giving the caller the full LP supply even though the lock
// account should permanently hold MINIMUM_LIQUIDITY. The guard must still refuse to burn
// through the permanent floor.
let _post_states = remove_liquidity(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_with_balance(BalanceForTests::lp_supply_init()),
NonZero::new(BalanceForTests::lp_supply_init()).unwrap(),
1,
1,
);
}
#[should_panic(
expected = "Insufficient minimal withdraw amount (Token B) provided for liquidity amount"
)]
#[test]
fn test_call_remove_liquidity_insufficient_balance_2() {
let _post_states = remove_liquidity(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_init(),
NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(),
BalanceForTests::remove_min_amount_a(),
BalanceForTests::remove_min_amount_b(),
);
}
#[should_panic(expected = "Minimum withdraw amount must be nonzero")]
#[test]
fn test_call_remove_liquidity_min_bal_zero_1() {
let _post_states = remove_liquidity(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_init(),
NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(),
0,
BalanceForTests::remove_min_amount_b(),
);
}
#[should_panic(expected = "Minimum withdraw amount must be nonzero")]
#[test]
fn test_call_remove_liquidity_min_bal_zero_2() {
let _post_states = remove_liquidity(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_init(),
NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(),
BalanceForTests::remove_min_amount_a(),
0,
);
}
#[test]
fn test_call_remove_liquidity_chained_call_successful() {
let (post_states, chained_calls) = remove_liquidity(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_init(),
NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(),
BalanceForTests::remove_min_amount_a(),
BalanceForTests::remove_min_amount_b_low(),
);
let pool_post = post_states[0].clone();
assert!(
AccountWithMetadataForTests::pool_definition_remove_successful().account
== *pool_post.account()
);
let chained_call_lp = chained_calls[0].clone();
let chained_call_b = chained_calls[1].clone();
let chained_call_a = chained_calls[2].clone();
assert!(chained_call_a == ChainedCallForTests::cc_remove_token_a());
assert!(chained_call_b == ChainedCallForTests::cc_remove_token_b());
assert!(chained_call_lp == ChainedCallForTests::cc_remove_pool_lp());
}
#[should_panic(expected = "Balances must be nonzero")]
#[test]
fn test_call_new_definition_with_zero_balance_1() {
let _post_states = new_definition(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::lp_lock_holding_uninit(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
NonZero::new(0).expect("Balances must be nonzero"),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
BalanceForTests::fee_tier(),
AMM_PROGRAM_ID,
);
}
#[should_panic(expected = "Balances must be nonzero")]
#[test]
fn test_call_new_definition_with_zero_balance_2() {
let _post_states = new_definition(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::lp_lock_holding_uninit(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(0).expect("Balances must be nonzero"),
BalanceForTests::fee_tier(),
AMM_PROGRAM_ID,
);
}
#[should_panic(expected = "Cannot set up a swap for a token with itself")]
#[test]
fn test_call_new_definition_same_token_definition() {
let _post_states = new_definition(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::lp_lock_holding_uninit(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
BalanceForTests::fee_tier(),
AMM_PROGRAM_ID,
);
}
#[should_panic(expected = "Liquidity pool Token Definition Account ID does not match PDA")]
#[test]
fn test_call_new_definition_wrong_liquidity_id() {
let _post_states = new_definition(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_with_wrong_id(),
AccountWithMetadataForTests::lp_lock_holding_uninit(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
BalanceForTests::fee_tier(),
AMM_PROGRAM_ID,
);
}
#[should_panic(expected = "LP lock holding Account ID does not match PDA")]
#[test]
fn test_call_new_definition_wrong_lp_lock_holding_id() {
let _post_states = new_definition(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::lp_lock_holding_with_wrong_id(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
BalanceForTests::fee_tier(),
AMM_PROGRAM_ID,
);
}
#[should_panic(expected = "Pool Definition Account ID does not match PDA")]
#[test]
fn test_call_new_definition_wrong_pool_id() {
let _post_states = new_definition(
AccountWithMetadataForTests::pool_definition_with_wrong_id(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::lp_lock_holding_uninit(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
BalanceForTests::fee_tier(),
AMM_PROGRAM_ID,
);
}
#[should_panic(expected = "Vault ID does not match PDA")]
#[test]
fn test_call_new_definition_wrong_vault_id_1() {
let _post_states = new_definition(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_with_wrong_id(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::lp_lock_holding_uninit(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
BalanceForTests::fee_tier(),
AMM_PROGRAM_ID,
);
}
#[should_panic(expected = "Vault ID does not match PDA")]
#[test]
fn test_call_new_definition_wrong_vault_id_2() {
let _post_states = new_definition(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_with_wrong_id(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::lp_lock_holding_uninit(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
BalanceForTests::fee_tier(),
AMM_PROGRAM_ID,
);
}
#[should_panic(expected = "Cannot initialize a Pool Definition with nonzero LP supply")]
#[test]
fn test_call_new_definition_cannot_initialize_nonzero_supply_pool() {
let _post_states = new_definition(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::lp_lock_holding_uninit(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
BalanceForTests::fee_tier(),
AMM_PROGRAM_ID,
);
}
#[should_panic(expected = "Initial liquidity must exceed minimum liquidity lock")]
#[test]
fn test_call_new_definition_initial_lp_too_small() {
// isqrt(1000 * 1000) = 1000 == MINIMUM_LIQUIDITY, so the assertion fires.
let _post_states = new_definition(
AccountWithMetadataForTests::pool_definition_zero_supply_reinitializable(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_reinitializable(),
AccountWithMetadataForTests::lp_lock_holding_uninit(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
NonZero::new(MINIMUM_LIQUIDITY).unwrap(),
NonZero::new(MINIMUM_LIQUIDITY).unwrap(),
BalanceForTests::fee_tier(),
AMM_PROGRAM_ID,
);
}
#[test]
fn test_call_new_definition_chained_call_successful() {
let (post_states, chained_calls) = new_definition(
AccountWithMetadataForTests::pool_definition_zero_supply_reinitializable(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_reinitializable(),
AccountWithMetadataForTests::lp_lock_holding_uninit(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
BalanceForTests::fee_tier(),
AMM_PROGRAM_ID,
);
let pool_post = post_states[0].clone();
assert!(AccountWithMetadataForTests::pool_definition_init().account == *pool_post.account());
let chained_call_lp_lock = chained_calls[0].clone();
let chained_call_lp_user = chained_calls[1].clone();
let chained_call_b = chained_calls[2].clone();
let chained_call_a = chained_calls[3].clone();
assert!(chained_call_a == ChainedCallForTests::cc_new_definition_token_a());
assert!(chained_call_b == ChainedCallForTests::cc_new_definition_token_b());
assert!(chained_call_lp_lock == ChainedCallForTests::cc_new_definition_token_lp_lock());
assert!(chained_call_lp_user == ChainedCallForTests::cc_new_definition_token_lp_user());
}
#[should_panic(expected = "AccountId is not a token type for the pool")]
#[test]
fn test_call_swap_incorrect_token_type() {
let _post_states = swap_exact_input(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::min_amount_out(),
IdForTests::token_lp_definition_id(),
);
}
#[should_panic(expected = "Vault A was not provided")]
#[test]
fn test_call_swap_vault_a_omitted() {
let _post_states = swap_exact_input(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_with_wrong_id(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::min_amount_out(),
IdForTests::token_a_definition_id(),
);
}
#[should_panic(expected = "Vault B was not provided")]
#[test]
fn test_call_swap_vault_b_omitted() {
let _post_states = swap_exact_input(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_with_wrong_id(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::min_amount_out(),
IdForTests::token_a_definition_id(),
);
}
#[should_panic(expected = "Reserve for Token A exceeds vault balance")]
#[test]
fn test_call_swap_reserves_vault_mismatch_1() {
let _post_states = swap_exact_input(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init_low(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::min_amount_out(),
IdForTests::token_a_definition_id(),
);
}
#[should_panic(expected = "Reserve for Token B exceeds vault balance")]
#[test]
fn test_call_swap_reserves_vault_mismatch_2() {
let _post_states = swap_exact_input(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init_low(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::min_amount_out(),
IdForTests::token_a_definition_id(),
);
}
#[should_panic(expected = "Pool liquidity supply is below minimum liquidity")]
#[test]
fn test_call_swap_below_minimum_liquidity() {
let _post_states = swap_exact_input(
AccountWithMetadataForTests::pool_definition_below_minimum_liquidity(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::min_amount_out(),
IdForTests::token_a_definition_id(),
);
}
#[should_panic(expected = "Fee tier must be one of 1, 5, 30, or 100 basis points")]
#[test]
fn test_call_swap_rejects_unsupported_fee_tier() {
let mut pool = AccountWithMetadataForTests::pool_definition_init();
let mut pool_def = PoolDefinition::try_from(&pool.account.data).unwrap();
pool_def.fees = 2;
pool.account.data = Data::from(&pool_def);
let _post_states = swap_exact_input(
pool,
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::add_max_amount_a_low(),
IdForTests::token_a_definition_id(),
);
}
#[should_panic(expected = "Withdraw amount is less than minimal amount out")]
#[test]
fn test_call_swap_below_min_out() {
let _post_states = swap_exact_input(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::min_amount_out_too_high(),
IdForTests::token_a_definition_id(),
);
}
#[test]
fn test_call_swap_chained_call_successful_1() {
let (post_states, chained_calls) = swap_exact_input(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::add_max_amount_a_low(),
IdForTests::token_a_definition_id(),
);
let pool_post = post_states[0].clone();
assert!(
AccountWithMetadataForTests::pool_definition_swap_test_1().account == *pool_post.account()
);
let chained_call_a = chained_calls[0].clone();
let chained_call_b = chained_calls[1].clone();
assert_eq!(
chained_call_a,
ChainedCallForTests::cc_swap_token_a_test_1()
);
assert_eq!(
chained_call_b,
ChainedCallForTests::cc_swap_token_b_test_1()
);
}
#[test]
fn test_call_swap_chained_call_successful_2() {
let (post_states, chained_calls) = swap_exact_input(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::add_max_amount_b(),
BalanceForTests::min_amount_out(),
IdForTests::token_b_definition_id(),
);
let pool_post = post_states[0].clone();
assert!(
AccountWithMetadataForTests::pool_definition_swap_test_2().account == *pool_post.account()
);
let chained_call_a = chained_calls[1].clone();
let chained_call_b = chained_calls[0].clone();
assert_eq!(
chained_call_a,
ChainedCallForTests::cc_swap_token_a_test_2()
);
assert_eq!(
chained_call_b,
ChainedCallForTests::cc_swap_token_b_test_2()
);
}
#[should_panic(expected = "AccountId is not a token type for the pool")]
#[test]
fn call_swap_exact_output_incorrect_token_type() {
let _post_states = swap_exact_output(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::max_amount_in(),
IdForTests::token_lp_definition_id(),
);
}
#[should_panic(expected = "Vault A was not provided")]
#[test]
fn call_swap_exact_output_vault_a_omitted() {
let _post_states = swap_exact_output(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_with_wrong_id(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::max_amount_in(),
IdForTests::token_a_definition_id(),
);
}
#[should_panic(expected = "Vault B was not provided")]
#[test]
fn call_swap_exact_output_vault_b_omitted() {
let _post_states = swap_exact_output(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_with_wrong_id(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::max_amount_in(),
IdForTests::token_a_definition_id(),
);
}
#[should_panic(expected = "Reserve for Token A exceeds vault balance")]
#[test]
fn call_swap_exact_output_reserves_vault_mismatch_1() {
let _post_states = swap_exact_output(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init_low(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::max_amount_in(),
IdForTests::token_a_definition_id(),
);
}
#[should_panic(expected = "Reserve for Token B exceeds vault balance")]
#[test]
fn call_swap_exact_output_reserves_vault_mismatch_2() {
let _post_states = swap_exact_output(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init_low(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::max_amount_in(),
IdForTests::token_a_definition_id(),
);
}
#[should_panic(expected = "Pool liquidity supply is below minimum liquidity")]
#[test]
fn call_swap_exact_output_below_minimum_liquidity() {
let _post_states = swap_exact_output(
AccountWithMetadataForTests::pool_definition_below_minimum_liquidity(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::max_amount_in(),
IdForTests::token_a_definition_id(),
);
}
#[should_panic(expected = "Required input exceeds maximum amount in")]
#[test]
fn call_swap_exact_output_exceeds_max_in() {
let _post_states = swap_exact_output(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
166_u128,
100_u128,
IdForTests::token_a_definition_id(),
);
}
#[should_panic(expected = "Exact amount out must be nonzero")]
#[test]
fn call_swap_exact_output_zero() {
let _post_states = swap_exact_output(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
0_u128,
500_u128,
IdForTests::token_a_definition_id(),
);
}
#[should_panic(expected = "Exact amount out exceeds reserve")]
#[test]
fn call_swap_exact_output_exceeds_reserve() {
let _post_states = swap_exact_output(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::vault_b_reserve_init(),
BalanceForTests::max_amount_in(),
IdForTests::token_a_definition_id(),
);
}
#[test]
fn call_swap_exact_output_chained_call_successful() {
let (post_states, chained_calls) = swap_exact_output(
AccountWithMetadataForTests::pool_definition_swap_exact_output_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::max_amount_in(),
BalanceForTests::vault_b_reserve_init(),
IdForTests::token_a_definition_id(),
);
let pool_post = post_states[0].clone();
assert!(
AccountWithMetadataForTests::pool_definition_swap_exact_output_test_1().account
== *pool_post.account()
);
let chained_call_a = chained_calls[0].clone();
let chained_call_b = chained_calls[1].clone();
assert_eq!(
chained_call_a,
ChainedCallForTests::cc_swap_exact_output_token_a_test_1()
);
assert_eq!(
chained_call_b,
ChainedCallForTests::cc_swap_exact_output_token_b_test_1()
);
}
#[test]
fn call_swap_exact_output_chained_call_successful_2() {
let (post_states, chained_calls) = swap_exact_output(
AccountWithMetadataForTests::pool_definition_swap_exact_output_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
285,
300,
IdForTests::token_b_definition_id(),
);
let pool_post = post_states[0].clone();
assert!(
AccountWithMetadataForTests::pool_definition_swap_exact_output_test_2().account
== *pool_post.account()
);
let chained_call_a = chained_calls[1].clone();
let chained_call_b = chained_calls[0].clone();
assert_eq!(
chained_call_a,
ChainedCallForTests::cc_swap_exact_output_token_a_test_2()
);
assert_eq!(
chained_call_b,
ChainedCallForTests::cc_swap_exact_output_token_b_test_2()
);
}
// Without the fix, `reserve_a * exact_amount_out` silently wraps to 0 in release mode,
// making `deposit_amount = 0`. The slippage check `0 <= max_amount_in` always passes,
// so an attacker receives `exact_amount_out` tokens while paying nothing.
#[should_panic(expected = "reserve * amount_out overflows u128")]
#[test]
fn swap_exact_output_overflow_protection() {
// reserve_a chosen so that reserve_a * 2 overflows u128:
// (u128::MAX / 2 + 1) * 2 = u128::MAX + 1 → wraps to 0
let large_reserve: u128 = u128::MAX / 2 + 1;
let reserve_b: u128 = 1_000;
let pool = AccountWithMetadata {
account: Account {
program_owner: ProgramId::default(),
balance: 0,
data: Data::from(&PoolDefinition {
definition_token_a_id: IdForTests::token_a_definition_id(),
definition_token_b_id: IdForTests::token_b_definition_id(),
vault_a_id: IdForTests::vault_a_id(),
vault_b_id: IdForTests::vault_b_id(),
liquidity_pool_id: IdForTests::token_lp_definition_id(),
liquidity_pool_supply: MINIMUM_LIQUIDITY,
reserve_a: large_reserve,
reserve_b,
fees: BalanceForTests::fee_tier(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
};
let vault_a = AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_a_definition_id(),
balance: large_reserve,
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::vault_a_id(),
};
let vault_b = AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_b_definition_id(),
balance: reserve_b,
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::vault_b_id(),
};
let _result = swap_exact_output(
pool,
vault_a,
vault_b,
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
2, // exact_amount_out: small, valid (< reserve_b)
1, // max_amount_in: tiny — real deposit would be enormous, but
// overflow wraps it to 0, making 0 <= 1 pass silently
IdForTests::token_a_definition_id(),
);
}
#[test]
fn test_new_definition_lp_asymmetric_amounts() {
let (post_states, chained_calls) = new_definition(
AccountWithMetadataForTests::pool_definition_zero_supply_reinitializable(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_reinitializable(),
AccountWithMetadataForTests::lp_lock_holding_uninit(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
BalanceForTests::fee_tier(),
AMM_PROGRAM_ID,
);
// check the minted LP amount
let pool_post = post_states[0].clone();
let pool_def = PoolDefinition::try_from(&pool_post.account().data).unwrap();
assert_eq!(
pool_def.liquidity_pool_supply,
BalanceForTests::lp_supply_init()
);
let chained_call_lp_lock = chained_calls[0].clone();
let chained_call_lp_user = chained_calls[1].clone();
assert!(chained_call_lp_lock == ChainedCallForTests::cc_new_definition_token_lp_lock());
assert!(chained_call_lp_user == ChainedCallForTests::cc_new_definition_token_lp_user());
}
#[test]
fn test_new_definition_lp_symmetric_amounts() {
// token_a=2000, token_b=2000 → LP=sqrt(4_000_000)=2000
let token_a_amount = 2_000u128;
let token_b_amount = 2_000u128;
let expected_lp = (token_a_amount * token_b_amount).isqrt();
assert_eq!(expected_lp, 2_000);
let (post_states, chained_calls) = new_definition(
AccountWithMetadataForTests::pool_definition_zero_supply_reinitializable(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_reinitializable(),
AccountWithMetadataForTests::lp_lock_holding_uninit(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
NonZero::new(token_a_amount).unwrap(),
NonZero::new(token_b_amount).unwrap(),
BalanceForTests::fee_tier(),
AMM_PROGRAM_ID,
);
let pool_post = post_states[0].clone();
let pool_def = PoolDefinition::try_from(&pool_post.account().data).unwrap();
assert_eq!(pool_def.liquidity_pool_supply, expected_lp);
let chained_call_lp_lock = chained_calls[0].clone();
let chained_call_lp_user = chained_calls[1].clone();
let mut pool_lp_auth = AccountForTests::pool_lp_reinitializable();
pool_lp_auth.is_authorized = true;
let expected_lp_lock_call = ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![
pool_lp_auth.clone(),
AccountForTests::lp_lock_holding_uninit(),
],
&token_core::Instruction::Mint {
amount_to_mint: MINIMUM_LIQUIDITY,
},
)
.with_pda_seeds(vec![compute_liquidity_token_pda_seed(
IdForTests::pool_definition_id(),
)]);
let expected_lp_user_call = ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![
AccountForTests::pool_lp_reinitialized_after_lock(),
AccountForTests::user_holding_lp_uninit(),
],
&token_core::Instruction::Mint {
amount_to_mint: expected_lp - MINIMUM_LIQUIDITY,
},
)
.with_pda_seeds(vec![compute_liquidity_token_pda_seed(
IdForTests::pool_definition_id(),
)]);
assert_eq!(chained_call_lp_lock, expected_lp_lock_call);
assert_eq!(chained_call_lp_user, expected_lp_user_call);
}
#[should_panic(expected = "Cannot initialize a Pool Definition with nonzero LP supply")]
#[test]
fn test_call_new_definition_reinitialization_requires_zero_pool_supply() {
let _post_states = new_definition(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_reinitializable(),
AccountWithMetadataForTests::lp_lock_holding_uninit(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
BalanceForTests::fee_tier(),
AMM_PROGRAM_ID,
);
}
#[should_panic(
expected = "New definition: existing LP Token Definition Account must have zero supply before reinitialization"
)]
#[test]
fn test_call_new_definition_reinitialization_requires_zero_lp_definition_supply() {
let _post_states = new_definition(
AccountWithMetadataForTests::pool_definition_zero_supply_reinitializable(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::lp_lock_holding_uninit(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
BalanceForTests::fee_tier(),
AMM_PROGRAM_ID,
);
}
#[test]
fn test_minimum_liquidity_lock_and_remove_all_user_lp() {
let pool_uninitialized = AccountWithMetadata {
account: Account::default(),
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
};
let token_a_amount = BalanceForTests::vault_a_reserve_init();
let token_b_amount = BalanceForTests::vault_b_reserve_init();
let initial_lp = (token_a_amount * token_b_amount).isqrt();
let user_lp = initial_lp - MINIMUM_LIQUIDITY;
let (post_states, chained_calls) = new_definition(
pool_uninitialized,
AccountForTests::vault_a_init(),
AccountForTests::vault_b_init(),
AccountForTests::pool_lp_uninit(),
AccountForTests::lp_lock_holding_uninit(),
AccountForTests::user_holding_a(),
AccountForTests::user_holding_b(),
AccountForTests::user_holding_lp_uninit(),
NonZero::new(token_a_amount).unwrap(),
NonZero::new(token_b_amount).unwrap(),
BalanceForTests::fee_tier(),
AMM_PROGRAM_ID,
);
let mut pool_lp_auth = AccountForTests::pool_lp_uninit();
pool_lp_auth.is_authorized = true;
let expected_lock_call = ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![
pool_lp_auth.clone(),
AccountForTests::lp_lock_holding_uninit(),
],
&token_core::Instruction::NewFungibleDefinition {
name: String::from("LP Token"),
total_supply: MINIMUM_LIQUIDITY,
},
)
.with_pda_seeds(vec![compute_liquidity_token_pda_seed(
IdForTests::pool_definition_id(),
)]);
let expected_user_call = ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![
AccountForTests::pool_lp_created_after_lock(),
AccountForTests::user_holding_lp_uninit(),
],
&token_core::Instruction::Mint {
amount_to_mint: user_lp,
},
)
.with_pda_seeds(vec![compute_liquidity_token_pda_seed(
IdForTests::pool_definition_id(),
)]);
assert_eq!(chained_calls[0], expected_lock_call);
assert_eq!(chained_calls[1], expected_user_call);
let pool_post = PoolDefinition::try_from(&post_states[0].account().data).unwrap();
assert_eq!(pool_post.liquidity_pool_supply, initial_lp);
let pool_for_remove = AccountWithMetadata {
account: post_states[0].account().clone(),
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
};
let (remove_post_states, _) = remove_liquidity(
pool_for_remove,
AccountForTests::vault_a_init(),
AccountForTests::vault_b_init(),
AccountForTests::pool_lp_init(),
AccountForTests::user_holding_a(),
AccountForTests::user_holding_b(),
AccountForTests::user_holding_lp_with_balance(user_lp),
NonZero::new(user_lp).unwrap(),
1,
1,
);
let pool_after_remove =
PoolDefinition::try_from(&remove_post_states[0].account().data).unwrap();
assert_eq!(pool_after_remove.liquidity_pool_supply, MINIMUM_LIQUIDITY);
assert!(pool_after_remove.reserve_a > 0);
assert!(pool_after_remove.reserve_b > 0);
}
#[test]
fn test_sync_reserves_with_donation() {
let pool = AccountWithMetadataForTests::pool_definition_init();
let donation_a = 111u128;
let mut donated_vault_a = AccountWithMetadataForTests::vault_a_init();
donated_vault_a.account.data = Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_a_definition_id(),
balance: BalanceForTests::vault_a_reserve_init() + donation_a,
});
let pool_pre = PoolDefinition::try_from(&pool.account.data).unwrap();
assert_eq!(pool_pre.reserve_a, BalanceForTests::vault_a_reserve_init());
let (post_states, chained_calls) = sync_reserves(
pool,
donated_vault_a,
AccountWithMetadataForTests::vault_b_init(),
);
assert!(chained_calls.is_empty());
let pool_post = PoolDefinition::try_from(&post_states[0].account().data).unwrap();
assert_eq!(
pool_post.reserve_a,
BalanceForTests::vault_a_reserve_init() + donation_a
);
assert_eq!(pool_post.reserve_b, BalanceForTests::vault_b_reserve_init());
}
#[should_panic(expected = "Sync reserves: vault A balance is less than its reserve")]
#[test]
fn test_sync_reserves_panics_when_vault_a_under_collateralized() {
let _ = sync_reserves(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init_low(),
AccountWithMetadataForTests::vault_b_init(),
);
}
#[should_panic(expected = "Sync reserves: vault B balance is less than its reserve")]
#[test]
fn test_sync_reserves_panics_when_vault_b_under_collateralized() {
let _ = sync_reserves(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init_low(),
);
}
#[should_panic(expected = "Pool liquidity supply is below minimum liquidity")]
#[test]
fn test_sync_reserves_rejects_pool_below_minimum_liquidity() {
let _ = sync_reserves(
AccountWithMetadataForTests::pool_definition_below_minimum_liquidity(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
);
}
#[test]
fn test_donation_then_add_liquidity_sync_mitigates_mispricing() {
let donation_a = 100u128;
let mut donated_vault_a = AccountWithMetadataForTests::vault_a_init();
donated_vault_a.account.data = Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_a_definition_id(),
balance: BalanceForTests::vault_a_reserve_init() + donation_a,
});
let donated_vault_b = AccountWithMetadataForTests::vault_b_init();
let (post_unsynced, _) = add_liquidity(
AccountWithMetadataForTests::pool_definition_init(),
donated_vault_a.clone(),
donated_vault_b.clone(),
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_init(),
NonZero::new(1).unwrap(),
100,
50,
);
let unsynced_pool_post = PoolDefinition::try_from(&post_unsynced[0].account().data).unwrap();
let unsynced_delta_lp =
unsynced_pool_post.liquidity_pool_supply - BalanceForTests::lp_supply_init();
let donated_vault_a_for_synced_add = donated_vault_a.clone();
let donated_vault_b_for_synced_add = donated_vault_b.clone();
let (sync_post, _) = sync_reserves(
AccountWithMetadataForTests::pool_definition_init(),
donated_vault_a,
donated_vault_b,
);
let synced_pool = AccountWithMetadata {
account: sync_post[0].account().clone(),
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
};
let (post_synced, _) = add_liquidity(
synced_pool,
donated_vault_a_for_synced_add,
donated_vault_b_for_synced_add,
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_init(),
NonZero::new(1).unwrap(),
100,
50,
);
let synced_pool_post = PoolDefinition::try_from(&post_synced[0].account().data).unwrap();
let synced_delta_lp = synced_pool_post.liquidity_pool_supply
- PoolDefinition::try_from(&sync_post[0].account().data)
.unwrap()
.liquidity_pool_supply;
assert!(synced_delta_lp < unsynced_delta_lp);
}
#[should_panic(expected = "token_a * token_b overflows u128")]
#[test]
fn new_definition_overflow_protection() {
let large_amount = u128::MAX / 2 + 1;
let _result = new_definition(
AccountWithMetadataForTests::pool_definition_zero_supply_reinitializable(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_reinitializable(),
AccountWithMetadataForTests::lp_lock_holding_uninit(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
NonZero::new(large_amount).unwrap(),
NonZero::new(2).unwrap(),
BalanceForTests::fee_tier(),
AMM_PROGRAM_ID,
);
}
#[should_panic(expected = "reserve_a * max_amount_b overflows u128")]
#[test]
fn add_liquidity_overflow_protection() {
let large_reserve: u128 = u128::MAX / 2 + 1;
let reserve_b: u128 = 1_000;
let pool = AccountWithMetadata {
account: Account {
program_owner: ProgramId::default(),
balance: 0,
data: Data::from(&PoolDefinition {
definition_token_a_id: IdForTests::token_a_definition_id(),
definition_token_b_id: IdForTests::token_b_definition_id(),
vault_a_id: IdForTests::vault_a_id(),
vault_b_id: IdForTests::vault_b_id(),
liquidity_pool_id: IdForTests::token_lp_definition_id(),
liquidity_pool_supply: MINIMUM_LIQUIDITY,
reserve_a: large_reserve,
reserve_b,
fees: BalanceForTests::fee_tier(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
};
let vault_a = AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_a_definition_id(),
balance: large_reserve,
}),
nonce: Nonce(0),
},
is_authorized: false,
account_id: IdForTests::vault_a_id(),
};
let vault_b = AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_b_definition_id(),
balance: reserve_b,
}),
nonce: Nonce(0),
},
is_authorized: false,
account_id: IdForTests::vault_b_id(),
};
let _result = add_liquidity(
pool,
vault_a,
vault_b,
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_init(),
NonZero::new(1).unwrap(),
500,
2, // max_amount_b=2 → reserve_a * 2 overflows
);
}
#[should_panic(expected = "reserve_a * remove_liquidity_amount overflows u128")]
#[test]
fn remove_liquidity_overflow_protection() {
let large_reserve: u128 = u128::MAX / 2 + 1;
let reserve_b: u128 = 1_000;
let lp_supply: u128 = 1_002; // must exceed MINIMUM_LIQUIDITY so remove_amount=2 passes the lock check
let pool = AccountWithMetadata {
account: Account {
program_owner: ProgramId::default(),
balance: 0,
data: Data::from(&PoolDefinition {
definition_token_a_id: IdForTests::token_a_definition_id(),
definition_token_b_id: IdForTests::token_b_definition_id(),
vault_a_id: IdForTests::vault_a_id(),
vault_b_id: IdForTests::vault_b_id(),
liquidity_pool_id: IdForTests::token_lp_definition_id(),
liquidity_pool_supply: lp_supply,
reserve_a: large_reserve,
reserve_b,
fees: BalanceForTests::fee_tier(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
};
let vault_a = AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_a_definition_id(),
balance: large_reserve,
}),
nonce: Nonce(0),
},
is_authorized: false,
account_id: IdForTests::vault_a_id(),
};
let vault_b = AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_b_definition_id(),
balance: reserve_b,
}),
nonce: Nonce(0),
},
is_authorized: false,
account_id: IdForTests::vault_b_id(),
};
let user_lp = AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_lp_definition_id(),
balance: 2,
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::user_token_lp_id(),
};
let _result = remove_liquidity(
pool,
vault_a,
vault_b,
AccountWithMetadataForTests::pool_lp_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
user_lp,
NonZero::new(2).unwrap(), // remove_amount=2 → reserve_a * 2 overflows
1,
1,
);
}
#[should_panic(expected = "reserve * amount_in overflows u128")]
#[test]
fn swap_exact_input_overflow_protection() {
let large_reserve: u128 = u128::MAX / 2 + 1;
let reserve_b: u128 = 1_000;
let pool = AccountWithMetadata {
account: Account {
program_owner: ProgramId::default(),
balance: 0,
data: Data::from(&PoolDefinition {
definition_token_a_id: IdForTests::token_a_definition_id(),
definition_token_b_id: IdForTests::token_b_definition_id(),
vault_a_id: IdForTests::vault_a_id(),
vault_b_id: IdForTests::vault_b_id(),
liquidity_pool_id: IdForTests::token_lp_definition_id(),
liquidity_pool_supply: MINIMUM_LIQUIDITY,
reserve_a: 1_000,
reserve_b: large_reserve,
fees: BalanceForTests::fee_tier(),
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
};
let vault_a = AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_a_definition_id(),
balance: reserve_b,
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::vault_a_id(),
};
let vault_b = AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_b_definition_id(),
balance: large_reserve,
}),
nonce: Nonce(0),
},
is_authorized: true,
account_id: IdForTests::vault_b_id(),
};
// Swap token_a in: withdraw_amount = reserve_b * swap_amount_in / (reserve_a + swap_amount_in)
// reserve_b is large, so reserve_b * 2 overflows
let _result = swap_exact_input(
pool,
vault_a,
vault_b,
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
2,
1,
IdForTests::token_a_definition_id(),
);
}
#[test]
fn test_new_definition_supports_all_fee_tiers() {
for fees in [
FEE_TIER_BPS_1,
FEE_TIER_BPS_5,
FEE_TIER_BPS_30,
FEE_TIER_BPS_100,
] {
let (post_states, _) = new_definition(
AccountWithMetadataForTests::pool_definition_zero_supply_reinitializable(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_reinitializable(),
AccountWithMetadataForTests::lp_lock_holding_uninit(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
fees,
AMM_PROGRAM_ID,
);
let pool_post = post_states[0].clone();
let pool_def = PoolDefinition::try_from(&pool_post.account().data).unwrap();
assert_eq!(pool_def.fees, fees);
}
}
#[should_panic(expected = "Fee tier must be one of 1, 5, 30, or 100 basis points")]
#[test]
fn test_new_definition_rejects_unsupported_fee_tier() {
let _ = new_definition(
AccountWithMetadataForTests::pool_definition_zero_supply_reinitializable(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::pool_lp_reinitializable(),
AccountWithMetadataForTests::lp_lock_holding_uninit(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
2,
AMM_PROGRAM_ID,
);
}