2026-05-06 17:08:15 -03:00
|
|
|
|
#![expect(
|
|
|
|
|
|
clippy::arithmetic_side_effects,
|
|
|
|
|
|
reason = "integration fixtures use fixed balances to assert AMM state transitions"
|
|
|
|
|
|
)]
|
|
|
|
|
|
|
2026-03-31 20:45:57 -03:00
|
|
|
|
use amm_core::{
|
|
|
|
|
|
PoolDefinition, FEE_TIER_BPS_1, FEE_TIER_BPS_100, FEE_TIER_BPS_30, FEE_TIER_BPS_5,
|
|
|
|
|
|
MINIMUM_LIQUIDITY,
|
|
|
|
|
|
};
|
2026-03-17 18:08:53 +01:00
|
|
|
|
use nssa::{
|
2026-03-31 20:45:57 -03:00
|
|
|
|
error::NssaError,
|
2026-03-17 18:08:53 +01:00
|
|
|
|
program_deployment_transaction::{self, ProgramDeploymentTransaction},
|
|
|
|
|
|
public_transaction, PrivateKey, PublicKey, PublicTransaction, V03State,
|
2026-06-18 09:01:10 +02:00
|
|
|
|
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
2026-03-17 18:08:53 +01:00
|
|
|
|
};
|
|
|
|
|
|
use nssa_core::account::{Account, AccountId, Data, Nonce};
|
|
|
|
|
|
use token_core::{TokenDefinition, TokenHolding};
|
|
|
|
|
|
|
|
|
|
|
|
struct Keys;
|
|
|
|
|
|
struct Ids;
|
|
|
|
|
|
struct Balances;
|
|
|
|
|
|
struct Accounts;
|
|
|
|
|
|
|
|
|
|
|
|
impl Keys {
|
|
|
|
|
|
fn user_a() -> PrivateKey {
|
|
|
|
|
|
PrivateKey::try_new([31; 32]).expect("valid private key")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_b() -> PrivateKey {
|
|
|
|
|
|
PrivateKey::try_new([32; 32]).expect("valid private key")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_lp() -> PrivateKey {
|
|
|
|
|
|
PrivateKey::try_new([33; 32]).expect("valid private key")
|
|
|
|
|
|
}
|
2026-06-18 16:46:38 +02:00
|
|
|
|
|
|
|
|
|
|
fn admin() -> PrivateKey {
|
|
|
|
|
|
PrivateKey::try_new([34; 32]).expect("valid private key")
|
|
|
|
|
|
}
|
2026-03-17 18:08:53 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Ids {
|
|
|
|
|
|
fn token_program() -> nssa_core::program::ProgramId {
|
|
|
|
|
|
token_methods::TOKEN_ID
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn amm_program() -> nssa_core::program::ProgramId {
|
|
|
|
|
|
amm_methods::AMM_ID
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-18 09:01:10 +02:00
|
|
|
|
fn twap_oracle_program() -> nssa_core::program::ProgramId {
|
|
|
|
|
|
twap_oracle_methods::TWAP_ORACLE_ID
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
fn config() -> AccountId {
|
|
|
|
|
|
amm_core::compute_config_pda(Self::amm_program())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-18 09:01:10 +02:00
|
|
|
|
fn price_observations(window_duration: u64) -> AccountId {
|
|
|
|
|
|
twap_oracle_core::compute_price_observations_pda(
|
|
|
|
|
|
Self::twap_oracle_program(),
|
|
|
|
|
|
Self::pool_definition(),
|
|
|
|
|
|
window_duration,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn current_tick_account() -> AccountId {
|
|
|
|
|
|
twap_oracle_core::compute_current_tick_account_pda(
|
|
|
|
|
|
Self::twap_oracle_program(),
|
|
|
|
|
|
Self::pool_definition(),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-23 17:28:22 +02:00
|
|
|
|
fn oracle_price_account(window_duration: u64) -> AccountId {
|
|
|
|
|
|
twap_oracle_core::compute_oracle_price_account_pda(
|
|
|
|
|
|
Self::twap_oracle_program(),
|
|
|
|
|
|
Self::pool_definition(),
|
|
|
|
|
|
window_duration,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 18:08:53 +01:00
|
|
|
|
fn token_a_definition() -> AccountId {
|
|
|
|
|
|
AccountId::new([3; 32])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn token_b_definition() -> AccountId {
|
|
|
|
|
|
AccountId::new([4; 32])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn pool_definition() -> AccountId {
|
|
|
|
|
|
amm_core::compute_pool_pda(
|
|
|
|
|
|
Self::amm_program(),
|
|
|
|
|
|
Self::token_a_definition(),
|
|
|
|
|
|
Self::token_b_definition(),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn token_lp_definition() -> AccountId {
|
|
|
|
|
|
amm_core::compute_liquidity_token_pda(Self::amm_program(), Self::pool_definition())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 17:48:13 -03:00
|
|
|
|
fn lp_lock_holding() -> AccountId {
|
|
|
|
|
|
amm_core::compute_lp_lock_holding_pda(Self::amm_program(), Self::pool_definition())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 18:08:53 +01:00
|
|
|
|
fn vault_a() -> AccountId {
|
|
|
|
|
|
amm_core::compute_vault_pda(
|
|
|
|
|
|
Self::amm_program(),
|
|
|
|
|
|
Self::pool_definition(),
|
|
|
|
|
|
Self::token_a_definition(),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn vault_b() -> AccountId {
|
|
|
|
|
|
amm_core::compute_vault_pda(
|
|
|
|
|
|
Self::amm_program(),
|
|
|
|
|
|
Self::pool_definition(),
|
|
|
|
|
|
Self::token_b_definition(),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_a() -> AccountId {
|
|
|
|
|
|
AccountId::from(&PublicKey::new_from_private_key(&Keys::user_a()))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_b() -> AccountId {
|
|
|
|
|
|
AccountId::from(&PublicKey::new_from_private_key(&Keys::user_b()))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_lp() -> AccountId {
|
|
|
|
|
|
AccountId::from(&PublicKey::new_from_private_key(&Keys::user_lp()))
|
|
|
|
|
|
}
|
2026-06-18 16:46:38 +02:00
|
|
|
|
|
|
|
|
|
|
fn admin() -> AccountId {
|
|
|
|
|
|
AccountId::from(&PublicKey::new_from_private_key(&Keys::admin()))
|
|
|
|
|
|
}
|
2026-03-17 18:08:53 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Balances {
|
2026-03-31 20:45:57 -03:00
|
|
|
|
fn fee_tier() -> u128 {
|
|
|
|
|
|
FEE_TIER_BPS_30
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 18:08:53 +01:00
|
|
|
|
fn user_a_init() -> u128 {
|
|
|
|
|
|
10_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_b_init() -> u128 {
|
|
|
|
|
|
10_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_lp_init() -> u128 {
|
|
|
|
|
|
2_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn vault_a_init() -> u128 {
|
|
|
|
|
|
5_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn vault_b_init() -> u128 {
|
|
|
|
|
|
2_500
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn pool_lp_supply_init() -> u128 {
|
|
|
|
|
|
5_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn token_a_supply() -> u128 {
|
|
|
|
|
|
100_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn token_b_supply() -> u128 {
|
|
|
|
|
|
100_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn token_lp_supply() -> u128 {
|
|
|
|
|
|
5_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn remove_lp() -> u128 {
|
|
|
|
|
|
1_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn remove_min_a() -> u128 {
|
|
|
|
|
|
500
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn remove_min_b() -> u128 {
|
|
|
|
|
|
500
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn add_min_lp() -> u128 {
|
|
|
|
|
|
1_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn add_max_a() -> u128 {
|
|
|
|
|
|
2_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn add_max_b() -> u128 {
|
|
|
|
|
|
1_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn swap_amount_in() -> u128 {
|
|
|
|
|
|
1_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn swap_min_out() -> u128 {
|
|
|
|
|
|
200
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 23:15:10 -03:00
|
|
|
|
fn reserve_a_swap_1() -> u128 {
|
|
|
|
|
|
3_575
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn reserve_b_swap_1() -> u128 {
|
|
|
|
|
|
3_500
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 18:08:53 +01:00
|
|
|
|
fn vault_a_swap_1() -> u128 {
|
2026-03-31 23:15:10 -03:00
|
|
|
|
3_575
|
2026-03-17 18:08:53 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn vault_b_swap_1() -> u128 {
|
|
|
|
|
|
3_500
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_a_swap_1() -> u128 {
|
2026-03-31 23:15:10 -03:00
|
|
|
|
11_425
|
2026-03-17 18:08:53 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_b_swap_1() -> u128 {
|
|
|
|
|
|
9_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 23:15:10 -03:00
|
|
|
|
fn reserve_a_swap_2() -> u128 {
|
|
|
|
|
|
6_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn reserve_b_swap_2() -> u128 {
|
|
|
|
|
|
2_085
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 18:08:53 +01:00
|
|
|
|
fn vault_a_swap_2() -> u128 {
|
|
|
|
|
|
6_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn vault_b_swap_2() -> u128 {
|
2026-03-31 23:15:10 -03:00
|
|
|
|
2_085
|
2026-03-17 18:08:53 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_a_swap_2() -> u128 {
|
|
|
|
|
|
9_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_b_swap_2() -> u128 {
|
2026-03-31 23:15:10 -03:00
|
|
|
|
10_415
|
2026-03-17 18:08:53 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn vault_a_add() -> u128 {
|
|
|
|
|
|
7_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn vault_b_add() -> u128 {
|
|
|
|
|
|
3_500
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_a_add() -> u128 {
|
|
|
|
|
|
8_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_b_add() -> u128 {
|
|
|
|
|
|
9_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_lp_add() -> u128 {
|
|
|
|
|
|
4_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn token_lp_supply_add() -> u128 {
|
|
|
|
|
|
7_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn vault_a_remove() -> u128 {
|
|
|
|
|
|
4_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn vault_b_remove() -> u128 {
|
|
|
|
|
|
2_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_a_remove() -> u128 {
|
|
|
|
|
|
11_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_b_remove() -> u128 {
|
|
|
|
|
|
10_500
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_lp_remove() -> u128 {
|
|
|
|
|
|
1_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn token_lp_supply_remove() -> u128 {
|
|
|
|
|
|
4_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_a_new_definition() -> u128 {
|
|
|
|
|
|
5_000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_b_new_definition() -> u128 {
|
|
|
|
|
|
7_500
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn lp_supply_init() -> u128 {
|
|
|
|
|
|
(Self::vault_a_init() * Self::vault_b_init()).isqrt()
|
|
|
|
|
|
}
|
2026-04-08 17:48:13 -03:00
|
|
|
|
|
|
|
|
|
|
fn lp_user_init() -> u128 {
|
|
|
|
|
|
Self::lp_supply_init() - MINIMUM_LIQUIDITY
|
|
|
|
|
|
}
|
2026-03-17 18:08:53 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Accounts {
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
fn config() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::amm_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&amm_core::AmmConfig {
|
|
|
|
|
|
token_program_id: Ids::token_program(),
|
2026-06-18 09:01:10 +02:00
|
|
|
|
twap_oracle_program_id: Ids::twap_oracle_program(),
|
2026-06-18 16:46:38 +02:00
|
|
|
|
authority: Ids::admin(),
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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 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),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 18:08:53 +01:00
|
|
|
|
fn user_a_holding() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_a_definition(),
|
|
|
|
|
|
balance: Balances::user_a_init(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_b_holding() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_b_definition(),
|
|
|
|
|
|
balance: Balances::user_b_init(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn pool_definition_init() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::amm_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&PoolDefinition {
|
|
|
|
|
|
definition_token_a_id: Ids::token_a_definition(),
|
|
|
|
|
|
definition_token_b_id: Ids::token_b_definition(),
|
|
|
|
|
|
vault_a_id: Ids::vault_a(),
|
|
|
|
|
|
vault_b_id: Ids::vault_b(),
|
|
|
|
|
|
liquidity_pool_id: Ids::token_lp_definition(),
|
|
|
|
|
|
liquidity_pool_supply: Balances::pool_lp_supply_init(),
|
|
|
|
|
|
reserve_a: Balances::vault_a_init(),
|
|
|
|
|
|
reserve_b: Balances::vault_b_init(),
|
2026-03-31 20:45:57 -03:00
|
|
|
|
fees: Balances::fee_tier(),
|
2026-03-17 18:08:53 +01:00
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn token_a_definition_account() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenDefinition::Fungible {
|
|
|
|
|
|
name: String::from("test"),
|
|
|
|
|
|
total_supply: Balances::token_a_supply(),
|
|
|
|
|
|
metadata_id: None,
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn token_b_definition_account() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenDefinition::Fungible {
|
|
|
|
|
|
name: String::from("test"),
|
|
|
|
|
|
total_supply: Balances::token_b_supply(),
|
|
|
|
|
|
metadata_id: None,
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn token_lp_definition_account() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenDefinition::Fungible {
|
|
|
|
|
|
name: String::from("LP Token"),
|
|
|
|
|
|
total_supply: Balances::token_lp_supply(),
|
|
|
|
|
|
metadata_id: None,
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn vault_a_init() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_a_definition(),
|
|
|
|
|
|
balance: Balances::vault_a_init(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn vault_b_init() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_b_definition(),
|
|
|
|
|
|
balance: Balances::vault_b_init(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_lp_holding() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_lp_definition(),
|
|
|
|
|
|
balance: Balances::user_lp_init(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 17:48:13 -03:00
|
|
|
|
fn user_lp_holding_with_balance(balance: u128) -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_lp_definition(),
|
|
|
|
|
|
balance,
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 18:08:53 +01:00
|
|
|
|
// --- Expected post-state accounts ---
|
|
|
|
|
|
|
|
|
|
|
|
fn pool_definition_swap_1() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::amm_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&PoolDefinition {
|
|
|
|
|
|
definition_token_a_id: Ids::token_a_definition(),
|
|
|
|
|
|
definition_token_b_id: Ids::token_b_definition(),
|
|
|
|
|
|
vault_a_id: Ids::vault_a(),
|
|
|
|
|
|
vault_b_id: Ids::vault_b(),
|
|
|
|
|
|
liquidity_pool_id: Ids::token_lp_definition(),
|
|
|
|
|
|
liquidity_pool_supply: Balances::pool_lp_supply_init(),
|
2026-03-31 23:15:10 -03:00
|
|
|
|
reserve_a: Balances::reserve_a_swap_1(),
|
|
|
|
|
|
reserve_b: Balances::reserve_b_swap_1(),
|
2026-03-31 20:45:57 -03:00
|
|
|
|
fees: Balances::fee_tier(),
|
2026-03-17 18:08:53 +01:00
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn vault_a_swap_1() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_a_definition(),
|
|
|
|
|
|
balance: Balances::vault_a_swap_1(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn vault_b_swap_1() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_b_definition(),
|
|
|
|
|
|
balance: Balances::vault_b_swap_1(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_a_holding_swap_1() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_a_definition(),
|
|
|
|
|
|
balance: Balances::user_a_swap_1(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_b_holding_swap_1() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_b_definition(),
|
|
|
|
|
|
balance: Balances::user_b_swap_1(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(1),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn pool_definition_swap_2() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::amm_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&PoolDefinition {
|
|
|
|
|
|
definition_token_a_id: Ids::token_a_definition(),
|
|
|
|
|
|
definition_token_b_id: Ids::token_b_definition(),
|
|
|
|
|
|
vault_a_id: Ids::vault_a(),
|
|
|
|
|
|
vault_b_id: Ids::vault_b(),
|
|
|
|
|
|
liquidity_pool_id: Ids::token_lp_definition(),
|
|
|
|
|
|
liquidity_pool_supply: Balances::pool_lp_supply_init(),
|
2026-03-31 23:15:10 -03:00
|
|
|
|
reserve_a: Balances::reserve_a_swap_2(),
|
|
|
|
|
|
reserve_b: Balances::reserve_b_swap_2(),
|
2026-03-31 20:45:57 -03:00
|
|
|
|
fees: Balances::fee_tier(),
|
2026-03-17 18:08:53 +01:00
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn vault_a_swap_2() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_a_definition(),
|
|
|
|
|
|
balance: Balances::vault_a_swap_2(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn vault_b_swap_2() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_b_definition(),
|
|
|
|
|
|
balance: Balances::vault_b_swap_2(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_a_holding_swap_2() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_a_definition(),
|
|
|
|
|
|
balance: Balances::user_a_swap_2(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(1),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_b_holding_swap_2() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_b_definition(),
|
|
|
|
|
|
balance: Balances::user_b_swap_2(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn pool_definition_add() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::amm_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&PoolDefinition {
|
|
|
|
|
|
definition_token_a_id: Ids::token_a_definition(),
|
|
|
|
|
|
definition_token_b_id: Ids::token_b_definition(),
|
|
|
|
|
|
vault_a_id: Ids::vault_a(),
|
|
|
|
|
|
vault_b_id: Ids::vault_b(),
|
|
|
|
|
|
liquidity_pool_id: Ids::token_lp_definition(),
|
|
|
|
|
|
liquidity_pool_supply: Balances::token_lp_supply_add(),
|
|
|
|
|
|
reserve_a: Balances::vault_a_add(),
|
|
|
|
|
|
reserve_b: Balances::vault_b_add(),
|
2026-03-31 20:45:57 -03:00
|
|
|
|
fees: Balances::fee_tier(),
|
2026-03-17 18:08:53 +01:00
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn vault_a_add() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_a_definition(),
|
|
|
|
|
|
balance: Balances::vault_a_add(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn vault_b_add() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_b_definition(),
|
|
|
|
|
|
balance: Balances::vault_b_add(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_a_holding_add() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_a_definition(),
|
|
|
|
|
|
balance: Balances::user_a_add(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(1),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_b_holding_add() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_b_definition(),
|
|
|
|
|
|
balance: Balances::user_b_add(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(1),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_lp_holding_add() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_lp_definition(),
|
|
|
|
|
|
balance: Balances::user_lp_add(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn token_lp_definition_add() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenDefinition::Fungible {
|
|
|
|
|
|
name: String::from("LP Token"),
|
|
|
|
|
|
total_supply: Balances::token_lp_supply_add(),
|
|
|
|
|
|
metadata_id: None,
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn pool_definition_remove() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::amm_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&PoolDefinition {
|
|
|
|
|
|
definition_token_a_id: Ids::token_a_definition(),
|
|
|
|
|
|
definition_token_b_id: Ids::token_b_definition(),
|
|
|
|
|
|
vault_a_id: Ids::vault_a(),
|
|
|
|
|
|
vault_b_id: Ids::vault_b(),
|
|
|
|
|
|
liquidity_pool_id: Ids::token_lp_definition(),
|
|
|
|
|
|
liquidity_pool_supply: Balances::token_lp_supply_remove(),
|
|
|
|
|
|
reserve_a: Balances::vault_a_remove(),
|
|
|
|
|
|
reserve_b: Balances::vault_b_remove(),
|
2026-03-31 20:45:57 -03:00
|
|
|
|
fees: Balances::fee_tier(),
|
2026-03-17 18:08:53 +01:00
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn vault_a_remove() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_a_definition(),
|
|
|
|
|
|
balance: Balances::vault_a_remove(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn vault_b_remove() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_b_definition(),
|
|
|
|
|
|
balance: Balances::vault_b_remove(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_a_holding_remove() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_a_definition(),
|
|
|
|
|
|
balance: Balances::user_a_remove(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_b_holding_remove() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_b_definition(),
|
|
|
|
|
|
balance: Balances::user_b_remove(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_lp_holding_remove() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_lp_definition(),
|
|
|
|
|
|
balance: Balances::user_lp_remove(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(1),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn token_lp_definition_remove() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenDefinition::Fungible {
|
|
|
|
|
|
name: String::from("LP Token"),
|
|
|
|
|
|
total_supply: Balances::token_lp_supply_remove(),
|
|
|
|
|
|
metadata_id: None,
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-10 15:43:13 -03:00
|
|
|
|
fn token_lp_definition_reinitializable() -> Account {
|
2026-03-17 18:08:53 +01:00
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenDefinition::Fungible {
|
|
|
|
|
|
name: String::from("LP Token"),
|
|
|
|
|
|
total_supply: 0,
|
|
|
|
|
|
metadata_id: None,
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-10 15:43:13 -03:00
|
|
|
|
fn vault_a_reinitializable() -> Account {
|
2026-03-17 18:08:53 +01:00
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_a_definition(),
|
|
|
|
|
|
balance: 0,
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-10 15:43:13 -03:00
|
|
|
|
fn vault_b_reinitializable() -> Account {
|
2026-03-17 18:08:53 +01:00
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_b_definition(),
|
|
|
|
|
|
balance: 0,
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-10 15:43:13 -03:00
|
|
|
|
fn pool_definition_zero_supply_reinitializable() -> Account {
|
2026-03-17 18:08:53 +01:00
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::amm_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&PoolDefinition {
|
|
|
|
|
|
definition_token_a_id: Ids::token_a_definition(),
|
|
|
|
|
|
definition_token_b_id: Ids::token_b_definition(),
|
|
|
|
|
|
vault_a_id: Ids::vault_a(),
|
|
|
|
|
|
vault_b_id: Ids::vault_b(),
|
|
|
|
|
|
liquidity_pool_id: Ids::token_lp_definition(),
|
|
|
|
|
|
liquidity_pool_supply: 0,
|
|
|
|
|
|
reserve_a: 0,
|
|
|
|
|
|
reserve_b: 0,
|
2026-03-31 20:45:57 -03:00
|
|
|
|
fees: Balances::fee_tier(),
|
2026-03-17 18:08:53 +01:00
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_a_holding_new_init() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_a_definition(),
|
|
|
|
|
|
balance: Balances::user_a_new_definition(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(1),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_b_holding_new_init() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_b_definition(),
|
|
|
|
|
|
balance: Balances::user_b_new_definition(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(1),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_lp_holding_new_init() -> Account {
|
2026-04-15 14:55:04 -03:00
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_lp_definition(),
|
|
|
|
|
|
balance: Balances::lp_user_init(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(1),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_lp_holding_new_init_precreated() -> Account {
|
2026-03-17 18:08:53 +01:00
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_lp_definition(),
|
2026-04-08 17:48:13 -03:00
|
|
|
|
balance: Balances::lp_user_init(),
|
2026-03-17 18:08:53 +01:00
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn token_lp_definition_new_init() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenDefinition::Fungible {
|
|
|
|
|
|
name: String::from("LP Token"),
|
|
|
|
|
|
total_supply: Balances::lp_supply_init(),
|
|
|
|
|
|
metadata_id: None,
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 17:48:13 -03:00
|
|
|
|
fn lp_lock_holding_new_init() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_lp_definition(),
|
|
|
|
|
|
balance: MINIMUM_LIQUIDITY,
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 18:08:53 +01:00
|
|
|
|
fn pool_definition_new_init() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::amm_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&PoolDefinition {
|
|
|
|
|
|
definition_token_a_id: Ids::token_a_definition(),
|
|
|
|
|
|
definition_token_b_id: Ids::token_b_definition(),
|
|
|
|
|
|
vault_a_id: Ids::vault_a(),
|
|
|
|
|
|
vault_b_id: Ids::vault_b(),
|
|
|
|
|
|
liquidity_pool_id: Ids::token_lp_definition(),
|
|
|
|
|
|
liquidity_pool_supply: Balances::lp_supply_init(),
|
|
|
|
|
|
reserve_a: Balances::vault_a_init(),
|
|
|
|
|
|
reserve_b: Balances::vault_b_init(),
|
2026-03-31 20:45:57 -03:00
|
|
|
|
fees: Balances::fee_tier(),
|
2026-03-17 18:08:53 +01:00
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn user_lp_holding_init_zero() -> Account {
|
|
|
|
|
|
Account {
|
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: Ids::token_lp_definition(),
|
|
|
|
|
|
balance: 0,
|
|
|
|
|
|
}),
|
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn deploy_programs(state: &mut V03State) {
|
|
|
|
|
|
let token_message =
|
|
|
|
|
|
program_deployment_transaction::Message::new(token_methods::TOKEN_ELF.to_vec());
|
|
|
|
|
|
state
|
|
|
|
|
|
.transition_from_program_deployment_transaction(&ProgramDeploymentTransaction::new(
|
|
|
|
|
|
token_message,
|
|
|
|
|
|
))
|
|
|
|
|
|
.expect("token program deployment must succeed");
|
|
|
|
|
|
|
|
|
|
|
|
let amm_message = program_deployment_transaction::Message::new(amm_methods::AMM_ELF.to_vec());
|
|
|
|
|
|
state
|
|
|
|
|
|
.transition_from_program_deployment_transaction(&ProgramDeploymentTransaction::new(
|
|
|
|
|
|
amm_message,
|
|
|
|
|
|
))
|
|
|
|
|
|
.expect("amm program deployment must succeed");
|
2026-06-18 09:01:10 +02:00
|
|
|
|
|
|
|
|
|
|
let twap_message =
|
|
|
|
|
|
program_deployment_transaction::Message::new(twap_oracle_methods::TWAP_ORACLE_ELF.to_vec());
|
|
|
|
|
|
state
|
|
|
|
|
|
.transition_from_program_deployment_transaction(&ProgramDeploymentTransaction::new(
|
|
|
|
|
|
twap_message,
|
|
|
|
|
|
))
|
|
|
|
|
|
.expect("twap oracle program deployment must succeed");
|
2026-03-17 18:08:53 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn state_for_amm_tests() -> V03State {
|
2026-05-11 15:29:41 +02:00
|
|
|
|
let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0);
|
2026-03-17 18:08:53 +01:00
|
|
|
|
deploy_programs(&mut state);
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
state.force_insert_account(Ids::config(), Accounts::config());
|
2026-03-17 18:08:53 +01:00
|
|
|
|
state.force_insert_account(Ids::pool_definition(), Accounts::pool_definition_init());
|
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
|
|
|
|
// 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),
|
|
|
|
|
|
);
|
2026-03-17 18:08:53 +01:00
|
|
|
|
state.force_insert_account(
|
|
|
|
|
|
Ids::token_a_definition(),
|
|
|
|
|
|
Accounts::token_a_definition_account(),
|
|
|
|
|
|
);
|
|
|
|
|
|
state.force_insert_account(
|
|
|
|
|
|
Ids::token_b_definition(),
|
|
|
|
|
|
Accounts::token_b_definition_account(),
|
|
|
|
|
|
);
|
|
|
|
|
|
state.force_insert_account(
|
|
|
|
|
|
Ids::token_lp_definition(),
|
|
|
|
|
|
Accounts::token_lp_definition_account(),
|
|
|
|
|
|
);
|
|
|
|
|
|
state.force_insert_account(Ids::user_a(), Accounts::user_a_holding());
|
|
|
|
|
|
state.force_insert_account(Ids::user_b(), Accounts::user_b_holding());
|
|
|
|
|
|
state.force_insert_account(Ids::user_lp(), Accounts::user_lp_holding());
|
|
|
|
|
|
state.force_insert_account(Ids::vault_a(), Accounts::vault_a_init());
|
|
|
|
|
|
state.force_insert_account(Ids::vault_b(), Accounts::vault_b_init());
|
|
|
|
|
|
state
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn state_for_amm_tests_with_new_def() -> V03State {
|
2026-05-11 15:29:41 +02:00
|
|
|
|
let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0);
|
2026-03-17 18:08:53 +01:00
|
|
|
|
deploy_programs(&mut state);
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
state.force_insert_account(Ids::config(), Accounts::config());
|
2026-03-17 18:08:53 +01:00
|
|
|
|
state.force_insert_account(
|
|
|
|
|
|
Ids::token_a_definition(),
|
|
|
|
|
|
Accounts::token_a_definition_account(),
|
|
|
|
|
|
);
|
|
|
|
|
|
state.force_insert_account(
|
|
|
|
|
|
Ids::token_b_definition(),
|
|
|
|
|
|
Accounts::token_b_definition_account(),
|
|
|
|
|
|
);
|
|
|
|
|
|
state.force_insert_account(Ids::user_a(), Accounts::user_a_holding());
|
|
|
|
|
|
state.force_insert_account(Ids::user_b(), Accounts::user_b_holding());
|
|
|
|
|
|
state
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 23:15:10 -03:00
|
|
|
|
fn current_nonce(state: &V03State, account_id: AccountId) -> Nonce {
|
|
|
|
|
|
state.get_account_by_id(account_id).nonce
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 14:55:04 -03:00
|
|
|
|
fn state_for_amm_tests_with_precreated_user_lp_for_new_def() -> V03State {
|
|
|
|
|
|
let mut state = state_for_amm_tests_with_new_def();
|
|
|
|
|
|
state.force_insert_account(Ids::user_lp(), Accounts::user_lp_holding_init_zero());
|
|
|
|
|
|
state
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 17:08:15 -03:00
|
|
|
|
#[cfg(test)]
|
2026-04-15 14:55:04 -03:00
|
|
|
|
fn try_execute_new_definition(
|
|
|
|
|
|
state: &mut V03State,
|
|
|
|
|
|
fees: u128,
|
|
|
|
|
|
authorize_user_lp: bool,
|
|
|
|
|
|
) -> Result<(), NssaError> {
|
2026-03-31 20:45:57 -03:00
|
|
|
|
let instruction = amm_core::Instruction::NewDefinition {
|
|
|
|
|
|
token_a_amount: Balances::vault_a_init(),
|
|
|
|
|
|
token_b_amount: Balances::vault_b_init(),
|
|
|
|
|
|
fees,
|
2026-04-23 17:19:15 +02:00
|
|
|
|
deadline: u64::MAX,
|
2026-03-31 20:45:57 -03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
|
Ids::amm_program(),
|
|
|
|
|
|
vec![
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
Ids::config(),
|
2026-03-31 20:45:57 -03:00
|
|
|
|
Ids::pool_definition(),
|
|
|
|
|
|
Ids::vault_a(),
|
|
|
|
|
|
Ids::vault_b(),
|
|
|
|
|
|
Ids::token_lp_definition(),
|
|
|
|
|
|
Ids::lp_lock_holding(),
|
|
|
|
|
|
Ids::user_a(),
|
|
|
|
|
|
Ids::user_b(),
|
|
|
|
|
|
Ids::user_lp(),
|
2026-06-18 14:07:04 +02:00
|
|
|
|
Ids::current_tick_account(),
|
|
|
|
|
|
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
2026-03-31 20:45:57 -03:00
|
|
|
|
],
|
2026-04-15 14:55:04 -03:00
|
|
|
|
if authorize_user_lp {
|
|
|
|
|
|
vec![
|
|
|
|
|
|
current_nonce(state, Ids::user_a()),
|
|
|
|
|
|
current_nonce(state, Ids::user_b()),
|
|
|
|
|
|
current_nonce(state, Ids::user_lp()),
|
|
|
|
|
|
]
|
|
|
|
|
|
} else {
|
|
|
|
|
|
vec![
|
|
|
|
|
|
current_nonce(state, Ids::user_a()),
|
|
|
|
|
|
current_nonce(state, Ids::user_b()),
|
|
|
|
|
|
]
|
|
|
|
|
|
},
|
2026-03-31 20:45:57 -03:00
|
|
|
|
instruction,
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
2026-04-15 14:55:04 -03:00
|
|
|
|
let witness_set = if authorize_user_lp {
|
|
|
|
|
|
public_transaction::WitnessSet::for_message(
|
|
|
|
|
|
&message,
|
|
|
|
|
|
&[&Keys::user_a(), &Keys::user_b(), &Keys::user_lp()],
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a(), &Keys::user_b()])
|
|
|
|
|
|
};
|
2026-03-31 20:45:57 -03:00
|
|
|
|
|
|
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
2026-04-15 14:55:04 -03:00
|
|
|
|
state.transition_from_public_transaction(&tx, 0, 0)
|
2026-03-31 20:45:57 -03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 17:08:15 -03:00
|
|
|
|
#[cfg(test)]
|
2026-03-31 20:45:57 -03:00
|
|
|
|
fn execute_new_definition(state: &mut V03State, fees: u128) {
|
2026-04-15 14:55:04 -03:00
|
|
|
|
try_execute_new_definition(state, fees, true).unwrap();
|
2026-03-31 20:45:57 -03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 17:08:15 -03:00
|
|
|
|
#[cfg(test)]
|
2026-03-31 23:15:10 -03:00
|
|
|
|
fn execute_swap_a_to_b(state: &mut V03State, swap_amount_in: u128, min_amount_out: u128) {
|
|
|
|
|
|
let instruction = amm_core::Instruction::SwapExactInput {
|
|
|
|
|
|
swap_amount_in,
|
|
|
|
|
|
min_amount_out,
|
|
|
|
|
|
token_definition_id_in: Ids::token_a_definition(),
|
2026-04-23 17:19:15 +02:00
|
|
|
|
deadline: u64::MAX,
|
2026-03-31 23:15:10 -03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
|
Ids::amm_program(),
|
|
|
|
|
|
vec![
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
Ids::config(),
|
2026-03-31 23:15:10 -03:00
|
|
|
|
Ids::pool_definition(),
|
|
|
|
|
|
Ids::vault_a(),
|
|
|
|
|
|
Ids::vault_b(),
|
|
|
|
|
|
Ids::user_a(),
|
|
|
|
|
|
Ids::user_b(),
|
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
|
|
|
|
Ids::current_tick_account(),
|
|
|
|
|
|
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
2026-03-31 23:15:10 -03:00
|
|
|
|
],
|
|
|
|
|
|
vec![current_nonce(state, Ids::user_a())],
|
|
|
|
|
|
instruction,
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a()]);
|
|
|
|
|
|
|
|
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
2026-04-15 14:55:04 -03:00
|
|
|
|
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
2026-03-31 23:15:10 -03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 17:08:15 -03:00
|
|
|
|
#[cfg(test)]
|
2026-03-31 23:15:10 -03:00
|
|
|
|
fn execute_swap_b_to_a(state: &mut V03State, swap_amount_in: u128, min_amount_out: u128) {
|
|
|
|
|
|
let instruction = amm_core::Instruction::SwapExactInput {
|
|
|
|
|
|
swap_amount_in,
|
|
|
|
|
|
min_amount_out,
|
|
|
|
|
|
token_definition_id_in: Ids::token_b_definition(),
|
2026-04-23 17:19:15 +02:00
|
|
|
|
deadline: u64::MAX,
|
2026-03-31 23:15:10 -03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
|
Ids::amm_program(),
|
|
|
|
|
|
vec![
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
Ids::config(),
|
2026-03-31 23:15:10 -03:00
|
|
|
|
Ids::pool_definition(),
|
|
|
|
|
|
Ids::vault_a(),
|
|
|
|
|
|
Ids::vault_b(),
|
|
|
|
|
|
Ids::user_a(),
|
|
|
|
|
|
Ids::user_b(),
|
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
|
|
|
|
Ids::current_tick_account(),
|
|
|
|
|
|
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
2026-03-31 23:15:10 -03:00
|
|
|
|
],
|
|
|
|
|
|
vec![current_nonce(state, Ids::user_b())],
|
|
|
|
|
|
instruction,
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_b()]);
|
|
|
|
|
|
|
|
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
2026-04-15 14:55:04 -03:00
|
|
|
|
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
2026-03-31 23:15:10 -03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 17:08:15 -03:00
|
|
|
|
#[cfg(test)]
|
2026-03-31 23:15:10 -03:00
|
|
|
|
fn execute_add_liquidity(
|
|
|
|
|
|
state: &mut V03State,
|
|
|
|
|
|
min_amount_liquidity: u128,
|
|
|
|
|
|
max_amount_to_add_token_a: u128,
|
|
|
|
|
|
max_amount_to_add_token_b: u128,
|
|
|
|
|
|
) {
|
|
|
|
|
|
let instruction = amm_core::Instruction::AddLiquidity {
|
|
|
|
|
|
min_amount_liquidity,
|
|
|
|
|
|
max_amount_to_add_token_a,
|
|
|
|
|
|
max_amount_to_add_token_b,
|
2026-04-23 17:19:15 +02:00
|
|
|
|
deadline: u64::MAX,
|
2026-03-31 23:15:10 -03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
|
Ids::amm_program(),
|
|
|
|
|
|
vec![
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
Ids::config(),
|
2026-03-31 23:15:10 -03:00
|
|
|
|
Ids::pool_definition(),
|
|
|
|
|
|
Ids::vault_a(),
|
|
|
|
|
|
Ids::vault_b(),
|
|
|
|
|
|
Ids::token_lp_definition(),
|
|
|
|
|
|
Ids::user_a(),
|
|
|
|
|
|
Ids::user_b(),
|
|
|
|
|
|
Ids::user_lp(),
|
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
|
|
|
|
Ids::current_tick_account(),
|
|
|
|
|
|
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
2026-03-31 23:15:10 -03:00
|
|
|
|
],
|
|
|
|
|
|
vec![
|
|
|
|
|
|
current_nonce(state, Ids::user_a()),
|
|
|
|
|
|
current_nonce(state, Ids::user_b()),
|
|
|
|
|
|
],
|
|
|
|
|
|
instruction,
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let witness_set =
|
|
|
|
|
|
public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a(), &Keys::user_b()]);
|
|
|
|
|
|
|
|
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
2026-04-15 14:55:04 -03:00
|
|
|
|
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
2026-03-31 23:15:10 -03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 17:08:15 -03:00
|
|
|
|
#[cfg(test)]
|
2026-03-31 23:15:10 -03:00
|
|
|
|
fn execute_remove_liquidity(
|
|
|
|
|
|
state: &mut V03State,
|
|
|
|
|
|
remove_liquidity_amount: u128,
|
|
|
|
|
|
min_amount_to_remove_token_a: u128,
|
|
|
|
|
|
min_amount_to_remove_token_b: u128,
|
|
|
|
|
|
) {
|
|
|
|
|
|
let instruction = amm_core::Instruction::RemoveLiquidity {
|
|
|
|
|
|
remove_liquidity_amount,
|
|
|
|
|
|
min_amount_to_remove_token_a,
|
|
|
|
|
|
min_amount_to_remove_token_b,
|
2026-04-23 17:19:15 +02:00
|
|
|
|
deadline: u64::MAX,
|
2026-03-31 23:15:10 -03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
|
Ids::amm_program(),
|
|
|
|
|
|
vec![
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
Ids::config(),
|
2026-03-31 23:15:10 -03:00
|
|
|
|
Ids::pool_definition(),
|
|
|
|
|
|
Ids::vault_a(),
|
|
|
|
|
|
Ids::vault_b(),
|
|
|
|
|
|
Ids::token_lp_definition(),
|
|
|
|
|
|
Ids::user_a(),
|
|
|
|
|
|
Ids::user_b(),
|
|
|
|
|
|
Ids::user_lp(),
|
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
|
|
|
|
Ids::current_tick_account(),
|
|
|
|
|
|
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
2026-03-31 23:15:10 -03:00
|
|
|
|
],
|
|
|
|
|
|
vec![current_nonce(state, Ids::user_lp())],
|
|
|
|
|
|
instruction,
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_lp()]);
|
|
|
|
|
|
|
|
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
2026-04-15 14:55:04 -03:00
|
|
|
|
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
2026-03-31 23:15:10 -03:00
|
|
|
|
}
|
|
|
|
|
|
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
fn execute_initialize(state: &mut V03State) {
|
|
|
|
|
|
let instruction = amm_core::Instruction::Initialize {
|
|
|
|
|
|
token_program_id: Ids::token_program(),
|
2026-06-18 09:01:10 +02:00
|
|
|
|
twap_oracle_program_id: Ids::twap_oracle_program(),
|
2026-06-18 16:46:38 +02:00
|
|
|
|
authority: Ids::admin(),
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
|
Ids::amm_program(),
|
|
|
|
|
|
vec![Ids::config()],
|
|
|
|
|
|
vec![],
|
|
|
|
|
|
instruction,
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
|
|
|
|
|
|
|
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
|
|
|
|
|
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-18 09:01:10 +02:00
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
fn execute_create_price_observations(
|
|
|
|
|
|
state: &mut V03State,
|
|
|
|
|
|
window_duration: u64,
|
|
|
|
|
|
) -> Result<(), NssaError> {
|
|
|
|
|
|
let instruction = amm_core::Instruction::CreatePriceObservations { window_duration };
|
|
|
|
|
|
|
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
|
Ids::amm_program(),
|
|
|
|
|
|
vec![
|
|
|
|
|
|
Ids::config(),
|
|
|
|
|
|
Ids::pool_definition(),
|
|
|
|
|
|
Ids::current_tick_account(),
|
|
|
|
|
|
Ids::price_observations(window_duration),
|
|
|
|
|
|
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
|
|
|
|
|
],
|
|
|
|
|
|
vec![],
|
|
|
|
|
|
instruction,
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
|
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
|
|
|
|
|
state.transition_from_public_transaction(&tx, 0, 0)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-23 17:28:22 +02:00
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
fn execute_create_oracle_price_account(
|
|
|
|
|
|
state: &mut V03State,
|
|
|
|
|
|
window_duration: u64,
|
|
|
|
|
|
) -> Result<(), NssaError> {
|
|
|
|
|
|
let instruction = amm_core::Instruction::CreateOraclePriceAccount { window_duration };
|
|
|
|
|
|
|
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
|
Ids::amm_program(),
|
|
|
|
|
|
vec![
|
|
|
|
|
|
Ids::config(),
|
|
|
|
|
|
Ids::pool_definition(),
|
|
|
|
|
|
Ids::oracle_price_account(window_duration),
|
|
|
|
|
|
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
|
|
|
|
|
],
|
|
|
|
|
|
vec![],
|
|
|
|
|
|
instruction,
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
|
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
|
|
|
|
|
state.transition_from_public_transaction(&tx, 0, 0)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
#[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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-18 14:07:04 +02:00
|
|
|
|
/// 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.
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
fn state_with_pool_created_via_new_definition() -> V03State {
|
|
|
|
|
|
let mut state = state_for_amm_tests_with_new_def();
|
|
|
|
|
|
state.force_insert_account(Ids::vault_a(), Accounts::vault_a_reinitializable());
|
|
|
|
|
|
state.force_insert_account(Ids::vault_b(), Accounts::vault_b_reinitializable());
|
|
|
|
|
|
execute_new_definition(&mut state, Balances::fee_tier());
|
|
|
|
|
|
state
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 23:15:10 -03:00
|
|
|
|
fn fungible_balance(account: &Account) -> u128 {
|
|
|
|
|
|
let holding = TokenHolding::try_from(&account.data).expect("expected token holding");
|
|
|
|
|
|
let TokenHolding::Fungible {
|
|
|
|
|
|
definition_id: _,
|
|
|
|
|
|
balance,
|
|
|
|
|
|
} = holding
|
|
|
|
|
|
else {
|
|
|
|
|
|
panic!("expected fungible token holding")
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
balance
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn pool_definition(account: &Account) -> PoolDefinition {
|
|
|
|
|
|
PoolDefinition::try_from(&account.data).expect("expected pool definition")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn fungible_total_supply(account: &Account) -> u128 {
|
|
|
|
|
|
let definition = TokenDefinition::try_from(&account.data).expect("expected token definition");
|
|
|
|
|
|
let TokenDefinition::Fungible {
|
|
|
|
|
|
name: _,
|
|
|
|
|
|
total_supply,
|
|
|
|
|
|
metadata_id: _,
|
|
|
|
|
|
} = definition
|
|
|
|
|
|
else {
|
|
|
|
|
|
panic!("expected fungible token definition")
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
total_supply
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_initialize_creates_config_account() {
|
|
|
|
|
|
let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0);
|
|
|
|
|
|
deploy_programs(&mut state);
|
|
|
|
|
|
|
|
|
|
|
|
// Before initialization the config PDA does not exist.
|
|
|
|
|
|
assert_eq!(state.get_account_by_id(Ids::config()), Account::default());
|
|
|
|
|
|
|
|
|
|
|
|
execute_initialize(&mut state);
|
|
|
|
|
|
|
|
|
|
|
|
// Initialization creates the config PDA, owned by the AMM program.
|
|
|
|
|
|
let config_account = state.get_account_by_id(Ids::config());
|
|
|
|
|
|
assert_eq!(config_account, Accounts::config());
|
|
|
|
|
|
|
2026-06-18 16:46:38 +02:00
|
|
|
|
// Explicitly assert the stored Token Program ID and admin authority round-trip from the
|
|
|
|
|
|
// instruction arguments.
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
let config = amm_core::AmmConfig::try_from(&config_account.data)
|
|
|
|
|
|
.expect("config account must hold a valid AmmConfig");
|
|
|
|
|
|
assert_eq!(config.token_program_id, Ids::token_program());
|
2026-06-18 16:46:38 +02:00
|
|
|
|
assert_eq!(config.authority, Ids::admin());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
fn execute_update_config(
|
|
|
|
|
|
state: &mut V03State,
|
|
|
|
|
|
signer: &PrivateKey,
|
|
|
|
|
|
token_program_id: Option<nssa_core::program::ProgramId>,
|
2026-06-18 09:01:10 +02:00
|
|
|
|
twap_oracle_program_id: Option<nssa_core::program::ProgramId>,
|
2026-06-18 16:46:38 +02:00
|
|
|
|
new_authority: Option<AccountId>,
|
|
|
|
|
|
) -> Result<(), NssaError> {
|
|
|
|
|
|
let signer_id = AccountId::from(&PublicKey::new_from_private_key(signer));
|
|
|
|
|
|
let instruction = amm_core::Instruction::UpdateConfig {
|
|
|
|
|
|
token_program_id,
|
2026-06-18 09:01:10 +02:00
|
|
|
|
twap_oracle_program_id,
|
2026-06-18 16:46:38 +02:00
|
|
|
|
new_authority,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
|
Ids::amm_program(),
|
|
|
|
|
|
vec![Ids::config(), signer_id],
|
|
|
|
|
|
vec![current_nonce(state, signer_id)],
|
|
|
|
|
|
instruction,
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[signer]);
|
|
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
|
|
|
|
|
state.transition_from_public_transaction(&tx, 0, 0)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn config_data(state: &V03State) -> amm_core::AmmConfig {
|
|
|
|
|
|
amm_core::AmmConfig::try_from(&state.get_account_by_id(Ids::config()).data)
|
|
|
|
|
|
.expect("config account must hold a valid AmmConfig")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn initialized_amm_state() -> V03State {
|
|
|
|
|
|
let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0);
|
|
|
|
|
|
deploy_programs(&mut state);
|
|
|
|
|
|
execute_initialize(&mut state);
|
|
|
|
|
|
state
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_update_config_changes_token_program_id_and_authority() {
|
|
|
|
|
|
let mut state = initialized_amm_state();
|
|
|
|
|
|
|
|
|
|
|
|
let new_token_program = [123u32; 8];
|
|
|
|
|
|
let new_admin = Ids::user_a();
|
|
|
|
|
|
|
|
|
|
|
|
execute_update_config(
|
|
|
|
|
|
&mut state,
|
|
|
|
|
|
&Keys::admin(),
|
|
|
|
|
|
Some(new_token_program),
|
2026-06-18 09:01:10 +02:00
|
|
|
|
None,
|
2026-06-18 16:46:38 +02:00
|
|
|
|
Some(new_admin),
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let config = config_data(&state);
|
|
|
|
|
|
assert_eq!(config.token_program_id, new_token_program);
|
|
|
|
|
|
assert_eq!(config.authority, new_admin);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_update_config_rejects_non_admin() {
|
|
|
|
|
|
let mut state = initialized_amm_state();
|
|
|
|
|
|
|
|
|
|
|
|
// user_a is not the admin; even though they sign, the update is rejected and the config is
|
|
|
|
|
|
// left unchanged.
|
2026-06-18 09:01:10 +02:00
|
|
|
|
let result = execute_update_config(&mut state, &Keys::user_a(), Some([123u32; 8]), None, None);
|
2026-06-18 16:46:38 +02:00
|
|
|
|
assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_))));
|
|
|
|
|
|
|
|
|
|
|
|
let config = config_data(&state);
|
|
|
|
|
|
assert_eq!(config.token_program_id, Ids::token_program());
|
|
|
|
|
|
assert_eq!(config.authority, Ids::admin());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_update_config_authority_handoff_revokes_old_admin() {
|
|
|
|
|
|
let mut state = initialized_amm_state();
|
|
|
|
|
|
let new_admin = Ids::user_a();
|
|
|
|
|
|
|
|
|
|
|
|
// Admin hands off control to user_a.
|
2026-06-18 09:01:10 +02:00
|
|
|
|
execute_update_config(&mut state, &Keys::admin(), None, None, Some(new_admin)).unwrap();
|
2026-06-18 16:46:38 +02:00
|
|
|
|
assert_eq!(config_data(&state).authority, new_admin);
|
|
|
|
|
|
|
|
|
|
|
|
// The original admin can no longer update.
|
2026-06-18 09:01:10 +02:00
|
|
|
|
let result = execute_update_config(&mut state, &Keys::admin(), Some([123u32; 8]), None, None);
|
2026-06-18 16:46:38 +02:00
|
|
|
|
assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_))));
|
|
|
|
|
|
|
|
|
|
|
|
// The new admin can.
|
2026-06-18 09:01:10 +02:00
|
|
|
|
execute_update_config(&mut state, &Keys::user_a(), Some([124u32; 8]), None, None).unwrap();
|
2026-06-18 16:46:38 +02:00
|
|
|
|
assert_eq!(config_data(&state).token_program_id, [124u32; 8]);
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-18 09:01:10 +02:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_creates_price_observations_on_twap_oracle() {
|
2026-06-18 14:07:04 +02:00
|
|
|
|
let mut state = state_with_pool_created_via_new_definition();
|
2026-06-18 09:01:10 +02:00
|
|
|
|
let window_duration = 24 * 60 * 60 * 1_000u64;
|
|
|
|
|
|
|
2026-06-18 14:07:04 +02:00
|
|
|
|
// The current-tick account created during pool creation supplies the authoritative seed tick,
|
|
|
|
|
|
// derived from the opening reserves (reserve_b / reserve_a as a Q64.64 spot price).
|
|
|
|
|
|
let expected_tick = twap_oracle_core::price_to_tick(amm_core::spot_price_q64_64(
|
|
|
|
|
|
Balances::vault_a_init(),
|
|
|
|
|
|
Balances::vault_b_init(),
|
|
|
|
|
|
));
|
2026-06-18 09:01:10 +02:00
|
|
|
|
|
|
|
|
|
|
// The observations PDA does not exist before the AMM creates it.
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::price_observations(window_duration)),
|
|
|
|
|
|
Account::default()
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
execute_create_price_observations(&mut state, window_duration).unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
// The observations account now exists, is owned by the TWAP oracle program, and is seeded
|
|
|
|
|
|
// with the pool as its price source and the tick read from the current-tick account.
|
|
|
|
|
|
let account = state.get_account_by_id(Ids::price_observations(window_duration));
|
|
|
|
|
|
assert_ne!(account, Account::default());
|
|
|
|
|
|
assert_eq!(account.program_owner, Ids::twap_oracle_program());
|
|
|
|
|
|
|
|
|
|
|
|
let feed = twap_oracle_core::PriceObservations::try_from(&account.data)
|
|
|
|
|
|
.expect("observations account must hold a valid PriceObservations");
|
|
|
|
|
|
assert_eq!(feed.price_source_id, Ids::pool_definition());
|
2026-06-18 14:07:04 +02:00
|
|
|
|
assert_eq!(feed.last_recorded_tick, expected_tick);
|
2026-06-18 09:01:10 +02:00
|
|
|
|
assert_eq!(feed.write_index, 1);
|
|
|
|
|
|
assert_eq!(feed.total_entries, 1);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
feed.entries.len(),
|
|
|
|
|
|
usize::try_from(twap_oracle_core::OBSERVATIONS_CAPACITY)
|
|
|
|
|
|
.expect("OBSERVATIONS_CAPACITY fits in usize")
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// The AMM config and pool are left unchanged by the operation.
|
|
|
|
|
|
assert_eq!(state.get_account_by_id(Ids::config()), Accounts::config());
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::pool_definition()),
|
2026-06-18 14:07:04 +02:00
|
|
|
|
Accounts::pool_definition_new_init()
|
2026-06-18 09:01:10 +02:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-23 17:28:22 +02:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_creates_oracle_price_account_on_twap_oracle() {
|
|
|
|
|
|
let mut state = state_with_pool_created_via_new_definition();
|
|
|
|
|
|
let window_duration = 24 * 60 * 60 * 1_000u64;
|
|
|
|
|
|
|
|
|
|
|
|
// CreateOraclePriceAccount rejects a zero clock timestamp, so advance the clock first.
|
|
|
|
|
|
let now = 1_700_000_000_000u64;
|
|
|
|
|
|
advance_clock(&mut state, now);
|
|
|
|
|
|
|
|
|
|
|
|
// The price-account PDA does not exist before the AMM creates it.
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::oracle_price_account(window_duration)),
|
|
|
|
|
|
Account::default()
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
execute_create_oracle_price_account(&mut state, window_duration).unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
// The price account now exists, is owned by the TWAP oracle program, and is seeded with the
|
|
|
|
|
|
// pool as its source, the pool's asset pair, and the pool's current spot price (reserve_b /
|
|
|
|
|
|
// reserve_a as a Q64.64) — all derived on-chain, none caller-supplied — stamped with the clock.
|
|
|
|
|
|
let account = state.get_account_by_id(Ids::oracle_price_account(window_duration));
|
|
|
|
|
|
assert_ne!(account, Account::default());
|
|
|
|
|
|
assert_eq!(account.program_owner, Ids::twap_oracle_program());
|
|
|
|
|
|
|
|
|
|
|
|
let price = twap_oracle_core::OraclePriceAccount::try_from(&account.data)
|
|
|
|
|
|
.expect("price account must hold a valid OraclePriceAccount");
|
|
|
|
|
|
assert_eq!(price.source_id, Ids::pool_definition());
|
|
|
|
|
|
assert_eq!(price.base_asset, Ids::token_a_definition());
|
|
|
|
|
|
assert_eq!(price.quote_asset, Ids::token_b_definition());
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
price.price,
|
|
|
|
|
|
amm_core::spot_price_q64_64(Balances::vault_a_init(), Balances::vault_b_init())
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(price.timestamp, now);
|
|
|
|
|
|
assert_eq!(price.confidence_interval, 0);
|
|
|
|
|
|
|
|
|
|
|
|
// The AMM config and pool are left unchanged by the operation.
|
|
|
|
|
|
assert_eq!(state.get_account_by_id(Ids::config()), Accounts::config());
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::pool_definition()),
|
|
|
|
|
|
Accounts::pool_definition_new_init()
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_create_oracle_price_account_rejects_existing_account() {
|
|
|
|
|
|
let mut state = state_with_pool_created_via_new_definition();
|
|
|
|
|
|
let window_duration = 24 * 60 * 60 * 1_000u64;
|
|
|
|
|
|
advance_clock(&mut state, 1_700_000_000_000u64);
|
|
|
|
|
|
|
|
|
|
|
|
// First creation succeeds.
|
|
|
|
|
|
execute_create_oracle_price_account(&mut state, window_duration).unwrap();
|
|
|
|
|
|
let after_first = state.get_account_by_id(Ids::oracle_price_account(window_duration));
|
|
|
|
|
|
|
|
|
|
|
|
// A second creation for the same (pool, window) is rejected and leaves the account intact.
|
|
|
|
|
|
let result = execute_create_oracle_price_account(&mut state, window_duration);
|
|
|
|
|
|
assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_))));
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::oracle_price_account(window_duration)),
|
|
|
|
|
|
after_first
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-18 09:01:10 +02:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_create_price_observations_rejects_existing_account() {
|
2026-06-18 14:07:04 +02:00
|
|
|
|
let mut state = state_with_pool_created_via_new_definition();
|
2026-06-18 09:01:10 +02:00
|
|
|
|
let window_duration = 24 * 60 * 60 * 1_000u64;
|
|
|
|
|
|
|
|
|
|
|
|
// First creation succeeds.
|
|
|
|
|
|
execute_create_price_observations(&mut state, window_duration).unwrap();
|
|
|
|
|
|
let feed_after_first = twap_oracle_core::PriceObservations::try_from(
|
|
|
|
|
|
&state
|
|
|
|
|
|
.get_account_by_id(Ids::price_observations(window_duration))
|
|
|
|
|
|
.data,
|
|
|
|
|
|
)
|
|
|
|
|
|
.expect("observations account must hold a valid PriceObservations");
|
|
|
|
|
|
|
|
|
|
|
|
// A second creation for the same (pool, window) is rejected because the observations account
|
|
|
|
|
|
// already exists, and leaves the existing account intact.
|
|
|
|
|
|
let result = execute_create_price_observations(&mut state, window_duration);
|
|
|
|
|
|
assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_))));
|
|
|
|
|
|
let feed_after_second = twap_oracle_core::PriceObservations::try_from(
|
|
|
|
|
|
&state
|
|
|
|
|
|
.get_account_by_id(Ids::price_observations(window_duration))
|
|
|
|
|
|
.data,
|
|
|
|
|
|
)
|
|
|
|
|
|
.expect("observations account must hold a valid PriceObservations");
|
|
|
|
|
|
assert_eq!(feed_after_first, feed_after_second);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
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
|
|
|
|
// 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());
|
|
|
|
|
|
|
2026-06-18 09:01:10 +02:00
|
|
|
|
let result = execute_create_price_observations(&mut state, window_duration);
|
|
|
|
|
|
assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_))));
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::price_observations(window_duration)),
|
|
|
|
|
|
Account::default()
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-23 15:02:27 +02:00
|
|
|
|
/// Advances the canonical 1-block clock to `timestamp` by submitting a clock transaction, mirroring
|
|
|
|
|
|
/// how the sequencer ticks the clock between blocks. `RecordTick` reads this account, so the TWAP
|
|
|
|
|
|
/// tests use it to simulate the passage of time between observations.
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
fn advance_clock(state: &mut V03State, timestamp: u64) {
|
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
|
nssa::program::Program::clock().id(),
|
|
|
|
|
|
nssa::CLOCK_PROGRAM_ACCOUNT_IDS.to_vec(),
|
|
|
|
|
|
vec![],
|
|
|
|
|
|
timestamp,
|
|
|
|
|
|
)
|
|
|
|
|
|
.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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Calls the TWAP oracle's permissionless `RecordTick` directly (it is not wrapped by the AMM),
|
|
|
|
|
|
/// folding the pool's current tick into its observations ring buffer for the given window.
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
fn execute_record_tick(state: &mut V03State, window_duration: u64) -> Result<(), NssaError> {
|
|
|
|
|
|
let instruction = twap_oracle_core::Instruction::RecordTick {
|
|
|
|
|
|
price_source_id: Ids::pool_definition(),
|
|
|
|
|
|
window_duration,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
|
Ids::twap_oracle_program(),
|
|
|
|
|
|
vec![
|
|
|
|
|
|
Ids::price_observations(window_duration),
|
|
|
|
|
|
Ids::current_tick_account(),
|
|
|
|
|
|
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
|
|
|
|
|
],
|
|
|
|
|
|
vec![],
|
|
|
|
|
|
instruction,
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
|
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
|
|
|
|
|
state.transition_from_public_transaction(&tx, 0, 0)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
fn read_observations(
|
|
|
|
|
|
state: &V03State,
|
|
|
|
|
|
window_duration: u64,
|
|
|
|
|
|
) -> twap_oracle_core::PriceObservations {
|
|
|
|
|
|
twap_oracle_core::PriceObservations::try_from(
|
|
|
|
|
|
&state
|
|
|
|
|
|
.get_account_by_id(Ids::price_observations(window_duration))
|
|
|
|
|
|
.data,
|
|
|
|
|
|
)
|
|
|
|
|
|
.expect("observations account must hold a valid PriceObservations")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
fn read_current_tick(state: &V03State) -> i32 {
|
2026-06-23 17:00:33 +02:00
|
|
|
|
read_current_tick_account(state).tick
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
fn read_current_tick_account(state: &V03State) -> twap_oracle_core::CurrentTickAccount {
|
2026-06-23 15:02:27 +02:00
|
|
|
|
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")
|
2026-06-23 17:00:33 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
fn read_oracle_price(
|
|
|
|
|
|
state: &V03State,
|
|
|
|
|
|
window_duration: u64,
|
|
|
|
|
|
) -> twap_oracle_core::OraclePriceAccount {
|
|
|
|
|
|
twap_oracle_core::OraclePriceAccount::try_from(
|
|
|
|
|
|
&state
|
|
|
|
|
|
.get_account_by_id(Ids::oracle_price_account(window_duration))
|
|
|
|
|
|
.data,
|
|
|
|
|
|
)
|
|
|
|
|
|
.expect("oracle price account must hold a valid OraclePriceAccount")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Calls the oracle's permissionless `PublishPrice` directly (the AMM does not wrap it): computes
|
|
|
|
|
|
/// the TWAP from the pool's observations — extrapolating the tail from the current tick — and
|
|
|
|
|
|
/// writes it to the oracle price account.
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
fn execute_publish_price(state: &mut V03State, window_duration: u64) -> Result<(), NssaError> {
|
|
|
|
|
|
let instruction = twap_oracle_core::Instruction::PublishPrice {
|
|
|
|
|
|
price_source_id: Ids::pool_definition(),
|
|
|
|
|
|
window_duration,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
|
Ids::twap_oracle_program(),
|
|
|
|
|
|
vec![
|
|
|
|
|
|
Ids::price_observations(window_duration),
|
|
|
|
|
|
Ids::oracle_price_account(window_duration),
|
|
|
|
|
|
Ids::current_tick_account(),
|
|
|
|
|
|
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
|
|
|
|
|
],
|
|
|
|
|
|
vec![],
|
|
|
|
|
|
instruction,
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
|
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
|
|
|
|
|
state.transition_from_public_transaction(&tx, 0, 0)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Builds a state whose pool feed already holds several observations: creates the observations
|
|
|
|
|
|
/// account, then (advance clock, swap, record) three times at 60s spacing — above the sampling
|
|
|
|
|
|
/// guard's ~42s minimum, so every record is accepted. The clock ends at 180_000 and the buffer
|
|
|
|
|
|
/// holds 4 entries (1 from creation + 3 recorded). Shared scaffolding for the `PublishPrice` tests.
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
fn state_with_recorded_window(window_duration: u64) -> V03State {
|
|
|
|
|
|
let mut state = state_for_amm_tests();
|
|
|
|
|
|
execute_create_price_observations(&mut state, window_duration).unwrap();
|
|
|
|
|
|
for (step, swap) in [1u64, 2, 3]
|
|
|
|
|
|
.into_iter()
|
|
|
|
|
|
.zip([Swap::AtoB, Swap::AtoB, Swap::BtoA])
|
|
|
|
|
|
{
|
|
|
|
|
|
advance_clock(&mut state, step * 60_000);
|
|
|
|
|
|
match swap {
|
|
|
|
|
|
Swap::AtoB => execute_swap_a_to_b(&mut state, 1_000, 1),
|
|
|
|
|
|
Swap::BtoA => execute_swap_b_to_a(&mut state, 1_000, 1),
|
|
|
|
|
|
}
|
|
|
|
|
|
execute_record_tick(&mut state, window_duration).unwrap();
|
|
|
|
|
|
}
|
|
|
|
|
|
state
|
2026-06-23 15:02:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// End-to-end TWAP accumulation: a pool's price moves over time through real swaps, the oracle
|
|
|
|
|
|
/// folds each new tick into its observations buffer via `RecordTick`, and the time-weighted average
|
|
|
|
|
|
/// recovered from two snapshots matches the expected arithmetic mean of the intervening ticks.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// This is the headline path the rest of the suite never exercises: every other test stops at the
|
|
|
|
|
|
/// freshly-created buffer (`write_index == 1`), so the accumulator math and the consult subtraction
|
|
|
|
|
|
/// are only proven here, through the zkVM-facing instruction interface.
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_twap_observations_accumulate_across_swaps_and_yield_time_weighted_average() {
|
|
|
|
|
|
let mut state = state_for_amm_tests();
|
|
|
|
|
|
let window_duration = 24 * 60 * 60 * 1_000u64;
|
|
|
|
|
|
execute_create_price_observations(&mut state, window_duration).unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
// The creation observation sits at index 0 with a zero cumulative and the genesis timestamp.
|
|
|
|
|
|
let created = read_observations(&state, window_duration);
|
|
|
|
|
|
assert_eq!(created.write_index, 1);
|
|
|
|
|
|
assert_eq!(created.total_entries, 1);
|
|
|
|
|
|
assert_eq!(created.entries[0].timestamp, 0);
|
|
|
|
|
|
assert_eq!(created.entries[0].tick_cumulative, 0);
|
|
|
|
|
|
|
|
|
|
|
|
// Each step advances the clock by a fixed interval, moves the price with a swap, then records
|
|
|
|
|
|
// the resulting tick. The interval (60s) is above the sampling guard's minimum
|
|
|
|
|
|
// (window / OBSERVATIONS_CAPACITY ≈ 42s), so every record is accepted.
|
|
|
|
|
|
let step_ms = 60_000u64;
|
|
|
|
|
|
let mut prev_timestamp = 0u64;
|
|
|
|
|
|
let mut prev_cumulative = 0i64;
|
|
|
|
|
|
let mut prev_recorded_tick = created.last_recorded_tick;
|
|
|
|
|
|
let mut snapshots: Vec<(u64, i32, i64)> = Vec::new();
|
|
|
|
|
|
|
|
|
|
|
|
for (step, do_swap) in [1u64, 2, 3].into_iter().zip([
|
|
|
|
|
|
// a->b, a->b (price keeps falling), then b->a (price rebounds), so the recorded ticks vary
|
|
|
|
|
|
// in both directions across the window.
|
|
|
|
|
|
Swap::AtoB,
|
|
|
|
|
|
Swap::AtoB,
|
|
|
|
|
|
Swap::BtoA,
|
|
|
|
|
|
]) {
|
|
|
|
|
|
let now = step * step_ms;
|
|
|
|
|
|
advance_clock(&mut state, now);
|
|
|
|
|
|
match do_swap {
|
|
|
|
|
|
Swap::AtoB => execute_swap_a_to_b(&mut state, 1_000, 1),
|
|
|
|
|
|
Swap::BtoA => execute_swap_b_to_a(&mut state, 1_000, 1),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let current_tick = read_current_tick(&state);
|
|
|
|
|
|
// Keep moves within the per-observation clamp so the integrated tick equals the raw tick;
|
|
|
|
|
|
// the clamping path itself is covered by the oracle's unit tests.
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
(current_tick - prev_recorded_tick).abs() <= twap_oracle_core::MAX_TICK_DELTA,
|
|
|
|
|
|
"swap move must stay within MAX_TICK_DELTA for this test's exact-equality assertions"
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
execute_record_tick(&mut state, window_duration).unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let elapsed = i64::try_from(now - prev_timestamp).unwrap();
|
|
|
|
|
|
let expected_cumulative = prev_cumulative + i64::from(current_tick) * elapsed;
|
|
|
|
|
|
|
|
|
|
|
|
let obs = read_observations(&state, window_duration);
|
|
|
|
|
|
let written_index = usize::try_from(step).unwrap();
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
obs.total_entries,
|
|
|
|
|
|
step + 1,
|
|
|
|
|
|
"each accepted record appends exactly one entry"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(obs.write_index, u32::try_from(step + 1).unwrap());
|
|
|
|
|
|
assert_eq!(obs.entries[written_index].timestamp, now);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
obs.entries[written_index].tick_cumulative, expected_cumulative,
|
|
|
|
|
|
"cumulative must advance by tick × elapsed_ms"
|
|
|
|
|
|
);
|
|
|
|
|
|
// last_recorded_tick tracks the raw tick for the next delta computation.
|
|
|
|
|
|
assert_eq!(obs.last_recorded_tick, current_tick);
|
|
|
|
|
|
|
|
|
|
|
|
snapshots.push((now, current_tick, expected_cumulative));
|
|
|
|
|
|
prev_timestamp = now;
|
|
|
|
|
|
prev_cumulative = expected_cumulative;
|
|
|
|
|
|
prev_recorded_tick = current_tick;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Consult the oracle the way a consumer would: the arithmetic-mean tick over [t1, t3] is the
|
|
|
|
|
|
// difference of the two cumulative snapshots divided by the elapsed time.
|
|
|
|
|
|
let (t1, _tick1, cum1) = snapshots[0];
|
|
|
|
|
|
let (t3, _tick3, cum3) = snapshots[2];
|
|
|
|
|
|
let time_weighted_tick = (cum3 - cum1) / i64::try_from(t3 - t1).unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
// With equal 60s intervals the time-weighted mean reduces to the plain average of the ticks
|
|
|
|
|
|
// recorded at t2 and t3 (the two increments inside the (t1, t3] window).
|
|
|
|
|
|
let tick2 = snapshots[1].1;
|
|
|
|
|
|
let tick3 = snapshots[2].1;
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
time_weighted_tick,
|
|
|
|
|
|
(i64::from(tick2) + i64::from(tick3)) / 2
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Sanity: the average lies between the extremes it was built from.
|
|
|
|
|
|
let lo = i64::from(tick2.min(tick3));
|
|
|
|
|
|
let hi = i64::from(tick2.max(tick3));
|
|
|
|
|
|
assert!((lo..=hi).contains(&time_weighted_tick));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// `RecordTick` is permissionless and may be called on every block; its sampling guard silently
|
|
|
|
|
|
/// skips writes until `window / OBSERVATIONS_CAPACITY` ms have elapsed. This drives that guard
|
|
|
|
|
|
/// through the real instruction path: a too-soon call is a no-op that also leaves the delta
|
|
|
|
|
|
/// baseline untouched, and a later call past the interval resumes recording.
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_twap_record_tick_sampling_guard_skips_calls_below_min_interval() {
|
|
|
|
|
|
let mut state = state_for_amm_tests();
|
|
|
|
|
|
let window_duration = 24 * 60 * 60 * 1_000u64;
|
|
|
|
|
|
execute_create_price_observations(&mut state, window_duration).unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let baseline = read_observations(&state, window_duration);
|
|
|
|
|
|
let min_interval = window_duration / u64::from(twap_oracle_core::OBSERVATIONS_CAPACITY);
|
|
|
|
|
|
|
|
|
|
|
|
// Move the price, then record well within the minimum interval: the guard must skip the write.
|
|
|
|
|
|
advance_clock(&mut state, min_interval - 1);
|
|
|
|
|
|
execute_swap_a_to_b(&mut state, 1_000, 1);
|
|
|
|
|
|
execute_record_tick(&mut state, window_duration).unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let after_skip = read_observations(&state, window_duration);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
after_skip.write_index, baseline.write_index,
|
|
|
|
|
|
"a too-soon record must not advance the ring buffer"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(after_skip.total_entries, baseline.total_entries);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
after_skip.last_recorded_tick, baseline.last_recorded_tick,
|
|
|
|
|
|
"the skipped record must not move the delta baseline either"
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Past the minimum interval the same call resumes recording.
|
|
|
|
|
|
advance_clock(&mut state, min_interval + 1);
|
|
|
|
|
|
let current_tick = read_current_tick(&state);
|
|
|
|
|
|
execute_record_tick(&mut state, window_duration).unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let after_write = read_observations(&state, window_duration);
|
|
|
|
|
|
assert_eq!(after_write.write_index, baseline.write_index + 1);
|
|
|
|
|
|
assert_eq!(after_write.total_entries, baseline.total_entries + 1);
|
|
|
|
|
|
assert_eq!(after_write.last_recorded_tick, current_tick);
|
|
|
|
|
|
let written = usize::try_from(baseline.write_index).unwrap();
|
|
|
|
|
|
assert_eq!(after_write.entries[written].timestamp, min_interval + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
#[derive(Clone, Copy)]
|
|
|
|
|
|
enum Swap {
|
|
|
|
|
|
AtoB,
|
|
|
|
|
|
BtoA,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-23 17:00:33 +02:00
|
|
|
|
/// `CreateOraclePriceAccount` end-to-end through the zkVM: a signing account acts as the authorized
|
|
|
|
|
|
/// price source (the AMM does not route this for a pool-owned source), and the oracle claims and
|
|
|
|
|
|
/// initializes the consumer-facing [`twap_oracle_core::OraclePriceAccount`] PDA.
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_twap_create_oracle_price_account() {
|
|
|
|
|
|
let mut state = state_for_amm_tests();
|
|
|
|
|
|
let window_duration = 24 * 60 * 60 * 1_000u64;
|
|
|
|
|
|
|
|
|
|
|
|
// The price source must be authorized; a signing user provides that (a pool PDA cannot sign).
|
|
|
|
|
|
let source = Ids::user_a();
|
|
|
|
|
|
let price_account_id = twap_oracle_core::compute_oracle_price_account_pda(
|
|
|
|
|
|
Ids::twap_oracle_program(),
|
|
|
|
|
|
source,
|
|
|
|
|
|
window_duration,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// CreateOraclePriceAccount rejects a zero clock timestamp, so move the clock forward first.
|
|
|
|
|
|
advance_clock(&mut state, 5_000);
|
|
|
|
|
|
|
|
|
|
|
|
let initial_price = 1u128 << 64; // Q64.64 1.0
|
|
|
|
|
|
let instruction = twap_oracle_core::Instruction::CreateOraclePriceAccount {
|
|
|
|
|
|
base_asset: Ids::token_a_definition(),
|
|
|
|
|
|
quote_asset: Ids::token_b_definition(),
|
|
|
|
|
|
initial_price,
|
|
|
|
|
|
window_duration,
|
|
|
|
|
|
};
|
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
|
Ids::twap_oracle_program(),
|
|
|
|
|
|
vec![price_account_id, source, CLOCK_01_PROGRAM_ACCOUNT_ID],
|
|
|
|
|
|
vec![current_nonce(&state, source)],
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
|
|
let account = state.get_account_by_id(price_account_id);
|
|
|
|
|
|
assert_eq!(account.program_owner, Ids::twap_oracle_program());
|
|
|
|
|
|
let price = twap_oracle_core::OraclePriceAccount::try_from(&account.data)
|
|
|
|
|
|
.expect("oracle price account must hold a valid OraclePriceAccount");
|
|
|
|
|
|
assert_eq!(price.price, initial_price);
|
|
|
|
|
|
assert_eq!(price.timestamp, 5_000);
|
|
|
|
|
|
assert_eq!(price.source_id, source);
|
|
|
|
|
|
assert_eq!(price.base_asset, Ids::token_a_definition());
|
|
|
|
|
|
assert_eq!(price.quote_asset, Ids::token_b_definition());
|
|
|
|
|
|
assert_eq!(price.confidence_interval, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// End-to-end publish: with the clock at the newest observation the tail is empty, so the published
|
|
|
|
|
|
/// price is exactly the stored-window arithmetic-mean tick, converted to a `Q64.64` price. Proves
|
|
|
|
|
|
/// the full pipeline — observations built by real swaps + `RecordTick`, then consumed by
|
|
|
|
|
|
/// `PublishPrice` — composes through the zkVM-facing interface.
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_twap_publish_price_publishes_window_average() {
|
|
|
|
|
|
let window_duration = 24 * 60 * 60 * 1_000u64;
|
|
|
|
|
|
let mut state = state_with_recorded_window(window_duration);
|
|
|
|
|
|
// Register the consumer-facing price account through the AMM (seeded with the pool's spot
|
|
|
|
|
|
// price); PublishPrice overwrites its price/timestamp below.
|
|
|
|
|
|
execute_create_oracle_price_account(&mut state, window_duration).unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
// The clock sits at the last record (180_000) == the newest observation's timestamp, so
|
|
|
|
|
|
// [t2, now] is empty and the TWAP is the average over [t1, t2].
|
|
|
|
|
|
let obs = read_observations(&state, window_duration);
|
|
|
|
|
|
assert_eq!(obs.total_entries, 4);
|
|
|
|
|
|
let t1 = &obs.entries[0];
|
|
|
|
|
|
let t2 = &obs.entries[usize::try_from(obs.write_index).unwrap() - 1];
|
|
|
|
|
|
let elapsed = i64::try_from(t2.timestamp - t1.timestamp).unwrap();
|
|
|
|
|
|
let expected_tick = (t2.tick_cumulative - t1.tick_cumulative).div_euclid(elapsed);
|
|
|
|
|
|
let expected_price =
|
|
|
|
|
|
twap_oracle_core::tick_to_oracle_price(i32::try_from(expected_tick).unwrap());
|
|
|
|
|
|
|
|
|
|
|
|
execute_publish_price(&mut state, window_duration).unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let published = read_oracle_price(&state, window_duration);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
published.timestamp, t2.timestamp,
|
|
|
|
|
|
"publish stamps the price with now"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(published.price, expected_price);
|
|
|
|
|
|
// Identity fields are untouched by publish.
|
|
|
|
|
|
assert_eq!(published.source_id, Ids::pool_definition());
|
|
|
|
|
|
assert_eq!(published.base_asset, Ids::token_a_definition());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// End-to-end publish with an elapsed tail: the clock advances past the newest observation without
|
|
|
|
|
|
/// a new record, so `PublishPrice` must project the accumulator to `now` from the current tick. The
|
|
|
|
|
|
/// published timestamp is `now` (a fresh price, not a stale window), and the value reflects the
|
|
|
|
|
|
/// extrapolated tail.
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_twap_publish_price_extrapolates_tail_to_now() {
|
|
|
|
|
|
let window_duration = 24 * 60 * 60 * 1_000u64;
|
|
|
|
|
|
let mut state = state_with_recorded_window(window_duration);
|
|
|
|
|
|
// Register the consumer-facing price account through the AMM (seeded with the pool's spot
|
|
|
|
|
|
// price); PublishPrice overwrites its price/timestamp below.
|
|
|
|
|
|
execute_create_oracle_price_account(&mut state, window_duration).unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
// Advance well past the newest observation (180_000) with no intervening record.
|
|
|
|
|
|
let now = 240_000u64;
|
|
|
|
|
|
advance_clock(&mut state, now);
|
|
|
|
|
|
|
|
|
|
|
|
// Reproduce the tail split (see twap_oracle::publish_price): [t2, boundary] carries the tick
|
|
|
|
|
|
// stored at t2, [boundary, now] carries the clamped live tick.
|
|
|
|
|
|
let obs = read_observations(&state, window_duration);
|
|
|
|
|
|
let ct = read_current_tick_account(&state);
|
|
|
|
|
|
let t1 = &obs.entries[0];
|
|
|
|
|
|
let t2 = &obs.entries[usize::try_from(obs.write_index).unwrap() - 1];
|
|
|
|
|
|
let boundary = ct.last_updated.clamp(t2.timestamp, now);
|
|
|
|
|
|
let pre_ms = i64::try_from(boundary - t2.timestamp).unwrap();
|
|
|
|
|
|
let post_ms = i64::try_from(now - boundary).unwrap();
|
|
|
|
|
|
let delta = (ct.tick - obs.last_recorded_tick).clamp(
|
|
|
|
|
|
-twap_oracle_core::MAX_TICK_DELTA,
|
|
|
|
|
|
twap_oracle_core::MAX_TICK_DELTA,
|
|
|
|
|
|
);
|
|
|
|
|
|
let clamped_tick = obs.last_recorded_tick + delta;
|
|
|
|
|
|
let cum_now = t2.tick_cumulative
|
|
|
|
|
|
+ i64::from(obs.last_recorded_tick) * pre_ms
|
|
|
|
|
|
+ i64::from(clamped_tick) * post_ms;
|
|
|
|
|
|
let elapsed = i64::try_from(now - t1.timestamp).unwrap();
|
|
|
|
|
|
let expected_tick = (cum_now - t1.tick_cumulative).div_euclid(elapsed);
|
|
|
|
|
|
let expected_price =
|
|
|
|
|
|
twap_oracle_core::tick_to_oracle_price(i32::try_from(expected_tick).unwrap());
|
|
|
|
|
|
|
|
|
|
|
|
execute_publish_price(&mut state, window_duration).unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let published = read_oracle_price(&state, window_duration);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
published.timestamp, now,
|
|
|
|
|
|
"publish must project the timestamp forward to now"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(published.price, expected_price);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// `PublishPrice` is a no-op when fewer than two observations exist: there is nothing to average,
|
|
|
|
|
|
/// so the price account is left untouched (consumers keep seeing its prior value).
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_twap_publish_price_noop_with_fewer_than_two_observations() {
|
|
|
|
|
|
let mut state = state_for_amm_tests();
|
|
|
|
|
|
let window_duration = 24 * 60 * 60 * 1_000u64;
|
|
|
|
|
|
|
|
|
|
|
|
// Register the feed and its price account through the AMM. The clock must be non-zero for
|
|
|
|
|
|
// CreateOraclePriceAccount, so advance it first; the observations feed keeps only its single
|
|
|
|
|
|
// creation entry (no RecordTick), so PublishPrice has nothing to average.
|
|
|
|
|
|
advance_clock(&mut state, 100_000);
|
|
|
|
|
|
execute_create_price_observations(&mut state, window_duration).unwrap(); // total_entries == 1
|
|
|
|
|
|
execute_create_oracle_price_account(&mut state, window_duration).unwrap();
|
|
|
|
|
|
let seeded = read_oracle_price(&state, window_duration);
|
|
|
|
|
|
|
|
|
|
|
|
// Time passes, but the feed still has only the creation observation.
|
|
|
|
|
|
advance_clock(&mut state, 500_000);
|
|
|
|
|
|
execute_publish_price(&mut state, window_duration).unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
read_oracle_price(&state, window_duration),
|
|
|
|
|
|
seeded,
|
|
|
|
|
|
"publish with < 2 observations must leave the price account unchanged"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 18:08:53 +01:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_remove_liquidity() {
|
|
|
|
|
|
let mut state = state_for_amm_tests();
|
|
|
|
|
|
|
|
|
|
|
|
let instruction = amm_core::Instruction::RemoveLiquidity {
|
|
|
|
|
|
remove_liquidity_amount: Balances::remove_lp(),
|
|
|
|
|
|
min_amount_to_remove_token_a: Balances::remove_min_a(),
|
|
|
|
|
|
min_amount_to_remove_token_b: Balances::remove_min_b(),
|
2026-04-23 17:19:15 +02:00
|
|
|
|
deadline: u64::MAX,
|
2026-03-17 18:08:53 +01:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
|
Ids::amm_program(),
|
|
|
|
|
|
vec![
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
Ids::config(),
|
2026-03-17 18:08:53 +01:00
|
|
|
|
Ids::pool_definition(),
|
|
|
|
|
|
Ids::vault_a(),
|
|
|
|
|
|
Ids::vault_b(),
|
|
|
|
|
|
Ids::token_lp_definition(),
|
|
|
|
|
|
Ids::user_a(),
|
|
|
|
|
|
Ids::user_b(),
|
|
|
|
|
|
Ids::user_lp(),
|
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
|
|
|
|
Ids::current_tick_account(),
|
|
|
|
|
|
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
2026-03-17 18:08:53 +01:00
|
|
|
|
],
|
|
|
|
|
|
vec![Nonce(0)],
|
|
|
|
|
|
instruction,
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_lp()]);
|
|
|
|
|
|
|
|
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
2026-04-15 14:55:04 -03:00
|
|
|
|
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
2026-03-17 18:08:53 +01:00
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::pool_definition()),
|
|
|
|
|
|
Accounts::pool_definition_remove()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::vault_a()),
|
|
|
|
|
|
Accounts::vault_a_remove()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::vault_b()),
|
|
|
|
|
|
Accounts::vault_b_remove()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::token_lp_definition()),
|
|
|
|
|
|
Accounts::token_lp_definition_remove()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::user_a()),
|
|
|
|
|
|
Accounts::user_a_holding_remove()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::user_b()),
|
|
|
|
|
|
Accounts::user_b_holding_remove()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::user_lp()),
|
|
|
|
|
|
Accounts::user_lp_holding_remove()
|
|
|
|
|
|
);
|
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
|
|
|
|
|
|
|
|
|
|
// 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);
|
2026-03-17 18:08:53 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 17:48:13 -03:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_remove_liquidity_insufficient_user_lp_fails() {
|
|
|
|
|
|
let mut state = state_for_amm_tests();
|
|
|
|
|
|
state.force_insert_account(Ids::user_lp(), Accounts::user_lp_holding_with_balance(500));
|
|
|
|
|
|
|
|
|
|
|
|
let instruction = amm_core::Instruction::RemoveLiquidity {
|
|
|
|
|
|
remove_liquidity_amount: Balances::remove_lp(),
|
|
|
|
|
|
min_amount_to_remove_token_a: Balances::remove_min_a(),
|
|
|
|
|
|
min_amount_to_remove_token_b: Balances::remove_min_b(),
|
2026-04-23 17:19:15 +02:00
|
|
|
|
deadline: u64::MAX,
|
2026-04-08 17:48:13 -03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
|
Ids::amm_program(),
|
|
|
|
|
|
vec![
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
Ids::config(),
|
2026-04-08 17:48:13 -03:00
|
|
|
|
Ids::pool_definition(),
|
|
|
|
|
|
Ids::vault_a(),
|
|
|
|
|
|
Ids::vault_b(),
|
|
|
|
|
|
Ids::token_lp_definition(),
|
|
|
|
|
|
Ids::user_a(),
|
|
|
|
|
|
Ids::user_b(),
|
|
|
|
|
|
Ids::user_lp(),
|
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
|
|
|
|
Ids::current_tick_account(),
|
|
|
|
|
|
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
2026-04-08 17:48:13 -03:00
|
|
|
|
],
|
|
|
|
|
|
vec![Nonce(0)],
|
|
|
|
|
|
instruction,
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_lp()]);
|
|
|
|
|
|
|
|
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
2026-04-15 14:55:04 -03:00
|
|
|
|
assert!(state.transition_from_public_transaction(&tx, 0, 0).is_err());
|
2026-04-08 17:48:13 -03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 18:08:53 +01:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_new_definition_uninitialized_pool() {
|
|
|
|
|
|
let mut state = state_for_amm_tests_with_new_def();
|
2026-04-10 15:43:13 -03:00
|
|
|
|
state.force_insert_account(Ids::vault_a(), Accounts::vault_a_reinitializable());
|
|
|
|
|
|
state.force_insert_account(Ids::vault_b(), Accounts::vault_b_reinitializable());
|
2026-03-17 18:08:53 +01:00
|
|
|
|
|
2026-03-31 20:45:57 -03:00
|
|
|
|
execute_new_definition(&mut state, Balances::fee_tier());
|
2026-03-17 18:08:53 +01:00
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::pool_definition()),
|
|
|
|
|
|
Accounts::pool_definition_new_init()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::vault_a()),
|
|
|
|
|
|
Accounts::vault_a_init()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::vault_b()),
|
|
|
|
|
|
Accounts::vault_b_init()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::token_lp_definition()),
|
|
|
|
|
|
Accounts::token_lp_definition_new_init()
|
|
|
|
|
|
);
|
2026-04-08 17:48:13 -03:00
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::lp_lock_holding()),
|
|
|
|
|
|
Accounts::lp_lock_holding_new_init()
|
|
|
|
|
|
);
|
2026-03-17 18:08:53 +01:00
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::user_a()),
|
|
|
|
|
|
Accounts::user_a_holding_new_init()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::user_b()),
|
|
|
|
|
|
Accounts::user_b_holding_new_init()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::user_lp()),
|
|
|
|
|
|
Accounts::user_lp_holding_new_init()
|
|
|
|
|
|
);
|
2026-06-18 14:07:04 +02:00
|
|
|
|
|
|
|
|
|
|
// Pool creation also created the pool's TWAP current-tick account (a chained call to the
|
|
|
|
|
|
// oracle), owned by the oracle program and seeded with the tick derived from the opening
|
|
|
|
|
|
// reserves (reserve_b / reserve_a as a Q64.64 spot price).
|
|
|
|
|
|
let current_tick = state.get_account_by_id(Ids::current_tick_account());
|
|
|
|
|
|
assert_eq!(current_tick.program_owner, Ids::twap_oracle_program());
|
|
|
|
|
|
let tick_account = twap_oracle_core::CurrentTickAccount::try_from(¤t_tick.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(),
|
|
|
|
|
|
Balances::vault_b_init(),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(tick_account.tick, expected_tick);
|
2026-03-17 18:08:53 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 14:55:04 -03:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_new_definition_without_user_lp_authorization_fails() {
|
|
|
|
|
|
let mut state = state_for_amm_tests_with_new_def();
|
|
|
|
|
|
state.force_insert_account(Ids::vault_a(), Accounts::vault_a_reinitializable());
|
|
|
|
|
|
state.force_insert_account(Ids::vault_b(), Accounts::vault_b_reinitializable());
|
|
|
|
|
|
|
|
|
|
|
|
let result = try_execute_new_definition(&mut state, Balances::fee_tier(), false);
|
|
|
|
|
|
|
|
|
|
|
|
assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_))));
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::pool_definition()),
|
|
|
|
|
|
Account::default()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::vault_a()),
|
|
|
|
|
|
Accounts::vault_a_reinitializable()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::vault_b()),
|
|
|
|
|
|
Accounts::vault_b_reinitializable()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::token_lp_definition()),
|
|
|
|
|
|
Account::default()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::lp_lock_holding()),
|
|
|
|
|
|
Account::default()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::user_a()),
|
|
|
|
|
|
Accounts::user_a_holding()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::user_b()),
|
|
|
|
|
|
Accounts::user_b_holding()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(state.get_account_by_id(Ids::user_lp()), Account::default());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_new_definition_precreated_zero_balance_user_lp() {
|
|
|
|
|
|
let mut state = state_for_amm_tests_with_precreated_user_lp_for_new_def();
|
|
|
|
|
|
state.force_insert_account(Ids::vault_a(), Accounts::vault_a_reinitializable());
|
|
|
|
|
|
state.force_insert_account(Ids::vault_b(), Accounts::vault_b_reinitializable());
|
|
|
|
|
|
|
|
|
|
|
|
try_execute_new_definition(&mut state, Balances::fee_tier(), false).unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::pool_definition()),
|
|
|
|
|
|
Accounts::pool_definition_new_init()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::vault_a()),
|
|
|
|
|
|
Accounts::vault_a_init()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::vault_b()),
|
|
|
|
|
|
Accounts::vault_b_init()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::token_lp_definition()),
|
|
|
|
|
|
Accounts::token_lp_definition_new_init()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::lp_lock_holding()),
|
|
|
|
|
|
Accounts::lp_lock_holding_new_init()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::user_a()),
|
|
|
|
|
|
Accounts::user_a_holding_new_init()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::user_b()),
|
|
|
|
|
|
Accounts::user_b_holding_new_init()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::user_lp()),
|
|
|
|
|
|
Accounts::user_lp_holding_new_init_precreated()
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 20:45:57 -03:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_new_definition_supports_all_fee_tiers() {
|
|
|
|
|
|
for fees in [
|
|
|
|
|
|
FEE_TIER_BPS_1,
|
|
|
|
|
|
FEE_TIER_BPS_5,
|
|
|
|
|
|
FEE_TIER_BPS_30,
|
|
|
|
|
|
FEE_TIER_BPS_100,
|
|
|
|
|
|
] {
|
|
|
|
|
|
let mut state = state_for_amm_tests_with_new_def();
|
2026-04-10 15:43:13 -03:00
|
|
|
|
state.force_insert_account(Ids::vault_a(), Accounts::vault_a_reinitializable());
|
|
|
|
|
|
state.force_insert_account(Ids::vault_b(), Accounts::vault_b_reinitializable());
|
2026-03-31 20:45:57 -03:00
|
|
|
|
|
|
|
|
|
|
execute_new_definition(&mut state, fees);
|
|
|
|
|
|
|
|
|
|
|
|
let pool_definition =
|
|
|
|
|
|
PoolDefinition::try_from(&state.get_account_by_id(Ids::pool_definition()).data)
|
|
|
|
|
|
.expect("new definition should create a valid pool");
|
|
|
|
|
|
assert_eq!(pool_definition.fees, fees);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_new_definition_rejects_unsupported_fee_tier_transaction() {
|
2026-04-15 14:55:04 -03:00
|
|
|
|
let mut state = state_for_amm_tests_with_precreated_user_lp_for_new_def();
|
2026-04-10 15:43:13 -03:00
|
|
|
|
state.force_insert_account(Ids::vault_a(), Accounts::vault_a_reinitializable());
|
|
|
|
|
|
state.force_insert_account(Ids::vault_b(), Accounts::vault_b_reinitializable());
|
|
|
|
|
|
state.force_insert_account(
|
|
|
|
|
|
Ids::pool_definition(),
|
|
|
|
|
|
Accounts::pool_definition_zero_supply_reinitializable(),
|
|
|
|
|
|
);
|
2026-03-31 20:45:57 -03:00
|
|
|
|
state.force_insert_account(
|
|
|
|
|
|
Ids::token_lp_definition(),
|
2026-04-10 15:43:13 -03:00
|
|
|
|
Accounts::token_lp_definition_reinitializable(),
|
2026-03-31 20:45:57 -03:00
|
|
|
|
);
|
|
|
|
|
|
|
2026-04-15 14:55:04 -03:00
|
|
|
|
let result = try_execute_new_definition(&mut state, 2, false);
|
2026-03-31 20:45:57 -03:00
|
|
|
|
|
|
|
|
|
|
assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_))));
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::pool_definition()),
|
2026-04-10 15:43:13 -03:00
|
|
|
|
Accounts::pool_definition_zero_supply_reinitializable()
|
2026-03-31 20:45:57 -03:00
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::vault_a()),
|
2026-04-10 15:43:13 -03:00
|
|
|
|
Accounts::vault_a_reinitializable()
|
2026-03-31 20:45:57 -03:00
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::vault_b()),
|
2026-04-10 15:43:13 -03:00
|
|
|
|
Accounts::vault_b_reinitializable()
|
2026-03-31 20:45:57 -03:00
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::token_lp_definition()),
|
2026-04-10 15:43:13 -03:00
|
|
|
|
Accounts::token_lp_definition_reinitializable()
|
2026-03-31 20:45:57 -03:00
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::user_a()),
|
|
|
|
|
|
Accounts::user_a_holding()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::user_b()),
|
|
|
|
|
|
Accounts::user_b_holding()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::user_lp()),
|
|
|
|
|
|
Accounts::user_lp_holding_init_zero()
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 18:08:53 +01:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_add_liquidity() {
|
|
|
|
|
|
let mut state = state_for_amm_tests();
|
|
|
|
|
|
|
|
|
|
|
|
let instruction = amm_core::Instruction::AddLiquidity {
|
|
|
|
|
|
min_amount_liquidity: Balances::add_min_lp(),
|
|
|
|
|
|
max_amount_to_add_token_a: Balances::add_max_a(),
|
|
|
|
|
|
max_amount_to_add_token_b: Balances::add_max_b(),
|
2026-04-23 17:19:15 +02:00
|
|
|
|
deadline: u64::MAX,
|
2026-03-17 18:08:53 +01:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
|
Ids::amm_program(),
|
|
|
|
|
|
vec![
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
Ids::config(),
|
2026-03-17 18:08:53 +01:00
|
|
|
|
Ids::pool_definition(),
|
|
|
|
|
|
Ids::vault_a(),
|
|
|
|
|
|
Ids::vault_b(),
|
|
|
|
|
|
Ids::token_lp_definition(),
|
|
|
|
|
|
Ids::user_a(),
|
|
|
|
|
|
Ids::user_b(),
|
|
|
|
|
|
Ids::user_lp(),
|
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
|
|
|
|
Ids::current_tick_account(),
|
|
|
|
|
|
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
2026-03-17 18:08:53 +01:00
|
|
|
|
],
|
|
|
|
|
|
vec![Nonce(0), Nonce(0)],
|
|
|
|
|
|
instruction,
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let witness_set =
|
|
|
|
|
|
public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a(), &Keys::user_b()]);
|
|
|
|
|
|
|
|
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
2026-04-15 14:55:04 -03:00
|
|
|
|
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
2026-03-17 18:08:53 +01:00
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::pool_definition()),
|
|
|
|
|
|
Accounts::pool_definition_add()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::vault_a()),
|
|
|
|
|
|
Accounts::vault_a_add()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::vault_b()),
|
|
|
|
|
|
Accounts::vault_b_add()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::token_lp_definition()),
|
|
|
|
|
|
Accounts::token_lp_definition_add()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::user_a()),
|
|
|
|
|
|
Accounts::user_a_holding_add()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::user_b()),
|
|
|
|
|
|
Accounts::user_b_holding_add()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::user_lp()),
|
|
|
|
|
|
Accounts::user_lp_holding_add()
|
|
|
|
|
|
);
|
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
|
|
|
|
|
|
|
|
|
|
// 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);
|
2026-03-17 18:08:53 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_swap_b_to_a() {
|
|
|
|
|
|
let mut state = state_for_amm_tests();
|
|
|
|
|
|
|
2026-04-07 09:31:32 +02:00
|
|
|
|
let instruction = amm_core::Instruction::SwapExactInput {
|
2026-03-17 18:08:53 +01:00
|
|
|
|
swap_amount_in: Balances::swap_amount_in(),
|
|
|
|
|
|
min_amount_out: Balances::swap_min_out(),
|
|
|
|
|
|
token_definition_id_in: Ids::token_b_definition(),
|
2026-04-23 17:19:15 +02:00
|
|
|
|
deadline: u64::MAX,
|
2026-03-17 18:08:53 +01:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
|
Ids::amm_program(),
|
|
|
|
|
|
vec![
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
Ids::config(),
|
2026-03-17 18:08:53 +01:00
|
|
|
|
Ids::pool_definition(),
|
|
|
|
|
|
Ids::vault_a(),
|
|
|
|
|
|
Ids::vault_b(),
|
|
|
|
|
|
Ids::user_a(),
|
|
|
|
|
|
Ids::user_b(),
|
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
|
|
|
|
Ids::current_tick_account(),
|
|
|
|
|
|
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
2026-03-17 18:08:53 +01:00
|
|
|
|
],
|
|
|
|
|
|
vec![Nonce(0)],
|
|
|
|
|
|
instruction,
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_b()]);
|
|
|
|
|
|
|
|
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
2026-04-15 14:55:04 -03:00
|
|
|
|
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
2026-03-17 18:08:53 +01:00
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::pool_definition()),
|
|
|
|
|
|
Accounts::pool_definition_swap_1()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::vault_a()),
|
|
|
|
|
|
Accounts::vault_a_swap_1()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::vault_b()),
|
|
|
|
|
|
Accounts::vault_b_swap_1()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::user_a()),
|
|
|
|
|
|
Accounts::user_a_holding_swap_1()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::user_b()),
|
|
|
|
|
|
Accounts::user_b_holding_swap_1()
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_swap_a_to_b() {
|
|
|
|
|
|
let mut state = state_for_amm_tests();
|
|
|
|
|
|
|
2026-04-07 09:31:32 +02:00
|
|
|
|
let instruction = amm_core::Instruction::SwapExactInput {
|
2026-03-17 18:08:53 +01:00
|
|
|
|
swap_amount_in: Balances::swap_amount_in(),
|
|
|
|
|
|
min_amount_out: Balances::swap_min_out(),
|
|
|
|
|
|
token_definition_id_in: Ids::token_a_definition(),
|
2026-04-23 17:19:15 +02:00
|
|
|
|
deadline: u64::MAX,
|
2026-03-17 18:08:53 +01:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
|
Ids::amm_program(),
|
|
|
|
|
|
vec![
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
Ids::config(),
|
2026-03-17 18:08:53 +01:00
|
|
|
|
Ids::pool_definition(),
|
|
|
|
|
|
Ids::vault_a(),
|
|
|
|
|
|
Ids::vault_b(),
|
|
|
|
|
|
Ids::user_a(),
|
|
|
|
|
|
Ids::user_b(),
|
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
|
|
|
|
Ids::current_tick_account(),
|
|
|
|
|
|
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
2026-03-17 18:08:53 +01:00
|
|
|
|
],
|
|
|
|
|
|
vec![Nonce(0)],
|
|
|
|
|
|
instruction,
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a()]);
|
|
|
|
|
|
|
|
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
2026-04-15 14:55:04 -03:00
|
|
|
|
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
2026-03-17 18:08:53 +01:00
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::pool_definition()),
|
|
|
|
|
|
Accounts::pool_definition_swap_2()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::vault_a()),
|
|
|
|
|
|
Accounts::vault_a_swap_2()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::vault_b()),
|
|
|
|
|
|
Accounts::vault_b_swap_2()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::user_a()),
|
|
|
|
|
|
Accounts::user_a_holding_swap_2()
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
state.get_account_by_id(Ids::user_b()),
|
|
|
|
|
|
Accounts::user_b_holding_swap_2()
|
|
|
|
|
|
);
|
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 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);
|
2026-03-17 18:08:53 +01:00
|
|
|
|
}
|
2026-03-31 23:15:10 -03:00
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_fee_accumulates_across_multiple_swaps_and_pays_out_on_remove() {
|
|
|
|
|
|
let mut state = state_for_amm_tests();
|
|
|
|
|
|
|
|
|
|
|
|
execute_swap_a_to_b(&mut state, 1_000, 200);
|
|
|
|
|
|
execute_swap_b_to_a(&mut state, 1_000, 200);
|
|
|
|
|
|
|
|
|
|
|
|
let pool_before_remove = pool_definition(&state.get_account_by_id(Ids::pool_definition()));
|
|
|
|
|
|
assert_eq!(pool_before_remove.reserve_a, 4_060);
|
|
|
|
|
|
assert_eq!(pool_before_remove.reserve_b, 3_085);
|
|
|
|
|
|
assert_eq!(pool_before_remove.fees, Balances::fee_tier());
|
|
|
|
|
|
|
|
|
|
|
|
let vault_a_before_remove = fungible_balance(&state.get_account_by_id(Ids::vault_a()));
|
|
|
|
|
|
let vault_b_before_remove = fungible_balance(&state.get_account_by_id(Ids::vault_b()));
|
|
|
|
|
|
assert_eq!(vault_a_before_remove, 4_060);
|
|
|
|
|
|
assert_eq!(vault_b_before_remove, 3_085);
|
|
|
|
|
|
assert_eq!(vault_a_before_remove, pool_before_remove.reserve_a);
|
|
|
|
|
|
assert_eq!(vault_b_before_remove, pool_before_remove.reserve_b);
|
|
|
|
|
|
|
|
|
|
|
|
execute_remove_liquidity(&mut state, 1_000, 812, 617);
|
|
|
|
|
|
|
|
|
|
|
|
let pool_after_remove = pool_definition(&state.get_account_by_id(Ids::pool_definition()));
|
|
|
|
|
|
assert_eq!(pool_after_remove.reserve_a, 3_248);
|
|
|
|
|
|
assert_eq!(pool_after_remove.reserve_b, 2_468);
|
|
|
|
|
|
assert_eq!(pool_after_remove.liquidity_pool_supply, 4_000);
|
|
|
|
|
|
|
|
|
|
|
|
let vault_a_after_remove = fungible_balance(&state.get_account_by_id(Ids::vault_a()));
|
|
|
|
|
|
let vault_b_after_remove = fungible_balance(&state.get_account_by_id(Ids::vault_b()));
|
|
|
|
|
|
assert_eq!(vault_a_after_remove, 3_248);
|
|
|
|
|
|
assert_eq!(vault_b_after_remove, 2_468);
|
|
|
|
|
|
assert_eq!(vault_a_after_remove, pool_after_remove.reserve_a);
|
|
|
|
|
|
assert_eq!(vault_b_after_remove, pool_after_remove.reserve_b);
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
fungible_balance(&state.get_account_by_id(Ids::user_a())),
|
|
|
|
|
|
11_752
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
fungible_balance(&state.get_account_by_id(Ids::user_b())),
|
|
|
|
|
|
10_032
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
fungible_balance(&state.get_account_by_id(Ids::user_lp())),
|
|
|
|
|
|
1_000
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
fungible_total_supply(&state.get_account_by_id(Ids::token_lp_definition())),
|
|
|
|
|
|
4_000
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 17:19:15 +02:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_swap_rejects_expired_deadline() {
|
|
|
|
|
|
let mut state = state_for_amm_tests();
|
|
|
|
|
|
|
|
|
|
|
|
let deadline_ms = 1_000u64;
|
|
|
|
|
|
let block_timestamp_ms = 2_000u64;
|
|
|
|
|
|
|
|
|
|
|
|
let instruction = amm_core::Instruction::SwapExactInput {
|
|
|
|
|
|
swap_amount_in: Balances::swap_amount_in(),
|
|
|
|
|
|
min_amount_out: Balances::swap_min_out(),
|
|
|
|
|
|
token_definition_id_in: Ids::token_a_definition(),
|
|
|
|
|
|
deadline: deadline_ms,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
|
Ids::amm_program(),
|
|
|
|
|
|
vec![
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
Ids::config(),
|
2026-04-23 17:19:15 +02:00
|
|
|
|
Ids::pool_definition(),
|
|
|
|
|
|
Ids::vault_a(),
|
|
|
|
|
|
Ids::vault_b(),
|
|
|
|
|
|
Ids::user_a(),
|
|
|
|
|
|
Ids::user_b(),
|
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
|
|
|
|
Ids::current_tick_account(),
|
|
|
|
|
|
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
2026-04-23 17:19:15 +02:00
|
|
|
|
],
|
|
|
|
|
|
vec![Nonce(0)],
|
|
|
|
|
|
instruction,
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a()]);
|
|
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
|
state.transition_from_public_transaction(&tx, 0, block_timestamp_ms),
|
|
|
|
|
|
Err(NssaError::OutOfValidityWindow)
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_swap_exact_output_rejects_expired_deadline() {
|
|
|
|
|
|
let mut state = state_for_amm_tests();
|
|
|
|
|
|
|
|
|
|
|
|
let deadline_ms = 1_000u64;
|
|
|
|
|
|
let block_timestamp_ms = 2_000u64;
|
|
|
|
|
|
|
|
|
|
|
|
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: deadline_ms,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
|
Ids::amm_program(),
|
|
|
|
|
|
vec![
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
Ids::config(),
|
2026-04-23 17:19:15 +02:00
|
|
|
|
Ids::pool_definition(),
|
|
|
|
|
|
Ids::vault_a(),
|
|
|
|
|
|
Ids::vault_b(),
|
|
|
|
|
|
Ids::user_a(),
|
|
|
|
|
|
Ids::user_b(),
|
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
|
|
|
|
Ids::current_tick_account(),
|
|
|
|
|
|
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
2026-04-23 17:19:15 +02:00
|
|
|
|
],
|
|
|
|
|
|
vec![current_nonce(&state, Ids::user_a())],
|
|
|
|
|
|
instruction,
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a()]);
|
|
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
|
state.transition_from_public_transaction(&tx, 0, block_timestamp_ms),
|
|
|
|
|
|
Err(NssaError::OutOfValidityWindow)
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_add_liquidity_rejects_expired_deadline() {
|
|
|
|
|
|
let mut state = state_for_amm_tests();
|
|
|
|
|
|
|
|
|
|
|
|
let deadline_ms = 1_000u64;
|
|
|
|
|
|
let block_timestamp_ms = 2_000u64;
|
|
|
|
|
|
|
|
|
|
|
|
let instruction = amm_core::Instruction::AddLiquidity {
|
|
|
|
|
|
min_amount_liquidity: Balances::add_min_lp(),
|
|
|
|
|
|
max_amount_to_add_token_a: Balances::add_max_a(),
|
|
|
|
|
|
max_amount_to_add_token_b: Balances::add_max_b(),
|
|
|
|
|
|
deadline: deadline_ms,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
|
Ids::amm_program(),
|
|
|
|
|
|
vec![
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
Ids::config(),
|
2026-04-23 17:19:15 +02:00
|
|
|
|
Ids::pool_definition(),
|
|
|
|
|
|
Ids::vault_a(),
|
|
|
|
|
|
Ids::vault_b(),
|
|
|
|
|
|
Ids::token_lp_definition(),
|
|
|
|
|
|
Ids::user_a(),
|
|
|
|
|
|
Ids::user_b(),
|
|
|
|
|
|
Ids::user_lp(),
|
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
|
|
|
|
Ids::current_tick_account(),
|
|
|
|
|
|
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
2026-04-23 17:19:15 +02:00
|
|
|
|
],
|
|
|
|
|
|
vec![
|
|
|
|
|
|
current_nonce(&state, Ids::user_a()),
|
|
|
|
|
|
current_nonce(&state, Ids::user_b()),
|
|
|
|
|
|
],
|
|
|
|
|
|
instruction,
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let witness_set =
|
|
|
|
|
|
public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a(), &Keys::user_b()]);
|
|
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
|
state.transition_from_public_transaction(&tx, 0, block_timestamp_ms),
|
|
|
|
|
|
Err(NssaError::OutOfValidityWindow)
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_remove_liquidity_rejects_expired_deadline() {
|
|
|
|
|
|
let mut state = state_for_amm_tests();
|
|
|
|
|
|
|
|
|
|
|
|
let deadline_ms = 1_000u64;
|
|
|
|
|
|
let block_timestamp_ms = 2_000u64;
|
|
|
|
|
|
|
|
|
|
|
|
let instruction = amm_core::Instruction::RemoveLiquidity {
|
|
|
|
|
|
remove_liquidity_amount: Balances::remove_lp(),
|
|
|
|
|
|
min_amount_to_remove_token_a: Balances::remove_min_a(),
|
|
|
|
|
|
min_amount_to_remove_token_b: Balances::remove_min_b(),
|
|
|
|
|
|
deadline: deadline_ms,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
|
Ids::amm_program(),
|
|
|
|
|
|
vec![
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
Ids::config(),
|
2026-04-23 17:19:15 +02:00
|
|
|
|
Ids::pool_definition(),
|
|
|
|
|
|
Ids::vault_a(),
|
|
|
|
|
|
Ids::vault_b(),
|
|
|
|
|
|
Ids::token_lp_definition(),
|
|
|
|
|
|
Ids::user_a(),
|
|
|
|
|
|
Ids::user_b(),
|
|
|
|
|
|
Ids::user_lp(),
|
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
|
|
|
|
Ids::current_tick_account(),
|
|
|
|
|
|
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
2026-04-23 17:19:15 +02:00
|
|
|
|
],
|
|
|
|
|
|
vec![current_nonce(&state, Ids::user_lp())],
|
|
|
|
|
|
instruction,
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_lp()]);
|
|
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
|
state.transition_from_public_transaction(&tx, 0, block_timestamp_ms),
|
|
|
|
|
|
Err(NssaError::OutOfValidityWindow)
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_new_definition_rejects_expired_deadline() {
|
|
|
|
|
|
let mut state = state_for_amm_tests_with_precreated_user_lp_for_new_def();
|
|
|
|
|
|
|
|
|
|
|
|
let deadline_ms = 1_000u64;
|
|
|
|
|
|
let block_timestamp_ms = 2_000u64;
|
|
|
|
|
|
|
|
|
|
|
|
let instruction = amm_core::Instruction::NewDefinition {
|
|
|
|
|
|
token_a_amount: Balances::vault_a_init(),
|
|
|
|
|
|
token_b_amount: Balances::vault_b_init(),
|
|
|
|
|
|
fees: amm_core::FEE_TIER_BPS_30,
|
|
|
|
|
|
deadline: deadline_ms,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
|
Ids::amm_program(),
|
|
|
|
|
|
vec![
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
Ids::config(),
|
2026-04-23 17:19:15 +02:00
|
|
|
|
Ids::pool_definition(),
|
|
|
|
|
|
Ids::vault_a(),
|
|
|
|
|
|
Ids::vault_b(),
|
|
|
|
|
|
Ids::token_lp_definition(),
|
|
|
|
|
|
Ids::lp_lock_holding(),
|
|
|
|
|
|
Ids::user_a(),
|
|
|
|
|
|
Ids::user_b(),
|
|
|
|
|
|
Ids::user_lp(),
|
2026-06-18 14:07:04 +02:00
|
|
|
|
Ids::current_tick_account(),
|
|
|
|
|
|
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
2026-04-23 17:19:15 +02:00
|
|
|
|
],
|
|
|
|
|
|
vec![
|
|
|
|
|
|
current_nonce(&state, Ids::user_a()),
|
|
|
|
|
|
current_nonce(&state, Ids::user_b()),
|
|
|
|
|
|
current_nonce(&state, Ids::user_lp()),
|
|
|
|
|
|
],
|
|
|
|
|
|
instruction,
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(
|
|
|
|
|
|
&message,
|
|
|
|
|
|
&[&Keys::user_a(), &Keys::user_b(), &Keys::user_lp()],
|
|
|
|
|
|
);
|
|
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
|
state.transition_from_public_transaction(&tx, 0, block_timestamp_ms),
|
|
|
|
|
|
Err(NssaError::OutOfValidityWindow)
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 23:15:10 -03:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn amm_add_liquidity_after_fee_accrual() {
|
|
|
|
|
|
let mut state = state_for_amm_tests();
|
|
|
|
|
|
|
|
|
|
|
|
execute_swap_a_to_b(&mut state, 1_000, 200);
|
|
|
|
|
|
execute_swap_b_to_a(&mut state, 1_000, 200);
|
|
|
|
|
|
execute_swap_a_to_b(&mut state, 1_000, 200);
|
|
|
|
|
|
execute_swap_b_to_a(&mut state, 1_000, 200);
|
|
|
|
|
|
|
|
|
|
|
|
let pool_before_add = pool_definition(&state.get_account_by_id(Ids::pool_definition()));
|
|
|
|
|
|
let vault_a_before_add = fungible_balance(&state.get_account_by_id(Ids::vault_a()));
|
|
|
|
|
|
let vault_b_before_add = fungible_balance(&state.get_account_by_id(Ids::vault_b()));
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(pool_before_add.reserve_a, 3_608);
|
|
|
|
|
|
assert_eq!(pool_before_add.reserve_b, 3_477);
|
|
|
|
|
|
assert_eq!(vault_a_before_add, pool_before_add.reserve_a);
|
|
|
|
|
|
assert_eq!(vault_b_before_add, pool_before_add.reserve_b);
|
|
|
|
|
|
|
|
|
|
|
|
execute_add_liquidity(&mut state, 1_436, 2_000, 1_000);
|
|
|
|
|
|
|
|
|
|
|
|
let pool_after_add = pool_definition(&state.get_account_by_id(Ids::pool_definition()));
|
|
|
|
|
|
let vault_a_after_add = fungible_balance(&state.get_account_by_id(Ids::vault_a()));
|
|
|
|
|
|
let vault_b_after_add = fungible_balance(&state.get_account_by_id(Ids::vault_b()));
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(pool_after_add.reserve_a, 4_645);
|
|
|
|
|
|
assert_eq!(pool_after_add.reserve_b, 4_477);
|
|
|
|
|
|
assert_eq!(pool_after_add.liquidity_pool_supply, 6_437);
|
|
|
|
|
|
assert_eq!(vault_a_after_add, pool_after_add.reserve_a);
|
|
|
|
|
|
assert_eq!(vault_b_after_add, pool_after_add.reserve_b);
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
fungible_balance(&state.get_account_by_id(Ids::user_a())),
|
|
|
|
|
|
10_355
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
fungible_balance(&state.get_account_by_id(Ids::user_b())),
|
|
|
|
|
|
8_023
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
fungible_balance(&state.get_account_by_id(Ids::user_lp())),
|
|
|
|
|
|
3_437
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
fungible_total_supply(&state.get_account_by_id(Ids::token_lp_definition())),
|
|
|
|
|
|
6_437
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|