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-04-10 00:13:45 +02:00
committed by r4bbit
parent d0f398814c
commit 3956d8a46a
9 changed files with 274 additions and 120 deletions
+121 -92
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();