feat(amm): add configurable fee tiers

- accept a supported fee tier in pool creation
- store fee tiers in AMM pool state and validate them
- update AMM tests and IDL for the new pool creation argument
This commit is contained in:
Ricardo Guilherme Schmidt 2026-03-31 20:45:57 -03:00 committed by r4bbit
parent d0f398814c
commit 9824cd8f90
9 changed files with 274 additions and 120 deletions

View File

@ -63,6 +63,10 @@
"name": "token_b_amount",
"type": "u128"
},
{
"name": "fees",
"type": "u128"
},
{
"name": "amm_program_id",
"type": "program_id"

View File

@ -33,6 +33,7 @@ pub enum Instruction {
NewDefinition {
token_a_amount: u128,
token_b_amount: u128,
fees: u128,
amm_program_id: ProgramId,
},
@ -121,7 +122,7 @@ pub struct PoolDefinition {
pub liquidity_pool_supply: u128,
pub reserve_a: u128,
pub reserve_b: u128,
/// Fees are currently not used
/// Fee tier in basis points.
pub fees: u128,
/// Indicates whether the pool is initialized for use.
/// `MINIMUM_LIQUIDITY` LP tokens are permanently locked at initialization
@ -133,6 +134,26 @@ pub struct PoolDefinition {
pub active: bool,
}
pub const FEE_BPS_DENOMINATOR: u128 = 10_000;
pub const FEE_TIER_BPS_1: u128 = 1;
pub const FEE_TIER_BPS_5: u128 = 5;
pub const FEE_TIER_BPS_30: u128 = 30;
pub const FEE_TIER_BPS_100: u128 = 100;
pub fn is_supported_fee_tier(fees: u128) -> bool {
matches!(
fees,
FEE_TIER_BPS_1 | FEE_TIER_BPS_5 | FEE_TIER_BPS_30 | FEE_TIER_BPS_100
)
}
pub fn assert_supported_fee_tier(fees: u128) {
assert!(
is_supported_fee_tier(fees),
"Fee tier must be one of 1, 5, 30, or 100 basis points"
);
}
impl TryFrom<&Data> for PoolDefinition {
type Error = std::io::Error;

View File

@ -28,6 +28,7 @@ mod amm {
user_holding_lp: AccountWithMetadata,
token_a_amount: u128,
token_b_amount: u128,
fees: u128,
amm_program_id: ProgramId,
) -> SpelResult {
let (post_states, chained_calls) = amm_program::new_definition::new_definition(
@ -41,6 +42,7 @@ mod amm {
user_holding_lp,
NonZeroU128::new(token_a_amount).expect("token_a_amount must be nonzero"),
NonZeroU128::new(token_b_amount).expect("token_b_amount must be nonzero"),
fees,
amm_program_id,
);
Ok(SpelOutput::with_chained_calls(post_states, chained_calls))

View File

@ -1,6 +1,6 @@
use std::num::NonZeroU128;
use amm_core::{compute_liquidity_token_pda_seed, PoolDefinition};
use amm_core::{assert_supported_fee_tier, compute_liquidity_token_pda_seed, PoolDefinition};
use nssa_core::{
account::{AccountWithMetadata, Data},
program::{AccountPostState, ChainedCall},
@ -22,6 +22,7 @@ pub fn add_liquidity(
// 1. Fetch Pool state
let pool_def_data = PoolDefinition::try_from(&pool.account.data)
.expect("Add liquidity: AMM Program expects valid Pool Definition Account");
assert_supported_fee_tier(pool_def_data.fees);
assert_eq!(
vault_a.account_id, pool_def_data.vault_a_id,

View File

@ -1,8 +1,9 @@
use std::num::NonZeroU128;
use amm_core::{
compute_liquidity_token_pda, compute_liquidity_token_pda_seed, compute_lp_lock_holding_pda,
compute_pool_pda, compute_vault_pda, PoolDefinition, MINIMUM_LIQUIDITY,
assert_supported_fee_tier, compute_liquidity_token_pda, compute_liquidity_token_pda_seed,
compute_lp_lock_holding_pda, compute_pool_pda, compute_vault_pda, PoolDefinition,
MINIMUM_LIQUIDITY,
};
use nssa_core::{
account::{Account, AccountWithMetadata, Data},
@ -22,6 +23,7 @@ pub fn new_definition(
user_holding_lp: AccountWithMetadata,
token_a_amount: NonZeroU128,
token_b_amount: NonZeroU128,
fees: u128,
amm_program_id: ProgramId,
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
// Verify token_a and token_b are different
@ -68,6 +70,7 @@ pub fn new_definition(
compute_lp_lock_holding_pda(amm_program_id, pool.account_id),
"LP lock holding Account ID does not match PDA"
);
assert_supported_fee_tier(fees);
// TODO: return here
// Verify that Pool Account is not active
@ -113,7 +116,7 @@ pub fn new_definition(
liquidity_pool_supply: initial_lp,
reserve_a: token_a_amount.into(),
reserve_b: token_b_amount.into(),
fees: 0u128, // TODO: we assume all fees are 0 for now.
fees,
active: true,
};

View File

@ -1,7 +1,8 @@
use std::num::NonZeroU128;
use amm_core::{
compute_liquidity_token_pda_seed, compute_vault_pda_seed, PoolDefinition, MINIMUM_LIQUIDITY,
assert_supported_fee_tier, compute_liquidity_token_pda_seed, compute_vault_pda_seed,
PoolDefinition, MINIMUM_LIQUIDITY,
};
use nssa_core::{
account::{AccountWithMetadata, Data},
@ -26,6 +27,7 @@ pub fn remove_liquidity(
// 1. Fetch Pool state
let pool_def_data = PoolDefinition::try_from(&pool.account.data)
.expect("Remove liquidity: AMM Program expects a valid Pool Definition Account");
assert_supported_fee_tier(pool_def_data.fees);
assert!(pool_def_data.active, "Pool is inactive");
assert_eq!(

View File

@ -1,3 +1,4 @@
use amm_core::assert_supported_fee_tier;
pub use amm_core::{compute_liquidity_token_pda_seed, compute_vault_pda_seed, PoolDefinition};
use nssa_core::{
account::{AccountId, AccountWithMetadata, Data},
@ -12,6 +13,7 @@ fn validate_swap_setup(
) -> PoolDefinition {
let pool_def_data = PoolDefinition::try_from(&pool.account.data)
.expect("AMM Program expects a valid Pool Definition Account");
assert_supported_fee_tier(pool_def_data.fees);
assert!(pool_def_data.active, "Pool is inactive");
assert_eq!(

View File

@ -4,7 +4,8 @@ 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, MINIMUM_LIQUIDITY,
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},
@ -30,6 +31,10 @@ struct AccountWithMetadataForTests;
type AccountForTests = AccountWithMetadataForTests;
impl BalanceForTests {
fn fee_tier() -> u128 {
FEE_TIER_BPS_30
}
fn vault_a_reserve_init() -> u128 {
5_000
}
@ -869,7 +874,7 @@ impl AccountWithMetadataForTests {
liquidity_pool_supply: BalanceForTests::lp_supply_init(),
reserve_a: BalanceForTests::vault_a_reserve_init(),
reserve_b: BalanceForTests::vault_b_reserve_init(),
fees: 0u128,
fees: BalanceForTests::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -897,7 +902,7 @@ impl AccountWithMetadataForTests {
liquidity_pool_supply: BalanceForTests::lp_supply_init(),
reserve_a: 1_000,
reserve_b: 500,
fees: 0u128,
fees: BalanceForTests::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -921,7 +926,7 @@ impl AccountWithMetadataForTests {
liquidity_pool_supply: BalanceForTests::lp_supply_init(),
reserve_a: 0,
reserve_b: BalanceForTests::vault_b_reserve_init(),
fees: 0u128,
fees: BalanceForTests::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -945,7 +950,7 @@ impl AccountWithMetadataForTests {
liquidity_pool_supply: BalanceForTests::lp_supply_init(),
reserve_a: BalanceForTests::vault_a_reserve_init(),
reserve_b: 0,
fees: 0u128,
fees: BalanceForTests::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -969,7 +974,7 @@ impl AccountWithMetadataForTests {
liquidity_pool_supply: BalanceForTests::vault_a_reserve_low(),
reserve_a: BalanceForTests::vault_a_reserve_low(),
reserve_b: BalanceForTests::vault_b_reserve_high(),
fees: 0u128,
fees: BalanceForTests::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -993,7 +998,7 @@ impl AccountWithMetadataForTests {
liquidity_pool_supply: BalanceForTests::vault_a_reserve_high(),
reserve_a: BalanceForTests::vault_a_reserve_high(),
reserve_b: BalanceForTests::vault_b_reserve_low(),
fees: 0u128,
fees: BalanceForTests::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -1017,7 +1022,7 @@ impl AccountWithMetadataForTests {
liquidity_pool_supply: BalanceForTests::lp_supply_init(),
reserve_a: BalanceForTests::vault_a_swap_test_1(),
reserve_b: BalanceForTests::vault_b_swap_test_1(),
fees: 0u128,
fees: BalanceForTests::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -1041,7 +1046,7 @@ impl AccountWithMetadataForTests {
liquidity_pool_supply: BalanceForTests::lp_supply_init(),
reserve_a: BalanceForTests::vault_a_swap_test_2(),
reserve_b: BalanceForTests::vault_b_swap_test_2(),
fees: 0u128,
fees: BalanceForTests::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -1065,7 +1070,7 @@ impl AccountWithMetadataForTests {
liquidity_pool_supply: BalanceForTests::lp_supply_init(),
reserve_a: 1498_u128,
reserve_b: 334_u128,
fees: 0_u128,
fees: BalanceForTests::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -1089,7 +1094,7 @@ impl AccountWithMetadataForTests {
liquidity_pool_supply: BalanceForTests::lp_supply_init(),
reserve_a: 715_u128,
reserve_b: 700_u128,
fees: 0_u128,
fees: BalanceForTests::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -1113,7 +1118,7 @@ impl AccountWithMetadataForTests {
liquidity_pool_supply: BalanceForTests::vault_a_reserve_low(),
reserve_a: BalanceForTests::vault_a_reserve_init(),
reserve_b: BalanceForTests::vault_b_reserve_init(),
fees: 0u128,
fees: BalanceForTests::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -1137,7 +1142,7 @@ impl AccountWithMetadataForTests {
liquidity_pool_supply: BalanceForTests::add_lp_supply_successful(),
reserve_a: BalanceForTests::vault_a_add_successful(),
reserve_b: BalanceForTests::vault_b_add_successful(),
fees: 0u128,
fees: BalanceForTests::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -1161,7 +1166,7 @@ impl AccountWithMetadataForTests {
liquidity_pool_supply: BalanceForTests::remove_lp_supply_successful(),
reserve_a: BalanceForTests::vault_a_remove_successful(),
reserve_b: BalanceForTests::vault_b_remove_successful(),
fees: 0u128,
fees: BalanceForTests::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -1185,7 +1190,7 @@ impl AccountWithMetadataForTests {
liquidity_pool_supply: BalanceForTests::lp_supply_init(),
reserve_a: BalanceForTests::vault_a_reserve_init(),
reserve_b: BalanceForTests::vault_b_reserve_init(),
fees: 0u128,
fees: BalanceForTests::fee_tier(),
active: false,
}),
nonce: Nonce(0),
@ -1233,7 +1238,7 @@ impl AccountWithMetadataForTests {
liquidity_pool_supply: BalanceForTests::lp_supply_init(),
reserve_a: BalanceForTests::vault_a_reserve_init(),
reserve_b: BalanceForTests::vault_b_reserve_init(),
fees: 0u128,
fees: BalanceForTests::fee_tier(),
active: false,
}),
nonce: Nonce(0),
@ -1289,7 +1294,7 @@ impl AccountWithMetadataForTests {
liquidity_pool_supply: BalanceForTests::lp_supply_init(),
reserve_a: BalanceForTests::vault_a_reserve_init(),
reserve_b: BalanceForTests::vault_b_reserve_init(),
fees: 0u128,
fees: BalanceForTests::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -1315,7 +1320,7 @@ impl AccountWithMetadataForTests {
liquidity_pool_supply: MINIMUM_LIQUIDITY,
reserve_a: BalanceForTests::vault_a_reserve_init(),
reserve_b: BalanceForTests::vault_b_reserve_init(),
fees: 0u128,
fees: BalanceForTests::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -1801,6 +1806,7 @@ fn test_call_new_definition_with_zero_balance_1() {
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,
);
}
@ -1819,6 +1825,7 @@ fn test_call_new_definition_with_zero_balance_2() {
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,
);
}
@ -1837,6 +1844,7 @@ fn test_call_new_definition_same_token_definition() {
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,
);
}
@ -1855,6 +1863,7 @@ fn test_call_new_definition_wrong_liquidity_id() {
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,
);
}
@ -1873,6 +1882,7 @@ fn test_call_new_definition_wrong_lp_lock_holding_id() {
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,
);
}
@ -1891,6 +1901,7 @@ fn test_call_new_definition_wrong_pool_id() {
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,
);
}
@ -1909,6 +1920,7 @@ fn test_call_new_definition_wrong_vault_id_1() {
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,
);
}
@ -1927,6 +1939,7 @@ fn test_call_new_definition_wrong_vault_id_2() {
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,
);
}
@ -1945,6 +1958,7 @@ fn test_call_new_definition_cannot_initialize_active_pool() {
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,
);
}
@ -1964,6 +1978,7 @@ fn test_call_new_definition_initial_lp_too_small() {
AccountWithMetadataForTests::user_holding_lp_uninit(),
NonZero::new(MINIMUM_LIQUIDITY).unwrap(),
NonZero::new(MINIMUM_LIQUIDITY).unwrap(),
BalanceForTests::fee_tier(),
AMM_PROGRAM_ID,
);
}
@ -1981,6 +1996,7 @@ fn test_call_new_definition_chained_call_successful() {
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,
);
@ -2089,6 +2105,26 @@ fn test_call_swap_ianctive() {
);
}
#[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() {
@ -2393,7 +2429,7 @@ fn swap_exact_output_overflow_protection() {
liquidity_pool_supply: 1,
reserve_a: large_reserve,
reserve_b,
fees: 0,
fees: BalanceForTests::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -2456,6 +2492,7 @@ fn test_new_definition_lp_asymmetric_amounts() {
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,
);
@ -2492,6 +2529,7 @@ fn test_new_definition_lp_symmetric_amounts() {
AccountWithMetadataForTests::user_holding_lp_uninit(),
NonZero::new(token_a_amount).unwrap(),
NonZero::new(token_b_amount).unwrap(),
BalanceForTests::fee_tier(),
AMM_PROGRAM_ID,
);
@ -2552,6 +2590,7 @@ fn test_call_new_definition_reinitialization_requires_zero_pool_supply() {
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,
);
}
@ -2572,6 +2611,7 @@ fn test_call_new_definition_reinitialization_requires_zero_lp_definition_supply(
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,
);
}
@ -2599,6 +2639,7 @@ fn test_minimum_liquidity_lock_and_remove_all_user_lp() {
AccountForTests::user_holding_lp_uninit(),
NonZero::new(token_a_amount).unwrap(),
NonZero::new(token_b_amount).unwrap(),
BalanceForTests::fee_tier(),
AMM_PROGRAM_ID,
);
@ -2791,6 +2832,7 @@ fn new_definition_overflow_protection() {
AccountWithMetadataForTests::user_holding_lp_uninit(),
NonZero::new(large_amount).unwrap(),
NonZero::new(2).unwrap(),
BalanceForTests::fee_tier(),
AMM_PROGRAM_ID,
);
}
@ -2814,7 +2856,7 @@ fn add_liquidity_overflow_protection() {
liquidity_pool_supply: 1_000,
reserve_a: large_reserve,
reserve_b,
fees: 0,
fees: BalanceForTests::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -2885,7 +2927,7 @@ fn remove_liquidity_overflow_protection() {
liquidity_pool_supply: lp_supply,
reserve_a: large_reserve,
reserve_b,
fees: 0,
fees: BalanceForTests::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -2969,7 +3011,7 @@ fn swap_exact_input_overflow_protection() {
liquidity_pool_supply: 1,
reserve_a: 1_000,
reserve_b: large_reserve,
fees: 0,
fees: BalanceForTests::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -3019,3 +3061,51 @@ fn swap_exact_input_overflow_protection() {
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_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_inactive(),
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(),
2,
AMM_PROGRAM_ID,
);
}

View File

@ -1,5 +1,9 @@
use amm_core::{PoolDefinition, MINIMUM_LIQUIDITY};
use amm_core::{
PoolDefinition, FEE_TIER_BPS_1, FEE_TIER_BPS_100, FEE_TIER_BPS_30, FEE_TIER_BPS_5,
MINIMUM_LIQUIDITY,
};
use nssa::{
error::NssaError,
program_deployment_transaction::{self, ProgramDeploymentTransaction},
public_transaction, PrivateKey, PublicKey, PublicTransaction, V03State,
};
@ -88,6 +92,10 @@ impl Ids {
}
impl Balances {
fn fee_tier() -> u128 {
FEE_TIER_BPS_30
}
fn user_a_init() -> u128 {
10_000
}
@ -291,7 +299,7 @@ impl Accounts {
liquidity_pool_supply: Balances::pool_lp_supply_init(),
reserve_a: Balances::vault_a_init(),
reserve_b: Balances::vault_b_init(),
fees: 0_u128,
fees: Balances::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -400,7 +408,7 @@ impl Accounts {
liquidity_pool_supply: Balances::pool_lp_supply_init(),
reserve_a: Balances::vault_a_swap_1(),
reserve_b: Balances::vault_b_swap_1(),
fees: 0_u128,
fees: Balances::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -468,7 +476,7 @@ impl Accounts {
liquidity_pool_supply: Balances::pool_lp_supply_init(),
reserve_a: Balances::vault_a_swap_2(),
reserve_b: Balances::vault_b_swap_2(),
fees: 0_u128,
fees: Balances::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -536,7 +544,7 @@ impl Accounts {
liquidity_pool_supply: Balances::token_lp_supply_add(),
reserve_a: Balances::vault_a_add(),
reserve_b: Balances::vault_b_add(),
fees: 0_u128,
fees: Balances::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -629,7 +637,7 @@ impl Accounts {
liquidity_pool_supply: Balances::token_lp_supply_remove(),
reserve_a: Balances::vault_a_remove(),
reserve_b: Balances::vault_b_remove(),
fees: 0_u128,
fees: Balances::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -759,7 +767,7 @@ impl Accounts {
liquidity_pool_supply: 0,
reserve_a: 0,
reserve_b: 0,
fees: 0_u128,
fees: Balances::fee_tier(),
active: false,
}),
nonce: Nonce(0),
@ -840,7 +848,7 @@ impl Accounts {
liquidity_pool_supply: Balances::lp_supply_init(),
reserve_a: Balances::vault_a_init(),
reserve_b: Balances::vault_b_init(),
fees: 0_u128,
fees: Balances::fee_tier(),
active: true,
}),
nonce: Nonce(0),
@ -917,6 +925,42 @@ fn state_for_amm_tests_with_new_def() -> V03State {
state
}
fn try_execute_new_definition(state: &mut V03State, fees: u128) -> Result<(), NssaError> {
let instruction = amm_core::Instruction::NewDefinition {
token_a_amount: Balances::vault_a_init(),
token_b_amount: Balances::vault_b_init(),
fees,
amm_program_id: Ids::amm_program(),
};
let message = public_transaction::Message::try_new(
Ids::amm_program(),
vec![
Ids::pool_definition(),
Ids::vault_a(),
Ids::vault_b(),
Ids::token_lp_definition(),
Ids::lp_lock_holding(),
Ids::user_a(),
Ids::user_b(),
Ids::user_lp(),
],
vec![Nonce(0), Nonce(0)],
instruction,
)
.unwrap();
let witness_set =
public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a(), &Keys::user_b()]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 0)
}
fn execute_new_definition(state: &mut V03State, fees: u128) {
try_execute_new_definition(state, fees).unwrap();
}
#[test]
fn amm_remove_liquidity() {
let mut state = state_for_amm_tests();
@ -1022,34 +1066,7 @@ fn amm_new_definition_inactive_initialized_pool_and_uninit_user_lp() {
Accounts::token_lp_definition_init_inactive(),
);
let instruction = amm_core::Instruction::NewDefinition {
token_a_amount: Balances::vault_a_init(),
token_b_amount: Balances::vault_b_init(),
amm_program_id: Ids::amm_program(),
};
let message = public_transaction::Message::try_new(
Ids::amm_program(),
vec![
Ids::pool_definition(),
Ids::vault_a(),
Ids::vault_b(),
Ids::token_lp_definition(),
Ids::lp_lock_holding(),
Ids::user_a(),
Ids::user_b(),
Ids::user_lp(),
],
vec![Nonce(0), Nonce(0)],
instruction,
)
.unwrap();
let witness_set =
public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a(), &Keys::user_b()]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 0).unwrap();
execute_new_definition(&mut state, Balances::fee_tier());
assert_eq!(
state.get_account_by_id(Ids::pool_definition()),
@ -1097,34 +1114,7 @@ fn amm_new_definition_inactive_initialized_pool_init_user_lp() {
);
state.force_insert_account(Ids::user_lp(), Accounts::user_lp_holding_init_zero());
let instruction = amm_core::Instruction::NewDefinition {
token_a_amount: Balances::vault_a_init(),
token_b_amount: Balances::vault_b_init(),
amm_program_id: Ids::amm_program(),
};
let message = public_transaction::Message::try_new(
Ids::amm_program(),
vec![
Ids::pool_definition(),
Ids::vault_a(),
Ids::vault_b(),
Ids::token_lp_definition(),
Ids::lp_lock_holding(),
Ids::user_a(),
Ids::user_b(),
Ids::user_lp(),
],
vec![Nonce(0), Nonce(0)],
instruction,
)
.unwrap();
let witness_set =
public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a(), &Keys::user_b()]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 0).unwrap();
execute_new_definition(&mut state, Balances::fee_tier());
assert_eq!(
state.get_account_by_id(Ids::pool_definition()),
@ -1166,34 +1156,7 @@ fn amm_new_definition_uninitialized_pool() {
state.force_insert_account(Ids::vault_a(), Accounts::vault_a_init_inactive());
state.force_insert_account(Ids::vault_b(), Accounts::vault_b_init_inactive());
let instruction = amm_core::Instruction::NewDefinition {
token_a_amount: Balances::vault_a_init(),
token_b_amount: Balances::vault_b_init(),
amm_program_id: Ids::amm_program(),
};
let message = public_transaction::Message::try_new(
Ids::amm_program(),
vec![
Ids::pool_definition(),
Ids::vault_a(),
Ids::vault_b(),
Ids::token_lp_definition(),
Ids::lp_lock_holding(),
Ids::user_a(),
Ids::user_b(),
Ids::user_lp(),
],
vec![Nonce(0), Nonce(0)],
instruction,
)
.unwrap();
let witness_set =
public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a(), &Keys::user_b()]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 0).unwrap();
execute_new_definition(&mut state, Balances::fee_tier());
assert_eq!(
state.get_account_by_id(Ids::pool_definition()),
@ -1229,6 +1192,72 @@ fn amm_new_definition_uninitialized_pool() {
);
}
#[test]
fn amm_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 mut state = state_for_amm_tests_with_new_def();
state.force_insert_account(Ids::vault_a(), Accounts::vault_a_init_inactive());
state.force_insert_account(Ids::vault_b(), Accounts::vault_b_init_inactive());
execute_new_definition(&mut state, fees);
let pool_definition =
PoolDefinition::try_from(&state.get_account_by_id(Ids::pool_definition()).data)
.expect("new definition should create a valid pool");
assert_eq!(pool_definition.fees, fees);
}
}
#[test]
fn amm_new_definition_rejects_unsupported_fee_tier_transaction() {
let mut state = state_for_amm_tests_with_new_def();
state.force_insert_account(Ids::vault_a(), Accounts::vault_a_init_inactive());
state.force_insert_account(Ids::vault_b(), Accounts::vault_b_init_inactive());
state.force_insert_account(Ids::pool_definition(), Accounts::pool_definition_inactive());
state.force_insert_account(
Ids::token_lp_definition(),
Accounts::token_lp_definition_init_inactive(),
);
state.force_insert_account(Ids::user_lp(), Accounts::user_lp_holding_init_zero());
let result = try_execute_new_definition(&mut state, 2);
assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_))));
assert_eq!(
state.get_account_by_id(Ids::pool_definition()),
Accounts::pool_definition_inactive()
);
assert_eq!(
state.get_account_by_id(Ids::vault_a()),
Accounts::vault_a_init_inactive()
);
assert_eq!(
state.get_account_by_id(Ids::vault_b()),
Accounts::vault_b_init_inactive()
);
assert_eq!(
state.get_account_by_id(Ids::token_lp_definition()),
Accounts::token_lp_definition_init_inactive()
);
assert_eq!(
state.get_account_by_id(Ids::user_a()),
Accounts::user_a_holding()
);
assert_eq!(
state.get_account_by_id(Ids::user_b()),
Accounts::user_b_holding()
);
assert_eq!(
state.get_account_by_id(Ids::user_lp()),
Accounts::user_lp_holding_init_zero()
);
}
#[test]
fn amm_add_liquidity() {
let mut state = state_for_amm_tests();