2026-05-04 14:54:07 +02:00
|
|
|
use amm_core::{
|
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.
2026-06-18 15:50:11 +02:00
|
|
|
assert_supported_fee_tier, compute_config_pda, compute_pool_pda_seed,
|
|
|
|
|
read_vault_fungible_balances, spot_price_q64_64, AmmConfig, PoolDefinition, MINIMUM_LIQUIDITY,
|
2026-05-04 14:54:07 +02: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.
2026-06-18 15:50:11 +02:00
|
|
|
use clock_core::CLOCK_01_PROGRAM_ACCOUNT_ID;
|
2026-04-08 10:57:47 -03:00
|
|
|
use nssa_core::{
|
|
|
|
|
account::{AccountWithMetadata, Data},
|
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.
2026-06-18 15:50:11 +02:00
|
|
|
program::{AccountPostState, ChainedCall, ProgramId},
|
2026-04-08 10:57:47 -03: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.
2026-06-18 15:50:11 +02:00
|
|
|
use twap_oracle_core::compute_current_tick_account_pda;
|
2026-04-08 10:57:47 -03:00
|
|
|
|
|
|
|
|
pub fn sync_reserves(
|
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.
2026-06-18 15:50:11 +02:00
|
|
|
config: AccountWithMetadata,
|
2026-04-08 10:57:47 -03:00
|
|
|
pool: AccountWithMetadata,
|
|
|
|
|
vault_a: AccountWithMetadata,
|
|
|
|
|
vault_b: AccountWithMetadata,
|
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.
2026-06-18 15:50:11 +02:00
|
|
|
current_tick_account: AccountWithMetadata,
|
|
|
|
|
clock: AccountWithMetadata,
|
|
|
|
|
amm_program_id: ProgramId,
|
2026-04-08 10:57:47 -03:00
|
|
|
) -> (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");
|
2026-05-04 14:54:07 +02:00
|
|
|
assert_supported_fee_tier(pool_def_data.fees);
|
2026-04-08 10:57:47 -03: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.
2026-06-18 15:50:11 +02:00
|
|
|
// 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;
|
|
|
|
|
|
2026-04-10 15:43:13 -03:00
|
|
|
assert!(
|
|
|
|
|
pool_def_data.liquidity_pool_supply >= MINIMUM_LIQUIDITY,
|
|
|
|
|
"Pool liquidity supply is below minimum liquidity"
|
|
|
|
|
);
|
2026-04-08 10:57:47 -03:00
|
|
|
assert_eq!(
|
|
|
|
|
vault_a.account_id, pool_def_data.vault_a_id,
|
|
|
|
|
"Vault A was not provided"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
vault_b.account_id, pool_def_data.vault_b_id,
|
|
|
|
|
"Vault B was not provided"
|
|
|
|
|
);
|
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.
2026-06-18 15:50:11 +02:00
|
|
|
// 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"
|
|
|
|
|
);
|
2026-04-08 10:57:47 -03:00
|
|
|
|
|
|
|
|
let (vault_a_balance, vault_b_balance) =
|
|
|
|
|
read_vault_fungible_balances("Sync reserves", &vault_a, &vault_b);
|
|
|
|
|
assert!(
|
|
|
|
|
vault_a_balance >= pool_def_data.reserve_a,
|
|
|
|
|
"Sync reserves: vault A balance is less than its reserve"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
vault_b_balance >= pool_def_data.reserve_b,
|
|
|
|
|
"Sync reserves: vault B balance is less than its reserve"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let pool_post_definition = PoolDefinition {
|
|
|
|
|
reserve_a: vault_a_balance,
|
|
|
|
|
reserve_b: vault_b_balance,
|
|
|
|
|
..pool_def_data
|
|
|
|
|
};
|
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.
2026-06-18 15:50:11 +02:00
|
|
|
let mut pool_post = pool.account.clone();
|
2026-04-08 10:57:47 -03:00
|
|
|
pool_post.data = Data::from(&pool_post_definition);
|
|
|
|
|
|
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.
2026-06-18 15:50:11 +02:00
|
|
|
// 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,
|
|
|
|
|
)]);
|
|
|
|
|
|
2026-04-08 10:57:47 -03:00
|
|
|
(
|
|
|
|
|
vec![
|
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.
2026-06-18 15:50:11 +02:00
|
|
|
AccountPostState::new(config.account.clone()),
|
2026-04-08 10:57:47 -03:00
|
|
|
AccountPostState::new(pool_post),
|
|
|
|
|
AccountPostState::new(vault_a.account.clone()),
|
|
|
|
|
AccountPostState::new(vault_b.account.clone()),
|
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.
2026-06-18 15:50:11 +02:00
|
|
|
AccountPostState::new(current_tick_account.account.clone()),
|
|
|
|
|
AccountPostState::new(clock.account.clone()),
|
2026-04-08 10:57:47 -03: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.
2026-06-18 15:50:11 +02:00
|
|
|
vec![update_tick_call],
|
2026-04-08 10:57:47 -03:00
|
|
|
)
|
|
|
|
|
}
|