mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-06-29 11:39:25 +00:00
feat(amm): refresh TWAP current tick on every reserve-mutating instruction
Make swap_exact_input, swap_exact_output, add_liquidity, remove_liquidity, and sync_reserves keep the pool's TWAP current tick in sync with its reserves. Each now takes the current-tick and clock accounts, reads the TWAP program ID from the config account, validates the clock account and the current-tick PDA, and after computing the post-op pool chains an UpdateCurrentTick to the oracle carrying the post-op spot price, with the pool passed as the authorized price source via its pool PDA seed. sync_reserves additionally now takes the config account so it can resolve the TWAP program ID and gate on initialization, consistent with the other instructions. The invariant current_tick == tick(reserves) therefore holds after every operation. Proportional add/remove preserve the price, so the tick is unchanged for them, but the refresh still runs and lands on the correct value.
This commit is contained in:
parent
b997ca678e
commit
0e2c5f9329
@ -244,6 +244,18 @@
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "current_tick_account",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
@ -315,6 +327,18 @@
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "current_tick_account",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
@ -374,6 +398,18 @@
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "current_tick_account",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
@ -433,6 +469,18 @@
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "current_tick_account",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
@ -457,6 +505,12 @@
|
||||
{
|
||||
"name": "sync_reserves",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "config",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "pool",
|
||||
"writable": false,
|
||||
@ -474,6 +528,18 @@
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "current_tick_account",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
|
||||
@ -114,6 +114,10 @@ pub enum Instruction {
|
||||
/// - User Holding Account for Token A (authorized)
|
||||
/// - User Holding Account for Token B (authorized)
|
||||
/// - User Holding Account for Pool Liquidity
|
||||
/// - Current Tick Account, the pool's TWAP PDA derived as
|
||||
/// `compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id)`; refreshed
|
||||
/// with the new spot price
|
||||
/// - Clock Account (the canonical 1-block LEZ clock)
|
||||
AddLiquidity {
|
||||
min_amount_liquidity: u128,
|
||||
max_amount_to_add_token_a: u128,
|
||||
@ -132,6 +136,10 @@ pub enum Instruction {
|
||||
/// - User Holding Account for Token A (initialized)
|
||||
/// - User Holding Account for Token B (initialized)
|
||||
/// - User Holding Account for Pool Liquidity (authorized)
|
||||
/// - Current Tick Account, the pool's TWAP PDA derived as
|
||||
/// `compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id)`; refreshed
|
||||
/// with the new spot price
|
||||
/// - Clock Account (the canonical 1-block LEZ clock)
|
||||
RemoveLiquidity {
|
||||
remove_liquidity_amount: u128,
|
||||
min_amount_to_remove_token_a: u128,
|
||||
@ -149,6 +157,10 @@ pub enum Instruction {
|
||||
/// - Vault Holding Account for Token B (initialized)
|
||||
/// - User Holding Account for Token A
|
||||
/// - User Holding Account for Token B; either is authorized.
|
||||
/// - Current Tick Account, the pool's TWAP PDA derived as
|
||||
/// `compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id)`; refreshed
|
||||
/// with the new spot price
|
||||
/// - Clock Account (the canonical 1-block LEZ clock)
|
||||
SwapExactInput {
|
||||
swap_amount_in: u128,
|
||||
min_amount_out: u128,
|
||||
@ -166,6 +178,10 @@ pub enum Instruction {
|
||||
/// - Vault Holding Account for Token B (initialized)
|
||||
/// - User Holding Account for Token A
|
||||
/// - User Holding Account for Token B; either is authorized.
|
||||
/// - Current Tick Account, the pool's TWAP PDA derived as
|
||||
/// `compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id)`; refreshed
|
||||
/// with the new spot price
|
||||
/// - Clock Account (the canonical 1-block LEZ clock)
|
||||
SwapExactOutput {
|
||||
exact_amount_out: u128,
|
||||
max_amount_in: u128,
|
||||
@ -174,12 +190,16 @@ pub enum Instruction {
|
||||
deadline: u64,
|
||||
},
|
||||
|
||||
/// Sync pool reserves with current vault balances.
|
||||
/// Sync pool reserves with current vault balances, refreshing the pool's TWAP current tick.
|
||||
///
|
||||
/// Required accounts:
|
||||
/// - AMM Pool (initialized, with LP supply at or above minimum liquidity)
|
||||
/// - Vault Holding Account for Token A (initialized)
|
||||
/// - Vault Holding Account for Token B (initialized)
|
||||
/// - Current Tick Account, the pool's TWAP PDA derived as
|
||||
/// `compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id)`; refreshed
|
||||
/// with the new spot price
|
||||
/// - Clock Account (the canonical 1-block LEZ clock)
|
||||
SyncReserves,
|
||||
}
|
||||
|
||||
|
||||
@ -162,6 +162,8 @@ mod amm {
|
||||
user_holding_a: AccountWithMetadata,
|
||||
user_holding_b: AccountWithMetadata,
|
||||
user_holding_lp: AccountWithMetadata,
|
||||
current_tick_account: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
min_amount_liquidity: u128,
|
||||
max_amount_to_add_token_a: u128,
|
||||
max_amount_to_add_token_b: u128,
|
||||
@ -176,6 +178,8 @@ mod amm {
|
||||
user_holding_a,
|
||||
user_holding_b,
|
||||
user_holding_lp,
|
||||
current_tick_account,
|
||||
clock,
|
||||
NonZeroU128::new(min_amount_liquidity).expect("min_amount_liquidity must be nonzero"),
|
||||
max_amount_to_add_token_a,
|
||||
max_amount_to_add_token_b,
|
||||
@ -201,6 +205,8 @@ mod amm {
|
||||
user_holding_a: AccountWithMetadata,
|
||||
user_holding_b: AccountWithMetadata,
|
||||
user_holding_lp: AccountWithMetadata,
|
||||
current_tick_account: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
remove_liquidity_amount: u128,
|
||||
min_amount_to_remove_token_a: u128,
|
||||
min_amount_to_remove_token_b: u128,
|
||||
@ -215,6 +221,8 @@ mod amm {
|
||||
user_holding_a,
|
||||
user_holding_b,
|
||||
user_holding_lp,
|
||||
current_tick_account,
|
||||
clock,
|
||||
NonZeroU128::new(remove_liquidity_amount)
|
||||
.expect("remove_liquidity_amount must be nonzero"),
|
||||
min_amount_to_remove_token_a,
|
||||
@ -239,6 +247,8 @@ mod amm {
|
||||
vault_b: AccountWithMetadata,
|
||||
user_holding_a: AccountWithMetadata,
|
||||
user_holding_b: AccountWithMetadata,
|
||||
current_tick_account: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
swap_amount_in: u128,
|
||||
min_amount_out: u128,
|
||||
token_definition_id_in: AccountId,
|
||||
@ -251,6 +261,8 @@ mod amm {
|
||||
vault_b,
|
||||
user_holding_a,
|
||||
user_holding_b,
|
||||
current_tick_account,
|
||||
clock,
|
||||
swap_amount_in,
|
||||
min_amount_out,
|
||||
token_definition_id_in,
|
||||
@ -274,6 +286,8 @@ mod amm {
|
||||
vault_b: AccountWithMetadata,
|
||||
user_holding_a: AccountWithMetadata,
|
||||
user_holding_b: AccountWithMetadata,
|
||||
current_tick_account: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
exact_amount_out: u128,
|
||||
max_amount_in: u128,
|
||||
token_definition_id_in: AccountId,
|
||||
@ -286,6 +300,8 @@ mod amm {
|
||||
vault_b,
|
||||
user_holding_a,
|
||||
user_holding_b,
|
||||
current_tick_account,
|
||||
clock,
|
||||
exact_amount_out,
|
||||
max_amount_in,
|
||||
token_definition_id_in,
|
||||
@ -295,15 +311,26 @@ mod amm {
|
||||
.with_timestamp_validity_window(..deadline))
|
||||
}
|
||||
|
||||
/// Sync pool reserves with current vault balances.
|
||||
/// Sync pool reserves with current vault balances, refreshing the pool's TWAP current tick.
|
||||
#[instruction]
|
||||
pub fn sync_reserves(
|
||||
ctx: ProgramContext,
|
||||
config: AccountWithMetadata,
|
||||
pool: AccountWithMetadata,
|
||||
vault_a: AccountWithMetadata,
|
||||
vault_b: AccountWithMetadata,
|
||||
current_tick_account: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
) -> SpelResult {
|
||||
let (post_states, chained_calls) =
|
||||
amm_program::sync::sync_reserves(pool, vault_a, vault_b);
|
||||
let (post_states, chained_calls) = amm_program::sync::sync_reserves(
|
||||
config,
|
||||
pool,
|
||||
vault_a,
|
||||
vault_b,
|
||||
current_tick_account,
|
||||
clock,
|
||||
ctx.self_program_id,
|
||||
);
|
||||
Ok(spel_framework::SpelOutput::execute(post_states, chained_calls))
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,12 +2,15 @@ use std::num::NonZeroU128;
|
||||
|
||||
use amm_core::{
|
||||
assert_supported_fee_tier, compute_config_pda, compute_liquidity_token_pda_seed,
|
||||
read_vault_fungible_balances, AmmConfig, PoolDefinition,
|
||||
compute_pool_pda_seed, read_vault_fungible_balances, spot_price_q64_64, AmmConfig,
|
||||
PoolDefinition,
|
||||
};
|
||||
use clock_core::CLOCK_01_PROGRAM_ACCOUNT_ID;
|
||||
use nssa_core::{
|
||||
account::{AccountWithMetadata, Data},
|
||||
program::{AccountPostState, ChainedCall, ProgramId},
|
||||
};
|
||||
use twap_oracle_core::compute_current_tick_account_pda;
|
||||
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
@ -22,21 +25,24 @@ pub fn add_liquidity(
|
||||
user_holding_a: AccountWithMetadata,
|
||||
user_holding_b: AccountWithMetadata,
|
||||
user_holding_lp: AccountWithMetadata,
|
||||
current_tick_account: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
min_amount_liquidity: NonZeroU128,
|
||||
max_amount_to_add_token_a: u128,
|
||||
max_amount_to_add_token_b: u128,
|
||||
amm_program_id: ProgramId,
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
// The Token Program is taken from the config account, not trusted from a caller-supplied
|
||||
// The program IDs are 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;
|
||||
let config_data = AmmConfig::try_from(&config.account.data)
|
||||
.expect("Add liquidity: AMM Program must be initialized before use");
|
||||
let token_program_id = config_data.token_program_id;
|
||||
let twap_oracle_program_id = config_data.twap_oracle_program_id;
|
||||
|
||||
// 1. Fetch Pool state
|
||||
let pool_def_data = PoolDefinition::try_from(&pool.account.data)
|
||||
@ -74,6 +80,17 @@ pub fn add_liquidity(
|
||||
user_holding_b.account.program_owner, token_program_id,
|
||||
"User Token B holding must be owned by the configured Token Program"
|
||||
);
|
||||
// The current tick is refreshed by a chained call to the oracle; validate its PDA and the
|
||||
// clock here so the add is rejected early with an AMM-level error.
|
||||
assert_eq!(
|
||||
clock.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
"Add liquidity: clock account must be the canonical 1-block LEZ clock account"
|
||||
);
|
||||
assert_eq!(
|
||||
current_tick_account.account_id,
|
||||
compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id),
|
||||
"Add liquidity: current tick Account ID does not match PDA"
|
||||
);
|
||||
|
||||
assert!(
|
||||
max_amount_to_add_token_a != 0 && max_amount_to_add_token_b != 0,
|
||||
@ -204,7 +221,32 @@ pub fn add_liquidity(
|
||||
)
|
||||
.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];
|
||||
// Refresh the pool's TWAP current tick from the post-add spot price. The pool is already owned
|
||||
// by this program, so it is passed (in its post-add state) as the authorized price source.
|
||||
let new_price = spot_price_q64_64(
|
||||
pool_post_definition.reserve_a,
|
||||
pool_post_definition.reserve_b,
|
||||
);
|
||||
let pool_price_source = AccountWithMetadata {
|
||||
account: pool_post.clone(),
|
||||
is_authorized: true,
|
||||
account_id: pool.account_id,
|
||||
};
|
||||
let call_update_tick = ChainedCall::new(
|
||||
twap_oracle_program_id,
|
||||
vec![
|
||||
current_tick_account.clone(),
|
||||
pool_price_source,
|
||||
clock.clone(),
|
||||
],
|
||||
&twap_oracle_core::Instruction::UpdateCurrentTick { price: new_price },
|
||||
)
|
||||
.with_pda_seeds(vec![compute_pool_pda_seed(
|
||||
pool_def_data.definition_token_a_id,
|
||||
pool_def_data.definition_token_b_id,
|
||||
)]);
|
||||
|
||||
let chained_calls = vec![call_token_lp, call_token_b, call_token_a, call_update_tick];
|
||||
|
||||
let post_states = vec![
|
||||
AccountPostState::new(config.account.clone()),
|
||||
@ -215,6 +257,8 @@ pub fn add_liquidity(
|
||||
AccountPostState::new(user_holding_a.account.clone()),
|
||||
AccountPostState::new(user_holding_b.account.clone()),
|
||||
AccountPostState::new(user_holding_lp.account.clone()),
|
||||
AccountPostState::new(current_tick_account.account.clone()),
|
||||
AccountPostState::new(clock.account.clone()),
|
||||
];
|
||||
|
||||
(post_states, chained_calls)
|
||||
|
||||
@ -2,12 +2,15 @@ use std::num::NonZeroU128;
|
||||
|
||||
use amm_core::{
|
||||
assert_supported_fee_tier, compute_config_pda, compute_liquidity_token_pda_seed,
|
||||
compute_vault_pda_seed, AmmConfig, PoolDefinition, MINIMUM_LIQUIDITY,
|
||||
compute_pool_pda_seed, compute_vault_pda_seed, spot_price_q64_64, AmmConfig, PoolDefinition,
|
||||
MINIMUM_LIQUIDITY,
|
||||
};
|
||||
use clock_core::CLOCK_01_PROGRAM_ACCOUNT_ID;
|
||||
use nssa_core::{
|
||||
account::{AccountWithMetadata, Data},
|
||||
program::{AccountPostState, ChainedCall, ProgramId},
|
||||
};
|
||||
use twap_oracle_core::compute_current_tick_account_pda;
|
||||
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
@ -22,6 +25,8 @@ pub fn remove_liquidity(
|
||||
user_holding_a: AccountWithMetadata,
|
||||
user_holding_b: AccountWithMetadata,
|
||||
user_holding_lp: AccountWithMetadata,
|
||||
current_tick_account: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
remove_liquidity_amount: NonZeroU128,
|
||||
min_amount_to_remove_token_a: u128,
|
||||
min_amount_to_remove_token_b: u128,
|
||||
@ -29,16 +34,17 @@ pub fn remove_liquidity(
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
let remove_liquidity_amount: u128 = remove_liquidity_amount.into();
|
||||
|
||||
// The Token Program is taken from the config account, not trusted from a caller-supplied
|
||||
// The program IDs are 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;
|
||||
let config_data = AmmConfig::try_from(&config.account.data)
|
||||
.expect("Remove liquidity: AMM Program must be initialized before use");
|
||||
let token_program_id = config_data.token_program_id;
|
||||
let twap_oracle_program_id = config_data.twap_oracle_program_id;
|
||||
|
||||
// 1. Fetch Pool state
|
||||
let pool_def_data = PoolDefinition::try_from(&pool.account.data)
|
||||
@ -78,6 +84,17 @@ pub fn remove_liquidity(
|
||||
user_holding_b.account.program_owner, token_program_id,
|
||||
"User Token B holding must be owned by the configured Token Program"
|
||||
);
|
||||
// The current tick is refreshed by a chained call to the oracle; validate its PDA and the
|
||||
// clock here so the removal is rejected early with an AMM-level error.
|
||||
assert_eq!(
|
||||
clock.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
"Remove liquidity: clock account must be the canonical 1-block LEZ clock account"
|
||||
);
|
||||
assert_eq!(
|
||||
current_tick_account.account_id,
|
||||
compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id),
|
||||
"Remove liquidity: current tick Account ID does not match PDA"
|
||||
);
|
||||
|
||||
// Vault addresses do not need to be checked with PDA
|
||||
// calculation for setting authorization since stored
|
||||
@ -221,7 +238,33 @@ pub fn remove_liquidity(
|
||||
)
|
||||
.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];
|
||||
// Refresh the pool's TWAP current tick from the post-removal spot price. The pool is already
|
||||
// owned by this program, so it is passed (in its post-removal state) as the authorized price
|
||||
// source.
|
||||
let new_price = spot_price_q64_64(
|
||||
pool_post_definition.reserve_a,
|
||||
pool_post_definition.reserve_b,
|
||||
);
|
||||
let pool_price_source = AccountWithMetadata {
|
||||
account: pool_post.clone(),
|
||||
is_authorized: true,
|
||||
account_id: pool.account_id,
|
||||
};
|
||||
let call_update_tick = ChainedCall::new(
|
||||
twap_oracle_program_id,
|
||||
vec![
|
||||
current_tick_account.clone(),
|
||||
pool_price_source,
|
||||
clock.clone(),
|
||||
],
|
||||
&twap_oracle_core::Instruction::UpdateCurrentTick { price: new_price },
|
||||
)
|
||||
.with_pda_seeds(vec![compute_pool_pda_seed(
|
||||
pool_def_data.definition_token_a_id,
|
||||
pool_def_data.definition_token_b_id,
|
||||
)]);
|
||||
|
||||
let chained_calls = vec![call_token_lp, call_token_b, call_token_a, call_update_tick];
|
||||
|
||||
let post_states = vec![
|
||||
AccountPostState::new(config.account.clone()),
|
||||
@ -232,6 +275,8 @@ pub fn remove_liquidity(
|
||||
AccountPostState::new(user_holding_a.account.clone()),
|
||||
AccountPostState::new(user_holding_b.account.clone()),
|
||||
AccountPostState::new(user_holding_lp.account.clone()),
|
||||
AccountPostState::new(current_tick_account.account.clone()),
|
||||
AccountPostState::new(clock.account.clone()),
|
||||
];
|
||||
|
||||
(post_states, chained_calls)
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
use amm_core::{
|
||||
assert_supported_fee_tier, compute_config_pda, read_vault_fungible_balances, AmmConfig,
|
||||
FEE_BPS_DENOMINATOR, MINIMUM_LIQUIDITY,
|
||||
assert_supported_fee_tier, compute_config_pda, compute_pool_pda_seed,
|
||||
read_vault_fungible_balances, spot_price_q64_64, AmmConfig, FEE_BPS_DENOMINATOR,
|
||||
MINIMUM_LIQUIDITY,
|
||||
};
|
||||
pub use amm_core::{compute_liquidity_token_pda_seed, compute_vault_pda_seed, PoolDefinition};
|
||||
use clock_core::CLOCK_01_PROGRAM_ACCOUNT_ID;
|
||||
use nssa_core::{
|
||||
account::{AccountId, AccountWithMetadata, Data},
|
||||
program::{AccountPostState, ChainedCall, ProgramId},
|
||||
};
|
||||
use twap_oracle_core::compute_current_tick_account_pda;
|
||||
|
||||
/// Validates swap setup: checks pool liquidity is ready, vaults match, and reserves are sufficient.
|
||||
fn validate_swap_setup(
|
||||
@ -46,16 +49,17 @@ fn validate_swap_setup(
|
||||
pool_def_data
|
||||
}
|
||||
|
||||
/// Creates post-state and returns reserves after swap.
|
||||
/// Assembles the swap post-states (including the echoed current-tick and clock accounts) and the
|
||||
/// chained call that refreshes the pool's TWAP current tick from the post-swap spot price.
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "post-state assembly keeps pool, vault, user account, and delta state explicit"
|
||||
reason = "post-state assembly keeps pool, vault, user, oracle, and delta state explicit"
|
||||
)]
|
||||
#[expect(
|
||||
clippy::needless_pass_by_value,
|
||||
reason = "consistent with codebase style"
|
||||
)]
|
||||
fn create_swap_post_states(
|
||||
fn finalize_swap(
|
||||
config: AccountWithMetadata,
|
||||
pool: AccountWithMetadata,
|
||||
pool_def_data: PoolDefinition,
|
||||
@ -63,12 +67,14 @@ fn create_swap_post_states(
|
||||
vault_b: AccountWithMetadata,
|
||||
user_holding_a: AccountWithMetadata,
|
||||
user_holding_b: AccountWithMetadata,
|
||||
current_tick_account: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
deposit_a: u128,
|
||||
withdraw_a: u128,
|
||||
deposit_b: u128,
|
||||
withdraw_b: u128,
|
||||
) -> Vec<AccountPostState> {
|
||||
let mut pool_post = pool.account;
|
||||
twap_oracle_program_id: ProgramId,
|
||||
) -> (Vec<AccountPostState>, ChainedCall) {
|
||||
let pool_post_definition = PoolDefinition {
|
||||
reserve_a: pool_def_data
|
||||
.reserve_a
|
||||
@ -85,16 +91,46 @@ fn create_swap_post_states(
|
||||
..pool_def_data
|
||||
};
|
||||
|
||||
let mut pool_post = pool.account.clone();
|
||||
pool_post.data = Data::from(&pool_post_definition);
|
||||
|
||||
vec![
|
||||
// Refresh the pool's TWAP current tick from the post-swap spot price. The pool is already owned
|
||||
// by this program, so it is passed (in its post-swap state) as the authorized price source.
|
||||
let new_price = spot_price_q64_64(
|
||||
pool_post_definition.reserve_a,
|
||||
pool_post_definition.reserve_b,
|
||||
);
|
||||
let pool_price_source = AccountWithMetadata {
|
||||
account: pool_post.clone(),
|
||||
is_authorized: true,
|
||||
account_id: pool.account_id,
|
||||
};
|
||||
let update_tick_call = ChainedCall::new(
|
||||
twap_oracle_program_id,
|
||||
vec![
|
||||
current_tick_account.clone(),
|
||||
pool_price_source,
|
||||
clock.clone(),
|
||||
],
|
||||
&twap_oracle_core::Instruction::UpdateCurrentTick { price: new_price },
|
||||
)
|
||||
.with_pda_seeds(vec![compute_pool_pda_seed(
|
||||
pool_def_data.definition_token_a_id,
|
||||
pool_def_data.definition_token_b_id,
|
||||
)]);
|
||||
|
||||
let post_states = vec![
|
||||
AccountPostState::new(config.account),
|
||||
AccountPostState::new(pool_post),
|
||||
AccountPostState::new(vault_a.account),
|
||||
AccountPostState::new(vault_b.account),
|
||||
AccountPostState::new(user_holding_a.account),
|
||||
AccountPostState::new(user_holding_b.account),
|
||||
]
|
||||
AccountPostState::new(current_tick_account.account),
|
||||
AccountPostState::new(clock.account),
|
||||
];
|
||||
|
||||
(post_states, update_tick_call)
|
||||
}
|
||||
|
||||
#[expect(
|
||||
@ -109,6 +145,8 @@ pub fn swap_exact_input(
|
||||
vault_b: AccountWithMetadata,
|
||||
user_holding_a: AccountWithMetadata,
|
||||
user_holding_b: AccountWithMetadata,
|
||||
current_tick_account: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
swap_amount_in: u128,
|
||||
min_amount_out: u128,
|
||||
token_in_id: AccountId,
|
||||
@ -116,16 +154,17 @@ pub fn swap_exact_input(
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
let pool_def_data = validate_swap_setup(&pool, &vault_a, &vault_b);
|
||||
|
||||
// The Token Program is taken from the config account, not trusted from a caller-supplied
|
||||
// The program IDs are 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;
|
||||
let config_data = AmmConfig::try_from(&config.account.data)
|
||||
.expect("Swap exact input: AMM Program must be initialized before use");
|
||||
let token_program_id = config_data.token_program_id;
|
||||
let twap_oracle_program_id = config_data.twap_oracle_program_id;
|
||||
assert_eq!(
|
||||
vault_a.account.program_owner, token_program_id,
|
||||
"Vault A must be owned by the configured Token Program"
|
||||
@ -142,6 +181,17 @@ pub fn swap_exact_input(
|
||||
user_holding_b.account.program_owner, token_program_id,
|
||||
"User Token B holding must be owned by the configured Token Program"
|
||||
);
|
||||
// The current tick is refreshed by a chained call to the oracle; validate its PDA and the
|
||||
// clock here so the swap is rejected early with an AMM-level error.
|
||||
assert_eq!(
|
||||
clock.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
"Swap exact input: clock account must be the canonical 1-block LEZ clock account"
|
||||
);
|
||||
assert_eq!(
|
||||
current_tick_account.account_id,
|
||||
compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id),
|
||||
"Swap exact input: current tick Account ID does not match PDA"
|
||||
);
|
||||
|
||||
let (chained_calls, [deposit_a, withdraw_a], [deposit_b, withdraw_b]) =
|
||||
if token_in_id == pool_def_data.definition_token_a_id {
|
||||
@ -178,7 +228,7 @@ pub fn swap_exact_input(
|
||||
panic!("AccountId is not a token type for the pool");
|
||||
};
|
||||
|
||||
let post_states = create_swap_post_states(
|
||||
let (post_states, update_tick_call) = finalize_swap(
|
||||
config,
|
||||
pool,
|
||||
pool_def_data,
|
||||
@ -186,12 +236,18 @@ pub fn swap_exact_input(
|
||||
vault_b,
|
||||
user_holding_a,
|
||||
user_holding_b,
|
||||
current_tick_account,
|
||||
clock,
|
||||
deposit_a,
|
||||
withdraw_a,
|
||||
deposit_b,
|
||||
withdraw_b,
|
||||
twap_oracle_program_id,
|
||||
);
|
||||
|
||||
let mut chained_calls = chained_calls;
|
||||
chained_calls.push(update_tick_call);
|
||||
|
||||
(post_states, chained_calls)
|
||||
}
|
||||
|
||||
@ -291,6 +347,8 @@ pub fn swap_exact_output(
|
||||
vault_b: AccountWithMetadata,
|
||||
user_holding_a: AccountWithMetadata,
|
||||
user_holding_b: AccountWithMetadata,
|
||||
current_tick_account: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
exact_amount_out: u128,
|
||||
max_amount_in: u128,
|
||||
token_in_id: AccountId,
|
||||
@ -298,16 +356,17 @@ pub fn swap_exact_output(
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
let pool_def_data = validate_swap_setup(&pool, &vault_a, &vault_b);
|
||||
|
||||
// The Token Program is taken from the config account, not trusted from a caller-supplied
|
||||
// The program IDs are 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;
|
||||
let config_data = AmmConfig::try_from(&config.account.data)
|
||||
.expect("Swap exact output: AMM Program must be initialized before use");
|
||||
let token_program_id = config_data.token_program_id;
|
||||
let twap_oracle_program_id = config_data.twap_oracle_program_id;
|
||||
assert_eq!(
|
||||
vault_a.account.program_owner, token_program_id,
|
||||
"Vault A must be owned by the configured Token Program"
|
||||
@ -324,6 +383,17 @@ pub fn swap_exact_output(
|
||||
user_holding_b.account.program_owner, token_program_id,
|
||||
"User Token B holding must be owned by the configured Token Program"
|
||||
);
|
||||
// The current tick is refreshed by a chained call to the oracle; validate its PDA and the
|
||||
// clock here so the swap is rejected early with an AMM-level error.
|
||||
assert_eq!(
|
||||
clock.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
"Swap exact output: clock account must be the canonical 1-block LEZ clock account"
|
||||
);
|
||||
assert_eq!(
|
||||
current_tick_account.account_id,
|
||||
compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id),
|
||||
"Swap exact output: current tick Account ID does not match PDA"
|
||||
);
|
||||
|
||||
let (chained_calls, [deposit_a, withdraw_a], [deposit_b, withdraw_b]) =
|
||||
if token_in_id == pool_def_data.definition_token_a_id {
|
||||
@ -360,7 +430,7 @@ pub fn swap_exact_output(
|
||||
panic!("AccountId is not a token type for the pool");
|
||||
};
|
||||
|
||||
let post_states = create_swap_post_states(
|
||||
let (post_states, update_tick_call) = finalize_swap(
|
||||
config,
|
||||
pool,
|
||||
pool_def_data,
|
||||
@ -368,12 +438,18 @@ pub fn swap_exact_output(
|
||||
vault_b,
|
||||
user_holding_a,
|
||||
user_holding_b,
|
||||
current_tick_account,
|
||||
clock,
|
||||
deposit_a,
|
||||
withdraw_a,
|
||||
deposit_b,
|
||||
withdraw_b,
|
||||
twap_oracle_program_id,
|
||||
);
|
||||
|
||||
let mut chained_calls = chained_calls;
|
||||
chained_calls.push(update_tick_call);
|
||||
|
||||
(post_states, chained_calls)
|
||||
}
|
||||
|
||||
|
||||
@ -1,20 +1,38 @@
|
||||
use amm_core::{
|
||||
assert_supported_fee_tier, read_vault_fungible_balances, PoolDefinition, MINIMUM_LIQUIDITY,
|
||||
assert_supported_fee_tier, compute_config_pda, compute_pool_pda_seed,
|
||||
read_vault_fungible_balances, spot_price_q64_64, AmmConfig, PoolDefinition, MINIMUM_LIQUIDITY,
|
||||
};
|
||||
use clock_core::CLOCK_01_PROGRAM_ACCOUNT_ID;
|
||||
use nssa_core::{
|
||||
account::{AccountWithMetadata, Data},
|
||||
program::{AccountPostState, ChainedCall},
|
||||
program::{AccountPostState, ChainedCall, ProgramId},
|
||||
};
|
||||
use twap_oracle_core::compute_current_tick_account_pda;
|
||||
|
||||
pub fn sync_reserves(
|
||||
config: AccountWithMetadata,
|
||||
pool: AccountWithMetadata,
|
||||
vault_a: AccountWithMetadata,
|
||||
vault_b: AccountWithMetadata,
|
||||
current_tick_account: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
amm_program_id: ProgramId,
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
let pool_def_data = PoolDefinition::try_from(&pool.account.data)
|
||||
.expect("Sync reserves: AMM Program expects a valid Pool Definition Account");
|
||||
assert_supported_fee_tier(pool_def_data.fees);
|
||||
|
||||
// The TWAP oracle program ID is taken from the config account. Validating the config PDA is
|
||||
// also the Program's initialization gate.
|
||||
assert_eq!(
|
||||
config.account_id,
|
||||
compute_config_pda(amm_program_id),
|
||||
"Sync reserves: AMM config Account ID does not match PDA"
|
||||
);
|
||||
let twap_oracle_program_id = AmmConfig::try_from(&config.account.data)
|
||||
.expect("Sync reserves: AMM Program must be initialized before use")
|
||||
.twap_oracle_program_id;
|
||||
|
||||
assert!(
|
||||
pool_def_data.liquidity_pool_supply >= MINIMUM_LIQUIDITY,
|
||||
"Pool liquidity supply is below minimum liquidity"
|
||||
@ -27,6 +45,17 @@ pub fn sync_reserves(
|
||||
vault_b.account_id, pool_def_data.vault_b_id,
|
||||
"Vault B was not provided"
|
||||
);
|
||||
// The current tick is refreshed by a chained call to the oracle; validate its PDA and the
|
||||
// clock here so the sync is rejected early with an AMM-level error.
|
||||
assert_eq!(
|
||||
clock.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
"Sync reserves: clock account must be the canonical 1-block LEZ clock account"
|
||||
);
|
||||
assert_eq!(
|
||||
current_tick_account.account_id,
|
||||
compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id),
|
||||
"Sync reserves: current tick Account ID does not match PDA"
|
||||
);
|
||||
|
||||
let (vault_a_balance, vault_b_balance) =
|
||||
read_vault_fungible_balances("Sync reserves", &vault_a, &vault_b);
|
||||
@ -39,20 +68,45 @@ pub fn sync_reserves(
|
||||
"Sync reserves: vault B balance is less than its reserve"
|
||||
);
|
||||
|
||||
let mut pool_post = pool.account.clone();
|
||||
let pool_post_definition = PoolDefinition {
|
||||
reserve_a: vault_a_balance,
|
||||
reserve_b: vault_b_balance,
|
||||
..pool_def_data
|
||||
};
|
||||
let mut pool_post = pool.account.clone();
|
||||
pool_post.data = Data::from(&pool_post_definition);
|
||||
|
||||
// Refresh the pool's TWAP current tick from the synced spot price. The pool is already owned by
|
||||
// this program, so it is passed (in its synced state) as the authorized price source.
|
||||
let new_price = spot_price_q64_64(vault_a_balance, vault_b_balance);
|
||||
let pool_price_source = AccountWithMetadata {
|
||||
account: pool_post.clone(),
|
||||
is_authorized: true,
|
||||
account_id: pool.account_id,
|
||||
};
|
||||
let update_tick_call = ChainedCall::new(
|
||||
twap_oracle_program_id,
|
||||
vec![
|
||||
current_tick_account.clone(),
|
||||
pool_price_source,
|
||||
clock.clone(),
|
||||
],
|
||||
&twap_oracle_core::Instruction::UpdateCurrentTick { price: new_price },
|
||||
)
|
||||
.with_pda_seeds(vec![compute_pool_pda_seed(
|
||||
pool_def_data.definition_token_a_id,
|
||||
pool_def_data.definition_token_b_id,
|
||||
)]);
|
||||
|
||||
(
|
||||
vec![
|
||||
AccountPostState::new(config.account.clone()),
|
||||
AccountPostState::new(pool_post),
|
||||
AccountPostState::new(vault_a.account.clone()),
|
||||
AccountPostState::new(vault_b.account.clone()),
|
||||
AccountPostState::new(current_tick_account.account.clone()),
|
||||
AccountPostState::new(clock.account.clone()),
|
||||
],
|
||||
Vec::new(),
|
||||
vec![update_tick_call],
|
||||
)
|
||||
}
|
||||
|
||||
@ -1520,6 +1520,8 @@ fn test_call_add_liquidity_vault_a_omitted() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::add_max_amount_b(),
|
||||
@ -1539,6 +1541,8 @@ fn test_call_add_liquidity_vault_b_omitted() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::add_max_amount_b(),
|
||||
@ -1558,6 +1562,8 @@ fn test_call_add_liquidity_lp_definition_mismatch() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::add_max_amount_b(),
|
||||
@ -1577,6 +1583,8 @@ fn test_call_add_liquidity_zero_balance_1() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
|
||||
0,
|
||||
BalanceForTests::add_max_amount_b(),
|
||||
@ -1596,6 +1604,8 @@ fn test_call_add_liquidity_zero_balance_2() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
|
||||
0,
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
@ -1615,6 +1625,8 @@ fn test_call_add_liquidity_vault_a_balance_below_reserve() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::add_max_amount_b(),
|
||||
@ -1634,6 +1646,8 @@ fn test_call_add_liquidity_vault_b_balance_below_reserve() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::add_max_amount_b(),
|
||||
@ -1653,6 +1667,8 @@ fn test_call_add_liquidity_vault_insufficient_balance_1() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::add_max_amount_b(),
|
||||
@ -1672,6 +1688,8 @@ fn test_call_add_liquidity_vault_insufficient_balance_2() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::add_max_amount_b(),
|
||||
@ -1691,6 +1709,8 @@ fn test_call_add_liquidity_actual_amount_zero_1() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::add_max_amount_b(),
|
||||
@ -1710,6 +1730,8 @@ fn test_call_add_liquidity_actual_amount_zero_2() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
|
||||
BalanceForTests::add_max_amount_a_low(),
|
||||
BalanceForTests::add_max_amount_b_low(),
|
||||
@ -1729,6 +1751,8 @@ fn test_call_add_liquidity_reserves_zero_1() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::add_max_amount_b(),
|
||||
@ -1748,6 +1772,8 @@ fn test_call_add_liquidity_reserves_zero_2() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::add_max_amount_b(),
|
||||
@ -1767,6 +1793,8 @@ fn test_call_add_liquidity_payable_lp_zero() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
|
||||
BalanceForTests::add_max_amount_a_low(),
|
||||
BalanceForTests::add_max_amount_b_low(),
|
||||
@ -1785,6 +1813,8 @@ fn test_call_add_liquidity_chained_call_successsful() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::add_max_amount_b(),
|
||||
@ -1806,6 +1836,10 @@ fn test_call_add_liquidity_chained_call_successsful() {
|
||||
assert!(chained_call_b == ChainedCallForTests::cc_add_token_b());
|
||||
assert!(chained_call_lp == ChainedCallForTests::cc_add_pool_lp());
|
||||
|
||||
// The fourth chained call refreshes the pool's TWAP current tick from the post-add price.
|
||||
assert_eq!(chained_calls.len(), 4);
|
||||
assert_update_tick_call(&chained_calls, post_states[1].account());
|
||||
|
||||
// The config account is echoed back unchanged as the first post-state.
|
||||
assert_eq!(
|
||||
*post_states[0].account(),
|
||||
@ -1825,6 +1859,8 @@ fn test_call_add_liquidity_uninitialized_config_panics() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::add_max_amount_b(),
|
||||
@ -1844,6 +1880,8 @@ fn test_call_add_liquidity_wrong_config_pda_panics() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::add_min_amount_lp()).unwrap(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::add_max_amount_b(),
|
||||
@ -1863,6 +1901,8 @@ fn test_call_remove_liquidity_vault_a_omitted() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(),
|
||||
BalanceForTests::remove_min_amount_a(),
|
||||
BalanceForTests::remove_min_amount_b(),
|
||||
@ -1882,6 +1922,8 @@ fn test_call_remove_liquidity_vault_b_omitted() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(),
|
||||
BalanceForTests::remove_min_amount_a(),
|
||||
BalanceForTests::remove_min_amount_b(),
|
||||
@ -1901,6 +1943,8 @@ fn test_call_remove_liquidity_lp_def_mismatch() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(),
|
||||
BalanceForTests::remove_min_amount_a(),
|
||||
BalanceForTests::remove_min_amount_b(),
|
||||
@ -1919,9 +1963,11 @@ fn test_call_remove_liquidity_insufficient_liquidity_amount() {
|
||||
AccountWithMetadataForTests::pool_lp_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_a(), /* different token account than lp to
|
||||
* create desired
|
||||
* error */
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(), /* 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(),
|
||||
@ -1943,6 +1989,8 @@ fn test_call_remove_liquidity_insufficient_balance_1() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::remove_amount_lp_1()).unwrap(),
|
||||
BalanceForTests::remove_min_amount_a(),
|
||||
BalanceForTests::remove_min_amount_b(),
|
||||
@ -1963,6 +2011,8 @@ fn test_call_remove_liquidity_pool_at_minimum_liquidity() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_with_balance(MINIMUM_LIQUIDITY),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(MINIMUM_LIQUIDITY).unwrap(),
|
||||
1,
|
||||
1,
|
||||
@ -1985,6 +2035,8 @@ fn test_call_remove_liquidity_exceeds_unlocked_supply() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_with_balance(BalanceForTests::lp_supply_init()),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::lp_supply_init()).unwrap(),
|
||||
1,
|
||||
1,
|
||||
@ -2006,6 +2058,8 @@ fn test_call_remove_liquidity_insufficient_balance_2() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(),
|
||||
BalanceForTests::remove_min_amount_a(),
|
||||
BalanceForTests::remove_min_amount_b(),
|
||||
@ -2025,6 +2079,8 @@ fn test_call_remove_liquidity_min_bal_zero_1() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(),
|
||||
0,
|
||||
BalanceForTests::remove_min_amount_b(),
|
||||
@ -2044,6 +2100,8 @@ fn test_call_remove_liquidity_min_bal_zero_2() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(),
|
||||
BalanceForTests::remove_min_amount_a(),
|
||||
0,
|
||||
@ -2062,6 +2120,8 @@ fn test_call_remove_liquidity_chained_call_successful() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::remove_amount_lp()).unwrap(),
|
||||
BalanceForTests::remove_min_amount_a(),
|
||||
BalanceForTests::remove_min_amount_b_low(),
|
||||
@ -2082,6 +2142,10 @@ fn test_call_remove_liquidity_chained_call_successful() {
|
||||
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());
|
||||
|
||||
// The fourth chained call refreshes the pool's TWAP current tick from the post-removal price.
|
||||
assert_eq!(chained_calls.len(), 4);
|
||||
assert_update_tick_call(&chained_calls, post_states[1].account());
|
||||
}
|
||||
|
||||
#[should_panic(expected = "Balances must be nonzero")]
|
||||
@ -2366,6 +2430,8 @@ fn test_call_swap_incorrect_token_type() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::min_amount_out(),
|
||||
IdForTests::token_lp_definition_id(),
|
||||
@ -2383,6 +2449,8 @@ fn test_call_swap_vault_a_omitted() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::min_amount_out(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -2400,6 +2468,8 @@ fn test_call_swap_vault_b_omitted() {
|
||||
AccountWithMetadataForTests::vault_b_with_wrong_id(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::min_amount_out(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -2417,6 +2487,8 @@ fn test_call_swap_reserves_vault_mismatch_1() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::min_amount_out(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -2434,6 +2506,8 @@ fn test_call_swap_reserves_vault_mismatch_2() {
|
||||
AccountWithMetadataForTests::vault_b_init_low(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::min_amount_out(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -2451,6 +2525,8 @@ fn test_call_swap_below_minimum_liquidity() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::min_amount_out(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -2473,6 +2549,8 @@ fn test_call_swap_rejects_unsupported_fee_tier() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::add_max_amount_a_low(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -2490,6 +2568,8 @@ fn test_call_swap_below_min_out() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::min_amount_out_too_high(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -2507,6 +2587,8 @@ fn test_call_swap_effective_amount_zero() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
1,
|
||||
0,
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -2524,6 +2606,8 @@ fn test_call_swap_output_rounds_to_zero() {
|
||||
AccountWithMetadataForTests::vault_b_init_low(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
2,
|
||||
0,
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -2541,6 +2625,8 @@ fn test_call_swap_exact_input_rejects_amount_that_rounds_down_below_target_outpu
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
2,
|
||||
1,
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -2557,6 +2643,8 @@ fn test_call_swap_exact_input_accepts_smallest_amount_for_rounded_boundary() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
3,
|
||||
1,
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -2583,6 +2671,39 @@ fn test_call_swap_exact_input_accepts_smallest_amount_for_rounded_boundary() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Asserts the last chained call is the oracle UpdateCurrentTick, carrying the post-operation
|
||||
/// spot price and the post-operation pool authorized as the price source.
|
||||
fn assert_update_tick_call(chained_calls: &[ChainedCall], pool_post_account: &Account) {
|
||||
let pool_def = PoolDefinition::try_from(&pool_post_account.data)
|
||||
.expect("pool post-state must hold a valid PoolDefinition");
|
||||
let expected_price_source = AccountWithMetadata {
|
||||
account: pool_post_account.clone(),
|
||||
is_authorized: true,
|
||||
account_id: IdForTests::pool_definition_id(),
|
||||
};
|
||||
let expected = ChainedCall::new(
|
||||
TWAP_ORACLE_PROGRAM_ID,
|
||||
vec![
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
expected_price_source,
|
||||
AccountWithMetadataForTests::clock(),
|
||||
],
|
||||
&twap_oracle_core::Instruction::UpdateCurrentTick {
|
||||
price: amm_core::spot_price_q64_64(pool_def.reserve_a, pool_def.reserve_b),
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![compute_pool_pda_seed(
|
||||
IdForTests::token_a_definition_id(),
|
||||
IdForTests::token_b_definition_id(),
|
||||
)]);
|
||||
assert_eq!(
|
||||
*chained_calls
|
||||
.last()
|
||||
.expect("expected an UpdateCurrentTick chained call"),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_call_swap_chained_call_successful_1() {
|
||||
let (post_states, chained_calls) = swap_exact_input(
|
||||
@ -2592,6 +2713,8 @@ fn test_call_swap_chained_call_successful_1() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::add_max_amount_a_low(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -2615,6 +2738,9 @@ fn test_call_swap_chained_call_successful_1() {
|
||||
chained_call_b,
|
||||
ChainedCallForTests::cc_swap_token_b_test_1()
|
||||
);
|
||||
|
||||
assert_eq!(chained_calls.len(), 3);
|
||||
assert_update_tick_call(&chained_calls, pool_post.account());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -2626,6 +2752,8 @@ fn test_call_swap_chained_call_successful_2() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
BalanceForTests::add_max_amount_b(),
|
||||
BalanceForTests::min_amount_out(),
|
||||
IdForTests::token_b_definition_id(),
|
||||
@ -2649,6 +2777,9 @@ fn test_call_swap_chained_call_successful_2() {
|
||||
chained_call_b,
|
||||
ChainedCallForTests::cc_swap_token_b_test_2()
|
||||
);
|
||||
|
||||
assert_eq!(chained_calls.len(), 3);
|
||||
assert_update_tick_call(&chained_calls, pool_post.account());
|
||||
}
|
||||
|
||||
#[should_panic(expected = "AccountId is not a token type for the pool")]
|
||||
@ -2661,6 +2792,8 @@ fn call_swap_exact_output_incorrect_token_type() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::max_amount_in(),
|
||||
IdForTests::token_lp_definition_id(),
|
||||
@ -2678,6 +2811,8 @@ fn call_swap_exact_output_vault_a_omitted() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::max_amount_in(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -2695,6 +2830,8 @@ fn call_swap_exact_output_vault_b_omitted() {
|
||||
AccountWithMetadataForTests::vault_b_with_wrong_id(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::max_amount_in(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -2712,6 +2849,8 @@ fn call_swap_exact_output_reserves_vault_mismatch_1() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::max_amount_in(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -2729,6 +2868,8 @@ fn call_swap_exact_output_reserves_vault_mismatch_2() {
|
||||
AccountWithMetadataForTests::vault_b_init_low(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::max_amount_in(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -2746,6 +2887,8 @@ fn call_swap_exact_output_below_minimum_liquidity() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::max_amount_in(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -2763,6 +2906,8 @@ fn call_swap_exact_output_exceeds_max_in() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
166_u128,
|
||||
100_u128,
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -2780,6 +2925,8 @@ fn call_swap_exact_output_zero() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
0_u128,
|
||||
500_u128,
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -2797,6 +2944,8 @@ fn call_swap_exact_output_exceeds_reserve() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
BalanceForTests::vault_b_reserve_init(),
|
||||
BalanceForTests::max_amount_in(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -2813,6 +2962,8 @@ fn call_swap_exact_output_chained_call_successful() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
BalanceForTests::max_amount_in(),
|
||||
BalanceForTests::vault_b_reserve_init(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -2848,6 +2999,8 @@ fn call_swap_exact_output_chained_call_successful_2() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
285,
|
||||
300,
|
||||
IdForTests::token_b_definition_id(),
|
||||
@ -2886,6 +3039,8 @@ fn call_swap_exact_output_fee_enforced() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
166_u128, // exact_amount_out: token_b
|
||||
499_u128, // max_amount_in: still one short after fee rounding
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -2906,6 +3061,8 @@ fn call_swap_exact_output_rejects_max_in_that_rounds_down_below_target_output()
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
1,
|
||||
2,
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -2922,6 +3079,8 @@ fn call_swap_exact_output_accepts_smallest_max_in_for_rounded_boundary() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
1,
|
||||
3,
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -3015,6 +3174,8 @@ fn swap_exact_output_overflow_protection() {
|
||||
vault_b,
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
2, // exact_amount_out: small, valid (< reserve_b)
|
||||
1, // max_amount_in: tiny — real deposit would be enormous, but
|
||||
// overflow wraps it to 0, making 0 <= 1 pass silently
|
||||
@ -3205,6 +3366,8 @@ fn test_minimum_liquidity_lock_and_remove_all_user_lp() {
|
||||
AccountForTests::user_holding_a(),
|
||||
AccountForTests::user_holding_b(),
|
||||
AccountForTests::user_holding_lp_with_balance(user_lp),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(user_lp).unwrap(),
|
||||
1,
|
||||
1,
|
||||
@ -3233,27 +3396,38 @@ fn test_sync_reserves_with_donation() {
|
||||
assert_eq!(pool_pre.reserve_a, BalanceForTests::vault_a_reserve_init());
|
||||
|
||||
let (post_states, chained_calls) = sync_reserves(
|
||||
AccountWithMetadataForTests::config_init(),
|
||||
pool,
|
||||
donated_vault_a,
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
AMM_PROGRAM_ID,
|
||||
);
|
||||
assert!(chained_calls.is_empty());
|
||||
|
||||
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.reserve_a,
|
||||
BalanceForTests::vault_a_reserve_init() + donation_a
|
||||
);
|
||||
assert_eq!(pool_post.reserve_b, BalanceForTests::vault_b_reserve_init());
|
||||
|
||||
// Sync refreshes the pool's TWAP current tick via a chained call carrying the synced spot
|
||||
// price, with the synced pool authorized as the price source.
|
||||
assert_eq!(chained_calls.len(), 1);
|
||||
assert_update_tick_call(&chained_calls, post_states[1].account());
|
||||
}
|
||||
|
||||
#[should_panic(expected = "Sync reserves: vault A balance is less than its reserve")]
|
||||
#[test]
|
||||
fn test_sync_reserves_panics_when_vault_a_under_collateralized() {
|
||||
let _ = sync_reserves(
|
||||
AccountWithMetadataForTests::config_init(),
|
||||
AccountWithMetadataForTests::pool_definition_init(),
|
||||
AccountWithMetadataForTests::vault_a_init_low(),
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
AMM_PROGRAM_ID,
|
||||
);
|
||||
}
|
||||
|
||||
@ -3261,9 +3435,13 @@ fn test_sync_reserves_panics_when_vault_a_under_collateralized() {
|
||||
#[test]
|
||||
fn test_sync_reserves_panics_when_vault_b_under_collateralized() {
|
||||
let _ = sync_reserves(
|
||||
AccountWithMetadataForTests::config_init(),
|
||||
AccountWithMetadataForTests::pool_definition_init(),
|
||||
AccountWithMetadataForTests::vault_a_init(),
|
||||
AccountWithMetadataForTests::vault_b_init_low(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
AMM_PROGRAM_ID,
|
||||
);
|
||||
}
|
||||
|
||||
@ -3271,9 +3449,13 @@ fn test_sync_reserves_panics_when_vault_b_under_collateralized() {
|
||||
#[test]
|
||||
fn test_sync_reserves_rejects_pool_below_minimum_liquidity() {
|
||||
let _ = sync_reserves(
|
||||
AccountWithMetadataForTests::config_init(),
|
||||
AccountWithMetadataForTests::pool_definition_below_minimum_liquidity(),
|
||||
AccountWithMetadataForTests::vault_a_init(),
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
AMM_PROGRAM_ID,
|
||||
);
|
||||
}
|
||||
|
||||
@ -3286,9 +3468,13 @@ fn test_sync_reserves_rejects_unsupported_fee_tier() {
|
||||
pool.account.data = Data::from(&pool_def);
|
||||
|
||||
let _ = sync_reserves(
|
||||
AccountWithMetadataForTests::config_init(),
|
||||
pool,
|
||||
AccountWithMetadataForTests::vault_a_init(),
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
AMM_PROGRAM_ID,
|
||||
);
|
||||
}
|
||||
|
||||
@ -3312,6 +3498,8 @@ fn test_donation_then_add_liquidity_sync_mitigates_mispricing() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(1).unwrap(),
|
||||
100,
|
||||
50,
|
||||
@ -3325,12 +3513,16 @@ fn test_donation_then_add_liquidity_sync_mitigates_mispricing() {
|
||||
let donated_vault_b_for_synced_add = donated_vault_b.clone();
|
||||
|
||||
let (sync_post, _) = sync_reserves(
|
||||
AccountWithMetadataForTests::config_init(),
|
||||
AccountWithMetadataForTests::pool_definition_init(),
|
||||
donated_vault_a,
|
||||
donated_vault_b,
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
AMM_PROGRAM_ID,
|
||||
);
|
||||
let synced_pool = AccountWithMetadata {
|
||||
account: sync_post[0].account().clone(),
|
||||
account: sync_post[1].account().clone(),
|
||||
is_authorized: true,
|
||||
account_id: IdForTests::pool_definition_id(),
|
||||
};
|
||||
@ -3344,6 +3536,8 @@ fn test_donation_then_add_liquidity_sync_mitigates_mispricing() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(1).unwrap(),
|
||||
100,
|
||||
50,
|
||||
@ -3351,7 +3545,7 @@ fn test_donation_then_add_liquidity_sync_mitigates_mispricing() {
|
||||
);
|
||||
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)
|
||||
- PoolDefinition::try_from(&sync_post[1].account().data)
|
||||
.unwrap()
|
||||
.liquidity_pool_supply;
|
||||
|
||||
@ -3446,6 +3640,8 @@ fn add_liquidity_overflow_protection() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(1).unwrap(),
|
||||
500,
|
||||
2, // max_amount_b=2 → reserve_a * 2 overflows
|
||||
@ -3532,6 +3728,8 @@ fn remove_liquidity_overflow_protection() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
user_lp,
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(2).unwrap(), /* remove_amount=2 → reserve_a * 2
|
||||
* overflows */
|
||||
1,
|
||||
@ -3605,6 +3803,8 @@ fn swap_exact_input_overflow_protection() {
|
||||
vault_b,
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
3,
|
||||
1,
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -3680,6 +3880,8 @@ fn test_add_liquidity_rejects_user_holding_a_wrong_program() {
|
||||
AccountWithMetadataForTests::user_holding_a_wrong_program(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::add_min_amount_lp()).expect("test value must be nonzero"),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::add_max_amount_b(),
|
||||
@ -3699,6 +3901,8 @@ fn test_add_liquidity_rejects_user_holding_b_wrong_program() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b_wrong_program(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::add_min_amount_lp()).expect("test value must be nonzero"),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::add_max_amount_b(),
|
||||
@ -3720,6 +3924,8 @@ fn test_remove_liquidity_rejects_user_holding_a_wrong_program() {
|
||||
AccountWithMetadataForTests::user_holding_lp_with_balance(
|
||||
BalanceForTests::remove_amount_lp(),
|
||||
),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::remove_amount_lp()).expect("test value must be nonzero"),
|
||||
BalanceForTests::remove_min_amount_a(),
|
||||
BalanceForTests::remove_min_amount_b_low(),
|
||||
@ -3741,6 +3947,8 @@ fn test_remove_liquidity_rejects_user_holding_b_wrong_program() {
|
||||
AccountWithMetadataForTests::user_holding_lp_with_balance(
|
||||
BalanceForTests::remove_amount_lp(),
|
||||
),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::remove_amount_lp()).expect("test value must be nonzero"),
|
||||
BalanceForTests::remove_min_amount_a(),
|
||||
BalanceForTests::remove_min_amount_b_low(),
|
||||
@ -3761,6 +3969,8 @@ fn test_remove_liquidity_rejects_amount_exceeding_user_lp_balance() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_with_balance(lp_balance),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
NonZero::new(BalanceForTests::remove_amount_lp()).expect("test value must be nonzero"),
|
||||
BalanceForTests::remove_min_amount_a(),
|
||||
BalanceForTests::remove_min_amount_b_low(),
|
||||
@ -3778,6 +3988,8 @@ fn test_swap_exact_input_rejects_user_holding_a_wrong_program() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a_wrong_program(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::min_amount_out(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -3795,6 +4007,8 @@ fn test_swap_exact_input_rejects_user_holding_b_wrong_program() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b_wrong_program(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::min_amount_out(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -3812,6 +4026,8 @@ fn test_swap_exact_output_rejects_user_holding_a_wrong_program() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a_wrong_program(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
166,
|
||||
BalanceForTests::max_amount_in(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
@ -3829,6 +4045,8 @@ fn test_swap_exact_output_rejects_user_holding_b_wrong_program() {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b_wrong_program(),
|
||||
AccountWithMetadataForTests::current_tick_account_uninit(),
|
||||
AccountWithMetadataForTests::clock(),
|
||||
166,
|
||||
BalanceForTests::max_amount_in(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
|
||||
@ -328,6 +328,20 @@ impl Accounts {
|
||||
}
|
||||
}
|
||||
|
||||
/// The pool's TWAP current-tick account, owned by the oracle program, holding `tick`. Seeded
|
||||
/// into state so swap/sync can refresh it via a chained UpdateCurrentTick call.
|
||||
fn current_tick_account(tick: i32) -> Account {
|
||||
Account {
|
||||
program_owner: Ids::twap_oracle_program(),
|
||||
balance: 0_u128,
|
||||
data: Data::from(&twap_oracle_core::CurrentTickAccount {
|
||||
tick,
|
||||
last_updated: 0,
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn user_a_holding() -> Account {
|
||||
Account {
|
||||
program_owner: Ids::token_program(),
|
||||
@ -969,6 +983,16 @@ fn state_for_amm_tests() -> V03State {
|
||||
deploy_programs(&mut state);
|
||||
state.force_insert_account(Ids::config(), Accounts::config());
|
||||
state.force_insert_account(Ids::pool_definition(), Accounts::pool_definition_init());
|
||||
// Seed the pool's current-tick account so swaps and syncs can refresh it. Its initial value is
|
||||
// the tick of the opening reserves; swap/sync tests assert it is updated to the new price.
|
||||
let initial_tick = twap_oracle_core::price_to_tick(amm_core::spot_price_q64_64(
|
||||
Balances::vault_a_init(),
|
||||
Balances::vault_b_init(),
|
||||
));
|
||||
state.force_insert_account(
|
||||
Ids::current_tick_account(),
|
||||
Accounts::current_tick_account(initial_tick),
|
||||
);
|
||||
state.force_insert_account(
|
||||
Ids::token_a_definition(),
|
||||
Accounts::token_a_definition_account(),
|
||||
@ -1096,6 +1120,8 @@ fn execute_swap_a_to_b(state: &mut V03State, swap_amount_in: u128, min_amount_ou
|
||||
Ids::vault_b(),
|
||||
Ids::user_a(),
|
||||
Ids::user_b(),
|
||||
Ids::current_tick_account(),
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
],
|
||||
vec![current_nonce(state, Ids::user_a())],
|
||||
instruction,
|
||||
@ -1126,6 +1152,8 @@ fn execute_swap_b_to_a(state: &mut V03State, swap_amount_in: u128, min_amount_ou
|
||||
Ids::vault_b(),
|
||||
Ids::user_a(),
|
||||
Ids::user_b(),
|
||||
Ids::current_tick_account(),
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
],
|
||||
vec![current_nonce(state, Ids::user_b())],
|
||||
instruction,
|
||||
@ -1163,6 +1191,8 @@ fn execute_add_liquidity(
|
||||
Ids::user_a(),
|
||||
Ids::user_b(),
|
||||
Ids::user_lp(),
|
||||
Ids::current_tick_account(),
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
],
|
||||
vec![
|
||||
current_nonce(state, Ids::user_a()),
|
||||
@ -1204,6 +1234,8 @@ fn execute_remove_liquidity(
|
||||
Ids::user_a(),
|
||||
Ids::user_b(),
|
||||
Ids::user_lp(),
|
||||
Ids::current_tick_account(),
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
],
|
||||
vec![current_nonce(state, Ids::user_lp())],
|
||||
instruction,
|
||||
@ -1264,6 +1296,28 @@ fn execute_create_price_observations(
|
||||
state.transition_from_public_transaction(&tx, 0, 0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn execute_sync_reserves(state: &mut V03State) {
|
||||
let message = public_transaction::Message::try_new(
|
||||
Ids::amm_program(),
|
||||
vec![
|
||||
Ids::config(),
|
||||
Ids::pool_definition(),
|
||||
Ids::vault_a(),
|
||||
Ids::vault_b(),
|
||||
Ids::current_tick_account(),
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
],
|
||||
vec![],
|
||||
amm_core::Instruction::SyncReserves,
|
||||
)
|
||||
.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();
|
||||
}
|
||||
|
||||
/// Builds a state whose pool was created through `new_definition`, which also creates the pool's
|
||||
/// TWAP current-tick account (seeded from the opening reserves). Used by the observation tests so
|
||||
/// they consume the real current-tick account rather than a hand-inserted one.
|
||||
@ -1500,7 +1554,10 @@ fn amm_create_price_observations_without_current_tick_account_fails() {
|
||||
let mut state = state_for_amm_tests();
|
||||
let window_duration = 24 * 60 * 60 * 1_000u64;
|
||||
|
||||
// No current-tick account was created, so there is no authoritative tick to seed from.
|
||||
// Remove the seeded current-tick account: with no authoritative tick to seed from, creation
|
||||
// must be rejected.
|
||||
state.force_insert_account(Ids::current_tick_account(), Account::default());
|
||||
|
||||
let result = execute_create_price_observations(&mut state, window_duration);
|
||||
assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_))));
|
||||
assert_eq!(
|
||||
@ -1531,6 +1588,8 @@ fn amm_remove_liquidity() {
|
||||
Ids::user_a(),
|
||||
Ids::user_b(),
|
||||
Ids::user_lp(),
|
||||
Ids::current_tick_account(),
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
],
|
||||
vec![Nonce(0)],
|
||||
instruction,
|
||||
@ -1570,6 +1629,18 @@ fn amm_remove_liquidity() {
|
||||
state.get_account_by_id(Ids::user_lp()),
|
||||
Accounts::user_lp_holding_remove()
|
||||
);
|
||||
|
||||
// Removing liquidity also refreshes the pool's TWAP current tick to the post-removal spot
|
||||
// price.
|
||||
let tick_account = twap_oracle_core::CurrentTickAccount::try_from(
|
||||
&state.get_account_by_id(Ids::current_tick_account()).data,
|
||||
)
|
||||
.expect("current tick account must hold a valid CurrentTickAccount");
|
||||
let expected_tick = twap_oracle_core::price_to_tick(amm_core::spot_price_q64_64(
|
||||
Balances::vault_a_remove(),
|
||||
Balances::vault_b_remove(),
|
||||
));
|
||||
assert_eq!(tick_account.tick, expected_tick);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1595,6 +1666,8 @@ fn amm_remove_liquidity_insufficient_user_lp_fails() {
|
||||
Ids::user_a(),
|
||||
Ids::user_b(),
|
||||
Ids::user_lp(),
|
||||
Ids::current_tick_account(),
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
],
|
||||
vec![Nonce(0)],
|
||||
instruction,
|
||||
@ -1834,6 +1907,8 @@ fn amm_add_liquidity() {
|
||||
Ids::user_a(),
|
||||
Ids::user_b(),
|
||||
Ids::user_lp(),
|
||||
Ids::current_tick_account(),
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
],
|
||||
vec![Nonce(0), Nonce(0)],
|
||||
instruction,
|
||||
@ -1874,6 +1949,17 @@ fn amm_add_liquidity() {
|
||||
state.get_account_by_id(Ids::user_lp()),
|
||||
Accounts::user_lp_holding_add()
|
||||
);
|
||||
|
||||
// Adding liquidity also refreshes the pool's TWAP current tick to the post-add spot price.
|
||||
let tick_account = twap_oracle_core::CurrentTickAccount::try_from(
|
||||
&state.get_account_by_id(Ids::current_tick_account()).data,
|
||||
)
|
||||
.expect("current tick account must hold a valid CurrentTickAccount");
|
||||
let expected_tick = twap_oracle_core::price_to_tick(amm_core::spot_price_q64_64(
|
||||
Balances::vault_a_add(),
|
||||
Balances::vault_b_add(),
|
||||
));
|
||||
assert_eq!(tick_account.tick, expected_tick);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1896,6 +1982,8 @@ fn amm_swap_b_to_a() {
|
||||
Ids::vault_b(),
|
||||
Ids::user_a(),
|
||||
Ids::user_b(),
|
||||
Ids::current_tick_account(),
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
],
|
||||
vec![Nonce(0)],
|
||||
instruction,
|
||||
@ -1949,6 +2037,8 @@ fn amm_swap_a_to_b() {
|
||||
Ids::vault_b(),
|
||||
Ids::user_a(),
|
||||
Ids::user_b(),
|
||||
Ids::current_tick_account(),
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
],
|
||||
vec![Nonce(0)],
|
||||
instruction,
|
||||
@ -1980,6 +2070,105 @@ fn amm_swap_a_to_b() {
|
||||
state.get_account_by_id(Ids::user_b()),
|
||||
Accounts::user_b_holding_swap_2()
|
||||
);
|
||||
|
||||
// The swap refreshed the pool's TWAP current tick to the post-swap spot price.
|
||||
let tick_account = twap_oracle_core::CurrentTickAccount::try_from(
|
||||
&state.get_account_by_id(Ids::current_tick_account()).data,
|
||||
)
|
||||
.expect("current tick account must hold a valid CurrentTickAccount");
|
||||
let expected_tick = twap_oracle_core::price_to_tick(amm_core::spot_price_q64_64(
|
||||
Balances::reserve_a_swap_2(),
|
||||
Balances::reserve_b_swap_2(),
|
||||
));
|
||||
assert_eq!(tick_account.tick, expected_tick);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn amm_swap_exact_output_refreshes_current_tick() {
|
||||
let mut state = state_for_amm_tests();
|
||||
|
||||
let initial_tick = twap_oracle_core::CurrentTickAccount::try_from(
|
||||
&state.get_account_by_id(Ids::current_tick_account()).data,
|
||||
)
|
||||
.expect("current tick account must hold a valid CurrentTickAccount")
|
||||
.tick;
|
||||
|
||||
let instruction = amm_core::Instruction::SwapExactOutput {
|
||||
exact_amount_out: Balances::swap_min_out(),
|
||||
max_amount_in: Balances::swap_amount_in(),
|
||||
token_definition_id_in: Ids::token_a_definition(),
|
||||
deadline: u64::MAX,
|
||||
};
|
||||
|
||||
let message = public_transaction::Message::try_new(
|
||||
Ids::amm_program(),
|
||||
vec![
|
||||
Ids::config(),
|
||||
Ids::pool_definition(),
|
||||
Ids::vault_a(),
|
||||
Ids::vault_b(),
|
||||
Ids::user_a(),
|
||||
Ids::user_b(),
|
||||
Ids::current_tick_account(),
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
],
|
||||
vec![Nonce(0)],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a()]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
||||
|
||||
// The swap refreshed the pool's TWAP current tick to the post-swap spot price, computed from
|
||||
// the reserves the swap actually settled on.
|
||||
let pool = pool_definition(&state.get_account_by_id(Ids::pool_definition()));
|
||||
let tick_account = twap_oracle_core::CurrentTickAccount::try_from(
|
||||
&state.get_account_by_id(Ids::current_tick_account()).data,
|
||||
)
|
||||
.expect("current tick account must hold a valid CurrentTickAccount");
|
||||
let expected_tick = twap_oracle_core::price_to_tick(amm_core::spot_price_q64_64(
|
||||
pool.reserve_a,
|
||||
pool.reserve_b,
|
||||
));
|
||||
assert_eq!(tick_account.tick, expected_tick);
|
||||
assert_ne!(
|
||||
tick_account.tick, initial_tick,
|
||||
"swap should have moved the current tick"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn amm_sync_reserves_updates_pool_and_current_tick() {
|
||||
let mut state = state_for_amm_tests();
|
||||
|
||||
// Donate token A straight into vault A, so the vault balance exceeds the recorded reserve.
|
||||
let donation = 1_000u128;
|
||||
let mut donated_vault_a = Accounts::vault_a_init();
|
||||
donated_vault_a.data = Data::from(&TokenHolding::Fungible {
|
||||
definition_id: Ids::token_a_definition(),
|
||||
balance: Balances::vault_a_init() + donation,
|
||||
});
|
||||
state.force_insert_account(Ids::vault_a(), donated_vault_a);
|
||||
|
||||
execute_sync_reserves(&mut state);
|
||||
|
||||
// Sync reconciles the pool reserves with the actual vault balances.
|
||||
let pool = pool_definition(&state.get_account_by_id(Ids::pool_definition()));
|
||||
assert_eq!(pool.reserve_a, Balances::vault_a_init() + donation);
|
||||
assert_eq!(pool.reserve_b, Balances::vault_b_init());
|
||||
|
||||
// And refreshes the current tick to the synced spot price.
|
||||
let tick_account = twap_oracle_core::CurrentTickAccount::try_from(
|
||||
&state.get_account_by_id(Ids::current_tick_account()).data,
|
||||
)
|
||||
.expect("current tick account must hold a valid CurrentTickAccount");
|
||||
let expected_tick = twap_oracle_core::price_to_tick(amm_core::spot_price_q64_64(
|
||||
Balances::vault_a_init() + donation,
|
||||
Balances::vault_b_init(),
|
||||
));
|
||||
assert_eq!(tick_account.tick, expected_tick);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -2056,6 +2245,8 @@ fn amm_swap_rejects_expired_deadline() {
|
||||
Ids::vault_b(),
|
||||
Ids::user_a(),
|
||||
Ids::user_b(),
|
||||
Ids::current_tick_account(),
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
],
|
||||
vec![Nonce(0)],
|
||||
instruction,
|
||||
@ -2093,6 +2284,8 @@ fn amm_swap_exact_output_rejects_expired_deadline() {
|
||||
Ids::vault_b(),
|
||||
Ids::user_a(),
|
||||
Ids::user_b(),
|
||||
Ids::current_tick_account(),
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
],
|
||||
vec![current_nonce(&state, Ids::user_a())],
|
||||
instruction,
|
||||
@ -2132,6 +2325,8 @@ fn amm_add_liquidity_rejects_expired_deadline() {
|
||||
Ids::user_a(),
|
||||
Ids::user_b(),
|
||||
Ids::user_lp(),
|
||||
Ids::current_tick_account(),
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
],
|
||||
vec![
|
||||
current_nonce(&state, Ids::user_a()),
|
||||
@ -2175,6 +2370,8 @@ fn amm_remove_liquidity_rejects_expired_deadline() {
|
||||
Ids::user_a(),
|
||||
Ids::user_b(),
|
||||
Ids::user_lp(),
|
||||
Ids::current_tick_account(),
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
],
|
||||
vec![current_nonce(&state, Ids::user_lp())],
|
||||
instruction,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user