mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-26 22:51:14 +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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user