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, }; use nssa_core::account::{Account, AccountId, Data, Nonce}; use token_core::{TokenDefinition, TokenHolding}; struct Keys; struct Ids; struct Balances; struct Accounts; impl Keys { fn user_a() -> PrivateKey { PrivateKey::try_new([31; 32]).expect("valid private key") } fn user_b() -> PrivateKey { PrivateKey::try_new([32; 32]).expect("valid private key") } fn user_lp() -> PrivateKey { PrivateKey::try_new([33; 32]).expect("valid private key") } } impl Ids { fn token_program() -> nssa_core::program::ProgramId { token_methods::TOKEN_ID } fn amm_program() -> nssa_core::program::ProgramId { amm_methods::AMM_ID } fn token_a_definition() -> AccountId { AccountId::new([3; 32]) } fn token_b_definition() -> AccountId { AccountId::new([4; 32]) } fn pool_definition() -> AccountId { amm_core::compute_pool_pda( Self::amm_program(), Self::token_a_definition(), Self::token_b_definition(), ) } fn token_lp_definition() -> AccountId { amm_core::compute_liquidity_token_pda(Self::amm_program(), Self::pool_definition()) } fn lp_lock_holding() -> AccountId { amm_core::compute_lp_lock_holding_pda(Self::amm_program(), Self::pool_definition()) } fn vault_a() -> AccountId { amm_core::compute_vault_pda( Self::amm_program(), Self::pool_definition(), Self::token_a_definition(), ) } fn vault_b() -> AccountId { amm_core::compute_vault_pda( Self::amm_program(), Self::pool_definition(), Self::token_b_definition(), ) } fn user_a() -> AccountId { AccountId::from(&PublicKey::new_from_private_key(&Keys::user_a())) } fn user_b() -> AccountId { AccountId::from(&PublicKey::new_from_private_key(&Keys::user_b())) } fn user_lp() -> AccountId { AccountId::from(&PublicKey::new_from_private_key(&Keys::user_lp())) } } impl Balances { fn fee_tier() -> u128 { FEE_TIER_BPS_30 } fn user_a_init() -> u128 { 10_000 } fn user_b_init() -> u128 { 10_000 } fn user_lp_init() -> u128 { 2_000 } fn vault_a_init() -> u128 { 5_000 } fn vault_b_init() -> u128 { 2_500 } fn pool_lp_supply_init() -> u128 { 5_000 } fn token_a_supply() -> u128 { 100_000 } fn token_b_supply() -> u128 { 100_000 } fn token_lp_supply() -> u128 { 5_000 } fn remove_lp() -> u128 { 1_000 } fn remove_min_a() -> u128 { 500 } fn remove_min_b() -> u128 { 500 } fn add_min_lp() -> u128 { 1_000 } fn add_max_a() -> u128 { 2_000 } fn add_max_b() -> u128 { 1_000 } fn swap_amount_in() -> u128 { 1_000 } fn swap_min_out() -> u128 { 200 } fn reserve_a_swap_1() -> u128 { 3_575 } fn reserve_b_swap_1() -> u128 { 3_500 } fn vault_a_swap_1() -> u128 { 3_575 } fn vault_b_swap_1() -> u128 { 3_500 } fn user_a_swap_1() -> u128 { 11_425 } fn user_b_swap_1() -> u128 { 9_000 } fn reserve_a_swap_2() -> u128 { 6_000 } fn reserve_b_swap_2() -> u128 { 2_085 } fn vault_a_swap_2() -> u128 { 6_000 } fn vault_b_swap_2() -> u128 { 2_085 } fn user_a_swap_2() -> u128 { 9_000 } fn user_b_swap_2() -> u128 { 10_415 } fn vault_a_add() -> u128 { 7_000 } fn vault_b_add() -> u128 { 3_500 } fn user_a_add() -> u128 { 8_000 } fn user_b_add() -> u128 { 9_000 } fn user_lp_add() -> u128 { 4_000 } fn token_lp_supply_add() -> u128 { 7_000 } fn vault_a_remove() -> u128 { 4_000 } fn vault_b_remove() -> u128 { 2_000 } fn user_a_remove() -> u128 { 11_000 } fn user_b_remove() -> u128 { 10_500 } fn user_lp_remove() -> u128 { 1_000 } fn token_lp_supply_remove() -> u128 { 4_000 } fn user_a_new_definition() -> u128 { 5_000 } fn user_b_new_definition() -> u128 { 7_500 } fn lp_supply_init() -> u128 { (Self::vault_a_init() * Self::vault_b_init()).isqrt() } fn lp_user_init() -> u128 { Self::lp_supply_init() - MINIMUM_LIQUIDITY } } impl Accounts { fn user_a_holding() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_a_definition(), balance: Balances::user_a_init(), }), nonce: Nonce(0), } } fn user_b_holding() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_b_definition(), balance: Balances::user_b_init(), }), nonce: Nonce(0), } } fn pool_definition_init() -> Account { Account { program_owner: Ids::amm_program(), balance: 0_u128, data: Data::from(&PoolDefinition { definition_token_a_id: Ids::token_a_definition(), definition_token_b_id: Ids::token_b_definition(), vault_a_id: Ids::vault_a(), vault_b_id: Ids::vault_b(), liquidity_pool_id: Ids::token_lp_definition(), liquidity_pool_supply: Balances::pool_lp_supply_init(), reserve_a: Balances::vault_a_init(), reserve_b: Balances::vault_b_init(), fees: Balances::fee_tier(), }), nonce: Nonce(0), } } fn token_a_definition_account() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenDefinition::Fungible { name: String::from("test"), total_supply: Balances::token_a_supply(), metadata_id: None, }), nonce: Nonce(0), } } fn token_b_definition_account() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenDefinition::Fungible { name: String::from("test"), total_supply: Balances::token_b_supply(), metadata_id: None, }), nonce: Nonce(0), } } fn token_lp_definition_account() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenDefinition::Fungible { name: String::from("LP Token"), total_supply: Balances::token_lp_supply(), metadata_id: None, }), nonce: Nonce(0), } } fn vault_a_init() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_a_definition(), balance: Balances::vault_a_init(), }), nonce: Nonce(0), } } fn vault_b_init() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_b_definition(), balance: Balances::vault_b_init(), }), nonce: Nonce(0), } } fn user_lp_holding() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_lp_definition(), balance: Balances::user_lp_init(), }), nonce: Nonce(0), } } fn user_lp_holding_with_balance(balance: u128) -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_lp_definition(), balance, }), nonce: Nonce(0), } } // --- Expected post-state accounts --- fn pool_definition_swap_1() -> Account { Account { program_owner: Ids::amm_program(), balance: 0_u128, data: Data::from(&PoolDefinition { definition_token_a_id: Ids::token_a_definition(), definition_token_b_id: Ids::token_b_definition(), vault_a_id: Ids::vault_a(), vault_b_id: Ids::vault_b(), liquidity_pool_id: Ids::token_lp_definition(), liquidity_pool_supply: Balances::pool_lp_supply_init(), reserve_a: Balances::reserve_a_swap_1(), reserve_b: Balances::reserve_b_swap_1(), fees: Balances::fee_tier(), }), nonce: Nonce(0), } } fn vault_a_swap_1() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_a_definition(), balance: Balances::vault_a_swap_1(), }), nonce: Nonce(0), } } fn vault_b_swap_1() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_b_definition(), balance: Balances::vault_b_swap_1(), }), nonce: Nonce(0), } } fn user_a_holding_swap_1() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_a_definition(), balance: Balances::user_a_swap_1(), }), nonce: Nonce(0), } } fn user_b_holding_swap_1() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_b_definition(), balance: Balances::user_b_swap_1(), }), nonce: Nonce(1), } } fn pool_definition_swap_2() -> Account { Account { program_owner: Ids::amm_program(), balance: 0_u128, data: Data::from(&PoolDefinition { definition_token_a_id: Ids::token_a_definition(), definition_token_b_id: Ids::token_b_definition(), vault_a_id: Ids::vault_a(), vault_b_id: Ids::vault_b(), liquidity_pool_id: Ids::token_lp_definition(), liquidity_pool_supply: Balances::pool_lp_supply_init(), reserve_a: Balances::reserve_a_swap_2(), reserve_b: Balances::reserve_b_swap_2(), fees: Balances::fee_tier(), }), nonce: Nonce(0), } } fn vault_a_swap_2() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_a_definition(), balance: Balances::vault_a_swap_2(), }), nonce: Nonce(0), } } fn vault_b_swap_2() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_b_definition(), balance: Balances::vault_b_swap_2(), }), nonce: Nonce(0), } } fn user_a_holding_swap_2() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_a_definition(), balance: Balances::user_a_swap_2(), }), nonce: Nonce(1), } } fn user_b_holding_swap_2() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_b_definition(), balance: Balances::user_b_swap_2(), }), nonce: Nonce(0), } } fn pool_definition_add() -> Account { Account { program_owner: Ids::amm_program(), balance: 0_u128, data: Data::from(&PoolDefinition { definition_token_a_id: Ids::token_a_definition(), definition_token_b_id: Ids::token_b_definition(), vault_a_id: Ids::vault_a(), vault_b_id: Ids::vault_b(), liquidity_pool_id: Ids::token_lp_definition(), liquidity_pool_supply: Balances::token_lp_supply_add(), reserve_a: Balances::vault_a_add(), reserve_b: Balances::vault_b_add(), fees: Balances::fee_tier(), }), nonce: Nonce(0), } } fn vault_a_add() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_a_definition(), balance: Balances::vault_a_add(), }), nonce: Nonce(0), } } fn vault_b_add() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_b_definition(), balance: Balances::vault_b_add(), }), nonce: Nonce(0), } } fn user_a_holding_add() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_a_definition(), balance: Balances::user_a_add(), }), nonce: Nonce(1), } } fn user_b_holding_add() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_b_definition(), balance: Balances::user_b_add(), }), nonce: Nonce(1), } } fn user_lp_holding_add() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_lp_definition(), balance: Balances::user_lp_add(), }), nonce: Nonce(0), } } fn token_lp_definition_add() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenDefinition::Fungible { name: String::from("LP Token"), total_supply: Balances::token_lp_supply_add(), metadata_id: None, }), nonce: Nonce(0), } } fn pool_definition_remove() -> Account { Account { program_owner: Ids::amm_program(), balance: 0_u128, data: Data::from(&PoolDefinition { definition_token_a_id: Ids::token_a_definition(), definition_token_b_id: Ids::token_b_definition(), vault_a_id: Ids::vault_a(), vault_b_id: Ids::vault_b(), liquidity_pool_id: Ids::token_lp_definition(), liquidity_pool_supply: Balances::token_lp_supply_remove(), reserve_a: Balances::vault_a_remove(), reserve_b: Balances::vault_b_remove(), fees: Balances::fee_tier(), }), nonce: Nonce(0), } } fn vault_a_remove() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_a_definition(), balance: Balances::vault_a_remove(), }), nonce: Nonce(0), } } fn vault_b_remove() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_b_definition(), balance: Balances::vault_b_remove(), }), nonce: Nonce(0), } } fn user_a_holding_remove() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_a_definition(), balance: Balances::user_a_remove(), }), nonce: Nonce(0), } } fn user_b_holding_remove() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_b_definition(), balance: Balances::user_b_remove(), }), nonce: Nonce(0), } } fn user_lp_holding_remove() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_lp_definition(), balance: Balances::user_lp_remove(), }), nonce: Nonce(1), } } fn token_lp_definition_remove() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenDefinition::Fungible { name: String::from("LP Token"), total_supply: Balances::token_lp_supply_remove(), metadata_id: None, }), nonce: Nonce(0), } } fn token_lp_definition_reinitializable() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenDefinition::Fungible { name: String::from("LP Token"), total_supply: 0, metadata_id: None, }), nonce: Nonce(0), } } fn vault_a_reinitializable() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_a_definition(), balance: 0, }), nonce: Nonce(0), } } fn vault_b_reinitializable() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_b_definition(), balance: 0, }), nonce: Nonce(0), } } fn pool_definition_zero_supply_reinitializable() -> Account { Account { program_owner: Ids::amm_program(), balance: 0_u128, data: Data::from(&PoolDefinition { definition_token_a_id: Ids::token_a_definition(), definition_token_b_id: Ids::token_b_definition(), vault_a_id: Ids::vault_a(), vault_b_id: Ids::vault_b(), liquidity_pool_id: Ids::token_lp_definition(), liquidity_pool_supply: 0, reserve_a: 0, reserve_b: 0, fees: Balances::fee_tier(), }), nonce: Nonce(0), } } fn user_a_holding_new_init() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_a_definition(), balance: Balances::user_a_new_definition(), }), nonce: Nonce(1), } } fn user_b_holding_new_init() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_b_definition(), balance: Balances::user_b_new_definition(), }), nonce: Nonce(1), } } fn user_lp_holding_new_init() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_lp_definition(), balance: Balances::lp_user_init(), }), nonce: Nonce(0), } } fn token_lp_definition_new_init() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenDefinition::Fungible { name: String::from("LP Token"), total_supply: Balances::lp_supply_init(), metadata_id: None, }), nonce: Nonce(0), } } fn lp_lock_holding_new_init() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_lp_definition(), balance: MINIMUM_LIQUIDITY, }), nonce: Nonce(0), } } fn pool_definition_new_init() -> Account { Account { program_owner: Ids::amm_program(), balance: 0_u128, data: Data::from(&PoolDefinition { definition_token_a_id: Ids::token_a_definition(), definition_token_b_id: Ids::token_b_definition(), vault_a_id: Ids::vault_a(), vault_b_id: Ids::vault_b(), liquidity_pool_id: Ids::token_lp_definition(), liquidity_pool_supply: Balances::lp_supply_init(), reserve_a: Balances::vault_a_init(), reserve_b: Balances::vault_b_init(), fees: Balances::fee_tier(), }), nonce: Nonce(0), } } fn user_lp_holding_init_zero() -> Account { Account { program_owner: Ids::token_program(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: Ids::token_lp_definition(), balance: 0, }), nonce: Nonce(0), } } } fn deploy_programs(state: &mut V03State) { let token_message = program_deployment_transaction::Message::new(token_methods::TOKEN_ELF.to_vec()); state .transition_from_program_deployment_transaction(&ProgramDeploymentTransaction::new( token_message, )) .expect("token program deployment must succeed"); let amm_message = program_deployment_transaction::Message::new(amm_methods::AMM_ELF.to_vec()); state .transition_from_program_deployment_transaction(&ProgramDeploymentTransaction::new( amm_message, )) .expect("amm program deployment must succeed"); } fn state_for_amm_tests() -> V03State { let mut state = V03State::new_with_genesis_accounts(&[], &[]); deploy_programs(&mut state); state.force_insert_account(Ids::pool_definition(), Accounts::pool_definition_init()); state.force_insert_account( Ids::token_a_definition(), Accounts::token_a_definition_account(), ); state.force_insert_account( Ids::token_b_definition(), Accounts::token_b_definition_account(), ); state.force_insert_account( Ids::token_lp_definition(), Accounts::token_lp_definition_account(), ); state.force_insert_account(Ids::user_a(), Accounts::user_a_holding()); state.force_insert_account(Ids::user_b(), Accounts::user_b_holding()); state.force_insert_account(Ids::user_lp(), Accounts::user_lp_holding()); state.force_insert_account(Ids::vault_a(), Accounts::vault_a_init()); state.force_insert_account(Ids::vault_b(), Accounts::vault_b_init()); state } fn state_for_amm_tests_with_new_def() -> V03State { let mut state = V03State::new_with_genesis_accounts(&[], &[]); deploy_programs(&mut state); state.force_insert_account( Ids::token_a_definition(), Accounts::token_a_definition_account(), ); state.force_insert_account( Ids::token_b_definition(), Accounts::token_b_definition_account(), ); state.force_insert_account(Ids::user_a(), Accounts::user_a_holding()); state.force_insert_account(Ids::user_b(), Accounts::user_b_holding()); state } fn current_nonce(state: &V03State, account_id: AccountId) -> Nonce { state.get_account_by_id(account_id).nonce } 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![ current_nonce(state, Ids::user_a()), current_nonce(state, Ids::user_b()), ], 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(); } fn execute_swap_a_to_b(state: &mut V03State, swap_amount_in: u128, min_amount_out: u128) { let instruction = amm_core::Instruction::SwapExactInput { swap_amount_in, min_amount_out, token_definition_id_in: Ids::token_a_definition(), }; let message = public_transaction::Message::try_new( Ids::amm_program(), vec![ Ids::pool_definition(), Ids::vault_a(), Ids::vault_b(), Ids::user_a(), Ids::user_b(), ], vec![current_nonce(state, Ids::user_a())], instruction, ) .unwrap(); let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a()]); let tx = PublicTransaction::new(message, witness_set); state.transition_from_public_transaction(&tx, 0).unwrap(); } fn execute_swap_b_to_a(state: &mut V03State, swap_amount_in: u128, min_amount_out: u128) { let instruction = amm_core::Instruction::SwapExactInput { swap_amount_in, min_amount_out, token_definition_id_in: Ids::token_b_definition(), }; let message = public_transaction::Message::try_new( Ids::amm_program(), vec![ Ids::pool_definition(), Ids::vault_a(), Ids::vault_b(), Ids::user_a(), Ids::user_b(), ], vec![current_nonce(state, Ids::user_b())], instruction, ) .unwrap(); let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_b()]); let tx = PublicTransaction::new(message, witness_set); state.transition_from_public_transaction(&tx, 0).unwrap(); } fn execute_add_liquidity( state: &mut V03State, min_amount_liquidity: u128, max_amount_to_add_token_a: u128, max_amount_to_add_token_b: u128, ) { let instruction = amm_core::Instruction::AddLiquidity { min_amount_liquidity, max_amount_to_add_token_a, max_amount_to_add_token_b, }; 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::user_a(), Ids::user_b(), Ids::user_lp(), ], vec![ current_nonce(state, Ids::user_a()), current_nonce(state, Ids::user_b()), ], 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(); } fn execute_remove_liquidity( state: &mut V03State, remove_liquidity_amount: u128, min_amount_to_remove_token_a: u128, min_amount_to_remove_token_b: u128, ) { let instruction = amm_core::Instruction::RemoveLiquidity { remove_liquidity_amount, min_amount_to_remove_token_a, min_amount_to_remove_token_b, }; 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::user_a(), Ids::user_b(), Ids::user_lp(), ], vec![current_nonce(state, Ids::user_lp())], instruction, ) .unwrap(); let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_lp()]); let tx = PublicTransaction::new(message, witness_set); state.transition_from_public_transaction(&tx, 0).unwrap(); } fn fungible_balance(account: &Account) -> u128 { let holding = TokenHolding::try_from(&account.data).expect("expected token holding"); let TokenHolding::Fungible { definition_id: _, balance, } = holding else { panic!("expected fungible token holding") }; balance } fn pool_definition(account: &Account) -> PoolDefinition { PoolDefinition::try_from(&account.data).expect("expected pool definition") } fn fungible_total_supply(account: &Account) -> u128 { let definition = TokenDefinition::try_from(&account.data).expect("expected token definition"); let TokenDefinition::Fungible { name: _, total_supply, metadata_id: _, } = definition else { panic!("expected fungible token definition") }; total_supply } #[test] fn amm_remove_liquidity() { let mut state = state_for_amm_tests(); let instruction = amm_core::Instruction::RemoveLiquidity { remove_liquidity_amount: Balances::remove_lp(), min_amount_to_remove_token_a: Balances::remove_min_a(), min_amount_to_remove_token_b: Balances::remove_min_b(), }; 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::user_a(), Ids::user_b(), Ids::user_lp(), ], vec![Nonce(0)], instruction, ) .unwrap(); let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_lp()]); let tx = PublicTransaction::new(message, witness_set); state.transition_from_public_transaction(&tx, 0).unwrap(); assert_eq!( state.get_account_by_id(Ids::pool_definition()), Accounts::pool_definition_remove() ); assert_eq!( state.get_account_by_id(Ids::vault_a()), Accounts::vault_a_remove() ); assert_eq!( state.get_account_by_id(Ids::vault_b()), Accounts::vault_b_remove() ); assert_eq!( state.get_account_by_id(Ids::token_lp_definition()), Accounts::token_lp_definition_remove() ); assert_eq!( state.get_account_by_id(Ids::user_a()), Accounts::user_a_holding_remove() ); assert_eq!( state.get_account_by_id(Ids::user_b()), Accounts::user_b_holding_remove() ); assert_eq!( state.get_account_by_id(Ids::user_lp()), Accounts::user_lp_holding_remove() ); } #[test] fn amm_remove_liquidity_insufficient_user_lp_fails() { let mut state = state_for_amm_tests(); state.force_insert_account(Ids::user_lp(), Accounts::user_lp_holding_with_balance(500)); let instruction = amm_core::Instruction::RemoveLiquidity { remove_liquidity_amount: Balances::remove_lp(), min_amount_to_remove_token_a: Balances::remove_min_a(), min_amount_to_remove_token_b: Balances::remove_min_b(), }; 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::user_a(), Ids::user_b(), Ids::user_lp(), ], vec![Nonce(0)], instruction, ) .unwrap(); let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_lp()]); let tx = PublicTransaction::new(message, witness_set); assert!(state.transition_from_public_transaction(&tx, 0).is_err()); } #[test] fn amm_new_definition_uninitialized_pool() { let mut state = state_for_amm_tests_with_new_def(); state.force_insert_account(Ids::vault_a(), Accounts::vault_a_reinitializable()); state.force_insert_account(Ids::vault_b(), Accounts::vault_b_reinitializable()); execute_new_definition(&mut state, Balances::fee_tier()); assert_eq!( state.get_account_by_id(Ids::pool_definition()), Accounts::pool_definition_new_init() ); assert_eq!( state.get_account_by_id(Ids::vault_a()), Accounts::vault_a_init() ); assert_eq!( state.get_account_by_id(Ids::vault_b()), Accounts::vault_b_init() ); assert_eq!( state.get_account_by_id(Ids::token_lp_definition()), Accounts::token_lp_definition_new_init() ); assert_eq!( state.get_account_by_id(Ids::lp_lock_holding()), Accounts::lp_lock_holding_new_init() ); assert_eq!( state.get_account_by_id(Ids::user_a()), Accounts::user_a_holding_new_init() ); assert_eq!( state.get_account_by_id(Ids::user_b()), Accounts::user_b_holding_new_init() ); assert_eq!( state.get_account_by_id(Ids::user_lp()), Accounts::user_lp_holding_new_init() ); } #[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_reinitializable()); state.force_insert_account(Ids::vault_b(), Accounts::vault_b_reinitializable()); 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_reinitializable()); state.force_insert_account(Ids::vault_b(), Accounts::vault_b_reinitializable()); state.force_insert_account( Ids::pool_definition(), Accounts::pool_definition_zero_supply_reinitializable(), ); state.force_insert_account( Ids::token_lp_definition(), Accounts::token_lp_definition_reinitializable(), ); 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_zero_supply_reinitializable() ); assert_eq!( state.get_account_by_id(Ids::vault_a()), Accounts::vault_a_reinitializable() ); assert_eq!( state.get_account_by_id(Ids::vault_b()), Accounts::vault_b_reinitializable() ); assert_eq!( state.get_account_by_id(Ids::token_lp_definition()), Accounts::token_lp_definition_reinitializable() ); 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(); let instruction = amm_core::Instruction::AddLiquidity { min_amount_liquidity: Balances::add_min_lp(), max_amount_to_add_token_a: Balances::add_max_a(), max_amount_to_add_token_b: Balances::add_max_b(), }; 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::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(); assert_eq!( state.get_account_by_id(Ids::pool_definition()), Accounts::pool_definition_add() ); assert_eq!( state.get_account_by_id(Ids::vault_a()), Accounts::vault_a_add() ); assert_eq!( state.get_account_by_id(Ids::vault_b()), Accounts::vault_b_add() ); assert_eq!( state.get_account_by_id(Ids::token_lp_definition()), Accounts::token_lp_definition_add() ); assert_eq!( state.get_account_by_id(Ids::user_a()), Accounts::user_a_holding_add() ); assert_eq!( state.get_account_by_id(Ids::user_b()), Accounts::user_b_holding_add() ); assert_eq!( state.get_account_by_id(Ids::user_lp()), Accounts::user_lp_holding_add() ); } #[test] fn amm_swap_b_to_a() { let mut state = state_for_amm_tests(); let instruction = amm_core::Instruction::SwapExactInput { swap_amount_in: Balances::swap_amount_in(), min_amount_out: Balances::swap_min_out(), token_definition_id_in: Ids::token_b_definition(), }; let message = public_transaction::Message::try_new( Ids::amm_program(), vec![ Ids::pool_definition(), Ids::vault_a(), Ids::vault_b(), Ids::user_a(), Ids::user_b(), ], vec![Nonce(0)], instruction, ) .unwrap(); let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_b()]); let tx = PublicTransaction::new(message, witness_set); state.transition_from_public_transaction(&tx, 0).unwrap(); assert_eq!( state.get_account_by_id(Ids::pool_definition()), Accounts::pool_definition_swap_1() ); assert_eq!( state.get_account_by_id(Ids::vault_a()), Accounts::vault_a_swap_1() ); assert_eq!( state.get_account_by_id(Ids::vault_b()), Accounts::vault_b_swap_1() ); assert_eq!( state.get_account_by_id(Ids::user_a()), Accounts::user_a_holding_swap_1() ); assert_eq!( state.get_account_by_id(Ids::user_b()), Accounts::user_b_holding_swap_1() ); } #[test] fn amm_swap_a_to_b() { let mut state = state_for_amm_tests(); let instruction = amm_core::Instruction::SwapExactInput { swap_amount_in: Balances::swap_amount_in(), min_amount_out: Balances::swap_min_out(), token_definition_id_in: Ids::token_a_definition(), }; let message = public_transaction::Message::try_new( Ids::amm_program(), vec![ Ids::pool_definition(), Ids::vault_a(), Ids::vault_b(), Ids::user_a(), Ids::user_b(), ], vec![Nonce(0)], instruction, ) .unwrap(); let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a()]); let tx = PublicTransaction::new(message, witness_set); state.transition_from_public_transaction(&tx, 0).unwrap(); assert_eq!( state.get_account_by_id(Ids::pool_definition()), Accounts::pool_definition_swap_2() ); assert_eq!( state.get_account_by_id(Ids::vault_a()), Accounts::vault_a_swap_2() ); assert_eq!( state.get_account_by_id(Ids::vault_b()), Accounts::vault_b_swap_2() ); assert_eq!( state.get_account_by_id(Ids::user_a()), Accounts::user_a_holding_swap_2() ); assert_eq!( state.get_account_by_id(Ids::user_b()), Accounts::user_b_holding_swap_2() ); } #[test] fn amm_fee_accumulates_across_multiple_swaps_and_pays_out_on_remove() { let mut state = state_for_amm_tests(); execute_swap_a_to_b(&mut state, 1_000, 200); execute_swap_b_to_a(&mut state, 1_000, 200); let pool_before_remove = pool_definition(&state.get_account_by_id(Ids::pool_definition())); assert_eq!(pool_before_remove.reserve_a, 4_060); assert_eq!(pool_before_remove.reserve_b, 3_085); assert_eq!(pool_before_remove.fees, Balances::fee_tier()); let vault_a_before_remove = fungible_balance(&state.get_account_by_id(Ids::vault_a())); let vault_b_before_remove = fungible_balance(&state.get_account_by_id(Ids::vault_b())); assert_eq!(vault_a_before_remove, 4_060); assert_eq!(vault_b_before_remove, 3_085); assert_eq!(vault_a_before_remove, pool_before_remove.reserve_a); assert_eq!(vault_b_before_remove, pool_before_remove.reserve_b); execute_remove_liquidity(&mut state, 1_000, 812, 617); let pool_after_remove = pool_definition(&state.get_account_by_id(Ids::pool_definition())); assert_eq!(pool_after_remove.reserve_a, 3_248); assert_eq!(pool_after_remove.reserve_b, 2_468); assert_eq!(pool_after_remove.liquidity_pool_supply, 4_000); let vault_a_after_remove = fungible_balance(&state.get_account_by_id(Ids::vault_a())); let vault_b_after_remove = fungible_balance(&state.get_account_by_id(Ids::vault_b())); assert_eq!(vault_a_after_remove, 3_248); assert_eq!(vault_b_after_remove, 2_468); assert_eq!(vault_a_after_remove, pool_after_remove.reserve_a); assert_eq!(vault_b_after_remove, pool_after_remove.reserve_b); assert_eq!( fungible_balance(&state.get_account_by_id(Ids::user_a())), 11_752 ); assert_eq!( fungible_balance(&state.get_account_by_id(Ids::user_b())), 10_032 ); assert_eq!( fungible_balance(&state.get_account_by_id(Ids::user_lp())), 1_000 ); assert_eq!( fungible_total_supply(&state.get_account_by_id(Ids::token_lp_definition())), 4_000 ); } #[test] fn amm_add_liquidity_after_fee_accrual() { let mut state = state_for_amm_tests(); execute_swap_a_to_b(&mut state, 1_000, 200); execute_swap_b_to_a(&mut state, 1_000, 200); execute_swap_a_to_b(&mut state, 1_000, 200); execute_swap_b_to_a(&mut state, 1_000, 200); let pool_before_add = pool_definition(&state.get_account_by_id(Ids::pool_definition())); let vault_a_before_add = fungible_balance(&state.get_account_by_id(Ids::vault_a())); let vault_b_before_add = fungible_balance(&state.get_account_by_id(Ids::vault_b())); assert_eq!(pool_before_add.reserve_a, 3_608); assert_eq!(pool_before_add.reserve_b, 3_477); assert_eq!(vault_a_before_add, pool_before_add.reserve_a); assert_eq!(vault_b_before_add, pool_before_add.reserve_b); execute_add_liquidity(&mut state, 1_436, 2_000, 1_000); let pool_after_add = pool_definition(&state.get_account_by_id(Ids::pool_definition())); let vault_a_after_add = fungible_balance(&state.get_account_by_id(Ids::vault_a())); let vault_b_after_add = fungible_balance(&state.get_account_by_id(Ids::vault_b())); assert_eq!(pool_after_add.reserve_a, 4_645); assert_eq!(pool_after_add.reserve_b, 4_477); assert_eq!(pool_after_add.liquidity_pool_supply, 6_437); assert_eq!(vault_a_after_add, pool_after_add.reserve_a); assert_eq!(vault_b_after_add, pool_after_add.reserve_b); assert_eq!( fungible_balance(&state.get_account_by_id(Ids::user_a())), 10_355 ); assert_eq!( fungible_balance(&state.get_account_by_id(Ids::user_b())), 8_023 ); assert_eq!( fungible_balance(&state.get_account_by_id(Ids::user_lp())), 3_437 ); assert_eq!( fungible_total_supply(&state.get_account_by_id(Ids::token_lp_definition())), 6_437 ); }