feat(amm): bootstrap pool TWAP current-tick account at pool creation

Extend new_definition to also create the pool's TWAP current-tick account
via a chained CreateCurrentTickAccount, so a pool and its price feed are
born together. The opening tick is derived on-chain from the pool's own
reserves (reserve_b / reserve_a as Q64.64), not caller-supplied, so it
cannot be forged. The pool is passed in its post-claim state and authorized
as the price source via its pool PDA seed.

Add spot_price_q64_64 to amm_core (not the oracle): the reserves -> price
mapping is the price source's concern; the oracle only converts price to a
tick.
This commit is contained in:
r4bbit 2026-06-18 14:07:04 +02:00
parent 4e4338945d
commit b997ca678e
9 changed files with 281 additions and 35 deletions

2
Cargo.lock generated
View File

@ -80,9 +80,11 @@ dependencies = [
name = "amm_core"
version = "0.1.0"
dependencies = [
"alloy-primitives",
"borsh",
"nssa_core",
"risc0-zkvm",
"ruint",
"serde",
"spel-framework-macros",
"token_core",

View File

@ -161,6 +161,18 @@
"writable": false,
"signer": false,
"init": false
},
{
"name": "current_tick_account",
"writable": false,
"signer": false,
"init": false
},
{
"name": "clock",
"writable": false,
"signer": false,
"init": false
}
],
"args": [

View File

@ -13,3 +13,8 @@ token_core = { path = "../../token/core" }
borsh = { version = "1.5", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
risc0-zkvm = { version = "=3.0.5", default-features = false }
alloy-primitives = { version = "1", default-features = false }
# Pin ruint (transitive via alloy-primitives) below 1.18, which raised its MSRV to rustc 1.90.
# The risc0 guest toolchain ships rustc 1.88, so 1.18+ fails the guest build. 1.17.0 (MSRV 1.85)
# is the newest compatible release. Remove this pin once the risc0 toolchain advances past 1.90.
ruint = { version = "=1.17.0", default-features = false }

View File

@ -222,6 +222,35 @@ pub fn assert_supported_fee_tier(fees: u128) {
);
}
/// Computes a `Q64.64` spot price (`reserve_quote` per `reserve_base`) from raw pool reserves.
///
/// This is the constant-product AMM's spot price (`reserve_quote / reserve_base`) expressed as a
/// `Q64.64` fixed-point value: `(reserve_quote / reserve_base) * 2^64`. It is computed in 256-bit
/// precision and saturates at `u128::MAX` if the ratio exceeds the representable range. The TWAP
/// oracle consumes exactly this representation (it converts the `Q64.64` price to a tick), so the
/// AMM owns the reserves → price mapping and the oracle stays agnostic to how the price is formed.
///
/// # Panics
/// Panics if `reserve_base` is zero.
#[must_use]
pub fn spot_price_q64_64(reserve_base: u128, reserve_quote: u128) -> u128 {
use alloy_primitives::U256;
assert!(
reserve_base != 0,
"spot_price_q64_64: reserve_base must be non-zero"
);
let numerator = U256::from(reserve_quote)
.checked_shl(64)
.expect("reserve_quote < 2^128, so reserve_quote << 64 fits in U256");
let price = numerator
.checked_div(U256::from(reserve_base))
.expect("reserve_base is non-zero after the assertion above");
u128::try_from(price).unwrap_or(u128::MAX)
}
impl TryFrom<&Data> for PoolDefinition {
type Error = std::io::Error;
@ -434,3 +463,45 @@ pub fn read_vault_fungible_balances(
(vault_a_balance, vault_b_balance)
}
#[cfg(test)]
mod tests {
use super::*;
/// `1.0` in Q64.64 is `2^64`.
const ONE_Q64_64: u128 = 1u128 << 64;
#[test]
fn equal_reserves_map_to_unit_price() {
assert_eq!(spot_price_q64_64(1_000, 1_000), ONE_Q64_64);
}
#[test]
fn spot_price_reflects_reserve_ratio() {
// reserve_quote / reserve_base = 2.0 -> 2 * 2^64.
assert_eq!(spot_price_q64_64(1_000, 2_000), ONE_Q64_64 * 2);
// reserve_quote / reserve_base = 0.5 -> 2^64 / 2.
assert_eq!(spot_price_q64_64(2_000, 1_000), ONE_Q64_64 / 2);
}
#[test]
fn spot_price_saturates_instead_of_overflowing() {
// A huge quote-to-base ratio would exceed u128 in Q64.64; it must saturate, not panic.
assert_eq!(spot_price_q64_64(1, u128::MAX), u128::MAX);
}
#[test]
fn spot_price_handles_large_reserves_without_intermediate_overflow() {
// reserve_quote >= 2^64 would overflow a naive `reserve_quote << 64` in u128; the U256
// intermediate keeps it exact. Ratio here is 4.0.
let base = 1u128 << 64;
let quote = 1u128 << 66;
assert_eq!(spot_price_q64_64(base, quote), ONE_Q64_64 * 4);
}
#[test]
#[should_panic(expected = "reserve_base must be non-zero")]
fn zero_reserve_base_panics() {
let _ = spot_price_q64_64(0, 1_000);
}
}

View File

@ -85,9 +85,11 @@ dependencies = [
name = "amm_core"
version = "0.1.0"
dependencies = [
"alloy-primitives",
"borsh",
"nssa_core",
"risc0-zkvm",
"ruint",
"serde",
"spel-framework-macros",
"token_core",

View File

@ -118,6 +118,8 @@ mod amm {
user_holding_a: AccountWithMetadata,
user_holding_b: AccountWithMetadata,
user_holding_lp: AccountWithMetadata,
current_tick_account: AccountWithMetadata,
clock: AccountWithMetadata,
token_a_amount: u128,
token_b_amount: u128,
fees: u128,
@ -133,6 +135,8 @@ mod amm {
user_holding_a,
user_holding_b,
user_holding_lp,
current_tick_account,
clock,
NonZeroU128::new(token_a_amount).expect("token_a_amount must be nonzero"),
NonZeroU128::new(token_b_amount).expect("token_b_amount must be nonzero"),
fees,

View File

@ -4,13 +4,15 @@ use amm_core::{
assert_supported_fee_tier, compute_config_pda, compute_liquidity_token_pda,
compute_liquidity_token_pda_seed, compute_lp_lock_holding_pda,
compute_lp_lock_holding_pda_seed, compute_pool_pda, compute_pool_pda_seed, compute_vault_pda,
compute_vault_pda_seed, AmmConfig, PoolDefinition, MINIMUM_LIQUIDITY,
compute_vault_pda_seed, spot_price_q64_64, AmmConfig, PoolDefinition, MINIMUM_LIQUIDITY,
};
use clock_core::CLOCK_01_PROGRAM_ACCOUNT_ID;
use nssa_core::{
account::{Account, AccountWithMetadata, Data},
program::{AccountPostState, ChainedCall, Claim, ProgramId},
};
use token_core::TokenDefinition;
use twap_oracle_core::compute_current_tick_account_pda;
#[expect(
clippy::too_many_arguments,
@ -26,6 +28,8 @@ pub fn new_definition(
user_holding_a: AccountWithMetadata,
user_holding_b: AccountWithMetadata,
user_holding_lp: AccountWithMetadata,
current_tick_account: AccountWithMetadata,
clock: AccountWithMetadata,
token_a_amount: NonZeroU128,
token_b_amount: NonZeroU128,
fees: u128,
@ -45,9 +49,10 @@ pub fn new_definition(
compute_config_pda(amm_program_id),
"New definition: AMM config Account ID does not match PDA"
);
let token_program_id = AmmConfig::try_from(&config.account.data)
.expect("New definition: AMM Program must be initialized before use")
.token_program_id;
let config_data = AmmConfig::try_from(&config.account.data)
.expect("New definition: AMM Program must be initialized before use");
let token_program_id = config_data.token_program_id;
let twap_oracle_program_id = config_data.twap_oracle_program_id;
assert_eq!(
user_holding_a.account.program_owner, token_program_id,
@ -100,6 +105,18 @@ pub fn new_definition(
"Fresh user LP holding requires user authorization"
);
// The pool's TWAP current-tick account is created in the same transaction (a chained call to
// the oracle). Validate its PDA and that the clock is the canonical 1-block LEZ clock.
assert_eq!(
current_tick_account.account_id,
compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id),
"New definition: current tick Account ID does not match PDA"
);
assert_eq!(
clock.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID,
"New definition: clock account must be the canonical 1-block LEZ clock account"
);
// LP Token minting calculation
let initial_lp = token_a_amount
.get()
@ -115,7 +132,6 @@ pub fn new_definition(
.expect("initial liquidity must exceed minimum liquidity after validation");
// Update pool account
let mut pool_post = pool.account.clone();
let pool_post_definition = PoolDefinition {
definition_token_a_id,
definition_token_b_id,
@ -128,9 +144,10 @@ pub fn new_definition(
fees,
};
pool_post.data = Data::from(&pool_post_definition);
let mut pool_initialized = pool.account.clone();
pool_initialized.data = Data::from(&pool_post_definition);
let pool_post: AccountPostState = AccountPostState::new_claimed(
pool_post.clone(),
pool_initialized.clone(),
Claim::Pda(compute_pool_pda_seed(
definition_token_a_id,
definition_token_b_id,
@ -202,11 +219,41 @@ pub fn new_definition(
)
.with_pda_seeds(vec![compute_liquidity_token_pda_seed(pool.account_id)]);
// Chain call to create the pool's TWAP current-tick account, with the pool as the price
// source. The oracle derives the tick from the opening spot price (reserve_b / reserve_a as a
// Q64.64 ratio), so the seed value is taken from the pool's own reserves, not the caller.
//
// The pool is claimed (and thus owned by this program) by this same instruction, so the
// chained call must present the pool in its post-claim state to match the accumulated state
// diff: the runtime sets the claimed pool's owner to this program, so we predict that here.
let initial_price = spot_price_q64_64(token_a_amount.get(), token_b_amount.get());
let mut pool_price_source_account = pool_initialized;
pool_price_source_account.program_owner = amm_program_id;
let pool_price_source = AccountWithMetadata {
account: pool_price_source_account,
is_authorized: true,
account_id: pool.account_id,
};
let call_create_current_tick = ChainedCall::new(
twap_oracle_program_id,
vec![
current_tick_account.clone(),
pool_price_source,
clock.clone(),
],
&twap_oracle_core::Instruction::CreateCurrentTickAccount { initial_price },
)
.with_pda_seeds(vec![compute_pool_pda_seed(
definition_token_a_id,
definition_token_b_id,
)]);
let chained_calls = vec![
call_token_lp_lock,
call_token_lp_user,
call_token_b,
call_token_a,
call_create_current_tick,
];
let post_states = vec![
@ -219,6 +266,8 @@ pub fn new_definition(
AccountPostState::new(user_holding_a.account.clone()),
AccountPostState::new(user_holding_b.account.clone()),
AccountPostState::new(user_holding_lp.account.clone()),
AccountPostState::new(current_tick_account.account.clone()),
AccountPostState::new(clock.account.clone()),
];
(post_states, chained_calls)

View File

@ -565,6 +565,33 @@ impl ChainedCallForTests {
IdForTests::pool_definition_id(),
)])
}
fn cc_new_definition_create_current_tick() -> ChainedCall {
// The pool is passed to the oracle in its post-claim state: owned by the AMM program and
// carrying the freshly written PoolDefinition, authorized as the price source.
let mut pool_price_source = AccountForTests::pool_definition_init();
pool_price_source.account.program_owner = AMM_PROGRAM_ID;
pool_price_source.is_authorized = true;
let initial_price = amm_core::spot_price_q64_64(
BalanceForTests::vault_a_reserve_init(),
BalanceForTests::vault_b_reserve_init(),
);
ChainedCall::new(
TWAP_ORACLE_PROGRAM_ID,
vec![
AccountForTests::current_tick_account_uninit(),
pool_price_source,
AccountForTests::clock(),
],
&twap_oracle_core::Instruction::CreateCurrentTickAccount { initial_price },
)
.with_pda_seeds(vec![compute_pool_pda_seed(
IdForTests::token_a_definition_id(),
IdForTests::token_b_definition_id(),
)])
}
}
impl IdForTests {
@ -655,6 +682,27 @@ impl AccountWithMetadataForTests {
config
}
/// The pool's TWAP current-tick PDA, uninitialized (created by `new_definition`).
fn current_tick_account_uninit() -> AccountWithMetadata {
AccountWithMetadata {
account: Account::default(),
is_authorized: false,
account_id: twap_oracle_core::compute_current_tick_account_pda(
TWAP_ORACLE_PROGRAM_ID,
IdForTests::pool_definition_id(),
),
}
}
/// The canonical 1-block LEZ clock account.
fn clock() -> AccountWithMetadata {
AccountWithMetadata {
account: Account::default(),
is_authorized: false,
account_id: clock_core::CLOCK_01_PROGRAM_ACCOUNT_ID,
}
}
fn user_holding_a() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
@ -2049,6 +2097,8 @@ fn test_call_new_definition_with_zero_balance_1() {
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
AccountWithMetadataForTests::current_tick_account_uninit(),
AccountWithMetadataForTests::clock(),
NonZero::new(0).expect("Balances must be nonzero"),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
BalanceForTests::fee_tier(),
@ -2069,6 +2119,8 @@ fn test_call_new_definition_with_zero_balance_2() {
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
AccountWithMetadataForTests::current_tick_account_uninit(),
AccountWithMetadataForTests::clock(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(0).expect("Balances must be nonzero"),
BalanceForTests::fee_tier(),
@ -2089,6 +2141,8 @@ fn test_call_new_definition_same_token_definition() {
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
AccountWithMetadataForTests::current_tick_account_uninit(),
AccountWithMetadataForTests::clock(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
BalanceForTests::fee_tier(),
@ -2109,6 +2163,8 @@ fn test_call_new_definition_wrong_liquidity_id() {
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
AccountWithMetadataForTests::current_tick_account_uninit(),
AccountWithMetadataForTests::clock(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
BalanceForTests::fee_tier(),
@ -2129,6 +2185,8 @@ fn test_call_new_definition_wrong_lp_lock_holding_id() {
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
AccountWithMetadataForTests::current_tick_account_uninit(),
AccountWithMetadataForTests::clock(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
BalanceForTests::fee_tier(),
@ -2149,6 +2207,8 @@ fn test_call_new_definition_wrong_pool_id() {
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
AccountWithMetadataForTests::current_tick_account_uninit(),
AccountWithMetadataForTests::clock(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
BalanceForTests::fee_tier(),
@ -2169,6 +2229,8 @@ fn test_call_new_definition_wrong_vault_id_1() {
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
AccountWithMetadataForTests::current_tick_account_uninit(),
AccountWithMetadataForTests::clock(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
BalanceForTests::fee_tier(),
@ -2189,6 +2251,8 @@ fn test_call_new_definition_wrong_vault_id_2() {
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
AccountWithMetadataForTests::current_tick_account_uninit(),
AccountWithMetadataForTests::clock(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
BalanceForTests::fee_tier(),
@ -2210,6 +2274,8 @@ fn test_call_new_definition_rejects_initialized_pool() {
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
AccountWithMetadataForTests::current_tick_account_uninit(),
AccountWithMetadataForTests::clock(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
BalanceForTests::fee_tier(),
@ -2231,6 +2297,8 @@ fn test_call_new_definition_initial_lp_too_small() {
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
AccountWithMetadataForTests::current_tick_account_uninit(),
AccountWithMetadataForTests::clock(),
NonZero::new(MINIMUM_LIQUIDITY).unwrap(),
NonZero::new(MINIMUM_LIQUIDITY).unwrap(),
BalanceForTests::fee_tier(),
@ -2250,6 +2318,8 @@ fn test_call_new_definition_chained_call_successful() {
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
AccountWithMetadataForTests::current_tick_account_uninit(),
AccountWithMetadataForTests::clock(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
BalanceForTests::fee_tier(),
@ -2276,6 +2346,14 @@ fn test_call_new_definition_chained_call_successful() {
assert!(chained_call_b == ChainedCallForTests::cc_new_definition_token_b());
assert!(chained_call_lp_lock == ChainedCallForTests::cc_new_definition_token_lp_lock());
assert!(chained_call_lp_user == ChainedCallForTests::cc_new_definition_token_lp_user());
// The fifth chained call creates the pool's TWAP current-tick account, seeding the tick from
// the opening reserves.
assert_eq!(chained_calls.len(), 5);
assert!(chained_calls[4] == ChainedCallForTests::cc_new_definition_create_current_tick());
// Two extra post-states (current-tick + clock) are echoed back unchanged.
assert_eq!(post_states.len(), 11);
}
#[should_panic(expected = "AccountId is not a token type for the pool")]
@ -2957,6 +3035,8 @@ fn test_new_definition_lp_asymmetric_amounts() {
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
AccountWithMetadataForTests::current_tick_account_uninit(),
AccountWithMetadataForTests::clock(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
BalanceForTests::fee_tier(),
@ -2995,6 +3075,8 @@ fn test_new_definition_lp_symmetric_amounts() {
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
AccountWithMetadataForTests::current_tick_account_uninit(),
AccountWithMetadataForTests::clock(),
NonZero::new(token_a_amount).unwrap(),
NonZero::new(token_b_amount).unwrap(),
BalanceForTests::fee_tier(),
@ -3065,6 +3147,8 @@ fn test_minimum_liquidity_lock_and_remove_all_user_lp() {
AccountForTests::user_holding_a(),
AccountForTests::user_holding_b(),
AccountForTests::user_holding_lp_uninit(),
AccountWithMetadataForTests::current_tick_account_uninit(),
AccountWithMetadataForTests::clock(),
NonZero::new(token_a_amount).unwrap(),
NonZero::new(token_b_amount).unwrap(),
BalanceForTests::fee_tier(),
@ -3289,6 +3373,8 @@ fn new_definition_overflow_protection() {
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
AccountWithMetadataForTests::current_tick_account_uninit(),
AccountWithMetadataForTests::clock(),
NonZero::new(large_amount).unwrap(),
NonZero::new(2).unwrap(),
BalanceForTests::fee_tier(),
@ -3544,6 +3630,8 @@ fn test_new_definition_supports_all_fee_tiers() {
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
AccountWithMetadataForTests::current_tick_account_uninit(),
AccountWithMetadataForTests::clock(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
fees,
@ -3569,6 +3657,8 @@ fn test_new_definition_rejects_unsupported_fee_tier() {
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
AccountWithMetadataForTests::current_tick_account_uninit(),
AccountWithMetadataForTests::clock(),
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
2,

View File

@ -328,20 +328,6 @@ impl Accounts {
}
}
/// The pool's TWAP current-tick account, owned by the oracle program. Seeded directly into
/// state so the AMM has an authoritative tick to read when creating observations.
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: 1_700_000_000_000,
}),
nonce: Nonce(0),
}
}
fn user_a_holding() -> Account {
Account {
program_owner: Ids::token_program(),
@ -1055,6 +1041,8 @@ fn try_execute_new_definition(
Ids::user_a(),
Ids::user_b(),
Ids::user_lp(),
Ids::current_tick_account(),
CLOCK_01_PROGRAM_ACCOUNT_ID,
],
if authorize_user_lp {
vec![
@ -1276,6 +1264,18 @@ fn execute_create_price_observations(
state.transition_from_public_transaction(&tx, 0, 0)
}
/// 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
}
fn fungible_balance(account: &Account) -> u128 {
let holding = TokenHolding::try_from(&account.data).expect("expected token holding");
let TokenHolding::Fungible {
@ -1424,15 +1424,15 @@ fn amm_update_config_authority_handoff_revokes_old_admin() {
#[test]
fn amm_creates_price_observations_on_twap_oracle() {
let mut state = state_for_amm_tests();
let mut state = state_with_pool_created_via_new_definition();
let window_duration = 24 * 60 * 60 * 1_000u64;
let current_tick = 1_234_i32;
// The pool already has an authoritative current-tick account written by the oracle.
state.force_insert_account(
Ids::current_tick_account(),
Accounts::current_tick_account(current_tick),
);
// 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(),
));
// The observations PDA does not exist before the AMM creates it.
assert_eq!(
@ -1451,7 +1451,7 @@ fn amm_creates_price_observations_on_twap_oracle() {
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());
assert_eq!(feed.last_recorded_tick, current_tick);
assert_eq!(feed.last_recorded_tick, expected_tick);
assert_eq!(feed.write_index, 1);
assert_eq!(feed.total_entries, 1);
assert_eq!(
@ -1464,18 +1464,14 @@ fn amm_creates_price_observations_on_twap_oracle() {
assert_eq!(state.get_account_by_id(Ids::config()), Accounts::config());
assert_eq!(
state.get_account_by_id(Ids::pool_definition()),
Accounts::pool_definition_init()
Accounts::pool_definition_new_init()
);
}
#[test]
fn amm_create_price_observations_rejects_existing_account() {
let mut state = state_for_amm_tests();
let mut state = state_with_pool_created_via_new_definition();
let window_duration = 24 * 60 * 60 * 1_000u64;
state.force_insert_account(
Ids::current_tick_account(),
Accounts::current_tick_account(1_234),
);
// First creation succeeds.
execute_create_price_observations(&mut state, window_duration).unwrap();
@ -1651,6 +1647,19 @@ fn amm_new_definition_uninitialized_pool() {
state.get_account_by_id(Ids::user_lp()),
Accounts::user_lp_holding_new_init()
);
// 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(&current_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);
}
#[test]
@ -2206,6 +2215,8 @@ fn amm_new_definition_rejects_expired_deadline() {
Ids::user_a(),
Ids::user_b(),
Ids::user_lp(),
Ids::current_tick_account(),
CLOCK_01_PROGRAM_ACCOUNT_ID,
],
vec![
current_nonce(&state, Ids::user_a()),