diff --git a/amm/amm-idl.json b/amm/amm-idl.json index ad83555..3003f2f 100644 --- a/amm/amm-idl.json +++ b/amm/amm-idl.json @@ -63,6 +63,10 @@ "name": "token_b_amount", "type": "u128" }, + { + "name": "fees", + "type": "u128" + }, { "name": "amm_program_id", "type": "program_id" diff --git a/amm/core/src/lib.rs b/amm/core/src/lib.rs index b1d00f0..67783de 100644 --- a/amm/core/src/lib.rs +++ b/amm/core/src/lib.rs @@ -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; diff --git a/amm/methods/guest/src/bin/amm.rs b/amm/methods/guest/src/bin/amm.rs index b1a1c67..f761a68 100644 --- a/amm/methods/guest/src/bin/amm.rs +++ b/amm/methods/guest/src/bin/amm.rs @@ -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)) diff --git a/amm/src/add.rs b/amm/src/add.rs index 73ec5a3..91f5962 100644 --- a/amm/src/add.rs +++ b/amm/src/add.rs @@ -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, diff --git a/amm/src/new_definition.rs b/amm/src/new_definition.rs index 3207dd4..b2c27ec 100644 --- a/amm/src/new_definition.rs +++ b/amm/src/new_definition.rs @@ -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, Vec) { // 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, }; diff --git a/amm/src/remove.rs b/amm/src/remove.rs index dcd4428..ab1f7d8 100644 --- a/amm/src/remove.rs +++ b/amm/src/remove.rs @@ -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!( diff --git a/amm/src/swap.rs b/amm/src/swap.rs index 54ca804..fe5c544 100644 --- a/amm/src/swap.rs +++ b/amm/src/swap.rs @@ -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!( diff --git a/amm/src/tests.rs b/amm/src/tests.rs index d997b8c..70b8434 100644 --- a/amm/src/tests.rs +++ b/amm/src/tests.rs @@ -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, + ); +} diff --git a/integration_tests/tests/amm.rs b/integration_tests/tests/amm.rs index 797b883..b36da68 100644 --- a/integration_tests/tests/amm.rs +++ b/integration_tests/tests/amm.rs @@ -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();