diff --git a/artifacts/amm-idl.json b/artifacts/amm-idl.json index b89de30..8e27428 100644 --- a/artifacts/amm-idl.json +++ b/artifacts/amm-idl.json @@ -2,9 +2,32 @@ "version": "0.1.0", "name": "amm", "instructions": [ + { + "name": "initialize", + "accounts": [ + { + "name": "config", + "writable": false, + "signer": false, + "init": false + } + ], + "args": [ + { + "name": "token_program_id", + "type": "program_id" + } + ] + }, { "name": "new_definition", "accounts": [ + { + "name": "config", + "writable": false, + "signer": false, + "init": false + }, { "name": "pool", "writable": false, @@ -76,6 +99,12 @@ { "name": "add_liquidity", "accounts": [ + { + "name": "config", + "writable": false, + "signer": false, + "init": false + }, { "name": "pool", "writable": false, @@ -141,6 +170,12 @@ { "name": "remove_liquidity", "accounts": [ + { + "name": "config", + "writable": false, + "signer": false, + "init": false + }, { "name": "pool", "writable": false, @@ -206,6 +241,12 @@ { "name": "swap_exact_input", "accounts": [ + { + "name": "config", + "writable": false, + "signer": false, + "init": false + }, { "name": "pool", "writable": false, @@ -259,6 +300,12 @@ { "name": "swap_exact_output", "accounts": [ + { + "name": "config", + "writable": false, + "signer": false, + "init": false + }, { "name": "pool", "writable": false, @@ -379,6 +426,18 @@ ] } }, + { + "name": "AmmConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "token_program_id", + "type": "program_id" + } + ] + } + }, { "name": "TokenDefinition", "type": { diff --git a/programs/amm/core/src/lib.rs b/programs/amm/core/src/lib.rs index b041429..324e8ef 100644 --- a/programs/amm/core/src/lib.rs +++ b/programs/amm/core/src/lib.rs @@ -16,6 +16,21 @@ const LP_LOCK_HOLDING_PDA_SEED: [u8; 32] = [1; 32]; /// AMM Program Instruction. #[derive(Serialize, Deserialize)] pub enum Instruction { + /// Initializes the AMM Program by creating its singleton configuration account. + /// + /// The configuration account is a PDA derived from the constant `"CONFIG"` seed + /// (`compute_config_pda(self_program_id)`). It stores the Token Program ID that the AMM + /// uses for every chained call. The Program must be initialized via this instruction before + /// any pool can be created or interacted with — the other instructions read the Token + /// Program ID from this account and reject calls when it does not yet exist. + /// + /// Required accounts: + /// - AMM Config Account, uninitialized, derived as `compute_config_pda(self_program_id)` + Initialize { + /// Program ID of the Token Program the AMM will issue chained calls to. + token_program_id: ProgramId, + }, + /// Initializes a new Pool (or re-initializes an existing zero-supply Pool). /// /// On initialization, `MINIMUM_LIQUIDITY` LP tokens are permanently locked @@ -177,6 +192,61 @@ impl From<&PoolDefinition> for Data { } } +/// Singleton configuration account for the AMM Program. +/// +/// Stored at the PDA derived from the constant `"CONFIG"` seed +/// (`compute_config_pda(amm_program_id)`). Created once via the `Initialize` instruction; its +/// existence is the Program's "initialized" flag. Every chained-call instruction reads +/// `token_program_id` from here instead of trusting the program owner of a caller-supplied +/// account. +#[account_type] +#[derive(Clone, Default, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct AmmConfig { + /// Program ID of the Token Program the AMM issues chained calls to. + pub token_program_id: ProgramId, +} + +impl TryFrom<&Data> for AmmConfig { + type Error = std::io::Error; + + fn try_from(data: &Data) -> Result { + AmmConfig::try_from_slice(data.as_ref()) + } +} + +impl From<&AmmConfig> for Data { + fn from(config: &AmmConfig) -> Self { + let mut data = Vec::with_capacity(std::mem::size_of_val(config)); + + BorshSerialize::serialize(config, &mut data).expect("Serialization to Vec should not fail"); + + Data::try_from(data).expect("AMM config encoded data should fit into Data") + } +} + +// Stable seed marker for the singleton config PDA. The literal `"CONFIG"` bytes are hashed into +// the 32-byte seed; this must stay unchanged for address compatibility. +const CONFIG_PDA_SEED: &[u8] = b"CONFIG"; + +/// Derives the [`AccountId`] of the AMM Program's singleton config PDA. +#[must_use] +pub fn compute_config_pda(amm_program_id: ProgramId) -> AccountId { + AccountId::for_public_pda(&amm_program_id, &compute_config_pda_seed()) +} + +/// Derives the [`PdaSeed`] of the AMM Program's singleton config PDA from the `"CONFIG"` bytes. +#[must_use] +pub fn compute_config_pda_seed() -> PdaSeed { + use risc0_zkvm::sha::{Impl, Sha256}; + + PdaSeed::new( + Impl::hash_bytes(CONFIG_PDA_SEED) + .as_bytes() + .try_into() + .expect("Hash output must be exactly 32 bytes long"), + ) +} + pub fn compute_pool_pda( amm_program_id: ProgramId, definition_token_a_id: AccountId, diff --git a/programs/amm/methods/guest/src/bin/amm.rs b/programs/amm/methods/guest/src/bin/amm.rs index ff08986..2c62ae3 100644 --- a/programs/amm/methods/guest/src/bin/amm.rs +++ b/programs/amm/methods/guest/src/bin/amm.rs @@ -6,6 +6,7 @@ use spel_framework::prelude::*; use spel_framework::context::ProgramContext; use nssa_core::{ account::{AccountId, AccountWithMetadata}, + program::ProgramId, }; #[cfg(not(test))] @@ -19,15 +20,31 @@ mod amm { )] use super::*; + /// Initializes the AMM Program by creating its singleton config account. + /// + /// Expected accounts: + /// 1. `config` — uninitialized config PDA derived from `compute_config_pda(self_program_id)`. + #[instruction] + pub fn initialize( + ctx: ProgramContext, + config: AccountWithMetadata, + token_program_id: ProgramId, + ) -> SpelResult { + let post_states = + amm_program::initialize::initialize(config, token_program_id, ctx.self_program_id); + Ok(spel_framework::SpelOutput::execute(post_states, vec![])) + } + /// Initializes a new Pool (or re-initializes an existing zero-supply Pool). /// A fresh user LP holding must be explicitly authorized by the caller. #[expect( clippy::too_many_arguments, - reason = "instruction interface requires explicit pool, vault, mint, lock, and user accounts" + reason = "instruction interface requires explicit config, pool, vault, mint, lock, and user accounts" )] #[instruction] pub fn new_definition( ctx: ProgramContext, + config: AccountWithMetadata, pool: AccountWithMetadata, vault_a: AccountWithMetadata, vault_b: AccountWithMetadata, @@ -42,6 +59,7 @@ mod amm { deadline: u64, ) -> SpelResult { let (post_states, chained_calls) = amm_program::new_definition::new_definition( + config, pool, vault_a, vault_b, @@ -66,6 +84,8 @@ mod amm { )] #[instruction] pub fn add_liquidity( + ctx: ProgramContext, + config: AccountWithMetadata, pool: AccountWithMetadata, vault_a: AccountWithMetadata, vault_b: AccountWithMetadata, @@ -79,6 +99,7 @@ mod amm { deadline: u64, ) -> SpelResult { let (post_states, chained_calls) = amm_program::add::add_liquidity( + config, pool, vault_a, vault_b, @@ -89,6 +110,7 @@ mod amm { NonZeroU128::new(min_amount_liquidity).expect("min_amount_liquidity must be nonzero"), max_amount_to_add_token_a, max_amount_to_add_token_b, + ctx.self_program_id, ); Ok(spel_framework::SpelOutput::execute(post_states, chained_calls) .with_timestamp_validity_window(..deadline)) @@ -101,6 +123,8 @@ mod amm { )] #[instruction] pub fn remove_liquidity( + ctx: ProgramContext, + config: AccountWithMetadata, pool: AccountWithMetadata, vault_a: AccountWithMetadata, vault_b: AccountWithMetadata, @@ -114,6 +138,7 @@ mod amm { deadline: u64, ) -> SpelResult { let (post_states, chained_calls) = amm_program::remove::remove_liquidity( + config, pool, vault_a, vault_b, @@ -125,6 +150,7 @@ mod amm { .expect("remove_liquidity_amount must be nonzero"), min_amount_to_remove_token_a, min_amount_to_remove_token_b, + ctx.self_program_id, ); Ok(spel_framework::SpelOutput::execute(post_states, chained_calls) .with_timestamp_validity_window(..deadline)) @@ -137,6 +163,8 @@ mod amm { )] #[instruction] pub fn swap_exact_input( + ctx: ProgramContext, + config: AccountWithMetadata, pool: AccountWithMetadata, vault_a: AccountWithMetadata, vault_b: AccountWithMetadata, @@ -148,6 +176,7 @@ mod amm { deadline: u64, ) -> SpelResult { let (post_states, chained_calls) = amm_program::swap::swap_exact_input( + config, pool, vault_a, vault_b, @@ -156,6 +185,7 @@ mod amm { swap_amount_in, min_amount_out, token_definition_id_in, + ctx.self_program_id, ); Ok(spel_framework::SpelOutput::execute(post_states, chained_calls) .with_timestamp_validity_window(..deadline)) @@ -168,6 +198,8 @@ mod amm { )] #[instruction] pub fn swap_exact_output( + ctx: ProgramContext, + config: AccountWithMetadata, pool: AccountWithMetadata, vault_a: AccountWithMetadata, vault_b: AccountWithMetadata, @@ -179,6 +211,7 @@ mod amm { deadline: u64, ) -> SpelResult { let (post_states, chained_calls) = amm_program::swap::swap_exact_output( + config, pool, vault_a, vault_b, @@ -187,6 +220,7 @@ mod amm { exact_amount_out, max_amount_in, token_definition_id_in, + ctx.self_program_id, ); Ok(spel_framework::SpelOutput::execute(post_states, chained_calls) .with_timestamp_validity_window(..deadline)) diff --git a/programs/amm/src/add.rs b/programs/amm/src/add.rs index c91e092..ec87386 100644 --- a/programs/amm/src/add.rs +++ b/programs/amm/src/add.rs @@ -1,12 +1,12 @@ use std::num::NonZeroU128; use amm_core::{ - assert_supported_fee_tier, compute_liquidity_token_pda_seed, read_vault_fungible_balances, - PoolDefinition, + assert_supported_fee_tier, compute_config_pda, compute_liquidity_token_pda_seed, + read_vault_fungible_balances, AmmConfig, PoolDefinition, }; use nssa_core::{ account::{AccountWithMetadata, Data}, - program::{AccountPostState, ChainedCall}, + program::{AccountPostState, ChainedCall, ProgramId}, }; #[expect( @@ -14,6 +14,7 @@ use nssa_core::{ reason = "instruction surface passes explicit pool, vault, and user accounts" )] pub fn add_liquidity( + config: AccountWithMetadata, pool: AccountWithMetadata, vault_a: AccountWithMetadata, vault_b: AccountWithMetadata, @@ -24,7 +25,19 @@ pub fn add_liquidity( min_amount_liquidity: NonZeroU128, max_amount_to_add_token_a: u128, max_amount_to_add_token_b: u128, + amm_program_id: ProgramId, ) -> (Vec, Vec) { + // The Token Program is taken from the config account, not trusted from a caller-supplied + // holding. Validating the config PDA is also the Program's initialization gate. + assert_eq!( + config.account_id, + compute_config_pda(amm_program_id), + "Add liquidity: AMM config Account ID does not match PDA" + ); + let token_program_id = AmmConfig::try_from(&config.account.data) + .expect("Add liquidity: AMM Program must be initialized before use") + .token_program_id; + // 1. Fetch Pool state let pool_def_data = PoolDefinition::try_from(&pool.account.data) .expect("Add liquidity: AMM Program expects valid Pool Definition Account"); @@ -45,14 +58,21 @@ pub fn add_liquidity( "Vault B was not provided" ); - let token_program_id = vault_a.account.program_owner; + assert_eq!( + vault_a.account.program_owner, token_program_id, + "Vault A must be owned by the configured Token Program" + ); + assert_eq!( + vault_b.account.program_owner, token_program_id, + "Vault B must be owned by the configured Token Program" + ); assert_eq!( user_holding_a.account.program_owner, token_program_id, - "User Token A holding must be owned by the vault's Token Program" + "User Token A holding must be owned by the configured Token Program" ); assert_eq!( user_holding_b.account.program_owner, token_program_id, - "User Token B holding must be owned by the vault's Token Program" + "User Token B holding must be owned by the configured Token Program" ); assert!( @@ -187,6 +207,7 @@ pub fn add_liquidity( let chained_calls = vec![call_token_lp, call_token_b, call_token_a]; let post_states = vec![ + AccountPostState::new(config.account.clone()), AccountPostState::new(pool_post), AccountPostState::new(vault_a.account.clone()), AccountPostState::new(vault_b.account.clone()), diff --git a/programs/amm/src/initialize.rs b/programs/amm/src/initialize.rs new file mode 100644 index 0000000..c3576c9 --- /dev/null +++ b/programs/amm/src/initialize.rs @@ -0,0 +1,97 @@ +use amm_core::{compute_config_pda, compute_config_pda_seed, AmmConfig}; +use nssa_core::{ + account::{Account, AccountWithMetadata, Data}, + program::{AccountPostState, Claim, ProgramId}, +}; + +/// Initializes the AMM Program by creating its singleton configuration account. +/// +/// The config account is a PDA derived from the constant `"CONFIG"` seed +/// (`compute_config_pda(amm_program_id)`) and stores `token_program_id`, the Token Program the +/// AMM issues every chained call to. Its existence is the Program's "initialized" flag: the +/// chained-call instructions read the Token Program ID from it and reject calls until it exists. +/// +/// # Panics +/// Panics if: +/// - `config.account_id` does not match `compute_config_pda(amm_program_id)`. +/// - `config.account` is not the default (the Program is already initialized). +pub fn initialize( + config: AccountWithMetadata, + token_program_id: ProgramId, + amm_program_id: ProgramId, +) -> Vec { + assert_eq!( + config.account_id, + compute_config_pda(amm_program_id), + "Initialize: AMM config Account ID does not match PDA" + ); + assert_eq!( + config.account, + Account::default(), + "Initialize: AMM config account must be uninitialized" + ); + + let mut config_post = config.account.clone(); + config_post.data = Data::from(&AmmConfig { token_program_id }); + + vec![AccountPostState::new_claimed( + config_post, + Claim::Pda(compute_config_pda_seed()), + )] +} + +#[cfg(test)] +mod tests { + use amm_core::compute_config_pda; + use nssa_core::account::{AccountId, Nonce}; + + use super::*; + + const AMM_PROGRAM_ID: ProgramId = [42; 8]; + const TOKEN_PROGRAM_ID: ProgramId = [15; 8]; + + fn config_uninit() -> AccountWithMetadata { + AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: compute_config_pda(AMM_PROGRAM_ID), + } + } + + #[test] + fn returns_single_pda_claimed_post_state() { + let post_states = initialize(config_uninit(), TOKEN_PROGRAM_ID, AMM_PROGRAM_ID); + assert_eq!(post_states.len(), 1); + assert_eq!( + post_states[0].required_claim(), + Some(Claim::Pda(compute_config_pda_seed())) + ); + } + + #[test] + fn stores_token_program_id() { + let post_states = initialize(config_uninit(), TOKEN_PROGRAM_ID, AMM_PROGRAM_ID); + let config = AmmConfig::try_from(&post_states[0].account().data) + .expect("post state must contain a valid AmmConfig"); + assert_eq!(config.token_program_id, TOKEN_PROGRAM_ID); + } + + #[test] + #[should_panic(expected = "AMM config Account ID does not match PDA")] + fn wrong_config_account_id_panics() { + let mut wrong = config_uninit(); + wrong.account_id = AccountId::new([0; 32]); + initialize(wrong, TOKEN_PROGRAM_ID, AMM_PROGRAM_ID); + } + + #[test] + #[should_panic(expected = "AMM config account must be uninitialized")] + fn already_initialized_config_panics() { + let mut initialized = config_uninit(); + initialized.account.data = Data::from(&AmmConfig { + token_program_id: TOKEN_PROGRAM_ID, + }); + initialized.account.nonce = Nonce(0); + initialize(initialized, TOKEN_PROGRAM_ID, AMM_PROGRAM_ID); + } +} diff --git a/programs/amm/src/lib.rs b/programs/amm/src/lib.rs index 77a59dd..3165998 100644 --- a/programs/amm/src/lib.rs +++ b/programs/amm/src/lib.rs @@ -3,6 +3,7 @@ pub use amm_core as core; pub mod add; +pub mod initialize; pub mod new_definition; pub mod remove; pub mod swap; diff --git a/programs/amm/src/new_definition.rs b/programs/amm/src/new_definition.rs index 8c47a6a..bf4b3b9 100644 --- a/programs/amm/src/new_definition.rs +++ b/programs/amm/src/new_definition.rs @@ -1,10 +1,10 @@ use std::num::NonZeroU128; use amm_core::{ - assert_supported_fee_tier, compute_liquidity_token_pda, compute_liquidity_token_pda_seed, - compute_lp_lock_holding_pda, compute_lp_lock_holding_pda_seed, compute_pool_pda, - compute_pool_pda_seed, compute_vault_pda, compute_vault_pda_seed, PoolDefinition, - MINIMUM_LIQUIDITY, + assert_supported_fee_tier, compute_config_pda, compute_liquidity_token_pda, + compute_liquidity_token_pda_seed, compute_lp_lock_holding_pda, + compute_lp_lock_holding_pda_seed, compute_pool_pda, compute_pool_pda_seed, compute_vault_pda, + compute_vault_pda_seed, AmmConfig, PoolDefinition, MINIMUM_LIQUIDITY, }; use nssa_core::{ account::{Account, AccountWithMetadata, Data}, @@ -17,6 +17,7 @@ use token_core::TokenDefinition; reason = "instruction surface passes explicit pool, vault, mint, lock, and user accounts" )] pub fn new_definition( + config: AccountWithMetadata, pool: AccountWithMetadata, vault_a: AccountWithMetadata, vault_b: AccountWithMetadata, @@ -37,12 +38,24 @@ pub fn new_definition( .expect("New definition: AMM Program expects valid Token Holding account for Token B") .definition_id(); - let token_program = user_holding_a.account.program_owner; + // The Token Program is taken from the config account, not trusted from a caller-supplied + // holding. Validating the config PDA is also the Program's initialization gate. + assert_eq!( + config.account_id, + compute_config_pda(amm_program_id), + "New definition: AMM config Account ID does not match PDA" + ); + let token_program_id = AmmConfig::try_from(&config.account.data) + .expect("New definition: AMM Program must be initialized before use") + .token_program_id; - // both instances of the same token program assert_eq!( - user_holding_b.account.program_owner, token_program, - "User Token holdings must use the same Token Program" + user_holding_a.account.program_owner, token_program_id, + "User Token A holding must be owned by the configured Token Program" + ); + assert_eq!( + user_holding_b.account.program_owner, token_program_id, + "User Token B holding must be owned by the configured Token Program" ); // Verify token_a and token_b are different assert!( @@ -124,8 +137,6 @@ pub fn new_definition( )), ); - let token_program_id = user_holding_a.account.program_owner; - // Chain call for Token A (user_holding_a -> Vault_A) let mut vault_a_authorized = vault_a.clone(); vault_a_authorized.is_authorized = true; @@ -199,6 +210,7 @@ pub fn new_definition( ]; let post_states = vec![ + AccountPostState::new(config.account.clone()), pool_post.clone(), AccountPostState::new(vault_a.account.clone()), AccountPostState::new(vault_b.account.clone()), diff --git a/programs/amm/src/remove.rs b/programs/amm/src/remove.rs index 3a57379..aca50d2 100644 --- a/programs/amm/src/remove.rs +++ b/programs/amm/src/remove.rs @@ -1,12 +1,12 @@ use std::num::NonZeroU128; use amm_core::{ - assert_supported_fee_tier, compute_liquidity_token_pda_seed, compute_vault_pda_seed, - PoolDefinition, MINIMUM_LIQUIDITY, + assert_supported_fee_tier, compute_config_pda, compute_liquidity_token_pda_seed, + compute_vault_pda_seed, AmmConfig, PoolDefinition, MINIMUM_LIQUIDITY, }; use nssa_core::{ account::{AccountWithMetadata, Data}, - program::{AccountPostState, ChainedCall}, + program::{AccountPostState, ChainedCall, ProgramId}, }; #[expect( @@ -14,6 +14,7 @@ use nssa_core::{ reason = "instruction surface passes explicit pool, vault, and user accounts" )] pub fn remove_liquidity( + config: AccountWithMetadata, pool: AccountWithMetadata, vault_a: AccountWithMetadata, vault_b: AccountWithMetadata, @@ -24,9 +25,21 @@ pub fn remove_liquidity( remove_liquidity_amount: NonZeroU128, min_amount_to_remove_token_a: u128, min_amount_to_remove_token_b: u128, + amm_program_id: ProgramId, ) -> (Vec, Vec) { let remove_liquidity_amount: u128 = remove_liquidity_amount.into(); + // The Token Program is taken from the config account, not trusted from a caller-supplied + // holding. Validating the config PDA is also the Program's initialization gate. + assert_eq!( + config.account_id, + compute_config_pda(amm_program_id), + "Remove liquidity: AMM config Account ID does not match PDA" + ); + let token_program_id = AmmConfig::try_from(&config.account.data) + .expect("Remove liquidity: AMM Program must be initialized before use") + .token_program_id; + // 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"); @@ -49,14 +62,21 @@ pub fn remove_liquidity( "Vault B was not provided" ); - let token_program_id = vault_a.account.program_owner; + assert_eq!( + vault_a.account.program_owner, token_program_id, + "Vault A must be owned by the configured Token Program" + ); + assert_eq!( + vault_b.account.program_owner, token_program_id, + "Vault B must be owned by the configured Token Program" + ); assert_eq!( user_holding_a.account.program_owner, token_program_id, - "User Token A holding must be owned by the vault's Token Program" + "User Token A holding must be owned by the configured Token Program" ); assert_eq!( user_holding_b.account.program_owner, token_program_id, - "User Token B holding must be owned by the vault's Token Program" + "User Token B holding must be owned by the configured Token Program" ); // Vault addresses do not need to be checked with PDA @@ -204,6 +224,7 @@ pub fn remove_liquidity( let chained_calls = vec![call_token_lp, call_token_b, call_token_a]; let post_states = vec![ + AccountPostState::new(config.account.clone()), AccountPostState::new(pool_post.clone()), AccountPostState::new(vault_a.account.clone()), AccountPostState::new(vault_b.account.clone()), diff --git a/programs/amm/src/swap.rs b/programs/amm/src/swap.rs index f490825..c867d04 100644 --- a/programs/amm/src/swap.rs +++ b/programs/amm/src/swap.rs @@ -1,10 +1,11 @@ use amm_core::{ - assert_supported_fee_tier, read_vault_fungible_balances, FEE_BPS_DENOMINATOR, MINIMUM_LIQUIDITY, + assert_supported_fee_tier, compute_config_pda, read_vault_fungible_balances, AmmConfig, + FEE_BPS_DENOMINATOR, MINIMUM_LIQUIDITY, }; pub use amm_core::{compute_liquidity_token_pda_seed, compute_vault_pda_seed, PoolDefinition}; use nssa_core::{ account::{AccountId, AccountWithMetadata, Data}, - program::{AccountPostState, ChainedCall}, + program::{AccountPostState, ChainedCall, ProgramId}, }; /// Validates swap setup: checks pool liquidity is ready, vaults match, and reserves are sufficient. @@ -55,6 +56,7 @@ fn validate_swap_setup( reason = "consistent with codebase style" )] fn create_swap_post_states( + config: AccountWithMetadata, pool: AccountWithMetadata, pool_def_data: PoolDefinition, vault_a: AccountWithMetadata, @@ -86,6 +88,7 @@ fn create_swap_post_states( pool_post.data = Data::from(&pool_post_definition); vec![ + AccountPostState::new(config.account), AccountPostState::new(pool_post), AccountPostState::new(vault_a.account), AccountPostState::new(vault_b.account), @@ -100,6 +103,7 @@ fn create_swap_post_states( )] #[must_use] pub fn swap_exact_input( + config: AccountWithMetadata, pool: AccountWithMetadata, vault_a: AccountWithMetadata, vault_b: AccountWithMetadata, @@ -108,17 +112,35 @@ pub fn swap_exact_input( swap_amount_in: u128, min_amount_out: u128, token_in_id: AccountId, + amm_program_id: ProgramId, ) -> (Vec, Vec) { let pool_def_data = validate_swap_setup(&pool, &vault_a, &vault_b); - let token_program_id = vault_a.account.program_owner; + // The Token Program is taken from the config account, not trusted from a caller-supplied + // account. Validating the config PDA is also the Program's initialization gate. + assert_eq!( + config.account_id, + compute_config_pda(amm_program_id), + "Swap exact input: AMM config Account ID does not match PDA" + ); + let token_program_id = AmmConfig::try_from(&config.account.data) + .expect("Swap exact input: AMM Program must be initialized before use") + .token_program_id; + assert_eq!( + vault_a.account.program_owner, token_program_id, + "Vault A must be owned by the configured Token Program" + ); + assert_eq!( + vault_b.account.program_owner, token_program_id, + "Vault B must be owned by the configured Token Program" + ); assert_eq!( user_holding_a.account.program_owner, token_program_id, - "User Token A holding must be owned by the vault's Token Program" + "User Token A holding must be owned by the configured Token Program" ); assert_eq!( user_holding_b.account.program_owner, token_program_id, - "User Token B holding must be owned by the vault's Token Program" + "User Token B holding must be owned by the configured Token Program" ); let (chained_calls, [deposit_a, withdraw_a], [deposit_b, withdraw_b]) = @@ -157,6 +179,7 @@ pub fn swap_exact_input( }; let post_states = create_swap_post_states( + config, pool, pool_def_data, vault_a, @@ -262,6 +285,7 @@ fn swap_logic( )] #[must_use] pub fn swap_exact_output( + config: AccountWithMetadata, pool: AccountWithMetadata, vault_a: AccountWithMetadata, vault_b: AccountWithMetadata, @@ -270,17 +294,35 @@ pub fn swap_exact_output( exact_amount_out: u128, max_amount_in: u128, token_in_id: AccountId, + amm_program_id: ProgramId, ) -> (Vec, Vec) { let pool_def_data = validate_swap_setup(&pool, &vault_a, &vault_b); - let token_program_id = vault_a.account.program_owner; + // The Token Program is taken from the config account, not trusted from a caller-supplied + // account. Validating the config PDA is also the Program's initialization gate. + assert_eq!( + config.account_id, + compute_config_pda(amm_program_id), + "Swap exact output: AMM config Account ID does not match PDA" + ); + let token_program_id = AmmConfig::try_from(&config.account.data) + .expect("Swap exact output: AMM Program must be initialized before use") + .token_program_id; + assert_eq!( + vault_a.account.program_owner, token_program_id, + "Vault A must be owned by the configured Token Program" + ); + assert_eq!( + vault_b.account.program_owner, token_program_id, + "Vault B must be owned by the configured Token Program" + ); assert_eq!( user_holding_a.account.program_owner, token_program_id, - "User Token A holding must be owned by the vault's Token Program" + "User Token A holding must be owned by the configured Token Program" ); assert_eq!( user_holding_b.account.program_owner, token_program_id, - "User Token B holding must be owned by the vault's Token Program" + "User Token B holding must be owned by the configured Token Program" ); let (chained_calls, [deposit_a, withdraw_a], [deposit_b, withdraw_b]) = @@ -319,6 +361,7 @@ pub fn swap_exact_output( }; let post_states = create_swap_post_states( + config, pool, pool_def_data, vault_a, diff --git a/programs/amm/src/tests.rs b/programs/amm/src/tests.rs index bb1831e..25cd7b9 100644 --- a/programs/amm/src/tests.rs +++ b/programs/amm/src/tests.rs @@ -8,10 +8,11 @@ use std::num::NonZero; use amm_core::{ - compute_liquidity_token_pda, compute_liquidity_token_pda_seed, compute_lp_lock_holding_pda, - compute_lp_lock_holding_pda_seed, compute_pool_pda, compute_pool_pda_seed, compute_vault_pda, - compute_vault_pda_seed, PoolDefinition, FEE_BPS_DENOMINATOR, FEE_TIER_BPS_1, FEE_TIER_BPS_100, - FEE_TIER_BPS_30, FEE_TIER_BPS_5, MINIMUM_LIQUIDITY, + compute_config_pda, compute_liquidity_token_pda, compute_liquidity_token_pda_seed, + compute_lp_lock_holding_pda, compute_lp_lock_holding_pda_seed, compute_pool_pda, + compute_pool_pda_seed, compute_vault_pda, compute_vault_pda_seed, AmmConfig, PoolDefinition, + FEE_BPS_DENOMINATOR, FEE_TIER_BPS_1, FEE_TIER_BPS_100, FEE_TIER_BPS_30, FEE_TIER_BPS_5, + MINIMUM_LIQUIDITY, }; use nssa_core::{ account::{Account, AccountId, AccountWithMetadata, Data, Nonce}, @@ -620,6 +621,37 @@ impl IdForTests { } impl AccountWithMetadataForTests { + fn config_init() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: AMM_PROGRAM_ID, + balance: 0u128, + data: Data::from(&AmmConfig { + token_program_id: TOKEN_PROGRAM_ID, + }), + nonce: Nonce(0), + }, + is_authorized: false, + account_id: compute_config_pda(AMM_PROGRAM_ID), + } + } + + /// Config PDA that has never been initialized (default, empty data). + fn config_uninit() -> AccountWithMetadata { + AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: compute_config_pda(AMM_PROGRAM_ID), + } + } + + /// An initialized config carrying valid data but stored at the wrong account ID. + fn config_with_wrong_id() -> AccountWithMetadata { + let mut config = AccountWithMetadataForTests::config_init(); + config.account_id = AccountId::new([7; 32]); + config + } + fn user_holding_a() -> AccountWithMetadata { AccountWithMetadata { account: Account { @@ -1429,6 +1461,7 @@ fn test_pool_pda_produces_unique_id_for_token_pair() { #[test] fn test_call_add_liquidity_vault_a_omitted() { let _post_states = add_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_with_wrong_id(), AccountWithMetadataForTests::vault_b_init(), @@ -1439,6 +1472,7 @@ fn test_call_add_liquidity_vault_a_omitted() { NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), BalanceForTests::add_max_amount_a(), BalanceForTests::add_max_amount_b(), + AMM_PROGRAM_ID, ); } @@ -1446,6 +1480,7 @@ fn test_call_add_liquidity_vault_a_omitted() { #[test] fn test_call_add_liquidity_vault_b_omitted() { let _post_states = add_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_with_wrong_id(), @@ -1456,6 +1491,7 @@ fn test_call_add_liquidity_vault_b_omitted() { NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), BalanceForTests::add_max_amount_a(), BalanceForTests::add_max_amount_b(), + AMM_PROGRAM_ID, ); } @@ -1463,6 +1499,7 @@ fn test_call_add_liquidity_vault_b_omitted() { #[test] fn test_call_add_liquidity_lp_definition_mismatch() { let _post_states = add_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -1473,6 +1510,7 @@ fn test_call_add_liquidity_lp_definition_mismatch() { NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), BalanceForTests::add_max_amount_a(), BalanceForTests::add_max_amount_b(), + AMM_PROGRAM_ID, ); } @@ -1480,6 +1518,7 @@ fn test_call_add_liquidity_lp_definition_mismatch() { #[test] fn test_call_add_liquidity_zero_balance_1() { let _post_states = add_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -1490,6 +1529,7 @@ fn test_call_add_liquidity_zero_balance_1() { NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), 0, BalanceForTests::add_max_amount_b(), + AMM_PROGRAM_ID, ); } @@ -1497,6 +1537,7 @@ fn test_call_add_liquidity_zero_balance_1() { #[test] fn test_call_add_liquidity_zero_balance_2() { let _post_states = add_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -1507,6 +1548,7 @@ fn test_call_add_liquidity_zero_balance_2() { NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), 0, BalanceForTests::add_max_amount_a(), + AMM_PROGRAM_ID, ); } @@ -1514,6 +1556,7 @@ fn test_call_add_liquidity_zero_balance_2() { #[test] fn test_call_add_liquidity_vault_a_balance_below_reserve() { let _post_states = add_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init_low(), AccountWithMetadataForTests::vault_b_init(), @@ -1524,6 +1567,7 @@ fn test_call_add_liquidity_vault_a_balance_below_reserve() { NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), BalanceForTests::add_max_amount_a(), BalanceForTests::add_max_amount_b(), + AMM_PROGRAM_ID, ); } @@ -1531,6 +1575,7 @@ fn test_call_add_liquidity_vault_a_balance_below_reserve() { #[test] fn test_call_add_liquidity_vault_b_balance_below_reserve() { let _post_states = add_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init_low(), @@ -1541,6 +1586,7 @@ fn test_call_add_liquidity_vault_b_balance_below_reserve() { NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), BalanceForTests::add_max_amount_a(), BalanceForTests::add_max_amount_b(), + AMM_PROGRAM_ID, ); } @@ -1548,6 +1594,7 @@ fn test_call_add_liquidity_vault_b_balance_below_reserve() { #[test] fn test_call_add_liquidity_vault_insufficient_balance_1() { let _post_states = add_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init_zero(), AccountWithMetadataForTests::vault_b_init(), @@ -1558,6 +1605,7 @@ fn test_call_add_liquidity_vault_insufficient_balance_1() { NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), BalanceForTests::add_max_amount_a(), BalanceForTests::add_max_amount_b(), + AMM_PROGRAM_ID, ); } @@ -1565,6 +1613,7 @@ fn test_call_add_liquidity_vault_insufficient_balance_1() { #[test] fn test_call_add_liquidity_vault_insufficient_balance_2() { let _post_states = add_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init_zero(), @@ -1575,6 +1624,7 @@ fn test_call_add_liquidity_vault_insufficient_balance_2() { NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), BalanceForTests::add_max_amount_a(), BalanceForTests::add_max_amount_b(), + AMM_PROGRAM_ID, ); } @@ -1582,6 +1632,7 @@ fn test_call_add_liquidity_vault_insufficient_balance_2() { #[test] fn test_call_add_liquidity_actual_amount_zero_1() { let _post_states = add_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init_reserve_a_low(), AccountWithMetadataForTests::vault_a_init_low(), AccountWithMetadataForTests::vault_b_init_high(), @@ -1592,6 +1643,7 @@ fn test_call_add_liquidity_actual_amount_zero_1() { NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), BalanceForTests::add_max_amount_a(), BalanceForTests::add_max_amount_b(), + AMM_PROGRAM_ID, ); } @@ -1599,6 +1651,7 @@ fn test_call_add_liquidity_actual_amount_zero_1() { #[test] fn test_call_add_liquidity_actual_amount_zero_2() { let _post_states = add_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init_reserve_b_low(), AccountWithMetadataForTests::vault_a_init_high(), AccountWithMetadataForTests::vault_b_init_low(), @@ -1609,6 +1662,7 @@ fn test_call_add_liquidity_actual_amount_zero_2() { NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), BalanceForTests::add_max_amount_a_low(), BalanceForTests::add_max_amount_b_low(), + AMM_PROGRAM_ID, ); } @@ -1616,6 +1670,7 @@ fn test_call_add_liquidity_actual_amount_zero_2() { #[test] fn test_call_add_liquidity_reserves_zero_1() { let _post_states = add_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init_reserve_a_zero(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -1626,6 +1681,7 @@ fn test_call_add_liquidity_reserves_zero_1() { NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), BalanceForTests::add_max_amount_a(), BalanceForTests::add_max_amount_b(), + AMM_PROGRAM_ID, ); } @@ -1633,6 +1689,7 @@ fn test_call_add_liquidity_reserves_zero_1() { #[test] fn test_call_add_liquidity_reserves_zero_2() { let _post_states = add_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init_reserve_b_zero(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -1643,6 +1700,7 @@ fn test_call_add_liquidity_reserves_zero_2() { NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), BalanceForTests::add_max_amount_a(), BalanceForTests::add_max_amount_b(), + AMM_PROGRAM_ID, ); } @@ -1650,6 +1708,7 @@ fn test_call_add_liquidity_reserves_zero_2() { #[test] fn test_call_add_liquidity_payable_lp_zero() { let _post_states = add_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_add_zero_lp(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -1660,12 +1719,14 @@ fn test_call_add_liquidity_payable_lp_zero() { NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), BalanceForTests::add_max_amount_a_low(), BalanceForTests::add_max_amount_b_low(), + AMM_PROGRAM_ID, ); } #[test] fn test_call_add_liquidity_chained_call_successsful() { let (post_states, chained_calls) = add_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -1676,9 +1737,10 @@ fn test_call_add_liquidity_chained_call_successsful() { NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), BalanceForTests::add_max_amount_a(), BalanceForTests::add_max_amount_b(), + AMM_PROGRAM_ID, ); - let pool_post = post_states[0].clone(); + let pool_post = post_states[1].clone(); assert!( AccountWithMetadataForTests::pool_definition_add_successful().account @@ -1692,12 +1754,57 @@ fn test_call_add_liquidity_chained_call_successsful() { 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()); + + // The config account is echoed back unchanged as the first post-state. + assert_eq!( + *post_states[0].account(), + AccountWithMetadataForTests::config_init().account + ); +} + +#[should_panic(expected = "AMM Program must be initialized before use")] +#[test] +fn test_call_add_liquidity_uninitialized_config_panics() { + let _post_states = add_liquidity( + AccountWithMetadataForTests::config_uninit(), + AccountWithMetadataForTests::pool_definition_init(), + AccountWithMetadataForTests::vault_a_init(), + AccountWithMetadataForTests::vault_b_init(), + AccountWithMetadataForTests::pool_lp_init(), + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + AccountWithMetadataForTests::user_holding_lp_init(), + NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + AMM_PROGRAM_ID, + ); +} + +#[should_panic(expected = "AMM config Account ID does not match PDA")] +#[test] +fn test_call_add_liquidity_wrong_config_pda_panics() { + let _post_states = add_liquidity( + AccountWithMetadataForTests::config_with_wrong_id(), + AccountWithMetadataForTests::pool_definition_init(), + AccountWithMetadataForTests::vault_a_init(), + AccountWithMetadataForTests::vault_b_init(), + AccountWithMetadataForTests::pool_lp_init(), + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + AccountWithMetadataForTests::user_holding_lp_init(), + NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + AMM_PROGRAM_ID, + ); } #[should_panic(expected = "Vault A was not provided")] #[test] fn test_call_remove_liquidity_vault_a_omitted() { let _post_states = remove_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_with_wrong_id(), AccountWithMetadataForTests::vault_b_init(), @@ -1708,6 +1815,7 @@ fn test_call_remove_liquidity_vault_a_omitted() { NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(), BalanceForTests::remove_min_amount_a(), BalanceForTests::remove_min_amount_b(), + AMM_PROGRAM_ID, ); } @@ -1715,6 +1823,7 @@ fn test_call_remove_liquidity_vault_a_omitted() { #[test] fn test_call_remove_liquidity_vault_b_omitted() { let _post_states = remove_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_with_wrong_id(), @@ -1725,6 +1834,7 @@ fn test_call_remove_liquidity_vault_b_omitted() { NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(), BalanceForTests::remove_min_amount_a(), BalanceForTests::remove_min_amount_b(), + AMM_PROGRAM_ID, ); } @@ -1732,6 +1842,7 @@ fn test_call_remove_liquidity_vault_b_omitted() { #[test] fn test_call_remove_liquidity_lp_def_mismatch() { let _post_states = remove_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -1742,6 +1853,7 @@ fn test_call_remove_liquidity_lp_def_mismatch() { NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(), BalanceForTests::remove_min_amount_a(), BalanceForTests::remove_min_amount_b(), + AMM_PROGRAM_ID, ); } @@ -1749,6 +1861,7 @@ fn test_call_remove_liquidity_lp_def_mismatch() { #[test] fn test_call_remove_liquidity_insufficient_liquidity_amount() { let _post_states = remove_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -1761,6 +1874,7 @@ fn test_call_remove_liquidity_insufficient_liquidity_amount() { NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(), BalanceForTests::remove_min_amount_a(), BalanceForTests::remove_min_amount_b(), + AMM_PROGRAM_ID, ); } @@ -1770,6 +1884,7 @@ fn test_call_remove_liquidity_insufficient_liquidity_amount() { #[test] fn test_call_remove_liquidity_insufficient_balance_1() { let _post_states = remove_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -1780,6 +1895,7 @@ fn test_call_remove_liquidity_insufficient_balance_1() { NonZero::new(BalanceForTests::remove_amount_lp_1()).unwrap(), BalanceForTests::remove_min_amount_a(), BalanceForTests::remove_min_amount_b(), + AMM_PROGRAM_ID, ); } @@ -1788,6 +1904,7 @@ fn test_call_remove_liquidity_insufficient_balance_1() { fn test_call_remove_liquidity_pool_at_minimum_liquidity() { // Removing from a legacy/corrupted pool that is already at the locked floor must be rejected. let _post_states = remove_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_at_minimum_liquidity(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -1798,6 +1915,7 @@ fn test_call_remove_liquidity_pool_at_minimum_liquidity() { NonZero::new(MINIMUM_LIQUIDITY).unwrap(), 1, 1, + AMM_PROGRAM_ID, ); } @@ -1808,6 +1926,7 @@ fn test_call_remove_liquidity_exceeds_unlocked_supply() { // account should permanently hold MINIMUM_LIQUIDITY. The guard must still refuse to burn // through the permanent floor. let _post_states = remove_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -1818,6 +1937,7 @@ fn test_call_remove_liquidity_exceeds_unlocked_supply() { NonZero::new(BalanceForTests::lp_supply_init()).unwrap(), 1, 1, + AMM_PROGRAM_ID, ); } @@ -1827,6 +1947,7 @@ fn test_call_remove_liquidity_exceeds_unlocked_supply() { #[test] fn test_call_remove_liquidity_insufficient_balance_2() { let _post_states = remove_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -1837,6 +1958,7 @@ fn test_call_remove_liquidity_insufficient_balance_2() { NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(), BalanceForTests::remove_min_amount_a(), BalanceForTests::remove_min_amount_b(), + AMM_PROGRAM_ID, ); } @@ -1844,6 +1966,7 @@ fn test_call_remove_liquidity_insufficient_balance_2() { #[test] fn test_call_remove_liquidity_min_bal_zero_1() { let _post_states = remove_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -1854,6 +1977,7 @@ fn test_call_remove_liquidity_min_bal_zero_1() { NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(), 0, BalanceForTests::remove_min_amount_b(), + AMM_PROGRAM_ID, ); } @@ -1861,6 +1985,7 @@ fn test_call_remove_liquidity_min_bal_zero_1() { #[test] fn test_call_remove_liquidity_min_bal_zero_2() { let _post_states = remove_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -1871,12 +1996,14 @@ fn test_call_remove_liquidity_min_bal_zero_2() { NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(), BalanceForTests::remove_min_amount_a(), 0, + AMM_PROGRAM_ID, ); } #[test] fn test_call_remove_liquidity_chained_call_successful() { let (post_states, chained_calls) = remove_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -1887,9 +2014,10 @@ fn test_call_remove_liquidity_chained_call_successful() { NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(), BalanceForTests::remove_min_amount_a(), BalanceForTests::remove_min_amount_b_low(), + AMM_PROGRAM_ID, ); - let pool_post = post_states[0].clone(); + let pool_post = post_states[1].clone(); assert!( AccountWithMetadataForTests::pool_definition_remove_successful().account @@ -1909,6 +2037,7 @@ fn test_call_remove_liquidity_chained_call_successful() { #[test] fn test_call_new_definition_with_zero_balance_1() { let _post_states = new_definition( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -1928,6 +2057,7 @@ fn test_call_new_definition_with_zero_balance_1() { #[test] fn test_call_new_definition_with_zero_balance_2() { let _post_states = new_definition( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -1947,6 +2077,7 @@ fn test_call_new_definition_with_zero_balance_2() { #[test] fn test_call_new_definition_same_token_definition() { let _post_states = new_definition( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -1966,6 +2097,7 @@ fn test_call_new_definition_same_token_definition() { #[test] fn test_call_new_definition_wrong_liquidity_id() { let _post_states = new_definition( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -1985,6 +2117,7 @@ fn test_call_new_definition_wrong_liquidity_id() { #[test] fn test_call_new_definition_wrong_lp_lock_holding_id() { let _post_states = new_definition( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2004,6 +2137,7 @@ fn test_call_new_definition_wrong_lp_lock_holding_id() { #[test] fn test_call_new_definition_wrong_pool_id() { let _post_states = new_definition( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_with_wrong_id(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2023,6 +2157,7 @@ fn test_call_new_definition_wrong_pool_id() { #[test] fn test_call_new_definition_wrong_vault_id_1() { let _post_states = new_definition( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_with_wrong_id(), AccountWithMetadataForTests::vault_b_init(), @@ -2042,6 +2177,7 @@ fn test_call_new_definition_wrong_vault_id_1() { #[test] fn test_call_new_definition_wrong_vault_id_2() { let _post_states = new_definition( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_with_wrong_id(), @@ -2062,6 +2198,7 @@ fn test_call_new_definition_wrong_vault_id_2() { fn test_call_new_definition_rejects_initialized_pool() { // Verify that new_definition fails if passed an already-initialized pool let _post_states = new_definition( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2082,6 +2219,7 @@ fn test_call_new_definition_rejects_initialized_pool() { fn test_call_new_definition_initial_lp_too_small() { // isqrt(1000 * 1000) = 1000 == MINIMUM_LIQUIDITY, so the assertion fires. let _post_states = new_definition( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_uninit(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2100,6 +2238,7 @@ fn test_call_new_definition_initial_lp_too_small() { #[test] fn test_call_new_definition_chained_call_successful() { let (post_states, chained_calls) = new_definition( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_uninit(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2114,7 +2253,7 @@ fn test_call_new_definition_chained_call_successful() { AMM_PROGRAM_ID, ); - let pool_post = post_states[0].clone(); + let pool_post = post_states[1].clone(); assert!(AccountWithMetadataForTests::pool_definition_init().account == *pool_post.account()); assert_eq!( @@ -2140,6 +2279,7 @@ fn test_call_new_definition_chained_call_successful() { #[test] fn test_call_swap_incorrect_token_type() { let _post_states = swap_exact_input( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2148,6 +2288,7 @@ fn test_call_swap_incorrect_token_type() { BalanceForTests::add_max_amount_a(), BalanceForTests::min_amount_out(), IdForTests::token_lp_definition_id(), + AMM_PROGRAM_ID, ); } @@ -2155,6 +2296,7 @@ fn test_call_swap_incorrect_token_type() { #[test] fn test_call_swap_vault_a_omitted() { let _post_states = swap_exact_input( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_with_wrong_id(), AccountWithMetadataForTests::vault_b_init(), @@ -2163,6 +2305,7 @@ fn test_call_swap_vault_a_omitted() { BalanceForTests::add_max_amount_a(), BalanceForTests::min_amount_out(), IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } @@ -2170,6 +2313,7 @@ fn test_call_swap_vault_a_omitted() { #[test] fn test_call_swap_vault_b_omitted() { let _post_states = swap_exact_input( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_with_wrong_id(), @@ -2178,6 +2322,7 @@ fn test_call_swap_vault_b_omitted() { BalanceForTests::add_max_amount_a(), BalanceForTests::min_amount_out(), IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } @@ -2185,6 +2330,7 @@ fn test_call_swap_vault_b_omitted() { #[test] fn test_call_swap_reserves_vault_mismatch_1() { let _post_states = swap_exact_input( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init_low(), AccountWithMetadataForTests::vault_b_init(), @@ -2193,6 +2339,7 @@ fn test_call_swap_reserves_vault_mismatch_1() { BalanceForTests::add_max_amount_a(), BalanceForTests::min_amount_out(), IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } @@ -2200,6 +2347,7 @@ fn test_call_swap_reserves_vault_mismatch_1() { #[test] fn test_call_swap_reserves_vault_mismatch_2() { let _post_states = swap_exact_input( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init_low(), @@ -2208,6 +2356,7 @@ fn test_call_swap_reserves_vault_mismatch_2() { BalanceForTests::add_max_amount_a(), BalanceForTests::min_amount_out(), IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } @@ -2215,6 +2364,7 @@ fn test_call_swap_reserves_vault_mismatch_2() { #[test] fn test_call_swap_below_minimum_liquidity() { let _post_states = swap_exact_input( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_below_minimum_liquidity(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2223,6 +2373,7 @@ fn test_call_swap_below_minimum_liquidity() { BalanceForTests::add_max_amount_a(), BalanceForTests::min_amount_out(), IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } @@ -2235,6 +2386,7 @@ fn test_call_swap_rejects_unsupported_fee_tier() { pool.account.data = Data::from(&pool_def); let _post_states = swap_exact_input( + AccountWithMetadataForTests::config_init(), pool, AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2243,6 +2395,7 @@ fn test_call_swap_rejects_unsupported_fee_tier() { BalanceForTests::add_max_amount_a(), BalanceForTests::add_max_amount_a_low(), IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } @@ -2250,6 +2403,7 @@ fn test_call_swap_rejects_unsupported_fee_tier() { #[test] fn test_call_swap_below_min_out() { let _post_states = swap_exact_input( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2258,6 +2412,7 @@ fn test_call_swap_below_min_out() { BalanceForTests::add_max_amount_a(), BalanceForTests::min_amount_out_too_high(), IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } @@ -2265,6 +2420,7 @@ fn test_call_swap_below_min_out() { #[test] fn test_call_swap_effective_amount_zero() { let _post_states = swap_exact_input( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2273,6 +2429,7 @@ fn test_call_swap_effective_amount_zero() { 1, 0, IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } @@ -2280,6 +2437,7 @@ fn test_call_swap_effective_amount_zero() { #[test] fn test_call_swap_output_rounds_to_zero() { let _post_states = swap_exact_input( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init_low_balances(), AccountWithMetadataForTests::vault_a_init_low(), AccountWithMetadataForTests::vault_b_init_low(), @@ -2288,6 +2446,7 @@ fn test_call_swap_output_rounds_to_zero() { 2, 0, IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } @@ -2295,6 +2454,7 @@ fn test_call_swap_output_rounds_to_zero() { #[test] fn test_call_swap_exact_input_rejects_amount_that_rounds_down_below_target_output() { let _post_states = swap_exact_input( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_swap_rounding_boundary_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2303,12 +2463,14 @@ fn test_call_swap_exact_input_rejects_amount_that_rounds_down_below_target_outpu 2, 1, IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } #[test] fn test_call_swap_exact_input_accepts_smallest_amount_for_rounded_boundary() { let (post_states, chained_calls) = swap_exact_input( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_swap_rounding_boundary_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2317,9 +2479,10 @@ fn test_call_swap_exact_input_accepts_smallest_amount_for_rounded_boundary() { 3, 1, IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); - let pool_post = post_states[0].clone(); + let pool_post = post_states[1].clone(); assert_eq!( AccountWithMetadataForTests::pool_definition_swap_rounding_boundary_post().account, @@ -2342,6 +2505,7 @@ fn test_call_swap_exact_input_accepts_smallest_amount_for_rounded_boundary() { #[test] fn test_call_swap_chained_call_successful_1() { let (post_states, chained_calls) = swap_exact_input( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2350,9 +2514,10 @@ fn test_call_swap_chained_call_successful_1() { BalanceForTests::add_max_amount_a(), BalanceForTests::add_max_amount_a_low(), IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); - let pool_post = post_states[0].clone(); + let pool_post = post_states[1].clone(); assert!( AccountWithMetadataForTests::pool_definition_swap_test_1().account == *pool_post.account() @@ -2374,6 +2539,7 @@ fn test_call_swap_chained_call_successful_1() { #[test] fn test_call_swap_chained_call_successful_2() { let (post_states, chained_calls) = swap_exact_input( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2382,9 +2548,10 @@ fn test_call_swap_chained_call_successful_2() { BalanceForTests::add_max_amount_b(), BalanceForTests::min_amount_out(), IdForTests::token_b_definition_id(), + AMM_PROGRAM_ID, ); - let pool_post = post_states[0].clone(); + let pool_post = post_states[1].clone(); assert!( AccountWithMetadataForTests::pool_definition_swap_test_2().account == *pool_post.account() @@ -2407,6 +2574,7 @@ fn test_call_swap_chained_call_successful_2() { #[test] fn call_swap_exact_output_incorrect_token_type() { let _post_states = swap_exact_output( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2415,6 +2583,7 @@ fn call_swap_exact_output_incorrect_token_type() { BalanceForTests::add_max_amount_a(), BalanceForTests::max_amount_in(), IdForTests::token_lp_definition_id(), + AMM_PROGRAM_ID, ); } @@ -2422,6 +2591,7 @@ fn call_swap_exact_output_incorrect_token_type() { #[test] fn call_swap_exact_output_vault_a_omitted() { let _post_states = swap_exact_output( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_with_wrong_id(), AccountWithMetadataForTests::vault_b_init(), @@ -2430,6 +2600,7 @@ fn call_swap_exact_output_vault_a_omitted() { BalanceForTests::add_max_amount_a(), BalanceForTests::max_amount_in(), IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } @@ -2437,6 +2608,7 @@ fn call_swap_exact_output_vault_a_omitted() { #[test] fn call_swap_exact_output_vault_b_omitted() { let _post_states = swap_exact_output( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_with_wrong_id(), @@ -2445,6 +2617,7 @@ fn call_swap_exact_output_vault_b_omitted() { BalanceForTests::add_max_amount_a(), BalanceForTests::max_amount_in(), IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } @@ -2452,6 +2625,7 @@ fn call_swap_exact_output_vault_b_omitted() { #[test] fn call_swap_exact_output_reserves_vault_mismatch_1() { let _post_states = swap_exact_output( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init_low(), AccountWithMetadataForTests::vault_b_init(), @@ -2460,6 +2634,7 @@ fn call_swap_exact_output_reserves_vault_mismatch_1() { BalanceForTests::add_max_amount_a(), BalanceForTests::max_amount_in(), IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } @@ -2467,6 +2642,7 @@ fn call_swap_exact_output_reserves_vault_mismatch_1() { #[test] fn call_swap_exact_output_reserves_vault_mismatch_2() { let _post_states = swap_exact_output( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init_low(), @@ -2475,6 +2651,7 @@ fn call_swap_exact_output_reserves_vault_mismatch_2() { BalanceForTests::add_max_amount_a(), BalanceForTests::max_amount_in(), IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } @@ -2482,6 +2659,7 @@ fn call_swap_exact_output_reserves_vault_mismatch_2() { #[test] fn call_swap_exact_output_below_minimum_liquidity() { let _post_states = swap_exact_output( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_below_minimum_liquidity(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2490,6 +2668,7 @@ fn call_swap_exact_output_below_minimum_liquidity() { BalanceForTests::add_max_amount_a(), BalanceForTests::max_amount_in(), IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } @@ -2497,6 +2676,7 @@ fn call_swap_exact_output_below_minimum_liquidity() { #[test] fn call_swap_exact_output_exceeds_max_in() { let _post_states = swap_exact_output( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2505,6 +2685,7 @@ fn call_swap_exact_output_exceeds_max_in() { 166_u128, 100_u128, IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } @@ -2512,6 +2693,7 @@ fn call_swap_exact_output_exceeds_max_in() { #[test] fn call_swap_exact_output_zero() { let _post_states = swap_exact_output( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2520,6 +2702,7 @@ fn call_swap_exact_output_zero() { 0_u128, 500_u128, IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } @@ -2527,6 +2710,7 @@ fn call_swap_exact_output_zero() { #[test] fn call_swap_exact_output_exceeds_reserve() { let _post_states = swap_exact_output( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2535,12 +2719,14 @@ fn call_swap_exact_output_exceeds_reserve() { BalanceForTests::vault_b_reserve_init(), BalanceForTests::max_amount_in(), IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } #[test] fn call_swap_exact_output_chained_call_successful() { let (post_states, chained_calls) = swap_exact_output( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_swap_exact_output_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2549,9 +2735,10 @@ fn call_swap_exact_output_chained_call_successful() { BalanceForTests::max_amount_in(), BalanceForTests::vault_b_reserve_init(), IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); - let pool_post = post_states[0].clone(); + let pool_post = post_states[1].clone(); assert!( AccountWithMetadataForTests::pool_definition_swap_exact_output_test_1().account @@ -2574,6 +2761,7 @@ fn call_swap_exact_output_chained_call_successful() { #[test] fn call_swap_exact_output_chained_call_successful_2() { let (post_states, chained_calls) = swap_exact_output( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_swap_exact_output_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2582,9 +2770,10 @@ fn call_swap_exact_output_chained_call_successful_2() { 285, 300, IdForTests::token_b_definition_id(), + AMM_PROGRAM_ID, ); - let pool_post = post_states[0].clone(); + let pool_post = post_states[1].clone(); assert!( AccountWithMetadataForTests::pool_definition_swap_exact_output_test_2().account @@ -2610,6 +2799,7 @@ fn call_swap_exact_output_chained_call_successful_2() { #[test] fn call_swap_exact_output_fee_enforced() { let _post_states = swap_exact_output( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_swap_exact_output_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2618,6 +2808,7 @@ fn call_swap_exact_output_fee_enforced() { 166_u128, // exact_amount_out: token_b 499_u128, // max_amount_in: still one short after fee rounding IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } @@ -2628,6 +2819,7 @@ fn call_swap_exact_output_fee_enforced() { #[test] fn call_swap_exact_output_rejects_max_in_that_rounds_down_below_target_output() { let _post_states = swap_exact_output( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_swap_rounding_boundary_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2636,12 +2828,14 @@ fn call_swap_exact_output_rejects_max_in_that_rounds_down_below_target_output() 1, 2, IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } #[test] fn call_swap_exact_output_accepts_smallest_max_in_for_rounded_boundary() { let (post_states, chained_calls) = swap_exact_output( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_swap_rounding_boundary_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2650,9 +2844,10 @@ fn call_swap_exact_output_accepts_smallest_max_in_for_rounded_boundary() { 1, 3, IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); - let pool_post = post_states[0].clone(); + let pool_post = post_states[1].clone(); assert_eq!( AccountWithMetadataForTests::pool_definition_swap_rounding_boundary_post().account, @@ -2733,6 +2928,7 @@ fn swap_exact_output_overflow_protection() { }; let _result = swap_exact_output( + AccountWithMetadataForTests::config_init(), pool, vault_a, vault_b, @@ -2742,12 +2938,14 @@ fn swap_exact_output_overflow_protection() { 1, // max_amount_in: tiny — real deposit would be enormous, but // overflow wraps it to 0, making 0 <= 1 pass silently IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } #[test] fn test_new_definition_lp_asymmetric_amounts() { let (post_states, chained_calls) = new_definition( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_uninit(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2763,7 +2961,7 @@ fn test_new_definition_lp_asymmetric_amounts() { ); // check the minted LP amount - let pool_post = post_states[0].clone(); + let pool_post = post_states[1].clone(); let pool_def = PoolDefinition::try_from(&pool_post.account().data).unwrap(); assert_eq!( pool_def.liquidity_pool_supply, @@ -2785,6 +2983,7 @@ fn test_new_definition_lp_symmetric_amounts() { assert_eq!(expected_lp, 2_000); let (post_states, chained_calls) = new_definition( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_uninit(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2799,7 +2998,7 @@ fn test_new_definition_lp_symmetric_amounts() { AMM_PROGRAM_ID, ); - let pool_post = post_states[0].clone(); + let pool_post = post_states[1].clone(); let pool_def = PoolDefinition::try_from(&pool_post.account().data).unwrap(); assert_eq!(pool_def.liquidity_pool_supply, expected_lp); @@ -2854,6 +3053,7 @@ fn test_minimum_liquidity_lock_and_remove_all_user_lp() { let user_lp = initial_lp - MINIMUM_LIQUIDITY; let (post_states, chained_calls) = new_definition( + AccountWithMetadataForTests::config_init(), pool_uninitialized, AccountForTests::vault_a_init(), AccountForTests::vault_b_init(), @@ -2901,15 +3101,16 @@ fn test_minimum_liquidity_lock_and_remove_all_user_lp() { assert_eq!(chained_calls[0], expected_lock_call); assert_eq!(chained_calls[1], expected_user_call); - let pool_post = PoolDefinition::try_from(&post_states[0].account().data).unwrap(); + let pool_post = PoolDefinition::try_from(&post_states[1].account().data).unwrap(); assert_eq!(pool_post.liquidity_pool_supply, initial_lp); let pool_for_remove = AccountWithMetadata { - account: post_states[0].account().clone(), + account: post_states[1].account().clone(), is_authorized: true, account_id: IdForTests::pool_definition_id(), }; let (remove_post_states, _) = remove_liquidity( + AccountWithMetadataForTests::config_init(), pool_for_remove, AccountForTests::vault_a_init(), AccountForTests::vault_b_init(), @@ -2920,10 +3121,11 @@ fn test_minimum_liquidity_lock_and_remove_all_user_lp() { NonZero::new(user_lp).unwrap(), 1, 1, + AMM_PROGRAM_ID, ); let pool_after_remove = - PoolDefinition::try_from(&remove_post_states[0].account().data).unwrap(); + PoolDefinition::try_from(&remove_post_states[1].account().data).unwrap(); assert_eq!(pool_after_remove.liquidity_pool_supply, MINIMUM_LIQUIDITY); assert!(pool_after_remove.reserve_a > 0); assert!(pool_after_remove.reserve_b > 0); @@ -3015,6 +3217,7 @@ fn test_donation_then_add_liquidity_sync_mitigates_mispricing() { let donated_vault_b = AccountWithMetadataForTests::vault_b_init(); let (post_unsynced, _) = add_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), donated_vault_a.clone(), donated_vault_b.clone(), @@ -3025,8 +3228,9 @@ fn test_donation_then_add_liquidity_sync_mitigates_mispricing() { NonZero::new(1).unwrap(), 100, 50, + AMM_PROGRAM_ID, ); - let unsynced_pool_post = PoolDefinition::try_from(&post_unsynced[0].account().data).unwrap(); + let unsynced_pool_post = PoolDefinition::try_from(&post_unsynced[1].account().data).unwrap(); let unsynced_delta_lp = unsynced_pool_post.liquidity_pool_supply - BalanceForTests::lp_supply_init(); @@ -3045,6 +3249,7 @@ fn test_donation_then_add_liquidity_sync_mitigates_mispricing() { }; let (post_synced, _) = add_liquidity( + AccountWithMetadataForTests::config_init(), synced_pool, donated_vault_a_for_synced_add, donated_vault_b_for_synced_add, @@ -3055,8 +3260,9 @@ fn test_donation_then_add_liquidity_sync_mitigates_mispricing() { NonZero::new(1).unwrap(), 100, 50, + AMM_PROGRAM_ID, ); - let synced_pool_post = PoolDefinition::try_from(&post_synced[0].account().data).unwrap(); + let synced_pool_post = PoolDefinition::try_from(&post_synced[1].account().data).unwrap(); let synced_delta_lp = synced_pool_post.liquidity_pool_supply - PoolDefinition::try_from(&sync_post[0].account().data) .unwrap() @@ -3071,6 +3277,7 @@ fn new_definition_overflow_protection() { let large_amount = u128::MAX / 2 + 1; let _result = new_definition( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_uninit(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -3142,6 +3349,7 @@ fn add_liquidity_overflow_protection() { }; let _result = add_liquidity( + AccountWithMetadataForTests::config_init(), pool, vault_a, vault_b, @@ -3152,6 +3360,7 @@ fn add_liquidity_overflow_protection() { NonZero::new(1).unwrap(), 500, 2, // max_amount_b=2 → reserve_a * 2 overflows + AMM_PROGRAM_ID, ); } @@ -3226,6 +3435,7 @@ fn remove_liquidity_overflow_protection() { }; let _result = remove_liquidity( + AccountWithMetadataForTests::config_init(), pool, vault_a, vault_b, @@ -3237,6 +3447,7 @@ fn remove_liquidity_overflow_protection() { * overflows */ 1, 1, + AMM_PROGRAM_ID, ); } @@ -3299,6 +3510,7 @@ fn swap_exact_input_overflow_protection() { // effective_amount_in) With fee_bps=30: effective_amount_in = 3 * 9970 / 10000 = 2 // reserve_b is large, so reserve_b * 2 overflows let _result = swap_exact_input( + AccountWithMetadataForTests::config_init(), pool, vault_a, vault_b, @@ -3307,6 +3519,7 @@ fn swap_exact_input_overflow_protection() { 3, 1, IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } @@ -3319,6 +3532,7 @@ fn test_new_definition_supports_all_fee_tiers() { FEE_TIER_BPS_100, ] { let (post_states, _) = new_definition( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_uninit(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -3333,7 +3547,7 @@ fn test_new_definition_supports_all_fee_tiers() { AMM_PROGRAM_ID, ); - let pool_post = post_states[0].clone(); + let pool_post = post_states[1].clone(); let pool_def = PoolDefinition::try_from(&pool_post.account().data).unwrap(); assert_eq!(pool_def.fees, fees); } @@ -3343,6 +3557,7 @@ fn test_new_definition_supports_all_fee_tiers() { #[test] fn test_new_definition_rejects_unsupported_fee_tier() { let _ = new_definition( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_uninit(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -3360,10 +3575,11 @@ fn test_new_definition_rejects_unsupported_fee_tier() { // --- Token program ownership validation tests --- -#[should_panic(expected = "User Token A holding must be owned by the vault's Token Program")] +#[should_panic(expected = "User Token A holding must be owned by the configured Token Program")] #[test] fn test_add_liquidity_rejects_user_holding_a_wrong_program() { let _ = add_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -3374,13 +3590,15 @@ fn test_add_liquidity_rejects_user_holding_a_wrong_program() { NonZero::new(BalanceForTests::add_min_amount_lp()).expect("test value must be nonzero"), BalanceForTests::add_max_amount_a(), BalanceForTests::add_max_amount_b(), + AMM_PROGRAM_ID, ); } -#[should_panic(expected = "User Token B holding must be owned by the vault's Token Program")] +#[should_panic(expected = "User Token B holding must be owned by the configured Token Program")] #[test] fn test_add_liquidity_rejects_user_holding_b_wrong_program() { let _ = add_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -3391,13 +3609,15 @@ fn test_add_liquidity_rejects_user_holding_b_wrong_program() { NonZero::new(BalanceForTests::add_min_amount_lp()).expect("test value must be nonzero"), BalanceForTests::add_max_amount_a(), BalanceForTests::add_max_amount_b(), + AMM_PROGRAM_ID, ); } -#[should_panic(expected = "User Token A holding must be owned by the vault's Token Program")] +#[should_panic(expected = "User Token A holding must be owned by the configured Token Program")] #[test] fn test_remove_liquidity_rejects_user_holding_a_wrong_program() { let _ = remove_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -3410,13 +3630,15 @@ fn test_remove_liquidity_rejects_user_holding_a_wrong_program() { NonZero::new(BalanceForTests::remove_amount_lp()).expect("test value must be nonzero"), BalanceForTests::remove_min_amount_a(), BalanceForTests::remove_min_amount_b_low(), + AMM_PROGRAM_ID, ); } -#[should_panic(expected = "User Token B holding must be owned by the vault's Token Program")] +#[should_panic(expected = "User Token B holding must be owned by the configured Token Program")] #[test] fn test_remove_liquidity_rejects_user_holding_b_wrong_program() { let _ = remove_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -3429,6 +3651,7 @@ fn test_remove_liquidity_rejects_user_holding_b_wrong_program() { NonZero::new(BalanceForTests::remove_amount_lp()).expect("test value must be nonzero"), BalanceForTests::remove_min_amount_a(), BalanceForTests::remove_min_amount_b_low(), + AMM_PROGRAM_ID, ); } @@ -3437,6 +3660,7 @@ fn test_remove_liquidity_rejects_user_holding_b_wrong_program() { fn test_remove_liquidity_rejects_amount_exceeding_user_lp_balance() { let lp_balance = BalanceForTests::remove_amount_lp() - 1; let _ = remove_liquidity( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -3447,13 +3671,15 @@ fn test_remove_liquidity_rejects_amount_exceeding_user_lp_balance() { NonZero::new(BalanceForTests::remove_amount_lp()).expect("test value must be nonzero"), BalanceForTests::remove_min_amount_a(), BalanceForTests::remove_min_amount_b_low(), + AMM_PROGRAM_ID, ); } -#[should_panic(expected = "User Token A holding must be owned by the vault's Token Program")] +#[should_panic(expected = "User Token A holding must be owned by the configured Token Program")] #[test] fn test_swap_exact_input_rejects_user_holding_a_wrong_program() { let _ = swap_exact_input( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -3462,13 +3688,15 @@ fn test_swap_exact_input_rejects_user_holding_a_wrong_program() { BalanceForTests::add_max_amount_a(), BalanceForTests::min_amount_out(), IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } -#[should_panic(expected = "User Token B holding must be owned by the vault's Token Program")] +#[should_panic(expected = "User Token B holding must be owned by the configured Token Program")] #[test] fn test_swap_exact_input_rejects_user_holding_b_wrong_program() { let _ = swap_exact_input( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -3477,13 +3705,15 @@ fn test_swap_exact_input_rejects_user_holding_b_wrong_program() { BalanceForTests::add_max_amount_a(), BalanceForTests::min_amount_out(), IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } -#[should_panic(expected = "User Token A holding must be owned by the vault's Token Program")] +#[should_panic(expected = "User Token A holding must be owned by the configured Token Program")] #[test] fn test_swap_exact_output_rejects_user_holding_a_wrong_program() { let _ = swap_exact_output( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_swap_exact_output_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -3492,13 +3722,15 @@ fn test_swap_exact_output_rejects_user_holding_a_wrong_program() { 166, BalanceForTests::max_amount_in(), IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } -#[should_panic(expected = "User Token B holding must be owned by the vault's Token Program")] +#[should_panic(expected = "User Token B holding must be owned by the configured Token Program")] #[test] fn test_swap_exact_output_rejects_user_holding_b_wrong_program() { let _ = swap_exact_output( + AccountWithMetadataForTests::config_init(), AccountWithMetadataForTests::pool_definition_swap_exact_output_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -3507,5 +3739,6 @@ fn test_swap_exact_output_rejects_user_holding_b_wrong_program() { 166, BalanceForTests::max_amount_in(), IdForTests::token_a_definition_id(), + AMM_PROGRAM_ID, ); } diff --git a/programs/integration_tests/tests/amm.rs b/programs/integration_tests/tests/amm.rs index bfb6feb..bd01402 100644 --- a/programs/integration_tests/tests/amm.rs +++ b/programs/integration_tests/tests/amm.rs @@ -43,6 +43,10 @@ impl Ids { amm_methods::AMM_ID } + fn config() -> AccountId { + amm_core::compute_config_pda(Self::amm_program()) + } + fn token_a_definition() -> AccountId { AccountId::new([3; 32]) } @@ -283,6 +287,17 @@ impl Balances { } impl Accounts { + fn config() -> Account { + Account { + program_owner: Ids::amm_program(), + balance: 0_u128, + data: Data::from(&amm_core::AmmConfig { + token_program_id: Ids::token_program(), + }), + nonce: Nonce(0), + } + } + fn user_a_holding() -> Account { Account { program_owner: Ids::token_program(), @@ -914,6 +929,7 @@ fn deploy_programs(state: &mut V03State) { fn state_for_amm_tests() -> V03State { let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); deploy_programs(&mut state); + state.force_insert_account(Ids::config(), Accounts::config()); state.force_insert_account(Ids::pool_definition(), Accounts::pool_definition_init()); state.force_insert_account( Ids::token_a_definition(), @@ -938,6 +954,7 @@ fn state_for_amm_tests() -> V03State { fn state_for_amm_tests_with_new_def() -> V03State { let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); deploy_programs(&mut state); + state.force_insert_account(Ids::config(), Accounts::config()); state.force_insert_account( Ids::token_a_definition(), Accounts::token_a_definition_account(), @@ -977,6 +994,7 @@ fn try_execute_new_definition( let message = public_transaction::Message::try_new( Ids::amm_program(), vec![ + Ids::config(), Ids::pool_definition(), Ids::vault_a(), Ids::vault_b(), @@ -1032,6 +1050,7 @@ fn execute_swap_a_to_b(state: &mut V03State, swap_amount_in: u128, min_amount_ou let message = public_transaction::Message::try_new( Ids::amm_program(), vec![ + Ids::config(), Ids::pool_definition(), Ids::vault_a(), Ids::vault_b(), @@ -1061,6 +1080,7 @@ fn execute_swap_b_to_a(state: &mut V03State, swap_amount_in: u128, min_amount_ou let message = public_transaction::Message::try_new( Ids::amm_program(), vec![ + Ids::config(), Ids::pool_definition(), Ids::vault_a(), Ids::vault_b(), @@ -1095,6 +1115,7 @@ fn execute_add_liquidity( let message = public_transaction::Message::try_new( Ids::amm_program(), vec![ + Ids::config(), Ids::pool_definition(), Ids::vault_a(), Ids::vault_b(), @@ -1135,6 +1156,7 @@ fn execute_remove_liquidity( let message = public_transaction::Message::try_new( Ids::amm_program(), vec![ + Ids::config(), Ids::pool_definition(), Ids::vault_a(), Ids::vault_b(), @@ -1154,6 +1176,26 @@ fn execute_remove_liquidity( state.transition_from_public_transaction(&tx, 0, 0).unwrap(); } +#[cfg(test)] +fn execute_initialize(state: &mut V03State) { + let instruction = amm_core::Instruction::Initialize { + token_program_id: Ids::token_program(), + }; + + let message = public_transaction::Message::try_new( + Ids::amm_program(), + vec![Ids::config()], + vec![], + instruction, + ) + .unwrap(); + + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); +} + fn fungible_balance(account: &Account) -> u128 { let holding = TokenHolding::try_from(&account.data).expect("expected token holding"); let TokenHolding::Fungible { @@ -1185,6 +1227,26 @@ fn fungible_total_supply(account: &Account) -> u128 { total_supply } +#[test] +fn amm_initialize_creates_config_account() { + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); + deploy_programs(&mut state); + + // Before initialization the config PDA does not exist. + assert_eq!(state.get_account_by_id(Ids::config()), Account::default()); + + execute_initialize(&mut state); + + // Initialization creates the config PDA, owned by the AMM program. + let config_account = state.get_account_by_id(Ids::config()); + assert_eq!(config_account, Accounts::config()); + + // Explicitly assert the stored Token Program ID round-trips from the instruction argument. + let config = amm_core::AmmConfig::try_from(&config_account.data) + .expect("config account must hold a valid AmmConfig"); + assert_eq!(config.token_program_id, Ids::token_program()); +} + #[test] fn amm_remove_liquidity() { let mut state = state_for_amm_tests(); @@ -1199,6 +1261,7 @@ fn amm_remove_liquidity() { let message = public_transaction::Message::try_new( Ids::amm_program(), vec![ + Ids::config(), Ids::pool_definition(), Ids::vault_a(), Ids::vault_b(), @@ -1262,6 +1325,7 @@ fn amm_remove_liquidity_insufficient_user_lp_fails() { let message = public_transaction::Message::try_new( Ids::amm_program(), vec![ + Ids::config(), Ids::pool_definition(), Ids::vault_a(), Ids::vault_b(), @@ -1487,6 +1551,7 @@ fn amm_add_liquidity() { let message = public_transaction::Message::try_new( Ids::amm_program(), vec![ + Ids::config(), Ids::pool_definition(), Ids::vault_a(), Ids::vault_b(), @@ -1550,6 +1615,7 @@ fn amm_swap_b_to_a() { let message = public_transaction::Message::try_new( Ids::amm_program(), vec![ + Ids::config(), Ids::pool_definition(), Ids::vault_a(), Ids::vault_b(), @@ -1602,6 +1668,7 @@ fn amm_swap_a_to_b() { let message = public_transaction::Message::try_new( Ids::amm_program(), vec![ + Ids::config(), Ids::pool_definition(), Ids::vault_a(), Ids::vault_b(), @@ -1708,6 +1775,7 @@ fn amm_swap_rejects_expired_deadline() { let message = public_transaction::Message::try_new( Ids::amm_program(), vec![ + Ids::config(), Ids::pool_definition(), Ids::vault_a(), Ids::vault_b(), @@ -1744,6 +1812,7 @@ fn amm_swap_exact_output_rejects_expired_deadline() { let message = public_transaction::Message::try_new( Ids::amm_program(), vec![ + Ids::config(), Ids::pool_definition(), Ids::vault_a(), Ids::vault_b(), @@ -1780,6 +1849,7 @@ fn amm_add_liquidity_rejects_expired_deadline() { let message = public_transaction::Message::try_new( Ids::amm_program(), vec![ + Ids::config(), Ids::pool_definition(), Ids::vault_a(), Ids::vault_b(), @@ -1822,6 +1892,7 @@ fn amm_remove_liquidity_rejects_expired_deadline() { let message = public_transaction::Message::try_new( Ids::amm_program(), vec![ + Ids::config(), Ids::pool_definition(), Ids::vault_a(), Ids::vault_b(), @@ -1860,6 +1931,7 @@ fn amm_new_definition_rejects_expired_deadline() { let message = public_transaction::Message::try_new( Ids::amm_program(), vec![ + Ids::config(), Ids::pool_definition(), Ids::vault_a(), Ids::vault_b(),