diff --git a/Cargo.lock b/Cargo.lock index 0bb78b5b..4ea5708c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -302,6 +302,25 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "amm_core" +version = "0.1.0" +dependencies = [ + "borsh", + "nssa_core", + "risc0-zkvm", + "serde", +] + +[[package]] +name = "amm_program" +version = "0.1.0" +dependencies = [ + "amm_core", + "nssa_core", + "token_core", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -4808,8 +4827,8 @@ checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" name = "nssa" version = "0.1.0" dependencies = [ + "amm_core", "borsh", - "bytemuck", "env_logger", "hex", "hex-literal 1.1.0", @@ -5339,6 +5358,8 @@ dependencies = [ name = "programs" version = "0.1.0" dependencies = [ + "amm_core", + "amm_program", "nssa_core", "risc0-zkvm", "serde", @@ -7754,6 +7775,7 @@ dependencies = [ name = "wallet" version = "0.1.0" dependencies = [ + "amm_core", "anyhow", "async-stream", "base58", @@ -7773,7 +7795,6 @@ dependencies = [ "nssa_core", "optfield", "rand 0.8.5", - "risc0-zkvm", "serde", "serde_json", "sha2", diff --git a/Cargo.toml b/Cargo.toml index bf17e885..d2f6064f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,10 @@ members = [ "common", "nssa", "nssa/core", + "programs/amm/core", + "programs/amm", + "programs/token/core", + "programs/token", "sequencer_core", "sequencer_rpc", "sequencer_runner", @@ -50,6 +54,8 @@ wallet = { path = "wallet" } wallet-ffi = { path = "wallet-ffi" } token_core = { path = "programs/token/core" } token_program = { path = "programs/token" } +amm_core = { path = "programs/amm/core" } +amm_program = { path = "programs/amm" } test_program_methods = { path = "test_program_methods" } bedrock_client = { path = "bedrock_client" } indexer_core = { path = "indexer_core" } diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index a9b6a4c8..91187d4b 100644 Binary files a/artifacts/program_methods/amm.bin and b/artifacts/program_methods/amm.bin differ diff --git a/artifacts/program_methods/authenticated_transfer.bin b/artifacts/program_methods/authenticated_transfer.bin index 3a032598..e725bd05 100644 Binary files a/artifacts/program_methods/authenticated_transfer.bin and b/artifacts/program_methods/authenticated_transfer.bin differ diff --git a/artifacts/program_methods/pinata.bin b/artifacts/program_methods/pinata.bin index 34e19e61..52c814f9 100644 Binary files a/artifacts/program_methods/pinata.bin and b/artifacts/program_methods/pinata.bin differ diff --git a/artifacts/program_methods/pinata_token.bin b/artifacts/program_methods/pinata_token.bin index d0dbbb97..f0c41bac 100644 Binary files a/artifacts/program_methods/pinata_token.bin and b/artifacts/program_methods/pinata_token.bin differ diff --git a/artifacts/program_methods/privacy_preserving_circuit.bin b/artifacts/program_methods/privacy_preserving_circuit.bin index 68837350..c0408d7c 100644 Binary files a/artifacts/program_methods/privacy_preserving_circuit.bin and b/artifacts/program_methods/privacy_preserving_circuit.bin differ diff --git a/artifacts/program_methods/token.bin b/artifacts/program_methods/token.bin index 1f71d2eb..f51d56c0 100644 Binary files a/artifacts/program_methods/token.bin and b/artifacts/program_methods/token.bin differ diff --git a/nssa/Cargo.toml b/nssa/Cargo.toml index e6952eee..7efa2006 100644 --- a/nssa/Cargo.toml +++ b/nssa/Cargo.toml @@ -16,7 +16,6 @@ borsh.workspace = true hex.workspace = true secp256k1 = "0.31.1" risc0-binfmt = "3.0.2" -bytemuck = "1.24.0" log.workspace = true [build-dependencies] @@ -25,6 +24,7 @@ risc0-binfmt = "3.0.2" [dev-dependencies] token_core.workspace = true +amm_core.workspace = true test_program_methods.workspace = true env_logger.workspace = true diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 08cc4e02..3fecfc61 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -311,6 +311,7 @@ pub mod tests { use std::collections::HashMap; + use amm_core::PoolDefinition; use nssa_core::{ Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, @@ -2329,137 +2330,6 @@ pub mod tests { )); } - // TODO repeated code should ultimately be removed; - fn compute_pool_pda( - amm_program_id: ProgramId, - definition_token_a_id: AccountId, - definition_token_b_id: AccountId, - ) -> AccountId { - AccountId::from(( - &amm_program_id, - &compute_pool_pda_seed(definition_token_a_id, definition_token_b_id), - )) - } - - fn compute_pool_pda_seed( - definition_token_a_id: AccountId, - definition_token_b_id: AccountId, - ) -> PdaSeed { - use risc0_zkvm::sha::{Impl, Sha256}; - - let mut i: usize = 0; - let (token_1, token_2) = loop { - if definition_token_a_id.value()[i] > definition_token_b_id.value()[i] { - let token_1 = definition_token_a_id; - let token_2 = definition_token_b_id; - break (token_1, token_2); - } else if definition_token_a_id.value()[i] < definition_token_b_id.value()[i] { - let token_1 = definition_token_b_id; - let token_2 = definition_token_a_id; - break (token_1, token_2); - } - - if i == 32 { - panic!("Definitions match"); - } else { - i += 1; - } - }; - - let mut bytes = [0; 64]; - bytes[0..32].copy_from_slice(&token_1.to_bytes()); - bytes[32..].copy_from_slice(&token_2.to_bytes()); - - PdaSeed::new( - Impl::hash_bytes(&bytes) - .as_bytes() - .try_into() - .expect("Hash output must be exactly 32 bytes long"), - ) - } - - fn compute_vault_pda( - amm_program_id: ProgramId, - pool_id: AccountId, - definition_token_id: AccountId, - ) -> AccountId { - AccountId::from(( - &amm_program_id, - &compute_vault_pda_seed(pool_id, definition_token_id), - )) - } - - fn compute_vault_pda_seed(pool_id: AccountId, definition_token_id: AccountId) -> PdaSeed { - use risc0_zkvm::sha::{Impl, Sha256}; - - let mut bytes = [0; 64]; - bytes[0..32].copy_from_slice(&pool_id.to_bytes()); - bytes[32..].copy_from_slice(&definition_token_id.to_bytes()); - - PdaSeed::new( - Impl::hash_bytes(&bytes) - .as_bytes() - .try_into() - .expect("Hash output must be exactly 32 bytes long"), - ) - } - - fn compute_liquidity_token_pda(amm_program_id: ProgramId, pool_id: AccountId) -> AccountId { - AccountId::from((&amm_program_id, &compute_liquidity_token_pda_seed(pool_id))) - } - - fn compute_liquidity_token_pda_seed(pool_id: AccountId) -> PdaSeed { - use risc0_zkvm::sha::{Impl, Sha256}; - - let mut bytes = [0; 64]; - bytes[0..32].copy_from_slice(&pool_id.to_bytes()); - bytes[32..].copy_from_slice(&[0; 32]); - - PdaSeed::new( - Impl::hash_bytes(&bytes) - .as_bytes() - .try_into() - .expect("Hash output must be exactly 32 bytes long"), - ) - } - - const POOL_DEFINITION_DATA_SIZE: usize = 225; - - #[derive(Default)] - struct PoolDefinition { - definition_token_a_id: AccountId, - definition_token_b_id: AccountId, - vault_a_id: AccountId, - vault_b_id: AccountId, - liquidity_pool_id: AccountId, - liquidity_pool_supply: u128, - reserve_a: u128, - reserve_b: u128, - fees: u128, - active: bool, - } - - impl PoolDefinition { - fn into_data(self) -> Data { - let mut bytes = [0; POOL_DEFINITION_DATA_SIZE]; - bytes[0..32].copy_from_slice(&self.definition_token_a_id.to_bytes()); - bytes[32..64].copy_from_slice(&self.definition_token_b_id.to_bytes()); - bytes[64..96].copy_from_slice(&self.vault_a_id.to_bytes()); - bytes[96..128].copy_from_slice(&self.vault_b_id.to_bytes()); - bytes[128..160].copy_from_slice(&self.liquidity_pool_id.to_bytes()); - bytes[160..176].copy_from_slice(&self.liquidity_pool_supply.to_le_bytes()); - bytes[176..192].copy_from_slice(&self.reserve_a.to_le_bytes()); - bytes[192..208].copy_from_slice(&self.reserve_b.to_le_bytes()); - bytes[208..224].copy_from_slice(&self.fees.to_le_bytes()); - bytes[224] = self.active as u8; - - bytes - .to_vec() - .try_into() - .expect("225 bytes should fit into Data") - } - } - struct PrivateKeysForTests; impl PrivateKeysForTests { @@ -2640,7 +2510,7 @@ pub mod tests { impl IdForTests { fn pool_definition_id() -> AccountId { - compute_pool_pda( + amm_core::compute_pool_pda( Program::amm().id(), IdForTests::token_a_definition_id(), IdForTests::token_b_definition_id(), @@ -2648,7 +2518,10 @@ pub mod tests { } fn token_lp_definition_id() -> AccountId { - compute_liquidity_token_pda(Program::amm().id(), IdForTests::pool_definition_id()) + amm_core::compute_liquidity_token_pda( + Program::amm().id(), + IdForTests::pool_definition_id(), + ) } fn token_a_definition_id() -> AccountId { @@ -2678,7 +2551,7 @@ pub mod tests { } fn vault_a_id() -> AccountId { - compute_vault_pda( + amm_core::compute_vault_pda( Program::amm().id(), IdForTests::pool_definition_id(), IdForTests::token_a_definition_id(), @@ -2686,7 +2559,7 @@ pub mod tests { } fn vault_b_id() -> AccountId { - compute_vault_pda( + amm_core::compute_vault_pda( Program::amm().id(), IdForTests::pool_definition_id(), IdForTests::token_b_definition_id(), @@ -2725,7 +2598,7 @@ pub mod tests { Account { program_owner: Program::amm().id(), balance: 0u128, - data: PoolDefinition::into_data(PoolDefinition { + data: Data::from(&PoolDefinition { definition_token_a_id: IdForTests::token_a_definition_id(), definition_token_b_id: IdForTests::token_b_definition_id(), vault_a_id: IdForTests::vault_a_id(), @@ -2844,7 +2717,7 @@ pub mod tests { Account { program_owner: Program::amm().id(), balance: 0u128, - data: PoolDefinition::into_data(PoolDefinition { + data: Data::from(&PoolDefinition { definition_token_a_id: IdForTests::token_a_definition_id(), definition_token_b_id: IdForTests::token_b_definition_id(), vault_a_id: IdForTests::vault_a_id(), @@ -2912,7 +2785,7 @@ pub mod tests { Account { program_owner: Program::amm().id(), balance: 0u128, - data: PoolDefinition::into_data(PoolDefinition { + data: Data::from(&PoolDefinition { definition_token_a_id: IdForTests::token_a_definition_id(), definition_token_b_id: IdForTests::token_b_definition_id(), vault_a_id: IdForTests::vault_a_id(), @@ -2980,7 +2853,7 @@ pub mod tests { Account { program_owner: Program::amm().id(), balance: 0u128, - data: PoolDefinition::into_data(PoolDefinition { + data: Data::from(&PoolDefinition { definition_token_a_id: IdForTests::token_a_definition_id(), definition_token_b_id: IdForTests::token_b_definition_id(), vault_a_id: IdForTests::vault_a_id(), @@ -3073,7 +2946,7 @@ pub mod tests { Account { program_owner: Program::amm().id(), balance: 0u128, - data: PoolDefinition::into_data(PoolDefinition { + data: Data::from(&PoolDefinition { definition_token_a_id: IdForTests::token_a_definition_id(), definition_token_b_id: IdForTests::token_b_definition_id(), vault_a_id: IdForTests::vault_a_id(), @@ -3179,7 +3052,7 @@ pub mod tests { Account { program_owner: Program::amm().id(), balance: 0u128, - data: PoolDefinition::into_data(PoolDefinition { + data: Data::from(&PoolDefinition { definition_token_a_id: IdForTests::token_a_definition_id(), definition_token_b_id: IdForTests::token_b_definition_id(), vault_a_id: IdForTests::vault_a_id(), @@ -3248,7 +3121,7 @@ pub mod tests { Account { program_owner: Program::amm().id(), balance: 0u128, - data: PoolDefinition::into_data(PoolDefinition { + data: Data::from(&PoolDefinition { definition_token_a_id: IdForTests::token_a_definition_id(), definition_token_b_id: IdForTests::token_b_definition_id(), vault_a_id: IdForTests::vault_a_id(), @@ -3277,11 +3150,6 @@ pub mod tests { } } - const AMM_NEW_DEFINITION: u8 = 0; - const AMM_SWAP: u8 = 1; - const AMM_ADD_LIQUIDITY: u8 = 2; - const AMM_REMOVE_LIQUIDITY: u8 = 3; - fn state_for_amm_tests() -> V02State { let initial_data = []; let mut state = @@ -3347,11 +3215,11 @@ pub mod tests { fn test_simple_amm_remove() { let mut state = state_for_amm_tests(); - let mut instruction: Vec = Vec::new(); - instruction.push(AMM_REMOVE_LIQUIDITY); - instruction.extend_from_slice(&BalanceForTests::remove_lp().to_le_bytes()); - instruction.extend_from_slice(&BalanceForTests::remove_min_amount_a().to_le_bytes()); - instruction.extend_from_slice(&BalanceForTests::remove_min_amount_b().to_le_bytes()); + let instruction = amm_core::Instruction::RemoveLiquidity { + remove_liquidity_amount: BalanceForTests::remove_lp(), + min_amount_to_remove_token_a: BalanceForTests::remove_min_amount_a(), + min_amount_to_remove_token_b: BalanceForTests::remove_min_amount_b(), + }; let message = public_transaction::Message::try_new( Program::amm().id(), @@ -3424,12 +3292,11 @@ pub mod tests { AccountForTests::token_lp_definition_init_inactive(), ); - let mut instruction: Vec = Vec::new(); - instruction.push(AMM_NEW_DEFINITION); - instruction.extend_from_slice(&BalanceForTests::vault_a_balance_init().to_le_bytes()); - instruction.extend_from_slice(&BalanceForTests::vault_b_balance_init().to_le_bytes()); - let amm_program_u8: [u8; 32] = bytemuck::cast(Program::amm().id()); - instruction.extend_from_slice(&amm_program_u8); + let instruction = amm_core::Instruction::NewDefinition { + token_a_amount: BalanceForTests::vault_a_balance_init(), + token_b_amount: BalanceForTests::vault_b_balance_init(), + amm_program_id: Program::amm().id(), + }; let message = public_transaction::Message::try_new( Program::amm().id(), @@ -3509,12 +3376,11 @@ pub mod tests { AccountForTests::user_token_lp_holding_init_zero(), ); - let mut instruction: Vec = Vec::new(); - instruction.push(AMM_NEW_DEFINITION); - instruction.extend_from_slice(&BalanceForTests::vault_a_balance_init().to_le_bytes()); - instruction.extend_from_slice(&BalanceForTests::vault_b_balance_init().to_le_bytes()); - let amm_program_u8: [u8; 32] = bytemuck::cast(Program::amm().id()); - instruction.extend_from_slice(&amm_program_u8); + let instruction = amm_core::Instruction::NewDefinition { + token_a_amount: BalanceForTests::vault_a_balance_init(), + token_b_amount: BalanceForTests::vault_b_balance_init(), + amm_program_id: Program::amm().id(), + }; let message = public_transaction::Message::try_new( Program::amm().id(), @@ -3582,12 +3448,11 @@ pub mod tests { AccountForTests::vault_b_init_inactive(), ); - let mut instruction: Vec = Vec::new(); - instruction.push(AMM_NEW_DEFINITION); - instruction.extend_from_slice(&BalanceForTests::vault_a_balance_init().to_le_bytes()); - instruction.extend_from_slice(&BalanceForTests::vault_b_balance_init().to_le_bytes()); - let amm_program_u8: [u8; 32] = bytemuck::cast(Program::amm().id()); - instruction.extend_from_slice(&amm_program_u8); + let instruction = amm_core::Instruction::NewDefinition { + token_a_amount: BalanceForTests::vault_a_balance_init(), + token_b_amount: BalanceForTests::vault_b_balance_init(), + amm_program_id: Program::amm().id(), + }; let message = public_transaction::Message::try_new( Program::amm().id(), @@ -3646,11 +3511,11 @@ pub mod tests { env_logger::init(); let mut state = state_for_amm_tests(); - let mut instruction: Vec = Vec::new(); - instruction.push(AMM_ADD_LIQUIDITY); - instruction.extend_from_slice(&BalanceForTests::add_min_amount_lp().to_le_bytes()); - instruction.extend_from_slice(&BalanceForTests::add_max_amount_a().to_le_bytes()); - instruction.extend_from_slice(&BalanceForTests::add_max_amount_b().to_le_bytes()); + let instruction = amm_core::Instruction::AddLiquidity { + min_amount_liquidity: BalanceForTests::add_min_amount_lp(), + max_amount_to_add_token_a: BalanceForTests::add_max_amount_a(), + max_amount_to_add_token_b: BalanceForTests::add_max_amount_b(), + }; let message = public_transaction::Message::try_new( Program::amm().id(), @@ -3708,11 +3573,11 @@ pub mod tests { fn test_simple_amm_swap_1() { let mut state = state_for_amm_tests(); - let mut instruction: Vec = Vec::new(); - instruction.push(AMM_SWAP); - instruction.extend_from_slice(&BalanceForTests::swap_amount_in().to_le_bytes()); - instruction.extend_from_slice(&BalanceForTests::swap_min_amount_out().to_le_bytes()); - instruction.extend_from_slice(&IdForTests::token_b_definition_id().to_bytes()); + let instruction = amm_core::Instruction::Swap { + swap_amount_in: BalanceForTests::swap_amount_in(), + min_amount_out: BalanceForTests::swap_min_amount_out(), + token_definition_id_in: IdForTests::token_b_definition_id(), + }; let message = public_transaction::Message::try_new( Program::amm().id(), @@ -3759,12 +3624,11 @@ pub mod tests { fn test_simple_amm_swap_2() { let mut state = state_for_amm_tests(); - let mut instruction: Vec = Vec::new(); - instruction.push(AMM_SWAP); - instruction.extend_from_slice(&BalanceForTests::swap_amount_in().to_le_bytes()); - instruction.extend_from_slice(&BalanceForTests::swap_min_amount_out().to_le_bytes()); - instruction.extend_from_slice(&IdForTests::token_a_definition_id().to_bytes()); - + let instruction = amm_core::Instruction::Swap { + swap_amount_in: BalanceForTests::swap_amount_in(), + min_amount_out: BalanceForTests::swap_min_amount_out(), + token_definition_id_in: IdForTests::token_a_definition_id(), + }; let message = public_transaction::Message::try_new( Program::amm().id(), vec![ diff --git a/program_methods/guest/Cargo.toml b/program_methods/guest/Cargo.toml index eda23348..a4627b86 100644 --- a/program_methods/guest/Cargo.toml +++ b/program_methods/guest/Cargo.toml @@ -8,5 +8,7 @@ license = { workspace = true } nssa_core.workspace = true token_core.workspace = true token_program.workspace = true +amm_core.workspace = true +amm_program.workspace = true risc0-zkvm.workspace = true serde = { workspace = true, default-features = false } diff --git a/program_methods/guest/src/bin/amm.rs b/program_methods/guest/src/bin/amm.rs index 2c7a7f8f..00fd39d3 100644 --- a/program_methods/guest/src/bin/amm.rs +++ b/program_methods/guest/src/bin/amm.rs @@ -1,158 +1,16 @@ -use nssa_core::{ - account::{Account, AccountId, AccountWithMetadata, Data}, - program::{ - AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, read_nssa_inputs, - write_nssa_outputs_with_chained_call, - }, -}; +//! The AMM Program. +//! +//! This program implements a simple AMM that supports multiple AMM pools (a single pool per +//! token pair). +//! +//! AMM program accepts [`Instruction`] as input, refer to the corresponding documentation +//! for more details. -// The AMM program has five functions (four directly accessible via instructions): -// 1. New AMM definition. Arguments to this function are: -// * Seven accounts: [amm_pool, vault_holding_a, vault_holding_b, pool_lp, user_holding_a, -// user_holding_b, user_holding_lp]. For new AMM Pool: amm_pool, vault_holding_a, -// vault_holding_b, pool_lp and user_holding_lp are default accounts. amm_pool is a default -// account that will initiate the amm definition account values vault_holding_a is a token -// holding account for token a vault_holding_b is a token holding account for token b pool_lp -// is a token holding account for the pool's lp token user_holding_a is a token holding -// account for token a user_holding_b is a token holding account for token b user_holding_lp -// is a token holding account for lp token -// * PDA remark: Accounts amm_pool, vault_holding_a, vault_holding_b and pool_lp are PDA. The -// AccountId for these accounts must be computed using: amm_pool AccountId <- -// compute_pool_pda vault_holding_a, vault_holding_b <- compute_vault_pda pool_lp -// <-compute_liquidity_token_pda -// * Requires authorization: user_holding_a, user_holding_b -// * An instruction data of 65-bytes, indicating the initial amm reserves' balances and -// token_program_id with the following layout: [0x00 || array of balances (little-endian 16 -// bytes) || AMM_PROGRAM_ID)] -// * Internally, calls compute_liquidity_token_pda_seed, compute_vault_pda_seed to authorize -// transfers. -// * Internally, calls compute_pool_da, compute_vault_pda and compute_vault_pda to check -// various AccountIds are correct. -// 2. Swap assets Arguments to this function are: -// * Five accounts: [amm_pool, vault_holding_a, vault_holding_b, user_holding_a, -// user_holding_b]. -// * Requires authorization: user holding account associated to TOKEN_DEFINITION_ID (either -// user_holding_a or user_holding_b) -// * An instruction data byte string of length 65, indicating which token type to swap, -// quantity of tokens put into the swap (of type TOKEN_DEFINITION_ID) and min_amount_out. -// [0x01 || amount (little-endian 16 bytes) || TOKEN_DEFINITION_ID]. -// * Internally, calls swap logic. -// * Four accounts: [user_deposit, vault_deposit, vault_withdraw, user_withdraw]. -// user_deposit and vault_deposit define deposit transaction. vault_withdraw and -// user_withdraw define withdraw transaction. -// * deposit_amount is the amount for user_deposit -> vault_deposit transfer. -// * reserve_amounts is the pool's reserves; used to compute the withdraw amount. -// * Outputs the token transfers as a Vec and the withdraw amount. -// 3. Add liquidity Arguments to this function are: -// * Seven accounts: [amm_pool, vault_holding_a, vault_holding_b, pool_lp, user_holding_a, -// user_holding_a, user_holding_lp]. -// * Requires authorization: user_holding_a, user_holding_b -// * An instruction data byte string of length 49, amounts for minimum amount of liquidity from -// add (min_amount_lp), -// * max amount added for each token (max_amount_a and max_amount_b); indicate [0x02 || array -// of of balances (little-endian 16 bytes)]. -// * Internally, calls compute_liquidity_token_pda_seed to compute liquidity pool PDA seed. -// 4. Remove liquidity -// * Seven accounts: [amm_pool, vault_holding_a, vault_holding_b, pool_lp, user_holding_a, -// user_holding_a, user_holding_lp]. -// * Requires authorization: user_holding_lp -// * An instruction data byte string of length 49, amounts for minimum amount of liquidity to -// redeem (balance_lp), -// * minimum balance of each token to remove (min_amount_a and min_amount_b); indicate [0x03 || -// array of balances (little-endian 16 bytes)]. -// * Internally, calls compute_vault_pda_seed to compute vault_a and vault_b's PDA seed. +use std::num::NonZero; -const POOL_DEFINITION_DATA_SIZE: usize = 225; +use amm_core::Instruction; +use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs_with_chained_call}; -#[derive(Clone, Default)] -struct PoolDefinition { - definition_token_a_id: AccountId, - definition_token_b_id: AccountId, - vault_a_id: AccountId, - vault_b_id: AccountId, - liquidity_pool_id: AccountId, - liquidity_pool_supply: u128, - reserve_a: u128, - reserve_b: u128, - /// Fees are currently not used - fees: u128, - /// A pool becomes inactive (active = false) - /// once all of its liquidity has been removed (e.g., reserves are emptied and - /// liquidity_pool_supply = 0) - active: bool, -} - -impl PoolDefinition { - fn into_data(self) -> Data { - let mut bytes = [0; POOL_DEFINITION_DATA_SIZE]; - bytes[0..32].copy_from_slice(&self.definition_token_a_id.to_bytes()); - bytes[32..64].copy_from_slice(&self.definition_token_b_id.to_bytes()); - bytes[64..96].copy_from_slice(&self.vault_a_id.to_bytes()); - bytes[96..128].copy_from_slice(&self.vault_b_id.to_bytes()); - bytes[128..160].copy_from_slice(&self.liquidity_pool_id.to_bytes()); - bytes[160..176].copy_from_slice(&self.liquidity_pool_supply.to_le_bytes()); - bytes[176..192].copy_from_slice(&self.reserve_a.to_le_bytes()); - bytes[192..208].copy_from_slice(&self.reserve_b.to_le_bytes()); - bytes[208..224].copy_from_slice(&self.fees.to_le_bytes()); - bytes[224] = self.active as u8; - - bytes - .to_vec() - .try_into() - .expect("225 bytes should fit into Data") - } - - fn parse(data: &[u8]) -> Option { - if data.len() != POOL_DEFINITION_DATA_SIZE { - None - } else { - let definition_token_a_id = AccountId::new(data[0..32].try_into().expect("Parse data: The AMM program must be provided a valid AccountId for Token A definition")); - let definition_token_b_id = AccountId::new(data[32..64].try_into().expect("Parse data: The AMM program must be provided a valid AccountId for Vault B definition")); - let vault_a_id = AccountId::new(data[64..96].try_into().expect( - "Parse data: The AMM program must be provided a valid AccountId for Vault A", - )); - let vault_b_id = AccountId::new(data[96..128].try_into().expect( - "Parse data: The AMM program must be provided a valid AccountId for Vault B", - )); - let liquidity_pool_id = AccountId::new(data[128..160].try_into().expect("Parse data: The AMM program must be provided a valid AccountId for Token liquidity pool definition")); - let liquidity_pool_supply = u128::from_le_bytes(data[160..176].try_into().expect( - "Parse data: The AMM program must be provided a valid u128 for liquidity cap", - )); - let reserve_a = u128::from_le_bytes(data[176..192].try_into().expect( - "Parse data: The AMM program must be provided a valid u128 for reserve A balance", - )); - let reserve_b = u128::from_le_bytes(data[192..208].try_into().expect( - "Parse data: The AMM program must be provided a valid u128 for reserve B balance", - )); - let fees = u128::from_le_bytes( - data[208..224] - .try_into() - .expect("Parse data: The AMM program must be provided a valid u128 for fees"), - ); - - let active = match data[224] { - 0 => false, - 1 => true, - _ => panic!("Parse data: The AMM program must be provided a valid bool for active"), - }; - - Some(Self { - definition_token_a_id, - definition_token_b_id, - vault_a_id, - vault_b_id, - liquidity_pool_id, - liquidity_pool_supply, - reserve_a, - reserve_b, - fees, - active, - }) - } - } -} - -type Instruction = Vec; fn main() { let ( ProgramInput { @@ -162,3301 +20,123 @@ fn main() { instruction_words, ) = read_nssa_inputs::(); - let (post_states, chained_calls) = - match instruction[0] { - 0 => { - let balance_a: u128 = u128::from_le_bytes( - instruction[1..17] - .try_into() - .expect("New definition: AMM Program expects u128 for balance a"), - ); - let balance_b: u128 = u128::from_le_bytes( - instruction[17..33] - .try_into() - .expect("New definition: AMM Program expects u128 for balance b"), - ); + let pre_states_clone = pre_states.clone(); - // Convert Vec to ProgramId ([u32;8]) - let mut amm_program_id: [u32; 8] = [0; 8]; - amm_program_id[0] = u32::from_le_bytes( - instruction[33..37] - .try_into() - .expect("New definition: AMM Program expects valid u32"), - ); - amm_program_id[1] = u32::from_le_bytes( - instruction[37..41] - .try_into() - .expect("New definition: AMM Program expects valid u32"), - ); - amm_program_id[2] = u32::from_le_bytes( - instruction[41..45] - .try_into() - .expect("New definition: AMM Program expects valid u32"), - ); - amm_program_id[3] = u32::from_le_bytes( - instruction[45..49] - .try_into() - .expect("New definition: AMM Program expects valid u32"), - ); - amm_program_id[4] = u32::from_le_bytes( - instruction[49..53] - .try_into() - .expect("New definition: AMM Program expects valid u32"), - ); - amm_program_id[5] = u32::from_le_bytes( - instruction[53..57] - .try_into() - .expect("New definition: AMM Program expects valid u32"), - ); - amm_program_id[6] = u32::from_le_bytes( - instruction[57..61] - .try_into() - .expect("New definition: AMM Program expects valid u32"), - ); - amm_program_id[7] = u32::from_le_bytes( - instruction[61..65] - .try_into() - .expect("New definition: AMM Program expects valid u32"), - ); - - new_definition(&pre_states, &[balance_a, balance_b], amm_program_id) - } - 1 => { - let mut token_in_id: [u8; 32] = [0; 32]; - token_in_id[0..].copy_from_slice(&instruction[33..65]); - let token_in_id = AccountId::new(token_in_id); - - let amount_in = u128::from_le_bytes( - instruction[1..17] - .try_into() - .expect("Swap: AMM Program expects valid u128 for balance to move"), - ); - let min_amount_out = u128::from_le_bytes( - instruction[17..33] - .try_into() - .expect("Swap: AMM Program expects valid u128 for balance to move"), - ); - - swap(&pre_states, &[amount_in, min_amount_out], token_in_id) - } - 2 => { - let min_amount_lp = u128::from_le_bytes(instruction[1..17].try_into().expect( - "Add liquidity: AMM Program expects valid u128 for min amount liquidity", - )); - let max_amount_a = u128::from_le_bytes( - instruction[17..33] - .try_into() - .expect("Add liquidity: AMM Program expects valid u128 for max amount a"), - ); - let max_amount_b = u128::from_le_bytes( - instruction[33..49] - .try_into() - .expect("Add liquidity: AMM Program expects valid u128 for max amount b"), - ); - - add_liquidity(&pre_states, &[min_amount_lp, max_amount_a, max_amount_b]) - } - 3 => { - let balance_lp = u128::from_le_bytes(instruction[1..17].try_into().expect( - "Remove liquidity: AMM Program expects valid u128 for balance liquidity", - )); - let min_amount_a = u128::from_le_bytes( - instruction[17..33] - .try_into() - .expect("Remove liquidity: AMM Program expects valid u128 for balance a"), - ); - let min_amount_b = u128::from_le_bytes( - instruction[33..49] - .try_into() - .expect("Remove liquidity: AMM Program expects valid u128 for balance b"), - ); - - remove_liquidity(&pre_states, &[balance_lp, min_amount_a, min_amount_b]) - } - _ => panic!("Invalid instruction"), - }; - - write_nssa_outputs_with_chained_call(instruction_words, pre_states, post_states, chained_calls); -} - -fn compute_pool_pda( - amm_program_id: ProgramId, - definition_token_a_id: AccountId, - definition_token_b_id: AccountId, -) -> AccountId { - AccountId::from(( - &amm_program_id, - &compute_pool_pda_seed(definition_token_a_id, definition_token_b_id), - )) -} - -fn compute_pool_pda_seed( - definition_token_a_id: AccountId, - definition_token_b_id: AccountId, -) -> PdaSeed { - use risc0_zkvm::sha::{Impl, Sha256}; - - let (token_1, token_2) = match definition_token_a_id - .value() - .cmp(definition_token_b_id.value()) - { - std::cmp::Ordering::Less => (definition_token_b_id, definition_token_a_id), - std::cmp::Ordering::Greater => (definition_token_a_id, definition_token_b_id), - std::cmp::Ordering::Equal => panic!("Definitions match"), + let (post_states, chained_calls) = match instruction { + Instruction::NewDefinition { + token_a_amount, + token_b_amount, + amm_program_id, + } => { + let [ + pool, + vault_a, + vault_b, + pool_definition_lp, + user_holding_a, + user_holding_b, + user_holding_lp, + ] = pre_states + .try_into() + .expect("Transfer instruction requires exactly seven accounts"); + amm_program::new_definition::new_definition( + pool, + vault_a, + vault_b, + pool_definition_lp, + user_holding_a, + user_holding_b, + user_holding_lp, + NonZero::new(token_a_amount).expect("Token A should have a nonzero amount"), + NonZero::new(token_b_amount).expect("Token B should have a nonzero amount"), + amm_program_id, + ) + } + Instruction::AddLiquidity { + min_amount_liquidity, + max_amount_to_add_token_a, + max_amount_to_add_token_b, + } => { + let [ + pool, + vault_a, + vault_b, + pool_definition_lp, + user_holding_a, + user_holding_b, + user_holding_lp, + ] = pre_states + .try_into() + .expect("Transfer instruction requires exactly seven accounts"); + amm_program::add::add_liquidity( + pool, + vault_a, + vault_b, + pool_definition_lp, + user_holding_a, + user_holding_b, + user_holding_lp, + NonZero::new(min_amount_liquidity) + .expect("Min amount of liquidity should be nonzero"), + max_amount_to_add_token_a, + max_amount_to_add_token_b, + ) + } + Instruction::RemoveLiquidity { + remove_liquidity_amount, + min_amount_to_remove_token_a, + min_amount_to_remove_token_b, + } => { + let [ + pool, + vault_a, + vault_b, + pool_definition_lp, + user_holding_a, + user_holding_b, + user_holding_lp, + ] = pre_states + .try_into() + .expect("Transfer instruction requires exactly seven accounts"); + amm_program::remove::remove_liquidity( + pool, + vault_a, + vault_b, + pool_definition_lp, + user_holding_a, + user_holding_b, + user_holding_lp, + NonZero::new(remove_liquidity_amount) + .expect("Remove liquidity amount must be nonzero"), + min_amount_to_remove_token_a, + min_amount_to_remove_token_b, + ) + } + Instruction::Swap { + swap_amount_in, + min_amount_out, + token_definition_id_in, + } => { + let [pool, vault_a, vault_b, user_holding_a, user_holding_b] = pre_states + .try_into() + .expect("Transfer instruction requires exactly five accounts"); + amm_program::swap::swap( + pool, + vault_a, + vault_b, + user_holding_a, + user_holding_b, + swap_amount_in, + min_amount_out, + token_definition_id_in, + ) + } }; - let mut bytes = [0; 64]; - bytes[0..32].copy_from_slice(&token_1.to_bytes()); - bytes[32..].copy_from_slice(&token_2.to_bytes()); - - PdaSeed::new( - Impl::hash_bytes(&bytes) - .as_bytes() - .try_into() - .expect("Hash output must be exactly 32 bytes long"), - ) -} - -fn compute_vault_pda( - amm_program_id: ProgramId, - pool_id: AccountId, - definition_token_id: AccountId, -) -> AccountId { - AccountId::from(( - &amm_program_id, - &compute_vault_pda_seed(pool_id, definition_token_id), - )) -} - -fn compute_vault_pda_seed(pool_id: AccountId, definition_token_id: AccountId) -> PdaSeed { - use risc0_zkvm::sha::{Impl, Sha256}; - - let mut bytes = [0; 64]; - bytes[0..32].copy_from_slice(&pool_id.to_bytes()); - bytes[32..].copy_from_slice(&definition_token_id.to_bytes()); - - PdaSeed::new( - Impl::hash_bytes(&bytes) - .as_bytes() - .try_into() - .expect("Hash output must be exactly 32 bytes long"), - ) -} - -fn compute_liquidity_token_pda(amm_program_id: ProgramId, pool_id: AccountId) -> AccountId { - AccountId::from((&amm_program_id, &compute_liquidity_token_pda_seed(pool_id))) -} - -fn compute_liquidity_token_pda_seed(pool_id: AccountId) -> PdaSeed { - use risc0_zkvm::sha::{Impl, Sha256}; - - let mut bytes = [0; 64]; - bytes[0..32].copy_from_slice(&pool_id.to_bytes()); - bytes[32..].copy_from_slice(&[0; 32]); - - PdaSeed::new( - Impl::hash_bytes(&bytes) - .as_bytes() - .try_into() - .expect("Hash output must be exactly 32 bytes long"), - ) -} - -fn new_definition( - pre_states: &[AccountWithMetadata], - balance_in: &[u128], - amm_program_id: ProgramId, -) -> (Vec, Vec) { - // Pool accounts: pool itself, and its 2 vaults and LP token - // 2 accounts for funding tokens - // initial funder's LP account - if pre_states.len() != 7 { - panic!("Invalid number of input accounts") - } - - if balance_in.len() != 2 { - panic!("Invalid number of input balances") - } - - let pool = &pre_states[0]; - let vault_a = &pre_states[1]; - let vault_b = &pre_states[2]; - let pool_lp = &pre_states[3]; - let user_holding_a = &pre_states[4]; - let user_holding_b = &pre_states[5]; - let user_holding_lp = &pre_states[6]; - - let amount_a = balance_in[0]; - let amount_b = balance_in[1]; - - // Prevents pool constant coefficient (k) from being 0. - if amount_a == 0 || amount_b == 0 { - panic!("Balances must be nonzero") - } - - // Verify token_a and token_b are different - let definition_token_a_id = token_core::TokenHolding::try_from(&user_holding_a.account.data) - .expect("New definition: AMM Program expects valid Token Holding account for Token A") - .definition_id(); - let definition_token_b_id = token_core::TokenHolding::try_from(&user_holding_b.account.data) - .expect("New definition: AMM Program expects valid Token Holding account for Token B") - .definition_id(); - - // both instances of the same token program - let token_program = user_holding_a.account.program_owner; - - if user_holding_b.account.program_owner != token_program { - panic!("User Token holdings must use the same Token Program"); - } - - if definition_token_a_id == definition_token_b_id { - panic!("Cannot set up a swap for a token with itself") - } - - if pool.account_id - != compute_pool_pda(amm_program_id, definition_token_a_id, definition_token_b_id) - { - panic!("Pool Definition Account ID does not match PDA"); - } - - if vault_a.account_id - != compute_vault_pda(amm_program_id, pool.account_id, definition_token_a_id) - || vault_b.account_id - != compute_vault_pda(amm_program_id, pool.account_id, definition_token_b_id) - { - panic!("Vault ID does not match PDA"); - } - - if pool_lp.account_id != compute_liquidity_token_pda(amm_program_id, pool.account_id) { - panic!("Liquidity pool Token Definition Account ID does not match PDA"); - } - - // Verify that Pool Account is not active - let pool_account_data = if pool.account == Account::default() { - PoolDefinition::default() - } else { - PoolDefinition::parse(&pool.account.data).expect("AMM program expects a valid Pool account") - }; - - if pool_account_data.active { - panic!("Cannot initialize an active Pool Definition") - } - - // LP Token minting calculation - // We assume LP is based on the initial deposit amount for Token_A. - - // Update pool account - let mut pool_post = pool.account.clone(); - let pool_post_definition = PoolDefinition { - definition_token_a_id, - definition_token_b_id, - vault_a_id: vault_a.account_id, - vault_b_id: vault_b.account_id, - liquidity_pool_id: pool_lp.account_id, - liquidity_pool_supply: amount_a, - reserve_a: amount_a, - reserve_b: amount_b, - fees: 0u128, // TODO: we assume all fees are 0 for now. - active: true, - }; - - pool_post.data = pool_post_definition.into_data(); - let pool_post: AccountPostState = if pool.account == Account::default() { - AccountPostState::new_claimed(pool_post.clone()) - } else { - AccountPostState::new(pool_post.clone()) - }; - - let token_program_id = user_holding_a.account.program_owner; - - // Chain call for Token A (user_holding_a -> Vault_A) - let call_token_a = ChainedCall::new( - token_program_id, - vec![user_holding_a.clone(), vault_a.clone()], - &token_core::Instruction::Transfer { - amount_to_transfer: amount_a, - }, + write_nssa_outputs_with_chained_call( + instruction_words, + pre_states_clone, + post_states, + chained_calls, ); - // Chain call for Token B (user_holding_b -> Vault_B) - let call_token_b = ChainedCall::new( - token_program_id, - vec![user_holding_b.clone(), vault_b.clone()], - &token_core::Instruction::Transfer { - amount_to_transfer: amount_b, - }, - ); - - // Chain call for liquidity token (TokenLP definition -> User LP Holding) - let instruction = if pool.account == Account::default() { - token_core::Instruction::NewFungibleDefinition { - name: String::from("LP Token"), - total_supply: amount_a, - } - } else { - token_core::Instruction::Mint { - amount_to_mint: amount_a, - } - }; - - let mut pool_lp_auth = pool_lp.clone(); - pool_lp_auth.is_authorized = true; - - let call_token_lp = ChainedCall::new( - token_program_id, - vec![pool_lp_auth.clone(), user_holding_lp.clone()], - &instruction, - ) - .with_pda_seeds(vec![compute_liquidity_token_pda_seed(pool.account_id)]); - - let chained_calls = vec![call_token_lp, call_token_b, call_token_a]; - - let post_states = vec![ - pool_post.clone(), - AccountPostState::new(pre_states[1].account.clone()), - AccountPostState::new(pre_states[2].account.clone()), - AccountPostState::new(pre_states[3].account.clone()), - AccountPostState::new(pre_states[4].account.clone()), - AccountPostState::new(pre_states[5].account.clone()), - AccountPostState::new(pre_states[6].account.clone()), - ]; - - (post_states.clone(), chained_calls) -} - -fn swap( - pre_states: &[AccountWithMetadata], - amounts: &[u128], - token_in_id: AccountId, -) -> (Vec, Vec) { - if pre_states.len() != 5 { - panic!("Invalid number of input accounts"); - } - - if amounts.len() != 2 { - panic!("Invalid number of amounts provided"); - } - - let pool = &pre_states[0]; - let vault_a = &pre_states[1]; - let vault_b = &pre_states[2]; - let user_holding_a = &pre_states[3]; - let user_holding_b = &pre_states[4]; - - // Verify vaults are in fact vaults - let pool_def_data = PoolDefinition::parse(&pool.account.data) - .expect("Swap: AMM Program expects a valid Pool Definition Account"); - - if !pool_def_data.active { - panic!("Pool is inactive"); - } - - if vault_a.account_id != pool_def_data.vault_a_id { - panic!("Vault A was not provided"); - } - - if vault_b.account_id != pool_def_data.vault_b_id { - panic!("Vault B was not provided"); - } - - // fetch pool reserves - // validates reserves is at least the vaults' balances - let vault_a_token_holding = token_core::TokenHolding::try_from(&vault_a.account.data) - .expect("Swap: AMM Program expects a valid Token Holding Account for Vault A"); - let token_core::TokenHolding::Fungible { - definition_id: _, - balance: vault_a_balance, - } = vault_a_token_holding - else { - panic!("Swap: AMM Program expects a valid Fungible Token Holding Account for Vault A"); - }; - if vault_a_balance < pool_def_data.reserve_a { - panic!("Reserve for Token A exceeds vault balance"); - } - - let vault_b_token_holding = token_core::TokenHolding::try_from(&vault_b.account.data) - .expect("Swap: AMM Program expects a valid Token Holding Account for Vault B"); - let token_core::TokenHolding::Fungible { - definition_id: _, - balance: vault_b_balance, - } = vault_b_token_holding - else { - panic!("Swap: AMM Program expects a valid Fungible Token Holding Account for Vault B"); - }; - - if vault_b_balance < pool_def_data.reserve_b { - panic!("Reserve for Token B exceeds vault balance"); - } - - let (chained_calls, [deposit_a, withdraw_a], [deposit_b, withdraw_b]) = - if token_in_id == pool_def_data.definition_token_a_id { - let (chained_calls, deposit_a, withdraw_b) = swap_logic( - user_holding_a.clone(), - vault_a.clone(), - vault_b.clone(), - user_holding_b.clone(), - amounts[0], - amounts[1], - &[pool_def_data.reserve_a, pool_def_data.reserve_b], - pool.account_id, - ); - - (chained_calls, [deposit_a, 0], [0, withdraw_b]) - } else if token_in_id == pool_def_data.definition_token_b_id { - let (chained_calls, deposit_b, withdraw_a) = swap_logic( - user_holding_b.clone(), - vault_b.clone(), - vault_a.clone(), - user_holding_a.clone(), - amounts[0], - amounts[1], - &[pool_def_data.reserve_b, pool_def_data.reserve_a], - pool.account_id, - ); - - (chained_calls, [0, withdraw_a], [deposit_b, 0]) - } else { - panic!("AccountId is not a token type for the pool"); - }; - - // Update pool account - let mut pool_post = pool.account.clone(); - let pool_post_definition = PoolDefinition { - reserve_a: pool_def_data.reserve_a + deposit_a - withdraw_a, - reserve_b: pool_def_data.reserve_b + deposit_b - withdraw_b, - ..pool_def_data - }; - - pool_post.data = pool_post_definition.into_data(); - - let post_states = vec![ - AccountPostState::new(pool_post.clone()), - AccountPostState::new(pre_states[1].account.clone()), - AccountPostState::new(pre_states[2].account.clone()), - AccountPostState::new(pre_states[3].account.clone()), - AccountPostState::new(pre_states[4].account.clone()), - ]; - - (post_states, chained_calls) -} - -#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] -fn swap_logic( - user_deposit: AccountWithMetadata, - vault_deposit: AccountWithMetadata, - vault_withdraw: AccountWithMetadata, - user_withdraw: AccountWithMetadata, - deposit_amount: u128, - min_amount_out: u128, - reserve_amounts: &[u128], - pool_id: AccountId, -) -> (Vec, u128, u128) { - let reserve_deposit_vault_amount = reserve_amounts[0]; - let reserve_withdraw_vault_amount = reserve_amounts[1]; - - // Compute withdraw amount - // Maintains pool constant product - // k = pool_def_data.reserve_a * pool_def_data.reserve_b; - let withdraw_amount = (reserve_withdraw_vault_amount * deposit_amount) - / (reserve_deposit_vault_amount + deposit_amount); - - // Slippage check - if min_amount_out > withdraw_amount { - panic!("Withdraw amount is less than minimal amount out"); - } - - if withdraw_amount == 0 { - panic!("Withdraw amount should be nonzero"); - } - - let token_program_id = user_deposit.account.program_owner; - - let mut chained_calls = Vec::new(); - chained_calls.push(ChainedCall::new( - token_program_id, - vec![user_deposit, vault_deposit], - &token_core::Instruction::Transfer { - amount_to_transfer: deposit_amount, - }, - )); - - let mut vault_withdraw = vault_withdraw.clone(); - vault_withdraw.is_authorized = true; - - let pda_seed = compute_vault_pda_seed( - pool_id, - token_core::TokenHolding::try_from(&vault_withdraw.account.data) - .expect("Swap Logic: AMM Program expects valid token data") - .definition_id(), - ); - - chained_calls.push( - ChainedCall::new( - token_program_id, - vec![vault_withdraw, user_withdraw], - &token_core::Instruction::Transfer { - amount_to_transfer: withdraw_amount, - }, - ) - .with_pda_seeds(vec![pda_seed]), - ); - - (chained_calls, deposit_amount, withdraw_amount) -} - -fn add_liquidity( - pre_states: &[AccountWithMetadata], - balances: &[u128], -) -> (Vec, Vec) { - if pre_states.len() != 7 { - panic!("Invalid number of input accounts"); - } - - let pool = &pre_states[0]; - let vault_a = &pre_states[1]; - let vault_b = &pre_states[2]; - let pool_definition_lp = &pre_states[3]; - let user_holding_a = &pre_states[4]; - let user_holding_b = &pre_states[5]; - let user_holding_lp = &pre_states[6]; - - // 1. Fetch Pool state - let pool_def_data = PoolDefinition::parse(&pool.account.data) - .expect("Add liquidity: AMM Program expects valid Pool Definition Account"); - if vault_a.account_id != pool_def_data.vault_a_id { - panic!("Vault A was not provided"); - } - - if pool_def_data.liquidity_pool_id != pool_definition_lp.account_id { - panic!("LP definition mismatch"); - } - - if vault_b.account_id != pool_def_data.vault_b_id { - panic!("Vault B was not provided"); - } - if balances.len() != 3 { - panic!("Invalid number of input balances"); - } - - let min_amount_lp = balances[0]; - let max_amount_a = balances[1]; - let max_amount_b = balances[2]; - - if max_amount_a == 0 || max_amount_b == 0 { - panic!("Both max-balances must be nonzero"); - } - - if min_amount_lp == 0 { - panic!("Min-lp must be nonzero"); - } - - // 2. Determine deposit amount - let vault_b_token_holding = token_core::TokenHolding::try_from(&vault_b.account.data) - .expect("Add liquidity: AMM Program expects valid Token Holding Account for Vault B"); - let token_core::TokenHolding::Fungible { - definition_id: _, - balance: vault_b_balance, - } = vault_b_token_holding - else { - panic!( - "Add liquidity: AMM Program expects valid Fungible Token Holding Account for Vault B" - ); - }; - - let vault_a_token_holding = token_core::TokenHolding::try_from(&vault_a.account.data) - .expect("Add liquidity: AMM Program expects valid Token Holding Account for Vault A"); - let token_core::TokenHolding::Fungible { - definition_id: _, - balance: vault_a_balance, - } = vault_a_token_holding - else { - panic!( - "Add liquidity: AMM Program expects valid Fungible Token Holding Account for Vault A" - ); - }; - - if pool_def_data.reserve_a == 0 || pool_def_data.reserve_b == 0 { - panic!("Reserves must be nonzero"); - } - - if vault_a_balance < pool_def_data.reserve_a || vault_b_balance < pool_def_data.reserve_b { - panic!("Vaults' balances must be at least the reserve amounts"); - } - - // Calculate actual_amounts - let ideal_a: u128 = (pool_def_data.reserve_a * max_amount_b) / pool_def_data.reserve_b; - let ideal_b: u128 = (pool_def_data.reserve_b * max_amount_a) / pool_def_data.reserve_a; - - let actual_amount_a = if ideal_a > max_amount_a { - max_amount_a - } else { - ideal_a - }; - let actual_amount_b = if ideal_b > max_amount_b { - max_amount_b - } else { - ideal_b - }; - - // 3. Validate amounts - if max_amount_a < actual_amount_a || max_amount_b < actual_amount_b { - panic!("Actual trade amounts cannot exceed max_amounts"); - } - - if actual_amount_a == 0 || actual_amount_b == 0 { - panic!("A trade amount is 0"); - } - - // 4. Calculate LP to mint - let delta_lp = std::cmp::min( - pool_def_data.liquidity_pool_supply * actual_amount_a / pool_def_data.reserve_a, - pool_def_data.liquidity_pool_supply * actual_amount_b / pool_def_data.reserve_b, - ); - - if delta_lp == 0 { - panic!("Payable LP must be nonzero"); - } - - if delta_lp < min_amount_lp { - panic!("Payable LP is less than provided minimum LP amount"); - } - - // 5. Update pool account - let mut pool_post = pool.account.clone(); - let pool_post_definition = PoolDefinition { - liquidity_pool_supply: pool_def_data.liquidity_pool_supply + delta_lp, - reserve_a: pool_def_data.reserve_a + actual_amount_a, - reserve_b: pool_def_data.reserve_b + actual_amount_b, - ..pool_def_data - }; - - pool_post.data = pool_post_definition.into_data(); - let token_program_id = user_holding_a.account.program_owner; - - // Chain call for Token A (UserHoldingA -> Vault_A) - let call_token_a = ChainedCall::new( - token_program_id, - vec![user_holding_a.clone(), vault_a.clone()], - &token_core::Instruction::Transfer { - amount_to_transfer: actual_amount_a, - }, - ); - // Chain call for Token B (UserHoldingB -> Vault_B) - let call_token_b = ChainedCall::new( - token_program_id, - vec![user_holding_b.clone(), vault_b.clone()], - &token_core::Instruction::Transfer { - amount_to_transfer: actual_amount_b, - }, - ); - // Chain call for LP (mint new tokens for user_holding_lp) - let mut pool_definition_lp_auth = pool_definition_lp.clone(); - pool_definition_lp_auth.is_authorized = true; - let call_token_lp = ChainedCall::new( - token_program_id, - vec![pool_definition_lp_auth.clone(), user_holding_lp.clone()], - &token_core::Instruction::Mint { - amount_to_mint: delta_lp, - }, - ) - .with_pda_seeds(vec![compute_liquidity_token_pda_seed(pool.account_id)]); - - let chained_calls = vec![call_token_lp, call_token_b, call_token_a]; - - let post_states = vec![ - AccountPostState::new(pool_post), - AccountPostState::new(pre_states[1].account.clone()), - AccountPostState::new(pre_states[2].account.clone()), - AccountPostState::new(pre_states[3].account.clone()), - AccountPostState::new(pre_states[4].account.clone()), - AccountPostState::new(pre_states[5].account.clone()), - AccountPostState::new(pre_states[6].account.clone()), - ]; - - (post_states, chained_calls) -} - -fn remove_liquidity( - pre_states: &[AccountWithMetadata], - amounts: &[u128], -) -> (Vec, Vec) { - if pre_states.len() != 7 { - panic!("Invalid number of input accounts"); - } - - let pool = &pre_states[0]; - let vault_a = &pre_states[1]; - let vault_b = &pre_states[2]; - let pool_definition_lp = &pre_states[3]; - let user_holding_a = &pre_states[4]; - let user_holding_b = &pre_states[5]; - let user_holding_lp = &pre_states[6]; - - if amounts.len() != 3 { - panic!("Invalid number of balances"); - } - - let amount_lp = amounts[0]; - let amount_min_a = amounts[1]; - let amount_min_b = amounts[2]; - - // 1. Fetch Pool state - let pool_def_data = PoolDefinition::parse(&pool.account.data) - .expect("Remove liquidity: AMM Program expects a valid Pool Definition Account"); - - if !pool_def_data.active { - panic!("Pool is inactive"); - } - - if pool_def_data.liquidity_pool_id != pool_definition_lp.account_id { - panic!("LP definition mismatch"); - } - - if vault_a.account_id != pool_def_data.vault_a_id { - panic!("Vault A was not provided"); - } - - if vault_b.account_id != pool_def_data.vault_b_id { - panic!("Vault B was not provided"); - } - - // Vault addresses do not need to be checked with PDA - // calculation for setting authorization since stored - // in the Pool Definition. - let mut running_vault_a = vault_a.clone(); - let mut running_vault_b = vault_b.clone(); - running_vault_a.is_authorized = true; - running_vault_b.is_authorized = true; - - if amount_min_a == 0 || amount_min_b == 0 { - panic!("Minimum withdraw amount must be nonzero"); - } - - if amount_lp == 0 { - panic!("Liquidity amount must be nonzero"); - } - - // 2. Compute withdrawal amounts - let user_holding_lp_data = token_core::TokenHolding::try_from(&user_holding_lp.account.data) - .expect("Remove liquidity: AMM Program expects a valid Token Account for liquidity token"); - let token_core::TokenHolding::Fungible { - definition_id: _, - balance: user_lp_balance, - } = user_holding_lp_data - else { - panic!( - "Remove liquidity: AMM Program expects a valid Fungible Token Holding Account for liquidity token" - ); - }; - - if user_lp_balance > pool_def_data.liquidity_pool_supply - || user_holding_lp_data.definition_id() != pool_def_data.liquidity_pool_id - { - panic!("Invalid liquidity account provided"); - } - - let withdraw_amount_a = - (pool_def_data.reserve_a * amount_lp) / pool_def_data.liquidity_pool_supply; - let withdraw_amount_b = - (pool_def_data.reserve_b * amount_lp) / pool_def_data.liquidity_pool_supply; - - // 3. Validate and slippage check - if withdraw_amount_a < amount_min_a { - panic!("Insufficient minimal withdraw amount (Token A) provided for liquidity amount"); - } - if withdraw_amount_b < amount_min_b { - panic!("Insufficient minimal withdraw amount (Token B) provided for liquidity amount"); - } - - // 4. Calculate LP to reduce cap by - let delta_lp: u128 = - (pool_def_data.liquidity_pool_supply * amount_lp) / pool_def_data.liquidity_pool_supply; - - let active: bool = pool_def_data.liquidity_pool_supply - delta_lp != 0; - - // 5. Update pool account - let mut pool_post = pool.account.clone(); - let pool_post_definition = PoolDefinition { - liquidity_pool_supply: pool_def_data.liquidity_pool_supply - delta_lp, - reserve_a: pool_def_data.reserve_a - withdraw_amount_a, - reserve_b: pool_def_data.reserve_b - withdraw_amount_b, - active, - ..pool_def_data.clone() - }; - - pool_post.data = pool_post_definition.into_data(); - - let token_program_id = user_holding_a.account.program_owner; - - // Chaincall for Token A withdraw - let call_token_a = ChainedCall::new( - token_program_id, - vec![running_vault_a, user_holding_a.clone()], - &token_core::Instruction::Transfer { - amount_to_transfer: withdraw_amount_a, - }, - ) - .with_pda_seeds(vec![compute_vault_pda_seed( - pool.account_id, - pool_def_data.definition_token_a_id, - )]); - // Chaincall for Token B withdraw - let call_token_b = ChainedCall::new( - token_program_id, - vec![running_vault_b, user_holding_b.clone()], - &token_core::Instruction::Transfer { - amount_to_transfer: withdraw_amount_b, - }, - ) - .with_pda_seeds(vec![compute_vault_pda_seed( - pool.account_id, - pool_def_data.definition_token_b_id, - )]); - // Chaincall for LP adjustment - let mut pool_definition_lp_auth = pool_definition_lp.clone(); - pool_definition_lp_auth.is_authorized = true; - let call_token_lp = ChainedCall::new( - token_program_id, - vec![pool_definition_lp_auth, user_holding_lp.clone()], - &token_core::Instruction::Burn { - amount_to_burn: delta_lp, - }, - ) - .with_pda_seeds(vec![compute_liquidity_token_pda_seed(pool.account_id)]); - - let chained_calls = vec![call_token_lp, call_token_b, call_token_a]; - - let post_states = vec![ - AccountPostState::new(pool_post.clone()), - AccountPostState::new(pre_states[1].account.clone()), - AccountPostState::new(pre_states[2].account.clone()), - AccountPostState::new(pre_states[3].account.clone()), - AccountPostState::new(pre_states[4].account.clone()), - AccountPostState::new(pre_states[5].account.clone()), - AccountPostState::new(pre_states[6].account.clone()), - ]; - - (post_states, chained_calls) -} - -#[cfg(test)] -mod tests { - use nssa_core::{ - account::{Account, AccountId, AccountWithMetadata, Data}, - program::{ChainedCall, ProgramId}, - }; - use token_core::{TokenDefinition, TokenHolding}; - - use crate::{ - PoolDefinition, add_liquidity, compute_liquidity_token_pda, - compute_liquidity_token_pda_seed, compute_pool_pda, compute_vault_pda, - compute_vault_pda_seed, new_definition, remove_liquidity, swap, - }; - - const TOKEN_PROGRAM_ID: ProgramId = [15; 8]; - const AMM_PROGRAM_ID: ProgramId = [42; 8]; - - struct BalanceForTests; - - impl BalanceForTests { - fn vault_a_reserve_init() -> u128 { - 1_000 - } - - fn vault_b_reserve_init() -> u128 { - 500 - } - - fn vault_a_reserve_low() -> u128 { - 10 - } - - fn vault_b_reserve_low() -> u128 { - 10 - } - - fn vault_a_reserve_high() -> u128 { - 500_000 - } - - fn vault_b_reserve_high() -> u128 { - 500_000 - } - - fn user_token_a_balance() -> u128 { - 1_000 - } - - fn user_token_b_balance() -> u128 { - 500 - } - - fn user_token_lp_balance() -> u128 { - 100 - } - - fn remove_min_amount_a() -> u128 { - 50 - } - - fn remove_min_amount_b() -> u128 { - 100 - } - - fn remove_actual_a_successful() -> u128 { - 100 - } - - fn remove_min_amount_b_low() -> u128 { - 50 - } - - fn remove_amount_lp() -> u128 { - 100 - } - - fn remove_amount_lp_1() -> u128 { - 30 - } - - fn add_max_amount_a() -> u128 { - 500 - } - - fn add_max_amount_b() -> u128 { - 200 - } - - fn add_max_amount_a_low() -> u128 { - 10 - } - - fn add_max_amount_b_low() -> u128 { - 10 - } - - fn add_min_amount_lp() -> u128 { - 20 - } - - fn vault_a_swap_test_1() -> u128 { - 1_500 - } - - fn vault_a_swap_test_2() -> u128 { - 715 - } - - fn vault_b_swap_test_1() -> u128 { - 334 - } - - fn vault_b_swap_test_2() -> u128 { - 700 - } - - fn min_amount_out() -> u128 { - 200 - } - - fn vault_a_add_successful() -> u128 { - 1_400 - } - - fn vault_b_add_successful() -> u128 { - 700 - } - - fn add_successful_amount_a() -> u128 { - 400 - } - - fn add_successful_amount_b() -> u128 { - 200 - } - - fn vault_a_remove_successful() -> u128 { - 900 - } - - fn vault_b_remove_successful() -> u128 { - 450 - } - } - - struct ChainedCallForTests; - - impl ChainedCallForTests { - fn cc_swap_token_a_test_1() -> ChainedCall { - ChainedCall::new( - TOKEN_PROGRAM_ID, - vec![ - AccountForTests::user_holding_a(), - AccountForTests::vault_a_init(), - ], - &token_core::Instruction::Transfer { - amount_to_transfer: BalanceForTests::add_max_amount_a(), - }, - ) - } - - fn cc_swap_token_b_test_1() -> ChainedCall { - let swap_amount: u128 = 166; - - let mut vault_b_auth = AccountForTests::vault_b_init(); - vault_b_auth.is_authorized = true; - - ChainedCall::new( - TOKEN_PROGRAM_ID, - vec![vault_b_auth, AccountForTests::user_holding_b()], - &token_core::Instruction::Transfer { - amount_to_transfer: swap_amount, - }, - ) - .with_pda_seeds(vec![compute_vault_pda_seed( - IdForTests::pool_definition_id(), - IdForTests::token_b_definition_id(), - )]) - } - - fn cc_swap_token_a_test_2() -> ChainedCall { - let swap_amount: u128 = 285; - - let mut vault_a_auth = AccountForTests::vault_a_init(); - vault_a_auth.is_authorized = true; - - ChainedCall::new( - TOKEN_PROGRAM_ID, - vec![vault_a_auth, AccountForTests::user_holding_a()], - &token_core::Instruction::Transfer { - amount_to_transfer: swap_amount, - }, - ) - .with_pda_seeds(vec![compute_vault_pda_seed( - IdForTests::pool_definition_id(), - IdForTests::token_a_definition_id(), - )]) - } - - fn cc_swap_token_b_test_2() -> ChainedCall { - ChainedCall::new( - TOKEN_PROGRAM_ID, - vec![ - AccountForTests::user_holding_b(), - AccountForTests::vault_b_init(), - ], - &token_core::Instruction::Transfer { - amount_to_transfer: BalanceForTests::add_max_amount_b(), - }, - ) - } - - fn cc_add_token_a() -> ChainedCall { - ChainedCall::new( - TOKEN_PROGRAM_ID, - vec![ - AccountForTests::user_holding_a(), - AccountForTests::vault_a_init(), - ], - &token_core::Instruction::Transfer { - amount_to_transfer: BalanceForTests::add_successful_amount_a(), - }, - ) - } - - fn cc_add_token_b() -> ChainedCall { - ChainedCall::new( - TOKEN_PROGRAM_ID, - vec![ - AccountForTests::user_holding_b(), - AccountForTests::vault_b_init(), - ], - &token_core::Instruction::Transfer { - amount_to_transfer: BalanceForTests::add_successful_amount_b(), - }, - ) - } - - fn cc_add_pool_lp() -> ChainedCall { - let mut pool_lp_auth = AccountForTests::pool_lp_init(); - pool_lp_auth.is_authorized = true; - - ChainedCall::new( - TOKEN_PROGRAM_ID, - vec![pool_lp_auth, AccountForTests::user_holding_lp_init()], - &token_core::Instruction::Mint { - amount_to_mint: BalanceForTests::add_successful_amount_a(), - }, - ) - .with_pda_seeds(vec![compute_liquidity_token_pda_seed( - IdForTests::pool_definition_id(), - )]) - } - - fn cc_remove_token_a() -> ChainedCall { - let mut vault_a_auth = AccountForTests::vault_a_init(); - vault_a_auth.is_authorized = true; - - ChainedCall::new( - TOKEN_PROGRAM_ID, - vec![vault_a_auth, AccountForTests::user_holding_a()], - &token_core::Instruction::Transfer { - amount_to_transfer: BalanceForTests::remove_actual_a_successful(), - }, - ) - .with_pda_seeds(vec![compute_vault_pda_seed( - IdForTests::pool_definition_id(), - IdForTests::token_a_definition_id(), - )]) - } - - fn cc_remove_token_b() -> ChainedCall { - let mut vault_b_auth = AccountForTests::vault_b_init(); - vault_b_auth.is_authorized = true; - - ChainedCall::new( - TOKEN_PROGRAM_ID, - vec![vault_b_auth, AccountForTests::user_holding_b()], - &token_core::Instruction::Transfer { - amount_to_transfer: BalanceForTests::remove_min_amount_b_low(), - }, - ) - .with_pda_seeds(vec![compute_vault_pda_seed( - IdForTests::pool_definition_id(), - IdForTests::token_b_definition_id(), - )]) - } - - fn cc_remove_pool_lp() -> ChainedCall { - let mut pool_lp_auth = AccountForTests::pool_lp_init(); - pool_lp_auth.is_authorized = true; - - ChainedCall::new( - TOKEN_PROGRAM_ID, - vec![pool_lp_auth, AccountForTests::user_holding_lp_init()], - &token_core::Instruction::Burn { - amount_to_burn: BalanceForTests::remove_amount_lp(), - }, - ) - .with_pda_seeds(vec![compute_liquidity_token_pda_seed( - IdForTests::pool_definition_id(), - )]) - } - - fn cc_new_definition_token_a() -> ChainedCall { - ChainedCall::new( - TOKEN_PROGRAM_ID, - vec![ - AccountForTests::user_holding_a(), - AccountForTests::vault_a_init(), - ], - &token_core::Instruction::Transfer { - amount_to_transfer: BalanceForTests::add_successful_amount_a(), - }, - ) - } - - fn cc_new_definition_token_b() -> ChainedCall { - ChainedCall::new( - TOKEN_PROGRAM_ID, - vec![ - AccountForTests::user_holding_b(), - AccountForTests::vault_b_init(), - ], - &token_core::Instruction::Transfer { - amount_to_transfer: BalanceForTests::add_successful_amount_b(), - }, - ) - } - - fn cc_new_definition_token_lp() -> ChainedCall { - ChainedCall::new( - TOKEN_PROGRAM_ID, - vec![ - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_lp_uninit(), - ], - &token_core::Instruction::Mint { - amount_to_mint: BalanceForTests::add_successful_amount_a(), - }, - ) - .with_pda_seeds(vec![compute_liquidity_token_pda_seed( - IdForTests::pool_definition_id(), - )]) - } - } - - struct IdForTests; - - impl IdForTests { - fn token_a_definition_id() -> AccountId { - AccountId::new([42; 32]) - } - - fn token_b_definition_id() -> AccountId { - AccountId::new([43; 32]) - } - - fn token_lp_definition_id() -> AccountId { - compute_liquidity_token_pda(AMM_PROGRAM_ID, IdForTests::pool_definition_id()) - } - - fn user_token_a_id() -> AccountId { - AccountId::new([45; 32]) - } - - fn user_token_b_id() -> AccountId { - AccountId::new([46; 32]) - } - - fn user_token_lp_id() -> AccountId { - AccountId::new([47; 32]) - } - - fn pool_definition_id() -> AccountId { - compute_pool_pda( - AMM_PROGRAM_ID, - IdForTests::token_a_definition_id(), - IdForTests::token_b_definition_id(), - ) - } - - fn vault_a_id() -> AccountId { - compute_vault_pda( - AMM_PROGRAM_ID, - IdForTests::pool_definition_id(), - IdForTests::token_a_definition_id(), - ) - } - - fn vault_b_id() -> AccountId { - compute_vault_pda( - AMM_PROGRAM_ID, - IdForTests::pool_definition_id(), - IdForTests::token_b_definition_id(), - ) - } - } - - struct AccountForTests; - - impl AccountForTests { - fn user_holding_a() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: TOKEN_PROGRAM_ID, - balance: 0u128, - data: Data::from(&TokenHolding::Fungible { - definition_id: IdForTests::token_a_definition_id(), - balance: BalanceForTests::user_token_a_balance(), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::user_token_a_id(), - } - } - - fn user_holding_b() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: TOKEN_PROGRAM_ID, - balance: 0u128, - data: Data::from(&TokenHolding::Fungible { - definition_id: IdForTests::token_b_definition_id(), - balance: BalanceForTests::user_token_b_balance(), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::user_token_b_id(), - } - } - - fn vault_a_init() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: TOKEN_PROGRAM_ID, - balance: 0u128, - data: Data::from(&TokenHolding::Fungible { - definition_id: IdForTests::token_a_definition_id(), - balance: BalanceForTests::vault_a_reserve_init(), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::vault_a_id(), - } - } - - fn vault_b_init() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: TOKEN_PROGRAM_ID, - balance: 0u128, - data: Data::from(&TokenHolding::Fungible { - definition_id: IdForTests::token_b_definition_id(), - balance: BalanceForTests::vault_b_reserve_init(), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::vault_b_id(), - } - } - - fn vault_a_init_high() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: TOKEN_PROGRAM_ID, - balance: 0u128, - data: Data::from(&TokenHolding::Fungible { - definition_id: IdForTests::token_a_definition_id(), - balance: BalanceForTests::vault_a_reserve_high(), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::vault_a_id(), - } - } - - fn vault_b_init_high() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: TOKEN_PROGRAM_ID, - balance: 0u128, - data: Data::from(&TokenHolding::Fungible { - definition_id: IdForTests::token_b_definition_id(), - balance: BalanceForTests::vault_b_reserve_high(), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::vault_b_id(), - } - } - - fn vault_a_init_low() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: TOKEN_PROGRAM_ID, - balance: 0u128, - data: Data::from(&TokenHolding::Fungible { - definition_id: IdForTests::token_a_definition_id(), - balance: BalanceForTests::vault_a_reserve_low(), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::vault_a_id(), - } - } - - fn vault_b_init_low() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: TOKEN_PROGRAM_ID, - balance: 0u128, - data: Data::from(&TokenHolding::Fungible { - definition_id: IdForTests::token_b_definition_id(), - balance: BalanceForTests::vault_b_reserve_low(), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::vault_b_id(), - } - } - - fn vault_a_init_zero() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: TOKEN_PROGRAM_ID, - balance: 0u128, - data: Data::from(&TokenHolding::Fungible { - definition_id: IdForTests::token_a_definition_id(), - balance: 0, - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::vault_a_id(), - } - } - - fn vault_b_init_zero() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: TOKEN_PROGRAM_ID, - balance: 0u128, - data: Data::from(&TokenHolding::Fungible { - definition_id: IdForTests::token_b_definition_id(), - balance: 0, - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::vault_b_id(), - } - } - - fn pool_lp_init() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: TOKEN_PROGRAM_ID, - balance: 0u128, - data: Data::from(&TokenDefinition::Fungible { - name: String::from("test"), - total_supply: BalanceForTests::vault_a_reserve_init(), - metadata_id: None, - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::token_lp_definition_id(), - } - } - - fn pool_lp_with_wrong_id() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: TOKEN_PROGRAM_ID, - balance: 0u128, - data: Data::from(&TokenDefinition::Fungible { - name: String::from("test"), - total_supply: BalanceForTests::vault_a_reserve_init(), - metadata_id: None, - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::vault_a_id(), - } - } - - fn user_holding_lp_uninit() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: TOKEN_PROGRAM_ID, - balance: 0u128, - data: Data::from(&TokenHolding::Fungible { - definition_id: IdForTests::token_lp_definition_id(), - balance: 0, - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::user_token_lp_id(), - } - } - - fn user_holding_lp_init() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: TOKEN_PROGRAM_ID, - balance: 0u128, - data: Data::from(&TokenHolding::Fungible { - definition_id: IdForTests::token_lp_definition_id(), - balance: BalanceForTests::user_token_lp_balance(), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::user_token_lp_id(), - } - } - - fn pool_definition_uninit() -> AccountWithMetadata { - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: IdForTests::pool_definition_id(), - } - } - - fn pool_definition_init() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: ProgramId::default(), - balance: 0u128, - data: PoolDefinition::into_data(PoolDefinition { - definition_token_a_id: IdForTests::token_a_definition_id(), - definition_token_b_id: IdForTests::token_b_definition_id(), - vault_a_id: IdForTests::vault_a_id(), - vault_b_id: IdForTests::vault_b_id(), - liquidity_pool_id: IdForTests::token_lp_definition_id(), - liquidity_pool_supply: BalanceForTests::vault_a_reserve_init(), - reserve_a: BalanceForTests::vault_a_reserve_init(), - reserve_b: BalanceForTests::vault_b_reserve_init(), - fees: 0u128, - active: true, - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::pool_definition_id(), - } - } - - fn pool_definition_init_reserve_a_zero() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: ProgramId::default(), - balance: 0u128, - data: PoolDefinition::into_data(PoolDefinition { - definition_token_a_id: IdForTests::token_a_definition_id(), - definition_token_b_id: IdForTests::token_b_definition_id(), - vault_a_id: IdForTests::vault_a_id(), - vault_b_id: IdForTests::vault_b_id(), - liquidity_pool_id: IdForTests::token_lp_definition_id(), - liquidity_pool_supply: BalanceForTests::vault_a_reserve_init(), - reserve_a: 0, - reserve_b: BalanceForTests::vault_b_reserve_init(), - fees: 0u128, - active: true, - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::pool_definition_id(), - } - } - - fn pool_definition_init_reserve_b_zero() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: ProgramId::default(), - balance: 0u128, - data: PoolDefinition::into_data(PoolDefinition { - definition_token_a_id: IdForTests::token_a_definition_id(), - definition_token_b_id: IdForTests::token_b_definition_id(), - vault_a_id: IdForTests::vault_a_id(), - vault_b_id: IdForTests::vault_b_id(), - liquidity_pool_id: IdForTests::token_lp_definition_id(), - liquidity_pool_supply: BalanceForTests::vault_a_reserve_init(), - reserve_a: BalanceForTests::vault_a_reserve_init(), - reserve_b: 0, - fees: 0u128, - active: true, - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::pool_definition_id(), - } - } - - fn pool_definition_init_reserve_a_low() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: ProgramId::default(), - balance: 0u128, - data: PoolDefinition::into_data(PoolDefinition { - definition_token_a_id: IdForTests::token_a_definition_id(), - definition_token_b_id: IdForTests::token_b_definition_id(), - vault_a_id: IdForTests::vault_a_id(), - vault_b_id: IdForTests::vault_b_id(), - liquidity_pool_id: IdForTests::token_lp_definition_id(), - liquidity_pool_supply: BalanceForTests::vault_a_reserve_low(), - reserve_a: BalanceForTests::vault_a_reserve_low(), - reserve_b: BalanceForTests::vault_b_reserve_high(), - fees: 0u128, - active: true, - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::pool_definition_id(), - } - } - - fn pool_definition_init_reserve_b_low() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: ProgramId::default(), - balance: 0u128, - data: PoolDefinition::into_data(PoolDefinition { - definition_token_a_id: IdForTests::token_a_definition_id(), - definition_token_b_id: IdForTests::token_b_definition_id(), - vault_a_id: IdForTests::vault_a_id(), - vault_b_id: IdForTests::vault_b_id(), - liquidity_pool_id: IdForTests::token_lp_definition_id(), - liquidity_pool_supply: BalanceForTests::vault_a_reserve_high(), - reserve_a: BalanceForTests::vault_a_reserve_high(), - reserve_b: BalanceForTests::vault_b_reserve_low(), - fees: 0u128, - active: true, - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::pool_definition_id(), - } - } - - fn pool_definition_swap_test_1() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: ProgramId::default(), - balance: 0u128, - data: PoolDefinition::into_data(PoolDefinition { - definition_token_a_id: IdForTests::token_a_definition_id(), - definition_token_b_id: IdForTests::token_b_definition_id(), - vault_a_id: IdForTests::vault_a_id(), - vault_b_id: IdForTests::vault_b_id(), - liquidity_pool_id: IdForTests::token_lp_definition_id(), - liquidity_pool_supply: BalanceForTests::vault_a_reserve_init(), - reserve_a: BalanceForTests::vault_a_swap_test_1(), - reserve_b: BalanceForTests::vault_b_swap_test_1(), - fees: 0u128, - active: true, - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::pool_definition_id(), - } - } - - fn pool_definition_swap_test_2() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: ProgramId::default(), - balance: 0u128, - data: PoolDefinition::into_data(PoolDefinition { - definition_token_a_id: IdForTests::token_a_definition_id(), - definition_token_b_id: IdForTests::token_b_definition_id(), - vault_a_id: IdForTests::vault_a_id(), - vault_b_id: IdForTests::vault_b_id(), - liquidity_pool_id: IdForTests::token_lp_definition_id(), - liquidity_pool_supply: BalanceForTests::vault_a_reserve_init(), - reserve_a: BalanceForTests::vault_a_swap_test_2(), - reserve_b: BalanceForTests::vault_b_swap_test_2(), - fees: 0u128, - active: true, - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::pool_definition_id(), - } - } - - fn pool_definition_add_zero_lp() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: ProgramId::default(), - balance: 0u128, - data: PoolDefinition::into_data(PoolDefinition { - definition_token_a_id: IdForTests::token_a_definition_id(), - definition_token_b_id: IdForTests::token_b_definition_id(), - vault_a_id: IdForTests::vault_a_id(), - vault_b_id: IdForTests::vault_b_id(), - liquidity_pool_id: IdForTests::token_lp_definition_id(), - liquidity_pool_supply: BalanceForTests::vault_a_reserve_low(), - reserve_a: BalanceForTests::vault_a_reserve_init(), - reserve_b: BalanceForTests::vault_b_reserve_init(), - fees: 0u128, - active: true, - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::pool_definition_id(), - } - } - - fn pool_definition_add_successful() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: ProgramId::default(), - balance: 0u128, - data: PoolDefinition::into_data(PoolDefinition { - definition_token_a_id: IdForTests::token_a_definition_id(), - definition_token_b_id: IdForTests::token_b_definition_id(), - vault_a_id: IdForTests::vault_a_id(), - vault_b_id: IdForTests::vault_b_id(), - liquidity_pool_id: IdForTests::token_lp_definition_id(), - liquidity_pool_supply: BalanceForTests::vault_a_add_successful(), - reserve_a: BalanceForTests::vault_a_add_successful(), - reserve_b: BalanceForTests::vault_b_add_successful(), - fees: 0u128, - active: true, - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::pool_definition_id(), - } - } - - fn pool_definition_remove_successful() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: ProgramId::default(), - balance: 0u128, - data: PoolDefinition::into_data(PoolDefinition { - definition_token_a_id: IdForTests::token_a_definition_id(), - definition_token_b_id: IdForTests::token_b_definition_id(), - vault_a_id: IdForTests::vault_a_id(), - vault_b_id: IdForTests::vault_b_id(), - liquidity_pool_id: IdForTests::token_lp_definition_id(), - liquidity_pool_supply: BalanceForTests::vault_a_remove_successful(), - reserve_a: BalanceForTests::vault_a_remove_successful(), - reserve_b: BalanceForTests::vault_b_remove_successful(), - fees: 0u128, - active: true, - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::pool_definition_id(), - } - } - - fn pool_definition_inactive() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: ProgramId::default(), - balance: 0u128, - data: PoolDefinition::into_data(PoolDefinition { - definition_token_a_id: IdForTests::token_a_definition_id(), - definition_token_b_id: IdForTests::token_b_definition_id(), - vault_a_id: IdForTests::vault_a_id(), - vault_b_id: IdForTests::vault_b_id(), - liquidity_pool_id: IdForTests::token_lp_definition_id(), - liquidity_pool_supply: BalanceForTests::vault_a_reserve_init(), - reserve_a: BalanceForTests::vault_a_reserve_init(), - reserve_b: BalanceForTests::vault_b_reserve_init(), - fees: 0u128, - active: false, - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::pool_definition_id(), - } - } - - fn pool_definition_with_wrong_id() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: ProgramId::default(), - balance: 0u128, - data: PoolDefinition::into_data(PoolDefinition { - definition_token_a_id: IdForTests::token_a_definition_id(), - definition_token_b_id: IdForTests::token_b_definition_id(), - vault_a_id: IdForTests::vault_a_id(), - vault_b_id: IdForTests::vault_b_id(), - liquidity_pool_id: IdForTests::token_lp_definition_id(), - liquidity_pool_supply: BalanceForTests::vault_a_reserve_init(), - reserve_a: BalanceForTests::vault_a_reserve_init(), - reserve_b: BalanceForTests::vault_b_reserve_init(), - fees: 0u128, - active: false, - }), - nonce: 0, - }, - is_authorized: true, - account_id: AccountId::new([4; 32]), - } - } - - fn vault_a_with_wrong_id() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: TOKEN_PROGRAM_ID, - balance: 0u128, - data: Data::from(&TokenHolding::Fungible { - definition_id: IdForTests::token_a_definition_id(), - balance: BalanceForTests::vault_a_reserve_init(), - }), - nonce: 0, - }, - is_authorized: true, - account_id: AccountId::new([4; 32]), - } - } - - fn vault_b_with_wrong_id() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: TOKEN_PROGRAM_ID, - balance: 0u128, - data: Data::from(&TokenHolding::Fungible { - definition_id: IdForTests::token_b_definition_id(), - balance: BalanceForTests::vault_b_reserve_init(), - }), - nonce: 0, - }, - is_authorized: true, - account_id: AccountId::new([4; 32]), - } - } - - fn pool_definition_active() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: ProgramId::default(), - balance: 0u128, - data: PoolDefinition::into_data(PoolDefinition { - definition_token_a_id: IdForTests::token_a_definition_id(), - definition_token_b_id: IdForTests::token_b_definition_id(), - vault_a_id: IdForTests::vault_a_id(), - vault_b_id: IdForTests::vault_b_id(), - liquidity_pool_id: IdForTests::token_lp_definition_id(), - liquidity_pool_supply: BalanceForTests::vault_a_reserve_init(), - reserve_a: BalanceForTests::vault_a_reserve_init(), - reserve_b: BalanceForTests::vault_b_reserve_init(), - fees: 0u128, - active: true, - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::pool_definition_id(), - } - } - } - - #[test] - fn test_pool_pda_produces_unique_id_for_token_pair() { - // compute_pool_pda(amm_program_id: ProgramId, definition_token_a_id: AccountId, - // definition_token_b_id: AccountId) - assert!( - compute_pool_pda( - AMM_PROGRAM_ID, - IdForTests::token_a_definition_id(), - IdForTests::token_b_definition_id() - ) == compute_pool_pda( - AMM_PROGRAM_ID, - IdForTests::token_b_definition_id(), - IdForTests::token_a_definition_id() - ) - ); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_new_definition_with_invalid_number_of_accounts_1() { - let pre_states = vec![AccountForTests::pool_definition_uninit()]; - let _post_states = new_definition( - &pre_states, - &[ - BalanceForTests::vault_a_reserve_init(), - BalanceForTests::vault_b_reserve_init(), - ], - AMM_PROGRAM_ID, - ); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_new_definition_with_invalid_number_of_accounts_2() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - ]; - let _post_states = new_definition( - &pre_states, - &[ - BalanceForTests::vault_a_reserve_init(), - BalanceForTests::vault_b_reserve_init(), - ], - AMM_PROGRAM_ID, - ); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_new_definition_with_invalid_number_of_accounts_3() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - ]; - let _post_states = new_definition( - &pre_states, - &[ - BalanceForTests::vault_a_reserve_init(), - BalanceForTests::vault_b_reserve_init(), - ], - AMM_PROGRAM_ID, - ); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_new_definition_with_invalid_number_of_accounts_4() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - ]; - let _post_states = new_definition( - &pre_states, - &[ - BalanceForTests::vault_a_reserve_init(), - BalanceForTests::vault_b_reserve_init(), - ], - AMM_PROGRAM_ID, - ); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_new_definition_with_invalid_number_of_accounts_5() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - ]; - let _post_states = new_definition( - &pre_states, - &[ - BalanceForTests::vault_a_reserve_init(), - BalanceForTests::vault_b_reserve_init(), - ], - AMM_PROGRAM_ID, - ); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_new_definition_with_invalid_number_of_accounts_6() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - ]; - let _post_states = new_definition( - &pre_states, - &[ - BalanceForTests::vault_a_reserve_init(), - BalanceForTests::vault_b_reserve_init(), - ], - AMM_PROGRAM_ID, - ); - } - - #[should_panic(expected = "Invalid number of input balances")] - #[test] - fn test_call_new_definition_with_invalid_number_of_balances() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_uninit(), - ]; - let _post_states = new_definition( - &pre_states, - &[BalanceForTests::vault_a_reserve_init()], - AMM_PROGRAM_ID, - ); - } - - #[should_panic(expected = "Balances must be nonzero")] - #[test] - fn test_call_new_definition_with_zero_balance_1() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_uninit(), - ]; - let _post_states = new_definition( - &pre_states, - &[0, BalanceForTests::vault_b_reserve_init()], - AMM_PROGRAM_ID, - ); - } - - #[should_panic(expected = "Balances must be nonzero")] - #[test] - fn test_call_new_definition_with_zero_balance_2() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_uninit(), - ]; - let _post_states = new_definition( - &pre_states, - &[BalanceForTests::vault_a_reserve_init(), 0], - AMM_PROGRAM_ID, - ); - } - - #[should_panic(expected = "Cannot set up a swap for a token with itself")] - #[test] - fn test_call_new_definition_same_token_definition() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_lp_uninit(), - ]; - let _post_states = new_definition( - &pre_states, - &[ - BalanceForTests::vault_a_reserve_init(), - BalanceForTests::vault_b_reserve_init(), - ], - AMM_PROGRAM_ID, - ); - } - - #[should_panic(expected = "Liquidity pool Token Definition Account ID does not match PDA")] - #[test] - fn test_call_new_definition_wrong_liquidity_id() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_with_wrong_id(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_uninit(), - ]; - let _post_states = new_definition( - &pre_states, - &[ - BalanceForTests::vault_a_reserve_init(), - BalanceForTests::vault_b_reserve_init(), - ], - AMM_PROGRAM_ID, - ); - } - - #[should_panic(expected = "Pool Definition Account ID does not match PDA")] - #[test] - fn test_call_new_definition_wrong_pool_id() { - let pre_states = vec![ - AccountForTests::pool_definition_with_wrong_id(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_uninit(), - ]; - let _post_states = new_definition( - &pre_states, - &[ - BalanceForTests::vault_a_reserve_init(), - BalanceForTests::vault_b_reserve_init(), - ], - AMM_PROGRAM_ID, - ); - } - - #[should_panic(expected = "Vault ID does not match PDA")] - #[test] - fn test_call_new_definition_wrong_vault_id_1() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_with_wrong_id(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_uninit(), - ]; - let _post_states = new_definition( - &pre_states, - &[ - BalanceForTests::vault_a_reserve_init(), - BalanceForTests::vault_b_reserve_init(), - ], - AMM_PROGRAM_ID, - ); - } - - #[should_panic(expected = "Vault ID does not match PDA")] - #[test] - fn test_call_new_definition_wrong_vault_id_2() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_with_wrong_id(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_uninit(), - ]; - let _post_states = new_definition( - &pre_states, - &[ - BalanceForTests::vault_a_reserve_init(), - BalanceForTests::vault_b_reserve_init(), - ], - AMM_PROGRAM_ID, - ); - } - - #[should_panic(expected = "Cannot initialize an active Pool Definition")] - #[test] - fn test_call_new_definition_cannot_initialize_active_pool() { - let pre_states = vec![ - AccountForTests::pool_definition_active(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_uninit(), - ]; - let _post_states = new_definition( - &pre_states, - &[ - BalanceForTests::vault_a_reserve_init(), - BalanceForTests::vault_b_reserve_init(), - ], - AMM_PROGRAM_ID, - ); - } - - #[should_panic(expected = "Cannot initialize an active Pool Definition")] - #[test] - fn test_call_new_definition_chained_call_successful() { - let pre_states = vec![ - AccountForTests::pool_definition_active(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_uninit(), - ]; - let (post_states, chained_calls) = new_definition( - &pre_states, - &[ - BalanceForTests::vault_a_reserve_init(), - BalanceForTests::vault_b_reserve_init(), - ], - AMM_PROGRAM_ID, - ); - - let pool_post = post_states[0].clone(); - - assert!(AccountForTests::pool_definition_add_successful().account == *pool_post.account()); - - let chained_call_lp = chained_calls[0].clone(); - let chained_call_b = chained_calls[1].clone(); - let chained_call_a = chained_calls[2].clone(); - - assert!(chained_call_a == ChainedCallForTests::cc_new_definition_token_a()); - assert!(chained_call_b == ChainedCallForTests::cc_new_definition_token_b()); - assert!(chained_call_lp == ChainedCallForTests::cc_new_definition_token_lp()); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_remove_liquidity_with_invalid_number_of_accounts_2() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - ]; - let _post_states = remove_liquidity( - &pre_states, - &[ - BalanceForTests::remove_amount_lp(), - BalanceForTests::remove_min_amount_a(), - BalanceForTests::remove_min_amount_b(), - ], - ); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_remove_liquidity_with_invalid_number_of_accounts_3() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - ]; - let _post_states = remove_liquidity( - &pre_states, - &[ - BalanceForTests::remove_amount_lp(), - BalanceForTests::remove_min_amount_a(), - BalanceForTests::remove_min_amount_b(), - ], - ); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_remove_liquidity_with_invalid_number_of_accounts_4() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - ]; - let _post_states = remove_liquidity( - &pre_states, - &[ - BalanceForTests::remove_amount_lp(), - BalanceForTests::remove_min_amount_a(), - BalanceForTests::remove_min_amount_b(), - ], - ); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_remove_liquidity_with_invalid_number_of_accounts_5() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - ]; - let _post_states = remove_liquidity( - &pre_states, - &[ - BalanceForTests::remove_amount_lp(), - BalanceForTests::remove_min_amount_a(), - BalanceForTests::remove_min_amount_b(), - ], - ); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_remove_liquidity_with_invalid_number_of_accounts_6() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - ]; - let _post_states = remove_liquidity( - &pre_states, - &[ - BalanceForTests::remove_amount_lp(), - BalanceForTests::remove_min_amount_a(), - BalanceForTests::remove_min_amount_b(), - ], - ); - } - - #[should_panic(expected = "Vault A was not provided")] - #[test] - fn test_call_remove_liquidity_vault_a_omitted() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_with_wrong_id(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let _post_states = remove_liquidity( - &pre_states, - &[ - BalanceForTests::remove_amount_lp(), - BalanceForTests::remove_min_amount_a(), - BalanceForTests::remove_min_amount_b(), - ], - ); - } - - #[should_panic(expected = "Vault B was not provided")] - #[test] - fn test_call_remove_liquidity_vault_b_omitted() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_with_wrong_id(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let _post_states = remove_liquidity( - &pre_states, - &[ - BalanceForTests::remove_amount_lp(), - BalanceForTests::remove_min_amount_a(), - BalanceForTests::remove_min_amount_b(), - ], - ); - } - - #[should_panic(expected = "LP definition mismatch")] - #[test] - fn test_call_remove_liquidity_lp_def_mismatch() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_with_wrong_id(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let _post_states = remove_liquidity( - &pre_states, - &[ - BalanceForTests::remove_amount_lp(), - BalanceForTests::remove_min_amount_a(), - BalanceForTests::remove_min_amount_b(), - ], - ); - } - - #[should_panic(expected = "Invalid liquidity account provided")] - #[test] - fn test_call_remove_liquidity_insufficient_liquidity_amount() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_a(), /* different token account than lp to create - * desired error */ - ]; - let _post_states = remove_liquidity( - &pre_states, - &[ - BalanceForTests::remove_amount_lp(), - BalanceForTests::remove_min_amount_a(), - BalanceForTests::remove_min_amount_b(), - ], - ); - } - - #[should_panic( - expected = "Insufficient minimal withdraw amount (Token A) provided for liquidity amount" - )] - #[test] - fn test_call_remove_liquidity_insufficient_balance_1() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let _post_states = remove_liquidity( - &pre_states, - &[ - BalanceForTests::remove_amount_lp_1(), - BalanceForTests::remove_min_amount_a(), - BalanceForTests::remove_min_amount_b(), - ], - ); - } - - #[should_panic( - expected = "Insufficient minimal withdraw amount (Token B) provided for liquidity amount" - )] - #[test] - fn test_call_remove_liquidity_insufficient_balance_2() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let _post_states = remove_liquidity( - &pre_states, - &[ - BalanceForTests::remove_amount_lp(), - BalanceForTests::remove_min_amount_a(), - BalanceForTests::remove_min_amount_b(), - ], - ); - } - - #[should_panic(expected = "Minimum withdraw amount must be nonzero")] - #[test] - fn test_call_remove_liquidity_min_bal_zero_1() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let _post_states = remove_liquidity( - &pre_states, - &[ - BalanceForTests::remove_amount_lp(), - 0, - BalanceForTests::remove_min_amount_b(), - ], - ); - } - - #[should_panic(expected = "Minimum withdraw amount must be nonzero")] - #[test] - fn test_call_remove_liquidity_min_bal_zero_2() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let _post_states = remove_liquidity( - &pre_states, - &[ - BalanceForTests::remove_amount_lp(), - BalanceForTests::remove_min_amount_a(), - 0, - ], - ); - } - - #[should_panic(expected = "Liquidity amount must be nonzero")] - #[test] - fn test_call_remove_liquidity_lp_bal_zero() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let _post_states = remove_liquidity( - &pre_states, - &[ - 0, - BalanceForTests::remove_min_amount_a(), - BalanceForTests::remove_min_amount_b(), - ], - ); - } - - #[test] - fn test_call_remove_liquidity_chained_call_successful() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let (post_states, chained_calls) = remove_liquidity( - &pre_states, - &[ - BalanceForTests::remove_amount_lp(), - BalanceForTests::remove_min_amount_a(), - BalanceForTests::remove_min_amount_b_low(), - ], - ); - - let pool_post = post_states[0].clone(); - - assert!( - AccountForTests::pool_definition_remove_successful().account == *pool_post.account() - ); - - let chained_call_lp = chained_calls[0].clone(); - let chained_call_b = chained_calls[1].clone(); - let chained_call_a = chained_calls[2].clone(); - - assert!(chained_call_a == ChainedCallForTests::cc_remove_token_a()); - assert!(chained_call_b == ChainedCallForTests::cc_remove_token_b()); - assert!(chained_call_lp == ChainedCallForTests::cc_remove_pool_lp()); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_add_liquidity_with_invalid_number_of_accounts_1() { - let pre_states = vec![AccountForTests::pool_definition_init()]; - let _post_states = add_liquidity( - &pre_states, - &[ - BalanceForTests::add_min_amount_lp(), - BalanceForTests::add_max_amount_a(), - BalanceForTests::add_max_amount_b(), - ], - ); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_add_liquidity_with_invalid_number_of_accounts_2() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - ]; - let _post_states = add_liquidity( - &pre_states, - &[ - BalanceForTests::add_min_amount_lp(), - BalanceForTests::add_max_amount_a(), - BalanceForTests::add_max_amount_b(), - ], - ); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_add_liquidity_with_invalid_number_of_accounts_3() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - ]; - let _post_states = add_liquidity( - &pre_states, - &[ - BalanceForTests::add_min_amount_lp(), - BalanceForTests::add_max_amount_a(), - BalanceForTests::add_max_amount_b(), - ], - ); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_add_liquidity_with_invalid_number_of_accounts_4() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - ]; - let _post_states = add_liquidity( - &pre_states, - &[ - BalanceForTests::add_min_amount_lp(), - BalanceForTests::add_max_amount_a(), - BalanceForTests::add_max_amount_b(), - ], - ); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_add_liquidity_with_invalid_number_of_accounts_5() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - ]; - let _post_states = add_liquidity( - &pre_states, - &[ - BalanceForTests::add_min_amount_lp(), - BalanceForTests::add_max_amount_a(), - BalanceForTests::add_max_amount_b(), - ], - ); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_add_liquidity_with_invalid_number_of_accounts_6() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - ]; - let _post_states = add_liquidity( - &pre_states, - &[ - BalanceForTests::add_min_amount_lp(), - BalanceForTests::add_max_amount_a(), - BalanceForTests::add_max_amount_b(), - ], - ); - } - - #[should_panic(expected = "Invalid number of input balances")] - #[test] - fn test_call_add_liquidity_invalid_number_of_balances_1() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let _post_states = add_liquidity(&pre_states, &[BalanceForTests::add_min_amount_lp()]); - } - - #[should_panic(expected = "Invalid number of input balances")] - #[test] - fn test_call_add_liquidity_invalid_number_of_balances_2() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let _post_states = add_liquidity( - &pre_states, - &[ - BalanceForTests::add_min_amount_lp(), - BalanceForTests::add_max_amount_a(), - ], - ); - } - - #[should_panic(expected = "Vault A was not provided")] - #[test] - fn test_call_add_liquidity_vault_a_omitted() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_with_wrong_id(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let _post_states = add_liquidity( - &pre_states, - &[ - BalanceForTests::add_min_amount_lp(), - BalanceForTests::add_max_amount_a(), - BalanceForTests::add_max_amount_b(), - ], - ); - } - - #[should_panic(expected = "Vault B was not provided")] - #[test] - fn test_call_add_liquidity_vault_b_omitted() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_with_wrong_id(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let _post_states = add_liquidity( - &pre_states, - &[ - BalanceForTests::add_min_amount_lp(), - BalanceForTests::add_max_amount_a(), - BalanceForTests::add_max_amount_b(), - ], - ); - } - - #[should_panic(expected = "LP definition mismatch")] - #[test] - fn test_call_add_liquidity_lp_definition_mismatch() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_with_wrong_id(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let _post_states = add_liquidity( - &pre_states, - &[ - BalanceForTests::add_min_amount_lp(), - BalanceForTests::add_max_amount_a(), - BalanceForTests::add_max_amount_b(), - ], - ); - } - - #[should_panic(expected = "Both max-balances must be nonzero")] - #[test] - fn test_call_add_liquidity_zero_balance_1() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let _post_states = add_liquidity( - &pre_states, - &[ - BalanceForTests::add_min_amount_lp(), - 0, - BalanceForTests::add_max_amount_b(), - ], - ); - } - - #[should_panic(expected = "Both max-balances must be nonzero")] - #[test] - fn test_call_add_liquidity_zero_balance_2() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let _post_states = add_liquidity( - &pre_states, - &[ - BalanceForTests::add_min_amount_lp(), - 0, - BalanceForTests::add_max_amount_a(), - ], - ); - } - - #[should_panic(expected = "Min-lp must be nonzero")] - #[test] - fn test_call_add_liquidity_zero_min_lp() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let _post_states = add_liquidity( - &pre_states, - &[ - 0, - BalanceForTests::add_max_amount_a(), - BalanceForTests::add_max_amount_b(), - ], - ); - } - - #[should_panic(expected = "Vaults' balances must be at least the reserve amounts")] - #[test] - fn test_call_add_liquidity_vault_insufficient_balance_1() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init_zero(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let _post_states = add_liquidity( - &pre_states, - &[ - BalanceForTests::add_max_amount_a(), - BalanceForTests::add_max_amount_b(), - BalanceForTests::add_min_amount_lp(), - ], - ); - } - - #[should_panic(expected = "Vaults' balances must be at least the reserve amounts")] - #[test] - fn test_call_add_liquidity_vault_insufficient_balance_2() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init_zero(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let _post_states = add_liquidity( - &pre_states, - &[ - BalanceForTests::add_max_amount_a(), - BalanceForTests::add_max_amount_b(), - BalanceForTests::add_min_amount_lp(), - ], - ); - } - - #[should_panic(expected = "A trade amount is 0")] - #[test] - fn test_call_add_liquidity_actual_amount_zero_1() { - let pre_states = vec![ - AccountForTests::pool_definition_init_reserve_a_low(), - AccountForTests::vault_a_init_low(), - AccountForTests::vault_b_init_high(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let _post_states = add_liquidity( - &pre_states, - &[ - BalanceForTests::add_min_amount_lp(), - BalanceForTests::add_max_amount_a(), - BalanceForTests::add_max_amount_b(), - ], - ); - } - - #[should_panic(expected = "A trade amount is 0")] - #[test] - fn test_call_add_liquidity_actual_amount_zero_2() { - let pre_states = vec![ - AccountForTests::pool_definition_init_reserve_b_low(), - AccountForTests::vault_a_init_high(), - AccountForTests::vault_b_init_low(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let _post_states = add_liquidity( - &pre_states, - &[ - BalanceForTests::add_min_amount_lp(), - BalanceForTests::add_max_amount_a_low(), - BalanceForTests::add_max_amount_b_low(), - ], - ); - } - - #[should_panic(expected = "Reserves must be nonzero")] - #[test] - fn test_call_add_liquidity_reserves_zero_1() { - let pre_states = vec![ - AccountForTests::pool_definition_init_reserve_a_zero(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let _post_states = add_liquidity( - &pre_states, - &[ - BalanceForTests::add_min_amount_lp(), - BalanceForTests::add_max_amount_a(), - BalanceForTests::add_max_amount_b(), - ], - ); - } - - #[should_panic(expected = "Reserves must be nonzero")] - #[test] - fn test_call_add_liquidity_reserves_zero_2() { - let pre_states = vec![ - AccountForTests::pool_definition_init_reserve_b_zero(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let _post_states = add_liquidity( - &pre_states, - &[ - BalanceForTests::add_min_amount_lp(), - BalanceForTests::add_max_amount_a(), - BalanceForTests::add_max_amount_b(), - ], - ); - } - - #[should_panic(expected = "Payable LP must be nonzero")] - #[test] - fn test_call_add_liquidity_payable_lp_zero() { - let pre_states = vec![ - AccountForTests::pool_definition_add_zero_lp(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let _post_states = add_liquidity( - &pre_states, - &[ - BalanceForTests::add_min_amount_lp(), - BalanceForTests::add_max_amount_a_low(), - BalanceForTests::add_max_amount_b_low(), - ], - ); - } - - #[test] - fn test_call_add_liquidity_chained_call_successsful() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - AccountForTests::user_holding_lp_init(), - ]; - let (post_states, chained_calls) = add_liquidity( - &pre_states, - &[ - BalanceForTests::add_min_amount_lp(), - BalanceForTests::add_max_amount_a(), - BalanceForTests::add_max_amount_b(), - ], - ); - - let pool_post = post_states[0].clone(); - - assert!(AccountForTests::pool_definition_add_successful().account == *pool_post.account()); - - let chained_call_lp = chained_calls[0].clone(); - let chained_call_b = chained_calls[1].clone(); - let chained_call_a = chained_calls[2].clone(); - - assert!(chained_call_a == ChainedCallForTests::cc_add_token_a()); - assert!(chained_call_b == ChainedCallForTests::cc_add_token_b()); - assert!(chained_call_lp == ChainedCallForTests::cc_add_pool_lp()); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_swap_with_invalid_number_of_accounts_1() { - let pre_states = vec![AccountForTests::pool_definition_init()]; - let _post_states = swap( - &pre_states, - &[ - BalanceForTests::add_max_amount_a(), - BalanceForTests::min_amount_out(), - ], - IdForTests::token_a_definition_id(), - ); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_swap_with_invalid_number_of_accounts_2() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - ]; - let _post_states = swap( - &pre_states, - &[ - BalanceForTests::add_max_amount_a(), - BalanceForTests::min_amount_out(), - ], - IdForTests::token_a_definition_id(), - ); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_swap_with_invalid_number_of_accounts_3() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - ]; - let _post_states = swap( - &pre_states, - &[ - BalanceForTests::add_max_amount_a(), - BalanceForTests::min_amount_out(), - ], - IdForTests::token_a_definition_id(), - ); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_swap_with_invalid_number_of_accounts_4() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::user_holding_a(), - ]; - let _post_states = swap( - &pre_states, - &[ - BalanceForTests::add_max_amount_a(), - BalanceForTests::min_amount_out(), - ], - IdForTests::token_a_definition_id(), - ); - } - - #[should_panic(expected = "Invalid number of amounts provided")] - #[test] - fn test_call_swap_with_invalid_number_of_amounts() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - ]; - let _post_states = swap( - &pre_states, - &[BalanceForTests::add_max_amount_a()], - IdForTests::token_a_definition_id(), - ); - } - - #[should_panic(expected = "AccountId is not a token type for the pool")] - #[test] - fn test_call_swap_incorrect_token_type() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - ]; - let _post_states = swap( - &pre_states, - &[ - BalanceForTests::add_max_amount_a(), - BalanceForTests::min_amount_out(), - ], - IdForTests::token_lp_definition_id(), - ); - } - - #[should_panic(expected = "Vault A was not provided")] - #[test] - fn test_call_swap_vault_a_omitted() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_with_wrong_id(), - AccountForTests::vault_b_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - ]; - let _post_states = swap( - &pre_states, - &[ - BalanceForTests::add_max_amount_a(), - BalanceForTests::min_amount_out(), - ], - IdForTests::token_a_definition_id(), - ); - } - - #[should_panic(expected = "Vault B was not provided")] - #[test] - fn test_call_swap_vault_b_omitted() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_with_wrong_id(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - ]; - let _post_states = swap( - &pre_states, - &[ - BalanceForTests::add_max_amount_a(), - BalanceForTests::min_amount_out(), - ], - IdForTests::token_a_definition_id(), - ); - } - - #[should_panic(expected = "Reserve for Token A exceeds vault balance")] - #[test] - fn test_call_swap_reserves_vault_mismatch_1() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init_low(), - AccountForTests::vault_b_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - ]; - let _post_states = swap( - &pre_states, - &[ - BalanceForTests::add_max_amount_a(), - BalanceForTests::min_amount_out(), - ], - IdForTests::token_a_definition_id(), - ); - } - - #[should_panic(expected = "Reserve for Token B exceeds vault balance")] - #[test] - fn test_call_swap_reserves_vault_mismatch_2() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init_low(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - ]; - let _post_states = swap( - &pre_states, - &[ - BalanceForTests::add_max_amount_a(), - BalanceForTests::min_amount_out(), - ], - IdForTests::token_a_definition_id(), - ); - } - - #[should_panic(expected = "Pool is inactive")] - #[test] - fn test_call_swap_ianctive() { - let pre_states = vec![ - AccountForTests::pool_definition_inactive(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - ]; - let _post_states = swap( - &pre_states, - &[ - BalanceForTests::add_max_amount_a(), - BalanceForTests::min_amount_out(), - ], - IdForTests::token_a_definition_id(), - ); - } - - #[should_panic(expected = "Withdraw amount is less than minimal amount out")] - #[test] - fn test_call_swap_below_min_out() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - ]; - let _post_states = swap( - &pre_states, - &[ - BalanceForTests::add_max_amount_a(), - BalanceForTests::min_amount_out(), - ], - IdForTests::token_a_definition_id(), - ); - } - - #[test] - fn test_call_swap_chained_call_successful_1() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - ]; - let (post_states, chained_calls) = swap( - &pre_states, - &[ - BalanceForTests::add_max_amount_a(), - BalanceForTests::add_max_amount_a_low(), - ], - IdForTests::token_a_definition_id(), - ); - - let pool_post = post_states[0].clone(); - - assert!(AccountForTests::pool_definition_swap_test_1().account == *pool_post.account()); - - let chained_call_a = chained_calls[0].clone(); - let chained_call_b = chained_calls[1].clone(); - - assert_eq!( - chained_call_a, - ChainedCallForTests::cc_swap_token_a_test_1() - ); - assert_eq!( - chained_call_b, - ChainedCallForTests::cc_swap_token_b_test_1() - ); - } - - #[test] - fn test_call_swap_chained_call_successful_2() { - let pre_states = vec![ - AccountForTests::pool_definition_init(), - AccountForTests::vault_a_init(), - AccountForTests::vault_b_init(), - AccountForTests::user_holding_a(), - AccountForTests::user_holding_b(), - ]; - let (post_states, chained_calls) = swap( - &pre_states, - &[ - BalanceForTests::add_max_amount_b(), - BalanceForTests::min_amount_out(), - ], - IdForTests::token_b_definition_id(), - ); - - let pool_post = post_states[0].clone(); - - assert!(AccountForTests::pool_definition_swap_test_2().account == *pool_post.account()); - - let chained_call_a = chained_calls[1].clone(); - let chained_call_b = chained_calls[0].clone(); - - assert_eq!( - chained_call_a, - ChainedCallForTests::cc_swap_token_a_test_2() - ); - assert_eq!( - chained_call_b, - ChainedCallForTests::cc_swap_token_b_test_2() - ); - } } diff --git a/programs/amm/Cargo.toml b/programs/amm/Cargo.toml new file mode 100644 index 00000000..252cb825 --- /dev/null +++ b/programs/amm/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "amm_program" +version = "0.1.0" +edition = "2024" +license = { workspace = true } + +[dependencies] +nssa_core.workspace = true +token_core.workspace = true +amm_core.workspace = true \ No newline at end of file diff --git a/programs/amm/core/Cargo.toml b/programs/amm/core/Cargo.toml new file mode 100644 index 00000000..188e87c8 --- /dev/null +++ b/programs/amm/core/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "amm_core" +version = "0.1.0" +edition = "2024" +license = { workspace = true } + +[dependencies] +nssa_core.workspace = true +serde.workspace = true +risc0-zkvm.workspace = true +borsh.workspace = true \ No newline at end of file diff --git a/programs/amm/core/src/lib.rs b/programs/amm/core/src/lib.rs new file mode 100644 index 00000000..f9d20dd3 --- /dev/null +++ b/programs/amm/core/src/lib.rs @@ -0,0 +1,197 @@ +//! This crate contains core data structures and utilities for the AMM Program. + +use borsh::{BorshDeserialize, BorshSerialize}; +use nssa_core::{ + account::{AccountId, Data}, + program::{PdaSeed, ProgramId}, +}; +use serde::{Deserialize, Serialize}; + +/// AMM Program Instruction. +#[derive(Serialize, Deserialize)] +pub enum Instruction { + /// Initializes a new Pool (or re-initializes an inactive Pool). + /// + /// Required accounts: + /// - AMM Pool + /// - Vault Holding Account for Token A + /// - Vault Holding Account for Token B + /// - Pool Liquidity Token Definition + /// - User Holding Account for Token A (authorized) + /// - User Holding Account for Token B (authorized) + /// - User Holding Account for Pool Liquidity + NewDefinition { + token_a_amount: u128, + token_b_amount: u128, + amm_program_id: ProgramId, + }, + + /// Adds liquidity to the Pool + /// + /// Required accounts: + /// - AMM Pool (initialized) + /// - Vault Holding Account for Token A (initialized) + /// - Vault Holding Account for Token B (initialized) + /// - Pool Liquidity Token Definition (initialized) + /// - User Holding Account for Token A (authorized) + /// - User Holding Account for Token B (authorized) + /// - User Holding Account for Pool Liquidity + AddLiquidity { + min_amount_liquidity: u128, + max_amount_to_add_token_a: u128, + max_amount_to_add_token_b: u128, + }, + + /// Removes liquidity from the Pool + /// + /// Required accounts: + /// - AMM Pool (initialized) + /// - Vault Holding Account for Token A (initialized) + /// - Vault Holding Account for Token B (initialized) + /// - Pool Liquidity Token Definition (initialized) + /// - User Holding Account for Token A (initialized) + /// - User Holding Account for Token B (initialized) + /// - User Holding Account for Pool Liquidity (authorized) + RemoveLiquidity { + remove_liquidity_amount: u128, + min_amount_to_remove_token_a: u128, + min_amount_to_remove_token_b: u128, + }, + + /// Swap some quantity of Tokens (either Token A or Token B) + /// while maintaining the Pool constant product. + /// + /// Required accounts: + /// - AMM Pool (initialized) + /// - Vault Holding Account for Token A (initialized) + /// - Vault Holding Account for Token B (initialized) + /// - User Holding Account for Token A + /// - User Holding Account for Token B Either User Holding Account for Token A or Token B is + /// authorized. + Swap { + swap_amount_in: u128, + min_amount_out: u128, + token_definition_id_in: AccountId, + }, +} + +#[derive(Clone, Default, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct PoolDefinition { + pub definition_token_a_id: AccountId, + pub definition_token_b_id: AccountId, + pub vault_a_id: AccountId, + pub vault_b_id: AccountId, + pub liquidity_pool_id: AccountId, + pub liquidity_pool_supply: u128, + pub reserve_a: u128, + pub reserve_b: u128, + /// Fees are currently not used + pub fees: u128, + /// A pool becomes inactive (active = false) + /// once all of its liquidity has been removed (e.g., reserves are emptied and + /// liquidity_pool_supply = 0) + pub active: bool, +} + +impl TryFrom<&Data> for PoolDefinition { + type Error = std::io::Error; + + fn try_from(data: &Data) -> Result { + PoolDefinition::try_from_slice(data.as_ref()) + } +} + +impl From<&PoolDefinition> for Data { + fn from(definition: &PoolDefinition) -> Self { + // Using size_of_val as size hint for Vec allocation + let mut data = Vec::with_capacity(std::mem::size_of_val(definition)); + + BorshSerialize::serialize(definition, &mut data) + .expect("Serialization to Vec should not fail"); + + Data::try_from(data).expect("Token definition encoded data should fit into Data") + } +} + +pub fn compute_pool_pda( + amm_program_id: ProgramId, + definition_token_a_id: AccountId, + definition_token_b_id: AccountId, +) -> AccountId { + AccountId::from(( + &amm_program_id, + &compute_pool_pda_seed(definition_token_a_id, definition_token_b_id), + )) +} + +pub fn compute_pool_pda_seed( + definition_token_a_id: AccountId, + definition_token_b_id: AccountId, +) -> PdaSeed { + use risc0_zkvm::sha::{Impl, Sha256}; + + let (token_1, token_2) = match definition_token_a_id + .value() + .cmp(definition_token_b_id.value()) + { + std::cmp::Ordering::Less => (definition_token_b_id, definition_token_a_id), + std::cmp::Ordering::Greater => (definition_token_a_id, definition_token_b_id), + std::cmp::Ordering::Equal => panic!("Definitions match"), + }; + + let mut bytes = [0; 64]; + bytes[0..32].copy_from_slice(&token_1.to_bytes()); + bytes[32..].copy_from_slice(&token_2.to_bytes()); + + PdaSeed::new( + Impl::hash_bytes(&bytes) + .as_bytes() + .try_into() + .expect("Hash output must be exactly 32 bytes long"), + ) +} + +pub fn compute_vault_pda( + amm_program_id: ProgramId, + pool_id: AccountId, + definition_token_id: AccountId, +) -> AccountId { + AccountId::from(( + &amm_program_id, + &compute_vault_pda_seed(pool_id, definition_token_id), + )) +} + +pub fn compute_vault_pda_seed(pool_id: AccountId, definition_token_id: AccountId) -> PdaSeed { + use risc0_zkvm::sha::{Impl, Sha256}; + + let mut bytes = [0; 64]; + bytes[0..32].copy_from_slice(&pool_id.to_bytes()); + bytes[32..].copy_from_slice(&definition_token_id.to_bytes()); + + PdaSeed::new( + Impl::hash_bytes(&bytes) + .as_bytes() + .try_into() + .expect("Hash output must be exactly 32 bytes long"), + ) +} + +pub fn compute_liquidity_token_pda(amm_program_id: ProgramId, pool_id: AccountId) -> AccountId { + AccountId::from((&amm_program_id, &compute_liquidity_token_pda_seed(pool_id))) +} + +pub fn compute_liquidity_token_pda_seed(pool_id: AccountId) -> PdaSeed { + use risc0_zkvm::sha::{Impl, Sha256}; + + let mut bytes = [0; 64]; + bytes[0..32].copy_from_slice(&pool_id.to_bytes()); + bytes[32..].copy_from_slice(&[0; 32]); + + PdaSeed::new( + Impl::hash_bytes(&bytes) + .as_bytes() + .try_into() + .expect("Hash output must be exactly 32 bytes long"), + ) +} diff --git a/programs/amm/src/add.rs b/programs/amm/src/add.rs new file mode 100644 index 00000000..cbecaa34 --- /dev/null +++ b/programs/amm/src/add.rs @@ -0,0 +1,178 @@ +use std::num::NonZeroU128; + +use amm_core::{PoolDefinition, compute_liquidity_token_pda_seed}; +use nssa_core::{ + account::{AccountWithMetadata, Data}, + program::{AccountPostState, ChainedCall}, +}; + +#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] +pub fn add_liquidity( + pool: AccountWithMetadata, + vault_a: AccountWithMetadata, + vault_b: AccountWithMetadata, + pool_definition_lp: AccountWithMetadata, + user_holding_a: AccountWithMetadata, + user_holding_b: AccountWithMetadata, + user_holding_lp: AccountWithMetadata, + min_amount_liquidity: NonZeroU128, + max_amount_to_add_token_a: u128, + max_amount_to_add_token_b: u128, +) -> (Vec, Vec) { + // 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_eq!( + vault_a.account_id, pool_def_data.vault_a_id, + "Vault A was not provided" + ); + + assert_eq!( + pool_def_data.liquidity_pool_id, pool_definition_lp.account_id, + "LP definition mismatch" + ); + + assert_eq!( + vault_b.account_id, pool_def_data.vault_b_id, + "Vault B was not provided" + ); + + assert!( + max_amount_to_add_token_a != 0 && max_amount_to_add_token_b != 0, + "Both max-balances must be nonzero" + ); + + // 2. Determine deposit amount + let vault_b_token_holding = token_core::TokenHolding::try_from(&vault_b.account.data) + .expect("Add liquidity: AMM Program expects valid Token Holding Account for Vault B"); + let token_core::TokenHolding::Fungible { + definition_id: _, + balance: vault_b_balance, + } = vault_b_token_holding + else { + panic!( + "Add liquidity: AMM Program expects valid Fungible Token Holding Account for Vault B" + ); + }; + + let vault_a_token_holding = token_core::TokenHolding::try_from(&vault_a.account.data) + .expect("Add liquidity: AMM Program expects valid Token Holding Account for Vault A"); + let token_core::TokenHolding::Fungible { + definition_id: _, + balance: vault_a_balance, + } = vault_a_token_holding + else { + panic!( + "Add liquidity: AMM Program expects valid Fungible Token Holding Account for Vault A" + ); + }; + + assert!(pool_def_data.reserve_a != 0, "Reserves must be nonzero"); + assert!(pool_def_data.reserve_b != 0, "Reserves must be nonzero"); + assert!( + vault_a_balance >= pool_def_data.reserve_a, + "Vaults' balances must be at least the reserve amounts" + ); + assert!( + vault_b_balance >= pool_def_data.reserve_b, + "Vaults' balances must be at least the reserve amounts" + ); + + // Calculate actual_amounts + let ideal_a: u128 = + (pool_def_data.reserve_a * max_amount_to_add_token_b) / pool_def_data.reserve_b; + let ideal_b: u128 = + (pool_def_data.reserve_b * max_amount_to_add_token_a) / pool_def_data.reserve_a; + + let actual_amount_a = if ideal_a > max_amount_to_add_token_a { + max_amount_to_add_token_a + } else { + ideal_a + }; + let actual_amount_b = if ideal_b > max_amount_to_add_token_b { + max_amount_to_add_token_b + } else { + ideal_b + }; + + // 3. Validate amounts + assert!( + max_amount_to_add_token_a >= actual_amount_a, + "Actual trade amounts cannot exceed max_amounts" + ); + assert!( + max_amount_to_add_token_b >= actual_amount_b, + "Actual trade amounts cannot exceed max_amounts" + ); + + assert!(actual_amount_a != 0, "A trade amount is 0"); + assert!(actual_amount_b != 0, "A trade amount is 0"); + + // 4. Calculate LP to mint + let delta_lp = std::cmp::min( + pool_def_data.liquidity_pool_supply * actual_amount_a / pool_def_data.reserve_a, + pool_def_data.liquidity_pool_supply * actual_amount_b / pool_def_data.reserve_b, + ); + + assert!(delta_lp != 0, "Payable LP must be nonzero"); + + assert!( + delta_lp >= min_amount_liquidity.into(), + "Payable LP is less than provided minimum LP amount" + ); + + // 5. Update pool account + let mut pool_post = pool.account.clone(); + let pool_post_definition = PoolDefinition { + liquidity_pool_supply: pool_def_data.liquidity_pool_supply + delta_lp, + reserve_a: pool_def_data.reserve_a + actual_amount_a, + reserve_b: pool_def_data.reserve_b + actual_amount_b, + ..pool_def_data + }; + + pool_post.data = Data::from(&pool_post_definition); + let token_program_id = user_holding_a.account.program_owner; + + // Chain call for Token A (UserHoldingA -> Vault_A) + let call_token_a = ChainedCall::new( + token_program_id, + vec![user_holding_a.clone(), vault_a.clone()], + &token_core::Instruction::Transfer { + amount_to_transfer: actual_amount_a, + }, + ); + // Chain call for Token B (UserHoldingB -> Vault_B) + let call_token_b = ChainedCall::new( + token_program_id, + vec![user_holding_b.clone(), vault_b.clone()], + &token_core::Instruction::Transfer { + amount_to_transfer: actual_amount_b, + }, + ); + // Chain call for LP (mint new tokens for user_holding_lp) + let mut pool_definition_lp_auth = pool_definition_lp.clone(); + pool_definition_lp_auth.is_authorized = true; + let call_token_lp = ChainedCall::new( + token_program_id, + vec![pool_definition_lp_auth.clone(), user_holding_lp.clone()], + &token_core::Instruction::Mint { + amount_to_mint: delta_lp, + }, + ) + .with_pda_seeds(vec![compute_liquidity_token_pda_seed(pool.account_id)]); + + let chained_calls = vec![call_token_lp, call_token_b, call_token_a]; + + let post_states = vec![ + AccountPostState::new(pool_post), + AccountPostState::new(vault_a.account.clone()), + AccountPostState::new(vault_b.account.clone()), + AccountPostState::new(pool_definition_lp.account.clone()), + AccountPostState::new(user_holding_a.account.clone()), + AccountPostState::new(user_holding_b.account.clone()), + AccountPostState::new(user_holding_lp.account.clone()), + ]; + + (post_states, chained_calls) +} diff --git a/programs/amm/src/lib.rs b/programs/amm/src/lib.rs new file mode 100644 index 00000000..e50c738e --- /dev/null +++ b/programs/amm/src/lib.rs @@ -0,0 +1,10 @@ +//! The AMM Program implementation. + +pub use amm_core as core; + +pub mod add; +pub mod new_definition; +pub mod remove; +pub mod swap; + +mod tests; diff --git a/programs/amm/src/new_definition.rs b/programs/amm/src/new_definition.rs new file mode 100644 index 00000000..ab0b241a --- /dev/null +++ b/programs/amm/src/new_definition.rs @@ -0,0 +1,158 @@ +use std::num::NonZeroU128; + +use amm_core::{ + PoolDefinition, compute_liquidity_token_pda, compute_liquidity_token_pda_seed, + compute_pool_pda, compute_vault_pda, +}; +use nssa_core::{ + account::{Account, AccountWithMetadata, Data}, + program::{AccountPostState, ChainedCall, ProgramId}, +}; + +#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] +pub fn new_definition( + pool: AccountWithMetadata, + vault_a: AccountWithMetadata, + vault_b: AccountWithMetadata, + pool_definition_lp: AccountWithMetadata, + user_holding_a: AccountWithMetadata, + user_holding_b: AccountWithMetadata, + user_holding_lp: AccountWithMetadata, + token_a_amount: NonZeroU128, + token_b_amount: NonZeroU128, + amm_program_id: ProgramId, +) -> (Vec, Vec) { + // Verify token_a and token_b are different + let definition_token_a_id = token_core::TokenHolding::try_from(&user_holding_a.account.data) + .expect("New definition: AMM Program expects valid Token Holding account for Token A") + .definition_id(); + let definition_token_b_id = token_core::TokenHolding::try_from(&user_holding_b.account.data) + .expect("New definition: AMM Program expects valid Token Holding account for Token B") + .definition_id(); + + // both instances of the same token program + let token_program = user_holding_a.account.program_owner; + + assert_eq!( + user_holding_b.account.program_owner, token_program, + "User Token holdings must use the same Token Program" + ); + assert!( + definition_token_a_id != definition_token_b_id, + "Cannot set up a swap for a token with itself" + ); + assert_eq!( + pool.account_id, + compute_pool_pda(amm_program_id, definition_token_a_id, definition_token_b_id), + "Pool Definition Account ID does not match PDA" + ); + assert_eq!( + vault_a.account_id, + compute_vault_pda(amm_program_id, pool.account_id, definition_token_a_id), + "Vault ID does not match PDA" + ); + assert_eq!( + vault_b.account_id, + compute_vault_pda(amm_program_id, pool.account_id, definition_token_b_id), + "Vault ID does not match PDA" + ); + assert_eq!( + pool_definition_lp.account_id, + compute_liquidity_token_pda(amm_program_id, pool.account_id), + "Liquidity pool Token Definition Account ID does not match PDA" + ); + + // TODO: return here + // Verify that Pool Account is not active + let pool_account_data = if pool.account == Account::default() { + PoolDefinition::default() + } else { + PoolDefinition::try_from(&pool.account.data) + .expect("AMM program expects a valid Pool account") + }; + + assert!( + !pool_account_data.active, + "Cannot initialize an active Pool Definition" + ); + + // LP Token minting calculation + // We assume LP is based on the initial deposit amount for Token_A. + + // Update pool account + let mut pool_post = pool.account.clone(); + let pool_post_definition = PoolDefinition { + definition_token_a_id, + definition_token_b_id, + vault_a_id: vault_a.account_id, + vault_b_id: vault_b.account_id, + liquidity_pool_id: pool_definition_lp.account_id, + liquidity_pool_supply: token_a_amount.into(), + reserve_a: token_a_amount.into(), + reserve_b: token_b_amount.into(), + fees: 0u128, // TODO: we assume all fees are 0 for now. + active: true, + }; + + pool_post.data = Data::from(&pool_post_definition); + let pool_post: AccountPostState = if pool.account == Account::default() { + AccountPostState::new_claimed(pool_post.clone()) + } else { + AccountPostState::new(pool_post.clone()) + }; + + let token_program_id = user_holding_a.account.program_owner; + + // Chain call for Token A (user_holding_a -> Vault_A) + let call_token_a = ChainedCall::new( + token_program_id, + vec![user_holding_a.clone(), vault_a.clone()], + &token_core::Instruction::Transfer { + amount_to_transfer: token_a_amount.into(), + }, + ); + // Chain call for Token B (user_holding_b -> Vault_B) + let call_token_b = ChainedCall::new( + token_program_id, + vec![user_holding_b.clone(), vault_b.clone()], + &token_core::Instruction::Transfer { + amount_to_transfer: token_b_amount.into(), + }, + ); + + // Chain call for liquidity token (TokenLP definition -> User LP Holding) + let instruction = if pool.account == Account::default() { + token_core::Instruction::NewFungibleDefinition { + name: String::from("LP Token"), + total_supply: token_a_amount.into(), + } + } else { + token_core::Instruction::Mint { + amount_to_mint: token_a_amount.into(), + } + }; + + let mut pool_lp_auth = pool_definition_lp.clone(); + pool_lp_auth.is_authorized = true; + + let call_token_lp = ChainedCall::new( + token_program_id, + vec![pool_lp_auth.clone(), user_holding_lp.clone()], + &instruction, + ) + .with_pda_seeds(vec![compute_liquidity_token_pda_seed(pool.account_id)]); + + let chained_calls = vec![call_token_lp, call_token_b, call_token_a]; + + let post_states = vec![ + pool_post.clone(), + AccountPostState::new(vault_a.account.clone()), + AccountPostState::new(vault_b.account.clone()), + AccountPostState::new(pool_definition_lp.account.clone()), + AccountPostState::new(user_holding_a.account.clone()), + AccountPostState::new(user_holding_b.account.clone()), + AccountPostState::new(user_holding_lp.account.clone()), + ]; + + (post_states.clone(), chained_calls) +} diff --git a/programs/amm/src/remove.rs b/programs/amm/src/remove.rs new file mode 100644 index 00000000..370b3609 --- /dev/null +++ b/programs/amm/src/remove.rs @@ -0,0 +1,166 @@ +use std::num::NonZeroU128; + +use amm_core::{PoolDefinition, compute_liquidity_token_pda_seed, compute_vault_pda_seed}; +use nssa_core::{ + account::{AccountWithMetadata, Data}, + program::{AccountPostState, ChainedCall}, +}; + +#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] +pub fn remove_liquidity( + pool: AccountWithMetadata, + vault_a: AccountWithMetadata, + vault_b: AccountWithMetadata, + pool_definition_lp: AccountWithMetadata, + user_holding_a: AccountWithMetadata, + user_holding_b: AccountWithMetadata, + user_holding_lp: AccountWithMetadata, + remove_liquidity_amount: NonZeroU128, + min_amount_to_remove_token_a: u128, + min_amount_to_remove_token_b: u128, +) -> (Vec, Vec) { + let remove_liquidity_amount: u128 = remove_liquidity_amount.into(); + + // 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!(pool_def_data.active, "Pool is inactive"); + assert_eq!( + pool_def_data.liquidity_pool_id, pool_definition_lp.account_id, + "LP definition mismatch" + ); + assert_eq!( + vault_a.account_id, pool_def_data.vault_a_id, + "Vault A was not provided" + ); + assert_eq!( + vault_b.account_id, pool_def_data.vault_b_id, + "Vault B was not provided" + ); + + // Vault addresses do not need to be checked with PDA + // calculation for setting authorization since stored + // in the Pool Definition. + let mut running_vault_a = vault_a.clone(); + let mut running_vault_b = vault_b.clone(); + running_vault_a.is_authorized = true; + running_vault_b.is_authorized = true; + + assert!( + min_amount_to_remove_token_a != 0, + "Minimum withdraw amount must be nonzero" + ); + assert!( + min_amount_to_remove_token_b != 0, + "Minimum withdraw amount must be nonzero" + ); + + // 2. Compute withdrawal amounts + let user_holding_lp_data = token_core::TokenHolding::try_from(&user_holding_lp.account.data) + .expect("Remove liquidity: AMM Program expects a valid Token Account for liquidity token"); + let token_core::TokenHolding::Fungible { + definition_id: _, + balance: user_lp_balance, + } = user_holding_lp_data + else { + panic!( + "Remove liquidity: AMM Program expects a valid Fungible Token Holding Account for liquidity token" + ); + }; + + assert!( + user_lp_balance <= pool_def_data.liquidity_pool_supply, + "Invalid liquidity account provided" + ); + assert_eq!( + user_holding_lp_data.definition_id(), + pool_def_data.liquidity_pool_id, + "Invalid liquidity account provided" + ); + + let withdraw_amount_a = + (pool_def_data.reserve_a * remove_liquidity_amount) / pool_def_data.liquidity_pool_supply; + let withdraw_amount_b = + (pool_def_data.reserve_b * remove_liquidity_amount) / pool_def_data.liquidity_pool_supply; + + // 3. Validate and slippage check + assert!( + withdraw_amount_a >= min_amount_to_remove_token_a, + "Insufficient minimal withdraw amount (Token A) provided for liquidity amount" + ); + assert!( + withdraw_amount_b >= min_amount_to_remove_token_b, + "Insufficient minimal withdraw amount (Token B) provided for liquidity amount" + ); + + // 4. Calculate LP to reduce cap by + let delta_lp: u128 = (pool_def_data.liquidity_pool_supply * remove_liquidity_amount) + / pool_def_data.liquidity_pool_supply; + + let active: bool = pool_def_data.liquidity_pool_supply - delta_lp != 0; + + // 5. Update pool account + let mut pool_post = pool.account.clone(); + let pool_post_definition = PoolDefinition { + liquidity_pool_supply: pool_def_data.liquidity_pool_supply - delta_lp, + reserve_a: pool_def_data.reserve_a - withdraw_amount_a, + reserve_b: pool_def_data.reserve_b - withdraw_amount_b, + active, + ..pool_def_data.clone() + }; + + pool_post.data = Data::from(&pool_post_definition); + + let token_program_id = user_holding_a.account.program_owner; + + // Chaincall for Token A withdraw + let call_token_a = ChainedCall::new( + token_program_id, + vec![running_vault_a, user_holding_a.clone()], + &token_core::Instruction::Transfer { + amount_to_transfer: withdraw_amount_a, + }, + ) + .with_pda_seeds(vec![compute_vault_pda_seed( + pool.account_id, + pool_def_data.definition_token_a_id, + )]); + // Chaincall for Token B withdraw + let call_token_b = ChainedCall::new( + token_program_id, + vec![running_vault_b, user_holding_b.clone()], + &token_core::Instruction::Transfer { + amount_to_transfer: withdraw_amount_b, + }, + ) + .with_pda_seeds(vec![compute_vault_pda_seed( + pool.account_id, + pool_def_data.definition_token_b_id, + )]); + // Chaincall for LP adjustment + let mut pool_definition_lp_auth = pool_definition_lp.clone(); + pool_definition_lp_auth.is_authorized = true; + let call_token_lp = ChainedCall::new( + token_program_id, + vec![pool_definition_lp_auth, user_holding_lp.clone()], + &token_core::Instruction::Burn { + amount_to_burn: delta_lp, + }, + ) + .with_pda_seeds(vec![compute_liquidity_token_pda_seed(pool.account_id)]); + + let chained_calls = vec![call_token_lp, call_token_b, call_token_a]; + + let post_states = vec![ + AccountPostState::new(pool_post.clone()), + AccountPostState::new(vault_a.account.clone()), + AccountPostState::new(vault_b.account.clone()), + AccountPostState::new(pool_definition_lp.account.clone()), + AccountPostState::new(user_holding_a.account.clone()), + AccountPostState::new(user_holding_b.account.clone()), + AccountPostState::new(user_holding_lp.account.clone()), + ]; + + (post_states, chained_calls) +} diff --git a/programs/amm/src/swap.rs b/programs/amm/src/swap.rs new file mode 100644 index 00000000..aa02ac24 --- /dev/null +++ b/programs/amm/src/swap.rs @@ -0,0 +1,176 @@ +pub use amm_core::{PoolDefinition, compute_liquidity_token_pda_seed, compute_vault_pda_seed}; +use nssa_core::{ + account::{AccountId, AccountWithMetadata, Data}, + program::{AccountPostState, ChainedCall}, +}; + +#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] +pub fn swap( + pool: AccountWithMetadata, + vault_a: AccountWithMetadata, + vault_b: AccountWithMetadata, + user_holding_a: AccountWithMetadata, + user_holding_b: AccountWithMetadata, + swap_amount_in: u128, + min_amount_out: u128, + token_in_id: AccountId, +) -> (Vec, Vec) { + // Verify vaults are in fact vaults + let pool_def_data = PoolDefinition::try_from(&pool.account.data) + .expect("Swap: AMM Program expects a valid Pool Definition Account"); + + assert!(pool_def_data.active, "Pool is inactive"); + assert_eq!( + vault_a.account_id, pool_def_data.vault_a_id, + "Vault A was not provided" + ); + assert_eq!( + vault_b.account_id, pool_def_data.vault_b_id, + "Vault B was not provided" + ); + + // fetch pool reserves + // validates reserves is at least the vaults' balances + let vault_a_token_holding = token_core::TokenHolding::try_from(&vault_a.account.data) + .expect("Swap: AMM Program expects a valid Token Holding Account for Vault A"); + let token_core::TokenHolding::Fungible { + definition_id: _, + balance: vault_a_balance, + } = vault_a_token_holding + else { + panic!("Swap: AMM Program expects a valid Fungible Token Holding Account for Vault A"); + }; + + assert!( + vault_a_balance >= pool_def_data.reserve_a, + "Reserve for Token A exceeds vault balance" + ); + + let vault_b_token_holding = token_core::TokenHolding::try_from(&vault_b.account.data) + .expect("Swap: AMM Program expects a valid Token Holding Account for Vault B"); + let token_core::TokenHolding::Fungible { + definition_id: _, + balance: vault_b_balance, + } = vault_b_token_holding + else { + panic!("Swap: AMM Program expects a valid Fungible Token Holding Account for Vault B"); + }; + + assert!( + vault_b_balance >= pool_def_data.reserve_b, + "Reserve for Token B exceeds vault balance" + ); + + let (chained_calls, [deposit_a, withdraw_a], [deposit_b, withdraw_b]) = + if token_in_id == pool_def_data.definition_token_a_id { + let (chained_calls, deposit_a, withdraw_b) = swap_logic( + user_holding_a.clone(), + vault_a.clone(), + vault_b.clone(), + user_holding_b.clone(), + swap_amount_in, + min_amount_out, + pool_def_data.reserve_a, + pool_def_data.reserve_b, + pool.account_id, + ); + + (chained_calls, [deposit_a, 0], [0, withdraw_b]) + } else if token_in_id == pool_def_data.definition_token_b_id { + let (chained_calls, deposit_b, withdraw_a) = swap_logic( + user_holding_b.clone(), + vault_b.clone(), + vault_a.clone(), + user_holding_a.clone(), + swap_amount_in, + min_amount_out, + pool_def_data.reserve_b, + pool_def_data.reserve_a, + pool.account_id, + ); + + (chained_calls, [0, withdraw_a], [deposit_b, 0]) + } else { + panic!("AccountId is not a token type for the pool"); + }; + + // Update pool account + let mut pool_post = pool.account.clone(); + let pool_post_definition = PoolDefinition { + reserve_a: pool_def_data.reserve_a + deposit_a - withdraw_a, + reserve_b: pool_def_data.reserve_b + deposit_b - withdraw_b, + ..pool_def_data + }; + + pool_post.data = Data::from(&pool_post_definition); + + let post_states = vec![ + AccountPostState::new(pool_post.clone()), + AccountPostState::new(vault_a.account.clone()), + AccountPostState::new(vault_b.account.clone()), + AccountPostState::new(user_holding_a.account.clone()), + AccountPostState::new(user_holding_b.account.clone()), + ]; + + (post_states, chained_calls) +} + +#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] +fn swap_logic( + user_deposit: AccountWithMetadata, + vault_deposit: AccountWithMetadata, + vault_withdraw: AccountWithMetadata, + user_withdraw: AccountWithMetadata, + swap_amount_in: u128, + min_amount_out: u128, + reserve_deposit_vault_amount: u128, + reserve_withdraw_vault_amount: u128, + pool_id: AccountId, +) -> (Vec, u128, u128) { + // Compute withdraw amount + // Maintains pool constant product + // k = pool_def_data.reserve_a * pool_def_data.reserve_b; + let withdraw_amount = (reserve_withdraw_vault_amount * swap_amount_in) + / (reserve_deposit_vault_amount + swap_amount_in); + + // Slippage check + assert!( + min_amount_out <= withdraw_amount, + "Withdraw amount is less than minimal amount out" + ); + assert!(withdraw_amount != 0, "Withdraw amount should be nonzero"); + + let token_program_id = user_deposit.account.program_owner; + + let mut chained_calls = Vec::new(); + chained_calls.push(ChainedCall::new( + token_program_id, + vec![user_deposit, vault_deposit], + &token_core::Instruction::Transfer { + amount_to_transfer: swap_amount_in, + }, + )); + + let mut vault_withdraw = vault_withdraw.clone(); + vault_withdraw.is_authorized = true; + + let pda_seed = compute_vault_pda_seed( + pool_id, + token_core::TokenHolding::try_from(&vault_withdraw.account.data) + .expect("Swap Logic: AMM Program expects valid token data") + .definition_id(), + ); + + chained_calls.push( + ChainedCall::new( + token_program_id, + vec![vault_withdraw, user_withdraw], + &token_core::Instruction::Transfer { + amount_to_transfer: withdraw_amount, + }, + ) + .with_pda_seeds(vec![pda_seed]), + ); + + (chained_calls, swap_amount_in, withdraw_amount) +} diff --git a/programs/amm/src/tests.rs b/programs/amm/src/tests.rs new file mode 100644 index 00000000..021e32b2 --- /dev/null +++ b/programs/amm/src/tests.rs @@ -0,0 +1,1719 @@ +#![cfg(test)] + +use std::num::NonZero; + +use amm_core::{ + PoolDefinition, compute_liquidity_token_pda, compute_liquidity_token_pda_seed, + compute_pool_pda, compute_vault_pda, compute_vault_pda_seed, +}; +use nssa_core::{ + account::{Account, AccountId, AccountWithMetadata, Data}, + program::{ChainedCall, ProgramId}, +}; +use token_core::{TokenDefinition, TokenHolding}; + +use crate::{ + add::add_liquidity, new_definition::new_definition, remove::remove_liquidity, swap::swap, +}; + +const TOKEN_PROGRAM_ID: ProgramId = [15; 8]; +const AMM_PROGRAM_ID: ProgramId = [42; 8]; + +struct BalanceForTests; +struct ChainedCallForTests; +struct IdForTests; +struct AccountForTests; + +impl BalanceForTests { + fn vault_a_reserve_init() -> u128 { + 1_000 + } + + fn vault_b_reserve_init() -> u128 { + 500 + } + + fn vault_a_reserve_low() -> u128 { + 10 + } + + fn vault_b_reserve_low() -> u128 { + 10 + } + + fn vault_a_reserve_high() -> u128 { + 500_000 + } + + fn vault_b_reserve_high() -> u128 { + 500_000 + } + + fn user_token_a_balance() -> u128 { + 1_000 + } + + fn user_token_b_balance() -> u128 { + 500 + } + + fn user_token_lp_balance() -> u128 { + 100 + } + + fn remove_min_amount_a() -> u128 { + 50 + } + + fn remove_min_amount_b() -> u128 { + 100 + } + + fn remove_actual_a_successful() -> u128 { + 100 + } + + fn remove_min_amount_b_low() -> u128 { + 50 + } + + fn remove_amount_lp() -> u128 { + 100 + } + + fn remove_amount_lp_1() -> u128 { + 30 + } + + fn add_max_amount_a() -> u128 { + 500 + } + + fn add_max_amount_b() -> u128 { + 200 + } + + fn add_max_amount_a_low() -> u128 { + 10 + } + + fn add_max_amount_b_low() -> u128 { + 10 + } + + fn add_min_amount_lp() -> u128 { + 20 + } + + fn vault_a_swap_test_1() -> u128 { + 1_500 + } + + fn vault_a_swap_test_2() -> u128 { + 715 + } + + fn vault_b_swap_test_1() -> u128 { + 334 + } + + fn vault_b_swap_test_2() -> u128 { + 700 + } + + fn min_amount_out() -> u128 { + 200 + } + + fn vault_a_add_successful() -> u128 { + 1_400 + } + + fn vault_b_add_successful() -> u128 { + 700 + } + + fn add_successful_amount_a() -> u128 { + 400 + } + + fn add_successful_amount_b() -> u128 { + 200 + } + + fn vault_a_remove_successful() -> u128 { + 900 + } + + fn vault_b_remove_successful() -> u128 { + 450 + } +} + +impl ChainedCallForTests { + fn cc_swap_token_a_test_1() -> ChainedCall { + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![ + AccountForTests::user_holding_a(), + AccountForTests::vault_a_init(), + ], + &token_core::Instruction::Transfer { + amount_to_transfer: BalanceForTests::add_max_amount_a(), + }, + ) + } + + fn cc_swap_token_b_test_1() -> ChainedCall { + let swap_amount: u128 = 166; + + let mut vault_b_auth = AccountForTests::vault_b_init(); + vault_b_auth.is_authorized = true; + + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![vault_b_auth, AccountForTests::user_holding_b()], + &token_core::Instruction::Transfer { + amount_to_transfer: swap_amount, + }, + ) + .with_pda_seeds(vec![compute_vault_pda_seed( + IdForTests::pool_definition_id(), + IdForTests::token_b_definition_id(), + )]) + } + + fn cc_swap_token_a_test_2() -> ChainedCall { + let swap_amount: u128 = 285; + + let mut vault_a_auth = AccountForTests::vault_a_init(); + vault_a_auth.is_authorized = true; + + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![vault_a_auth, AccountForTests::user_holding_a()], + &token_core::Instruction::Transfer { + amount_to_transfer: swap_amount, + }, + ) + .with_pda_seeds(vec![compute_vault_pda_seed( + IdForTests::pool_definition_id(), + IdForTests::token_a_definition_id(), + )]) + } + + fn cc_swap_token_b_test_2() -> ChainedCall { + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![ + AccountForTests::user_holding_b(), + AccountForTests::vault_b_init(), + ], + &token_core::Instruction::Transfer { + amount_to_transfer: BalanceForTests::add_max_amount_b(), + }, + ) + } + + fn cc_add_token_a() -> ChainedCall { + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![ + AccountForTests::user_holding_a(), + AccountForTests::vault_a_init(), + ], + &token_core::Instruction::Transfer { + amount_to_transfer: BalanceForTests::add_successful_amount_a(), + }, + ) + } + + fn cc_add_token_b() -> ChainedCall { + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![ + AccountForTests::user_holding_b(), + AccountForTests::vault_b_init(), + ], + &token_core::Instruction::Transfer { + amount_to_transfer: BalanceForTests::add_successful_amount_b(), + }, + ) + } + + fn cc_add_pool_lp() -> ChainedCall { + let mut pool_lp_auth = AccountForTests::pool_lp_init(); + pool_lp_auth.is_authorized = true; + + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![pool_lp_auth, AccountForTests::user_holding_lp_init()], + &token_core::Instruction::Mint { + amount_to_mint: BalanceForTests::add_successful_amount_a(), + }, + ) + .with_pda_seeds(vec![compute_liquidity_token_pda_seed( + IdForTests::pool_definition_id(), + )]) + } + + fn cc_remove_token_a() -> ChainedCall { + let mut vault_a_auth = AccountForTests::vault_a_init(); + vault_a_auth.is_authorized = true; + + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![vault_a_auth, AccountForTests::user_holding_a()], + &token_core::Instruction::Transfer { + amount_to_transfer: BalanceForTests::remove_actual_a_successful(), + }, + ) + .with_pda_seeds(vec![compute_vault_pda_seed( + IdForTests::pool_definition_id(), + IdForTests::token_a_definition_id(), + )]) + } + + fn cc_remove_token_b() -> ChainedCall { + let mut vault_b_auth = AccountForTests::vault_b_init(); + vault_b_auth.is_authorized = true; + + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![vault_b_auth, AccountForTests::user_holding_b()], + &token_core::Instruction::Transfer { + amount_to_transfer: BalanceForTests::remove_min_amount_b_low(), + }, + ) + .with_pda_seeds(vec![compute_vault_pda_seed( + IdForTests::pool_definition_id(), + IdForTests::token_b_definition_id(), + )]) + } + + fn cc_remove_pool_lp() -> ChainedCall { + let mut pool_lp_auth = AccountForTests::pool_lp_init(); + pool_lp_auth.is_authorized = true; + + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![pool_lp_auth, AccountForTests::user_holding_lp_init()], + &token_core::Instruction::Burn { + amount_to_burn: BalanceForTests::remove_amount_lp(), + }, + ) + .with_pda_seeds(vec![compute_liquidity_token_pda_seed( + IdForTests::pool_definition_id(), + )]) + } + + fn cc_new_definition_token_a() -> ChainedCall { + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![ + AccountForTests::user_holding_a(), + AccountForTests::vault_a_init(), + ], + &token_core::Instruction::Transfer { + amount_to_transfer: BalanceForTests::add_successful_amount_a(), + }, + ) + } + + fn cc_new_definition_token_b() -> ChainedCall { + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![ + AccountForTests::user_holding_b(), + AccountForTests::vault_b_init(), + ], + &token_core::Instruction::Transfer { + amount_to_transfer: BalanceForTests::add_successful_amount_b(), + }, + ) + } + + fn cc_new_definition_token_lp() -> ChainedCall { + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![ + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_lp_uninit(), + ], + &token_core::Instruction::Mint { + amount_to_mint: BalanceForTests::add_successful_amount_a(), + }, + ) + .with_pda_seeds(vec![compute_liquidity_token_pda_seed( + IdForTests::pool_definition_id(), + )]) + } +} + +impl IdForTests { + fn token_a_definition_id() -> AccountId { + AccountId::new([42; 32]) + } + + fn token_b_definition_id() -> AccountId { + AccountId::new([43; 32]) + } + + fn token_lp_definition_id() -> AccountId { + compute_liquidity_token_pda(AMM_PROGRAM_ID, IdForTests::pool_definition_id()) + } + + fn user_token_a_id() -> AccountId { + AccountId::new([45; 32]) + } + + fn user_token_b_id() -> AccountId { + AccountId::new([46; 32]) + } + + fn user_token_lp_id() -> AccountId { + AccountId::new([47; 32]) + } + + fn pool_definition_id() -> AccountId { + compute_pool_pda( + AMM_PROGRAM_ID, + IdForTests::token_a_definition_id(), + IdForTests::token_b_definition_id(), + ) + } + + fn vault_a_id() -> AccountId { + compute_vault_pda( + AMM_PROGRAM_ID, + IdForTests::pool_definition_id(), + IdForTests::token_a_definition_id(), + ) + } + + fn vault_b_id() -> AccountId { + compute_vault_pda( + AMM_PROGRAM_ID, + IdForTests::pool_definition_id(), + IdForTests::token_b_definition_id(), + ) + } +} + +impl AccountForTests { + fn user_holding_a() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::token_a_definition_id(), + balance: BalanceForTests::user_token_a_balance(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::user_token_a_id(), + } + } + + fn user_holding_b() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::token_b_definition_id(), + balance: BalanceForTests::user_token_b_balance(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::user_token_b_id(), + } + } + + fn vault_a_init() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::token_a_definition_id(), + balance: BalanceForTests::vault_a_reserve_init(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::vault_a_id(), + } + } + + fn vault_b_init() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::token_b_definition_id(), + balance: BalanceForTests::vault_b_reserve_init(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::vault_b_id(), + } + } + + fn vault_a_init_high() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::token_a_definition_id(), + balance: BalanceForTests::vault_a_reserve_high(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::vault_a_id(), + } + } + + fn vault_b_init_high() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::token_b_definition_id(), + balance: BalanceForTests::vault_b_reserve_high(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::vault_b_id(), + } + } + + fn vault_a_init_low() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::token_a_definition_id(), + balance: BalanceForTests::vault_a_reserve_low(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::vault_a_id(), + } + } + + fn vault_b_init_low() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::token_b_definition_id(), + balance: BalanceForTests::vault_b_reserve_low(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::vault_b_id(), + } + } + + fn vault_a_init_zero() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::token_a_definition_id(), + balance: 0, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::vault_a_id(), + } + } + + fn vault_b_init_zero() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::token_b_definition_id(), + balance: 0, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::vault_b_id(), + } + } + + fn pool_lp_init() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: Data::from(&TokenDefinition::Fungible { + name: String::from("test"), + total_supply: BalanceForTests::vault_a_reserve_init(), + metadata_id: None, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::token_lp_definition_id(), + } + } + + fn pool_lp_with_wrong_id() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: Data::from(&TokenDefinition::Fungible { + name: String::from("test"), + total_supply: BalanceForTests::vault_a_reserve_init(), + metadata_id: None, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::vault_a_id(), + } + } + + fn user_holding_lp_uninit() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::token_lp_definition_id(), + balance: 0, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::user_token_lp_id(), + } + } + + fn user_holding_lp_init() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::token_lp_definition_id(), + balance: BalanceForTests::user_token_lp_balance(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::user_token_lp_id(), + } + } + + fn pool_definition_init() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: Data::from(&PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_reserve_init(), + reserve_a: BalanceForTests::vault_a_reserve_init(), + reserve_b: BalanceForTests::vault_b_reserve_init(), + fees: 0u128, + active: true, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_init_reserve_a_zero() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: Data::from(&PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_reserve_init(), + reserve_a: 0, + reserve_b: BalanceForTests::vault_b_reserve_init(), + fees: 0u128, + active: true, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_init_reserve_b_zero() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: Data::from(&PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_reserve_init(), + reserve_a: BalanceForTests::vault_a_reserve_init(), + reserve_b: 0, + fees: 0u128, + active: true, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_init_reserve_a_low() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: Data::from(&PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_reserve_low(), + reserve_a: BalanceForTests::vault_a_reserve_low(), + reserve_b: BalanceForTests::vault_b_reserve_high(), + fees: 0u128, + active: true, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_init_reserve_b_low() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: Data::from(&PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_reserve_high(), + reserve_a: BalanceForTests::vault_a_reserve_high(), + reserve_b: BalanceForTests::vault_b_reserve_low(), + fees: 0u128, + active: true, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_swap_test_1() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: Data::from(&PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_reserve_init(), + reserve_a: BalanceForTests::vault_a_swap_test_1(), + reserve_b: BalanceForTests::vault_b_swap_test_1(), + fees: 0u128, + active: true, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_swap_test_2() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: Data::from(&PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_reserve_init(), + reserve_a: BalanceForTests::vault_a_swap_test_2(), + reserve_b: BalanceForTests::vault_b_swap_test_2(), + fees: 0u128, + active: true, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_add_zero_lp() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: Data::from(&PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_reserve_low(), + reserve_a: BalanceForTests::vault_a_reserve_init(), + reserve_b: BalanceForTests::vault_b_reserve_init(), + fees: 0u128, + active: true, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_add_successful() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: Data::from(&PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_add_successful(), + reserve_a: BalanceForTests::vault_a_add_successful(), + reserve_b: BalanceForTests::vault_b_add_successful(), + fees: 0u128, + active: true, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_remove_successful() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: Data::from(&PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_remove_successful(), + reserve_a: BalanceForTests::vault_a_remove_successful(), + reserve_b: BalanceForTests::vault_b_remove_successful(), + fees: 0u128, + active: true, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_inactive() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: Data::from(&PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_reserve_init(), + reserve_a: BalanceForTests::vault_a_reserve_init(), + reserve_b: BalanceForTests::vault_b_reserve_init(), + fees: 0u128, + active: false, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_with_wrong_id() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: Data::from(&PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_reserve_init(), + reserve_a: BalanceForTests::vault_a_reserve_init(), + reserve_b: BalanceForTests::vault_b_reserve_init(), + fees: 0u128, + active: false, + }), + nonce: 0, + }, + is_authorized: true, + account_id: AccountId::new([4; 32]), + } + } + + fn vault_a_with_wrong_id() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::token_a_definition_id(), + balance: BalanceForTests::vault_a_reserve_init(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: AccountId::new([4; 32]), + } + } + + fn vault_b_with_wrong_id() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::token_b_definition_id(), + balance: BalanceForTests::vault_b_reserve_init(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: AccountId::new([4; 32]), + } + } + + fn pool_definition_active() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: Data::from(&PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_reserve_init(), + reserve_a: BalanceForTests::vault_a_reserve_init(), + reserve_b: BalanceForTests::vault_b_reserve_init(), + fees: 0u128, + active: true, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } +} + +#[test] +fn test_pool_pda_produces_unique_id_for_token_pair() { + assert!( + amm_core::compute_pool_pda( + AMM_PROGRAM_ID, + IdForTests::token_a_definition_id(), + IdForTests::token_b_definition_id() + ) == compute_pool_pda( + AMM_PROGRAM_ID, + IdForTests::token_b_definition_id(), + IdForTests::token_a_definition_id() + ) + ); +} + +#[should_panic(expected = "Vault A was not provided")] +#[test] +fn test_call_add_liquidity_vault_a_omitted() { + let _post_states = add_liquidity( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_with_wrong_id(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + ); +} + +#[should_panic(expected = "Vault B was not provided")] +#[test] +fn test_call_add_liquidity_vault_b_omitted() { + let _post_states = add_liquidity( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_with_wrong_id(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + ); +} + +#[should_panic(expected = "LP definition mismatch")] +#[test] +fn test_call_add_liquidity_lp_definition_mismatch() { + let _post_states = add_liquidity( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_with_wrong_id(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + ); +} + +#[should_panic(expected = "Both max-balances must be nonzero")] +#[test] +fn test_call_add_liquidity_zero_balance_1() { + let _post_states = add_liquidity( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), + 0, + BalanceForTests::add_max_amount_b(), + ); +} + +#[should_panic(expected = "Both max-balances must be nonzero")] +#[test] +fn test_call_add_liquidity_zero_balance_2() { + let _post_states = add_liquidity( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), + 0, + BalanceForTests::add_max_amount_a(), + ); +} + +#[should_panic(expected = "Vaults' balances must be at least the reserve amounts")] +#[test] +fn test_call_add_liquidity_vault_insufficient_balance_1() { + let _post_states = add_liquidity( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init_zero(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + NonZero::new(BalanceForTests::add_max_amount_a()).unwrap(), + BalanceForTests::add_max_amount_b(), + BalanceForTests::add_min_amount_lp(), + ); +} + +#[should_panic(expected = "Vaults' balances must be at least the reserve amounts")] +#[test] +fn test_call_add_liquidity_vault_insufficient_balance_2() { + let _post_states = add_liquidity( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init_zero(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + NonZero::new(BalanceForTests::add_max_amount_a()).unwrap(), + BalanceForTests::add_max_amount_b(), + BalanceForTests::add_min_amount_lp(), + ); +} + +#[should_panic(expected = "A trade amount is 0")] +#[test] +fn test_call_add_liquidity_actual_amount_zero_1() { + let _post_states = add_liquidity( + AccountForTests::pool_definition_init_reserve_a_low(), + AccountForTests::vault_a_init_low(), + AccountForTests::vault_b_init_high(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + ); +} + +#[should_panic(expected = "A trade amount is 0")] +#[test] +fn test_call_add_liquidity_actual_amount_zero_2() { + let _post_states = add_liquidity( + AccountForTests::pool_definition_init_reserve_b_low(), + AccountForTests::vault_a_init_high(), + AccountForTests::vault_b_init_low(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), + BalanceForTests::add_max_amount_a_low(), + BalanceForTests::add_max_amount_b_low(), + ); +} + +#[should_panic(expected = "Reserves must be nonzero")] +#[test] +fn test_call_add_liquidity_reserves_zero_1() { + let _post_states = add_liquidity( + AccountForTests::pool_definition_init_reserve_a_zero(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + ); +} + +#[should_panic(expected = "Reserves must be nonzero")] +#[test] +fn test_call_add_liquidity_reserves_zero_2() { + let _post_states = add_liquidity( + AccountForTests::pool_definition_init_reserve_b_zero(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + ); +} + +#[should_panic(expected = "Payable LP must be nonzero")] +#[test] +fn test_call_add_liquidity_payable_lp_zero() { + let _post_states = add_liquidity( + AccountForTests::pool_definition_add_zero_lp(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), + BalanceForTests::add_max_amount_a_low(), + BalanceForTests::add_max_amount_b_low(), + ); +} + +#[test] +fn test_call_add_liquidity_chained_call_successsful() { + let (post_states, chained_calls) = add_liquidity( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + ); + + let pool_post = post_states[0].clone(); + + assert!(AccountForTests::pool_definition_add_successful().account == *pool_post.account()); + + let chained_call_lp = chained_calls[0].clone(); + let chained_call_b = chained_calls[1].clone(); + let chained_call_a = chained_calls[2].clone(); + + assert!(chained_call_a == ChainedCallForTests::cc_add_token_a()); + assert!(chained_call_b == ChainedCallForTests::cc_add_token_b()); + assert!(chained_call_lp == ChainedCallForTests::cc_add_pool_lp()); +} + +#[should_panic(expected = "Vault A was not provided")] +#[test] +fn test_call_remove_liquidity_vault_a_omitted() { + let _post_states = remove_liquidity( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_with_wrong_id(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(), + BalanceForTests::remove_min_amount_a(), + BalanceForTests::remove_min_amount_b(), + ); +} + +#[should_panic(expected = "Vault B was not provided")] +#[test] +fn test_call_remove_liquidity_vault_b_omitted() { + let _post_states = remove_liquidity( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_with_wrong_id(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(), + BalanceForTests::remove_min_amount_a(), + BalanceForTests::remove_min_amount_b(), + ); +} + +#[should_panic(expected = "LP definition mismatch")] +#[test] +fn test_call_remove_liquidity_lp_def_mismatch() { + let _post_states = remove_liquidity( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_with_wrong_id(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(), + BalanceForTests::remove_min_amount_a(), + BalanceForTests::remove_min_amount_b(), + ); +} + +#[should_panic(expected = "Invalid liquidity account provided")] +#[test] +fn test_call_remove_liquidity_insufficient_liquidity_amount() { + let _post_states = remove_liquidity( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_a(), /* different token account than lp to create desired + * error */ + NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(), + BalanceForTests::remove_min_amount_a(), + BalanceForTests::remove_min_amount_b(), + ); +} + +#[should_panic( + expected = "Insufficient minimal withdraw amount (Token A) provided for liquidity amount" +)] +#[test] +fn test_call_remove_liquidity_insufficient_balance_1() { + let _post_states = remove_liquidity( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + NonZero::new(BalanceForTests::remove_amount_lp_1()).unwrap(), + BalanceForTests::remove_min_amount_a(), + BalanceForTests::remove_min_amount_b(), + ); +} + +#[should_panic( + expected = "Insufficient minimal withdraw amount (Token B) provided for liquidity amount" +)] +#[test] +fn test_call_remove_liquidity_insufficient_balance_2() { + let _post_states = remove_liquidity( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(), + BalanceForTests::remove_min_amount_a(), + BalanceForTests::remove_min_amount_b(), + ); +} + +#[should_panic(expected = "Minimum withdraw amount must be nonzero")] +#[test] +fn test_call_remove_liquidity_min_bal_zero_1() { + let _post_states = remove_liquidity( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(), + 0, + BalanceForTests::remove_min_amount_b(), + ); +} + +#[should_panic(expected = "Minimum withdraw amount must be nonzero")] +#[test] +fn test_call_remove_liquidity_min_bal_zero_2() { + let _post_states = remove_liquidity( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(), + BalanceForTests::remove_min_amount_a(), + 0, + ); +} + +#[test] +fn test_call_remove_liquidity_chained_call_successful() { + let (post_states, chained_calls) = remove_liquidity( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(), + BalanceForTests::remove_min_amount_a(), + BalanceForTests::remove_min_amount_b_low(), + ); + + let pool_post = post_states[0].clone(); + + assert!(AccountForTests::pool_definition_remove_successful().account == *pool_post.account()); + + let chained_call_lp = chained_calls[0].clone(); + let chained_call_b = chained_calls[1].clone(); + let chained_call_a = chained_calls[2].clone(); + + assert!(chained_call_a == ChainedCallForTests::cc_remove_token_a()); + assert!(chained_call_b == ChainedCallForTests::cc_remove_token_b()); + assert!(chained_call_lp == ChainedCallForTests::cc_remove_pool_lp()); +} + +#[should_panic(expected = "Balances must be nonzero")] +#[test] +fn test_call_new_definition_with_zero_balance_1() { + let _post_states = new_definition( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_uninit(), + NonZero::new(0).expect("Balances must be nonzero"), + NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(), + AMM_PROGRAM_ID, + ); +} + +#[should_panic(expected = "Balances must be nonzero")] +#[test] +fn test_call_new_definition_with_zero_balance_2() { + let _post_states = new_definition( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_uninit(), + NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(), + NonZero::new(0).expect("Balances must be nonzero"), + AMM_PROGRAM_ID, + ); +} + +#[should_panic(expected = "Cannot set up a swap for a token with itself")] +#[test] +fn test_call_new_definition_same_token_definition() { + let _post_states = new_definition( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_lp_uninit(), + NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(), + NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(), + AMM_PROGRAM_ID, + ); +} + +#[should_panic(expected = "Liquidity pool Token Definition Account ID does not match PDA")] +#[test] +fn test_call_new_definition_wrong_liquidity_id() { + let _post_states = new_definition( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_with_wrong_id(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_uninit(), + NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(), + NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(), + AMM_PROGRAM_ID, + ); +} + +#[should_panic(expected = "Pool Definition Account ID does not match PDA")] +#[test] +fn test_call_new_definition_wrong_pool_id() { + let _post_states = new_definition( + AccountForTests::pool_definition_with_wrong_id(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_uninit(), + NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(), + NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(), + AMM_PROGRAM_ID, + ); +} + +#[should_panic(expected = "Vault ID does not match PDA")] +#[test] +fn test_call_new_definition_wrong_vault_id_1() { + let _post_states = new_definition( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_with_wrong_id(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_uninit(), + NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(), + NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(), + AMM_PROGRAM_ID, + ); +} + +#[should_panic(expected = "Vault ID does not match PDA")] +#[test] +fn test_call_new_definition_wrong_vault_id_2() { + let _post_states = new_definition( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_with_wrong_id(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_uninit(), + NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(), + NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(), + AMM_PROGRAM_ID, + ); +} + +#[should_panic(expected = "Cannot initialize an active Pool Definition")] +#[test] +fn test_call_new_definition_cannot_initialize_active_pool() { + let _post_states = new_definition( + AccountForTests::pool_definition_active(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_uninit(), + NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(), + NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(), + AMM_PROGRAM_ID, + ); +} + +#[should_panic(expected = "Cannot initialize an active Pool Definition")] +#[test] +fn test_call_new_definition_chained_call_successful() { + let (post_states, chained_calls) = new_definition( + AccountForTests::pool_definition_active(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_uninit(), + NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(), + NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(), + AMM_PROGRAM_ID, + ); + + let pool_post = post_states[0].clone(); + + assert!(AccountForTests::pool_definition_add_successful().account == *pool_post.account()); + + let chained_call_lp = chained_calls[0].clone(); + let chained_call_b = chained_calls[1].clone(); + let chained_call_a = chained_calls[2].clone(); + + assert!(chained_call_a == ChainedCallForTests::cc_new_definition_token_a()); + assert!(chained_call_b == ChainedCallForTests::cc_new_definition_token_b()); + assert!(chained_call_lp == ChainedCallForTests::cc_new_definition_token_lp()); +} + +#[should_panic(expected = "AccountId is not a token type for the pool")] +#[test] +fn test_call_swap_incorrect_token_type() { + let _post_states = swap( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::min_amount_out(), + IdForTests::token_lp_definition_id(), + ); +} + +#[should_panic(expected = "Vault A was not provided")] +#[test] +fn test_call_swap_vault_a_omitted() { + let _post_states = swap( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_with_wrong_id(), + AccountForTests::vault_b_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::min_amount_out(), + IdForTests::token_a_definition_id(), + ); +} + +#[should_panic(expected = "Vault B was not provided")] +#[test] +fn test_call_swap_vault_b_omitted() { + let _post_states = swap( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_with_wrong_id(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::min_amount_out(), + IdForTests::token_a_definition_id(), + ); +} + +#[should_panic(expected = "Reserve for Token A exceeds vault balance")] +#[test] +fn test_call_swap_reserves_vault_mismatch_1() { + let _post_states = swap( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init_low(), + AccountForTests::vault_b_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::min_amount_out(), + IdForTests::token_a_definition_id(), + ); +} + +#[should_panic(expected = "Reserve for Token B exceeds vault balance")] +#[test] +fn test_call_swap_reserves_vault_mismatch_2() { + let _post_states = swap( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init_low(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::min_amount_out(), + IdForTests::token_a_definition_id(), + ); +} + +#[should_panic(expected = "Pool is inactive")] +#[test] +fn test_call_swap_ianctive() { + let _post_states = swap( + AccountForTests::pool_definition_inactive(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::min_amount_out(), + IdForTests::token_a_definition_id(), + ); +} + +#[should_panic(expected = "Withdraw amount is less than minimal amount out")] +#[test] +fn test_call_swap_below_min_out() { + let _post_states = swap( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::min_amount_out(), + IdForTests::token_a_definition_id(), + ); +} + +#[test] +fn test_call_swap_chained_call_successful_1() { + let (post_states, chained_calls) = swap( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_a_low(), + IdForTests::token_a_definition_id(), + ); + + let pool_post = post_states[0].clone(); + + assert!(AccountForTests::pool_definition_swap_test_1().account == *pool_post.account()); + + let chained_call_a = chained_calls[0].clone(); + let chained_call_b = chained_calls[1].clone(); + + assert_eq!( + chained_call_a, + ChainedCallForTests::cc_swap_token_a_test_1() + ); + assert_eq!( + chained_call_b, + ChainedCallForTests::cc_swap_token_b_test_1() + ); +} + +#[test] +fn test_call_swap_chained_call_successful_2() { + let (post_states, chained_calls) = swap( + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + BalanceForTests::add_max_amount_b(), + BalanceForTests::min_amount_out(), + IdForTests::token_b_definition_id(), + ); + + let pool_post = post_states[0].clone(); + + assert!(AccountForTests::pool_definition_swap_test_2().account == *pool_post.account()); + + let chained_call_a = chained_calls[1].clone(); + let chained_call_b = chained_calls[0].clone(); + + assert_eq!( + chained_call_a, + ChainedCallForTests::cc_swap_token_a_test_2() + ); + assert_eq!( + chained_call_b, + ChainedCallForTests::cc_swap_token_b_test_2() + ); +} diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index ca8548bc..8151a1e4 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -10,6 +10,7 @@ nssa.workspace = true common.workspace = true key_protocol.workspace = true token_core.workspace = true +amm_core.workspace = true anyhow.workspace = true serde_json.workspace = true @@ -27,7 +28,6 @@ rand.workspace = true itertools.workspace = true sha2.workspace = true futures.workspace = true -risc0-zkvm.workspace = true async-stream = "0.3.6" indicatif = { version = "0.18.3", features = ["improved_unicode"] } optfield = "0.4.0" diff --git a/wallet/src/program_facades/amm.rs b/wallet/src/program_facades/amm.rs index 7039a937..fd5212fe 100644 --- a/wallet/src/program_facades/amm.rs +++ b/wallet/src/program_facades/amm.rs @@ -1,103 +1,9 @@ +use amm_core::{compute_liquidity_token_pda, compute_pool_pda, compute_vault_pda}; use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse}; -use nssa::{AccountId, ProgramId, program::Program}; -use nssa_core::program::PdaSeed; +use nssa::{AccountId, program::Program}; use token_core::TokenHolding; use crate::WalletCore; - -fn compute_pool_pda( - amm_program_id: ProgramId, - definition_token_a_id: AccountId, - definition_token_b_id: AccountId, -) -> AccountId { - AccountId::from(( - &amm_program_id, - &compute_pool_pda_seed(definition_token_a_id, definition_token_b_id), - )) -} - -fn compute_pool_pda_seed( - definition_token_a_id: AccountId, - definition_token_b_id: AccountId, -) -> PdaSeed { - use risc0_zkvm::sha::{Impl, Sha256}; - - let mut i: usize = 0; - let (token_1, token_2) = loop { - if definition_token_a_id.value()[i] > definition_token_b_id.value()[i] { - let token_1 = definition_token_a_id; - let token_2 = definition_token_b_id; - break (token_1, token_2); - } else if definition_token_a_id.value()[i] < definition_token_b_id.value()[i] { - let token_1 = definition_token_b_id; - let token_2 = definition_token_a_id; - break (token_1, token_2); - } - - if i == 32 { - panic!("Definitions match"); - } else { - i += 1; - } - }; - - let mut bytes = [0; 64]; - bytes[0..32].copy_from_slice(&token_1.to_bytes()); - bytes[32..].copy_from_slice(&token_2.to_bytes()); - - PdaSeed::new( - Impl::hash_bytes(&bytes) - .as_bytes() - .try_into() - .expect("Hash output must be exactly 32 bytes long"), - ) -} - -fn compute_vault_pda( - amm_program_id: ProgramId, - pool_id: AccountId, - definition_token_id: AccountId, -) -> AccountId { - AccountId::from(( - &amm_program_id, - &compute_vault_pda_seed(pool_id, definition_token_id), - )) -} - -fn compute_vault_pda_seed(pool_id: AccountId, definition_token_id: AccountId) -> PdaSeed { - use risc0_zkvm::sha::{Impl, Sha256}; - - let mut bytes = [0; 64]; - bytes[0..32].copy_from_slice(&pool_id.to_bytes()); - bytes[32..].copy_from_slice(&definition_token_id.to_bytes()); - - PdaSeed::new( - Impl::hash_bytes(&bytes) - .as_bytes() - .try_into() - .expect("Hash output must be exactly 32 bytes long"), - ) -} - -fn compute_liquidity_token_pda(amm_program_id: ProgramId, pool_id: AccountId) -> AccountId { - AccountId::from((&amm_program_id, &compute_liquidity_token_pda_seed(pool_id))) -} - -fn compute_liquidity_token_pda_seed(pool_id: AccountId) -> PdaSeed { - use risc0_zkvm::sha::{Impl, Sha256}; - - let mut bytes = [0; 64]; - bytes[0..32].copy_from_slice(&pool_id.to_bytes()); - bytes[32..].copy_from_slice(&[0; 32]); - - PdaSeed::new( - Impl::hash_bytes(&bytes) - .as_bytes() - .try_into() - .expect("Hash output must be exactly 32 bytes long"), - ) -} - pub struct Amm<'w>(pub &'w WalletCore); impl Amm<'_> { @@ -109,9 +15,13 @@ impl Amm<'_> { balance_a: u128, balance_b: u128, ) -> Result { - let (instruction, program) = amm_program_preparation_definition(balance_a, balance_b); - + let program = Program::amm(); let amm_program_id = Program::amm().id(); + let instruction = amm_core::Instruction::NewDefinition { + token_a_amount: balance_a, + token_b_amount: balance_b, + amm_program_id, + }; let user_a_acc = self .0 @@ -189,13 +99,16 @@ impl Amm<'_> { &self, user_holding_a: AccountId, user_holding_b: AccountId, - amount_in: u128, + swap_amount_in: u128, min_amount_out: u128, - token_definition_id: AccountId, + token_definition_id_in: AccountId, ) -> Result { - let (instruction, program) = - amm_program_preparation_swap(amount_in, min_amount_out, token_definition_id); - + let instruction = amm_core::Instruction::Swap { + swap_amount_in, + min_amount_out, + token_definition_id_in, + }; + let program = Program::amm(); let amm_program_id = Program::amm().id(); let user_a_acc = self @@ -248,12 +161,14 @@ impl Amm<'_> { let token_holder_b = TokenHolding::try_from(&token_holder_acc_b.data) .map_err(|_| ExecutionFailureKind::AccountDataError(user_holding_b))?; - if token_holder_a.definition_id() == token_definition_id { + if token_holder_a.definition_id() == token_definition_id_in { account_id_auth = user_holding_a; - } else if token_holder_b.definition_id() == token_definition_id { + } else if token_holder_b.definition_id() == token_definition_id_in { account_id_auth = user_holding_b; } else { - return Err(ExecutionFailureKind::AccountDataError(token_definition_id)); + return Err(ExecutionFailureKind::AccountDataError( + token_definition_id_in, + )); } let nonces = self @@ -290,13 +205,16 @@ impl Amm<'_> { user_holding_a: AccountId, user_holding_b: AccountId, user_holding_lp: AccountId, - min_amount_lp: u128, - max_amount_a: u128, - max_amount_b: u128, + min_amount_liquidity: u128, + max_amount_to_add_token_a: u128, + max_amount_to_add_token_b: u128, ) -> Result { - let (instruction, program) = - amm_program_preparation_add_liq(min_amount_lp, max_amount_a, max_amount_b); - + let instruction = amm_core::Instruction::AddLiquidity { + min_amount_liquidity, + max_amount_to_add_token_a, + max_amount_to_add_token_b, + }; + let program = Program::amm(); let amm_program_id = Program::amm().id(); let user_a_acc = self @@ -376,13 +294,16 @@ impl Amm<'_> { user_holding_a: AccountId, user_holding_b: AccountId, user_holding_lp: AccountId, - balance_lp: u128, - min_amount_a: u128, - min_amount_b: u128, + remove_liquidity_amount: u128, + min_amount_to_remove_token_a: u128, + min_amount_to_remove_token_b: u128, ) -> Result { - let (instruction, program) = - amm_program_preparation_remove_liq(balance_lp, min_amount_a, min_amount_b); - + let instruction = amm_core::Instruction::RemoveLiquidity { + remove_liquidity_amount, + min_amount_to_remove_token_a, + min_amount_to_remove_token_b, + }; + let program = Program::amm(); let amm_program_id = Program::amm().id(); let user_a_acc = self @@ -448,93 +369,3 @@ impl Amm<'_> { Ok(self.0.sequencer_client.send_tx_public(tx).await?) } } - -fn amm_program_preparation_definition(balance_a: u128, balance_b: u128) -> (Vec, Program) { - // An instruction data of 65-bytes, indicating the initial amm reserves' balances and - // token_program_id with the following layout: - // [0x00 || array of balances (little-endian 16 bytes) || AMM_PROGRAM_ID)] - let amm_program_id = Program::amm().id(); - - let mut instruction = [0; 65]; - instruction[1..17].copy_from_slice(&balance_a.to_le_bytes()); - instruction[17..33].copy_from_slice(&balance_b.to_le_bytes()); - - // This can be done less verbose, but it is better to use same way, as in amm program - instruction[33..37].copy_from_slice(&amm_program_id[0].to_le_bytes()); - instruction[37..41].copy_from_slice(&amm_program_id[1].to_le_bytes()); - instruction[41..45].copy_from_slice(&amm_program_id[2].to_le_bytes()); - instruction[45..49].copy_from_slice(&amm_program_id[3].to_le_bytes()); - instruction[49..53].copy_from_slice(&amm_program_id[4].to_le_bytes()); - instruction[53..57].copy_from_slice(&amm_program_id[5].to_le_bytes()); - instruction[57..61].copy_from_slice(&amm_program_id[6].to_le_bytes()); - instruction[61..].copy_from_slice(&amm_program_id[7].to_le_bytes()); - - let instruction_data = instruction.to_vec(); - let program = Program::amm(); - - (instruction_data, program) -} - -fn amm_program_preparation_swap( - amount_in: u128, - min_amount_out: u128, - token_definition_id: AccountId, -) -> (Vec, Program) { - // An instruction data byte string of length 65, indicating which token type to swap, quantity - // of tokens put into the swap (of type TOKEN_DEFINITION_ID) and min_amount_out. - // [0x01 || amount (little-endian 16 bytes) || TOKEN_DEFINITION_ID]. - let mut instruction = [0; 65]; - instruction[0] = 0x01; - instruction[1..17].copy_from_slice(&amount_in.to_le_bytes()); - instruction[17..33].copy_from_slice(&min_amount_out.to_le_bytes()); - - // This can be done less verbose, but it is better to use same way, as in amm program - instruction[33..].copy_from_slice(&token_definition_id.to_bytes()); - - let instruction_data = instruction.to_vec(); - let program = Program::amm(); - - (instruction_data, program) -} - -fn amm_program_preparation_add_liq( - min_amount_lp: u128, - max_amount_a: u128, - max_amount_b: u128, -) -> (Vec, Program) { - // An instruction data byte string of length 49, amounts for minimum amount of liquidity from - // add (min_amount_lp), max amount added for each token (max_amount_a and max_amount_b); - // indicate [0x02 || array of of balances (little-endian 16 bytes)]. - let mut instruction = [0; 49]; - instruction[0] = 0x02; - - instruction[1..17].copy_from_slice(&min_amount_lp.to_le_bytes()); - instruction[17..33].copy_from_slice(&max_amount_a.to_le_bytes()); - instruction[33..49].copy_from_slice(&max_amount_b.to_le_bytes()); - - let instruction_data = instruction.to_vec(); - let program = Program::amm(); - - (instruction_data, program) -} - -fn amm_program_preparation_remove_liq( - balance_lp: u128, - min_amount_a: u128, - min_amount_b: u128, -) -> (Vec, Program) { - // An instruction data byte string of length 49, amounts for minimum amount of liquidity to - // redeem (balance_lp), minimum balance of each token to remove (min_amount_a and - // min_amount_b); indicate [0x03 || array of balances (little-endian 16 bytes)]. - let mut instruction = [0; 49]; - instruction[0] = 0x03; - - instruction[1..17].copy_from_slice(&balance_lp.to_le_bytes()); - instruction[17..33].copy_from_slice(&min_amount_a.to_le_bytes()); - instruction[33..49].copy_from_slice(&min_amount_b.to_le_bytes()); - - let instruction_data = instruction.to_vec(); - let program = Program::amm(); - - (instruction_data, program) -}