mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-06-28 11:10:08 +00:00
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:
parent
4e4338945d
commit
b997ca678e
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -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": [
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
2
programs/amm/methods/guest/Cargo.lock
generated
2
programs/amm/methods/guest/Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(¤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);
|
||||
}
|
||||
|
||||
#[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()),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user