mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-06-30 12:09:30 +00:00
feat(amm): create TWAP price observations on behalf of the pool
Add a `CreatePriceObservations` instruction that registers a TWAP price-observations account for a pool over a time window, via a chained call to the configured TWAP oracle program. The pool acts as the price source: the AMM authorizes it with its pool PDA seed so the oracle ties the feed to that pool. The feed's initial tick is read from the pool's authoritative `CurrentTickAccount` (validated against its pool-derived PDA) rather than being supplied by the caller, so the feed cannot be seeded at a forged price — mirroring what `RecordTick` does. The clock is verified to be the canonical 1-block LEZ clock, and creation is rejected if the observations account already exists. To support the chained call, `AmmConfig` and the `Initialize` instruction are extended with a `twap_oracle_program_id` that the instruction reads.
This commit is contained in:
parent
1d9e3dcb49
commit
4e4338945d
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -93,8 +93,10 @@ name = "amm_program"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"amm_core",
|
"amm_core",
|
||||||
|
"clock_core",
|
||||||
"nssa_core",
|
"nssa_core",
|
||||||
"token_core",
|
"token_core",
|
||||||
|
"twap_oracle_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1988,6 +1990,8 @@ dependencies = [
|
|||||||
"stablecoin_core",
|
"stablecoin_core",
|
||||||
"token-methods",
|
"token-methods",
|
||||||
"token_core",
|
"token_core",
|
||||||
|
"twap-oracle-methods",
|
||||||
|
"twap_oracle_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@ -17,6 +17,10 @@
|
|||||||
"name": "token_program_id",
|
"name": "token_program_id",
|
||||||
"type": "program_id"
|
"type": "program_id"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "twap_oracle_program_id",
|
||||||
|
"type": "program_id"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "authority",
|
"name": "authority",
|
||||||
"type": "account_id"
|
"type": "account_id"
|
||||||
@ -46,6 +50,12 @@
|
|||||||
"option": "program_id"
|
"option": "program_id"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "twap_oracle_program_id",
|
||||||
|
"type": {
|
||||||
|
"option": "program_id"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "new_authority",
|
"name": "new_authority",
|
||||||
"type": {
|
"type": {
|
||||||
@ -54,6 +64,47 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "create_price_observations",
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "config",
|
||||||
|
"writable": false,
|
||||||
|
"signer": false,
|
||||||
|
"init": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pool",
|
||||||
|
"writable": false,
|
||||||
|
"signer": false,
|
||||||
|
"init": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "current_tick_account",
|
||||||
|
"writable": false,
|
||||||
|
"signer": false,
|
||||||
|
"init": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "price_observations",
|
||||||
|
"writable": false,
|
||||||
|
"signer": false,
|
||||||
|
"init": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "clock",
|
||||||
|
"writable": false,
|
||||||
|
"signer": false,
|
||||||
|
"init": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "window_duration",
|
||||||
|
"type": "u64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "new_definition",
|
"name": "new_definition",
|
||||||
"accounts": [
|
"accounts": [
|
||||||
@ -470,6 +521,10 @@
|
|||||||
"name": "token_program_id",
|
"name": "token_program_id",
|
||||||
"type": "program_id"
|
"type": "program_id"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "twap_oracle_program_id",
|
||||||
|
"type": "program_id"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "authority",
|
"name": "authority",
|
||||||
"type": "account_id"
|
"type": "account_id"
|
||||||
|
|||||||
@ -8,5 +8,7 @@ workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc3", features = ["host"] }
|
nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc3", features = ["host"] }
|
||||||
|
clock_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc3" }
|
||||||
amm_core = { path = "core" }
|
amm_core = { path = "core" }
|
||||||
token_core = { path = "../token/core" }
|
token_core = { path = "../token/core" }
|
||||||
|
twap_oracle_core = { path = "../twap_oracle/core" }
|
||||||
|
|||||||
@ -19,17 +19,20 @@ pub enum Instruction {
|
|||||||
/// Initializes the AMM Program by creating its singleton configuration account.
|
/// Initializes the AMM Program by creating its singleton configuration account.
|
||||||
///
|
///
|
||||||
/// The configuration account is a PDA derived from the constant `"CONFIG"` seed
|
/// The configuration account is a PDA derived from the constant `"CONFIG"` seed
|
||||||
/// (`compute_config_pda(self_program_id)`). It stores the Token Program ID that the AMM
|
/// (`compute_config_pda(self_program_id)`). It stores the program IDs the AMM issues chained
|
||||||
/// uses for every chained call, plus the admin `authority` allowed to change configuration
|
/// calls to (the Token Program and the TWAP oracle program), plus the admin `authority`
|
||||||
/// later via `UpdateConfig`. The Program must be initialized via this instruction before
|
/// allowed to change configuration later via `UpdateConfig`. The Program must be initialized
|
||||||
/// any pool can be created or interacted with — the other instructions read the Token
|
/// via this instruction before any pool can be created or interacted with — the other
|
||||||
/// Program ID from this account and reject calls when it does not yet exist.
|
/// instructions read these program IDs from this account and reject calls when it does not
|
||||||
|
/// yet exist.
|
||||||
///
|
///
|
||||||
/// Required accounts:
|
/// Required accounts:
|
||||||
/// - AMM Config Account, uninitialized, derived as `compute_config_pda(self_program_id)`
|
/// - AMM Config Account, uninitialized, derived as `compute_config_pda(self_program_id)`
|
||||||
Initialize {
|
Initialize {
|
||||||
/// Program ID of the Token Program the AMM will issue chained calls to.
|
/// Program ID of the Token Program the AMM will issue chained calls to.
|
||||||
token_program_id: ProgramId,
|
token_program_id: ProgramId,
|
||||||
|
/// Program ID of the TWAP oracle program the AMM will issue chained calls to.
|
||||||
|
twap_oracle_program_id: ProgramId,
|
||||||
/// Admin authority allowed to change configuration via `UpdateConfig`.
|
/// Admin authority allowed to change configuration via `UpdateConfig`.
|
||||||
authority: AccountId,
|
authority: AccountId,
|
||||||
},
|
},
|
||||||
@ -46,10 +49,38 @@ pub enum Instruction {
|
|||||||
UpdateConfig {
|
UpdateConfig {
|
||||||
/// New Token Program ID for chained calls, or `None` to keep the current one.
|
/// New Token Program ID for chained calls, or `None` to keep the current one.
|
||||||
token_program_id: Option<ProgramId>,
|
token_program_id: Option<ProgramId>,
|
||||||
|
/// New TWAP oracle program ID for chained calls, or `None` to keep the current one.
|
||||||
|
twap_oracle_program_id: Option<ProgramId>,
|
||||||
/// New admin authority (transfers control), or `None` to keep the current admin.
|
/// New admin authority (transfers control), or `None` to keep the current admin.
|
||||||
new_authority: Option<AccountId>,
|
new_authority: Option<AccountId>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Creates a TWAP price-observations account for a pool over a time window, on behalf of the
|
||||||
|
/// AMM, via a chained call to the configured TWAP oracle program.
|
||||||
|
///
|
||||||
|
/// The pool acts as the price source: the AMM authorizes it (via its pool PDA seed) so the
|
||||||
|
/// oracle ties the observations account to this pool. The feed's initial tick is read from the
|
||||||
|
/// pool's [`CurrentTickAccount`](twap_oracle_core::CurrentTickAccount) — the authoritative
|
||||||
|
/// tick the AMM previously wrote — rather than being supplied by the caller, so the feed
|
||||||
|
/// cannot be seeded at a forged price. Rejects if the observations account already exists.
|
||||||
|
/// The clock must be the canonical 1-block LEZ clock.
|
||||||
|
///
|
||||||
|
/// Required accounts:
|
||||||
|
/// - AMM Config Account (initialized)
|
||||||
|
/// - AMM Pool (initialized; acts as the price source)
|
||||||
|
/// - Current Tick Account, the pool's initialized TWAP PDA derived as
|
||||||
|
/// `compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id)`; supplies the
|
||||||
|
/// initial tick
|
||||||
|
/// - Price Observations Account, uninitialized TWAP PDA derived as
|
||||||
|
/// `compute_price_observations_pda(twap_oracle_program_id, pool.account_id,
|
||||||
|
/// window_duration)`
|
||||||
|
/// - Clock Account (the canonical 1-block LEZ clock)
|
||||||
|
CreatePriceObservations {
|
||||||
|
/// Duration of the TWAP window this feed serves, in milliseconds. Part of the
|
||||||
|
/// observations PDA seed, so each window gets a distinct account.
|
||||||
|
window_duration: u64,
|
||||||
|
},
|
||||||
|
|
||||||
/// Initializes a new Pool (or re-initializes an existing zero-supply Pool).
|
/// Initializes a new Pool (or re-initializes an existing zero-supply Pool).
|
||||||
///
|
///
|
||||||
/// On initialization, `MINIMUM_LIQUIDITY` LP tokens are permanently locked
|
/// On initialization, `MINIMUM_LIQUIDITY` LP tokens are permanently locked
|
||||||
@ -223,6 +254,8 @@ impl From<&PoolDefinition> for Data {
|
|||||||
pub struct AmmConfig {
|
pub struct AmmConfig {
|
||||||
/// Program ID of the Token Program the AMM issues chained calls to.
|
/// Program ID of the Token Program the AMM issues chained calls to.
|
||||||
pub token_program_id: ProgramId,
|
pub token_program_id: ProgramId,
|
||||||
|
/// Program ID of the TWAP oracle program the AMM issues chained calls to.
|
||||||
|
pub twap_oracle_program_id: ProgramId,
|
||||||
/// Admin authority allowed to change this configuration via `UpdateConfig`.
|
/// Admin authority allowed to change this configuration via `UpdateConfig`.
|
||||||
pub authority: AccountId,
|
pub authority: AccountId,
|
||||||
}
|
}
|
||||||
|
|||||||
800
programs/amm/methods/guest/Cargo.lock
generated
800
programs/amm/methods/guest/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -29,11 +29,13 @@ mod amm {
|
|||||||
ctx: ProgramContext,
|
ctx: ProgramContext,
|
||||||
config: AccountWithMetadata,
|
config: AccountWithMetadata,
|
||||||
token_program_id: ProgramId,
|
token_program_id: ProgramId,
|
||||||
|
twap_oracle_program_id: ProgramId,
|
||||||
authority: AccountId,
|
authority: AccountId,
|
||||||
) -> SpelResult {
|
) -> SpelResult {
|
||||||
let post_states = amm_program::initialize::initialize(
|
let post_states = amm_program::initialize::initialize(
|
||||||
config,
|
config,
|
||||||
token_program_id,
|
token_program_id,
|
||||||
|
twap_oracle_program_id,
|
||||||
authority,
|
authority,
|
||||||
ctx.self_program_id,
|
ctx.self_program_id,
|
||||||
);
|
);
|
||||||
@ -51,18 +53,53 @@ mod amm {
|
|||||||
config: AccountWithMetadata,
|
config: AccountWithMetadata,
|
||||||
authority: AccountWithMetadata,
|
authority: AccountWithMetadata,
|
||||||
token_program_id: Option<ProgramId>,
|
token_program_id: Option<ProgramId>,
|
||||||
|
twap_oracle_program_id: Option<ProgramId>,
|
||||||
new_authority: Option<AccountId>,
|
new_authority: Option<AccountId>,
|
||||||
) -> SpelResult {
|
) -> SpelResult {
|
||||||
let post_states = amm_program::update_config::update_config(
|
let post_states = amm_program::update_config::update_config(
|
||||||
config,
|
config,
|
||||||
authority,
|
authority,
|
||||||
token_program_id,
|
token_program_id,
|
||||||
|
twap_oracle_program_id,
|
||||||
new_authority,
|
new_authority,
|
||||||
ctx.self_program_id,
|
ctx.self_program_id,
|
||||||
);
|
);
|
||||||
Ok(spel_framework::SpelOutput::execute(post_states, vec![]))
|
Ok(spel_framework::SpelOutput::execute(post_states, vec![]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a TWAP price-observations account for a pool over a time window, on behalf of the
|
||||||
|
/// AMM, via a chained call to the configured TWAP oracle program.
|
||||||
|
///
|
||||||
|
/// Expected accounts:
|
||||||
|
/// 1. `config` — initialized AMM config account.
|
||||||
|
/// 2. `pool` — initialized AMM pool; acts as the (authorized) price source.
|
||||||
|
/// 3. `current_tick_account` — the pool's initialized TWAP current-tick PDA; supplies the
|
||||||
|
/// initial tick.
|
||||||
|
/// 4. `price_observations` — uninitialized TWAP PDA for `(pool, window_duration)`.
|
||||||
|
/// 5. `clock` — the canonical 1-block LEZ clock account.
|
||||||
|
#[instruction]
|
||||||
|
pub fn create_price_observations(
|
||||||
|
ctx: ProgramContext,
|
||||||
|
config: AccountWithMetadata,
|
||||||
|
pool: AccountWithMetadata,
|
||||||
|
current_tick_account: AccountWithMetadata,
|
||||||
|
price_observations: AccountWithMetadata,
|
||||||
|
clock: AccountWithMetadata,
|
||||||
|
window_duration: u64,
|
||||||
|
) -> SpelResult {
|
||||||
|
let (post_states, chained_calls) =
|
||||||
|
amm_program::create_price_observations::create_price_observations(
|
||||||
|
config,
|
||||||
|
pool,
|
||||||
|
current_tick_account,
|
||||||
|
price_observations,
|
||||||
|
clock,
|
||||||
|
window_duration,
|
||||||
|
ctx.self_program_id,
|
||||||
|
);
|
||||||
|
Ok(spel_framework::SpelOutput::execute(post_states, chained_calls))
|
||||||
|
}
|
||||||
|
|
||||||
/// Initializes a new Pool (or re-initializes an existing zero-supply Pool).
|
/// Initializes a new Pool (or re-initializes an existing zero-supply Pool).
|
||||||
/// A fresh user LP holding must be explicitly authorized by the caller.
|
/// A fresh user LP holding must be explicitly authorized by the caller.
|
||||||
#[expect(
|
#[expect(
|
||||||
|
|||||||
434
programs/amm/src/create_price_observations.rs
Normal file
434
programs/amm/src/create_price_observations.rs
Normal file
@ -0,0 +1,434 @@
|
|||||||
|
use amm_core::{
|
||||||
|
compute_config_pda, compute_pool_pda, compute_pool_pda_seed, AmmConfig, PoolDefinition,
|
||||||
|
};
|
||||||
|
use clock_core::CLOCK_01_PROGRAM_ACCOUNT_ID;
|
||||||
|
use nssa_core::{
|
||||||
|
account::{Account, AccountWithMetadata},
|
||||||
|
program::{AccountPostState, ChainedCall, ProgramId},
|
||||||
|
};
|
||||||
|
use twap_oracle_core::{
|
||||||
|
compute_current_tick_account_pda, compute_price_observations_pda, CurrentTickAccount,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Creates a TWAP price-observations account for `pool` over a time window, on behalf of the AMM.
|
||||||
|
///
|
||||||
|
/// The pool acts as the price source: the AMM authorizes it via its pool PDA seed so the oracle
|
||||||
|
/// ties the observations account to this pool. The work itself is delegated to the configured
|
||||||
|
/// TWAP oracle program through a single chained call to its `CreatePriceObservations` instruction,
|
||||||
|
/// which claims and initialises the observations PDA.
|
||||||
|
///
|
||||||
|
/// The initial tick is **not** caller-supplied: it is read from the pool's
|
||||||
|
/// [`CurrentTickAccount`] — the authoritative tick the AMM previously wrote via the oracle — so the
|
||||||
|
/// feed cannot be seeded at a forged price. This mirrors what `RecordTick` does, using the current
|
||||||
|
/// tick to seed the very first observation.
|
||||||
|
///
|
||||||
|
/// The TWAP oracle program ID is read from the AMM config account (the initialization gate). The
|
||||||
|
/// clock must be the canonical 1-block LEZ system clock, and the observations account must not yet
|
||||||
|
/// exist — both are checked here so the call is rejected early with an AMM-level error, in
|
||||||
|
/// addition to the oracle's own checks.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics if:
|
||||||
|
/// - `config.account_id` does not match `compute_config_pda(amm_program_id)`, or the config is
|
||||||
|
/// uninitialized (the AMM Program has not been initialized).
|
||||||
|
/// - `clock.account_id` is not [`CLOCK_01_PROGRAM_ACCOUNT_ID`].
|
||||||
|
/// - `pool.account` does not hold a valid [`PoolDefinition`], or `pool.account_id` does not match
|
||||||
|
/// its pool PDA.
|
||||||
|
/// - `current_tick_account.account_id` does not match the pool's current-tick PDA, or the account
|
||||||
|
/// does not hold a valid [`CurrentTickAccount`] (it has not been created yet).
|
||||||
|
/// - `price_observations.account_id` does not match the expected TWAP PDA for `(pool,
|
||||||
|
/// window_duration)`, or `price_observations.account` already exists.
|
||||||
|
pub fn create_price_observations(
|
||||||
|
config: AccountWithMetadata,
|
||||||
|
pool: AccountWithMetadata,
|
||||||
|
current_tick_account: AccountWithMetadata,
|
||||||
|
price_observations: AccountWithMetadata,
|
||||||
|
clock: AccountWithMetadata,
|
||||||
|
window_duration: u64,
|
||||||
|
amm_program_id: ProgramId,
|
||||||
|
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||||
|
// Config gate: validate the config PDA and read the TWAP oracle program ID from it.
|
||||||
|
assert_eq!(
|
||||||
|
config.account_id,
|
||||||
|
compute_config_pda(amm_program_id),
|
||||||
|
"Create price observations: AMM config Account ID does not match PDA"
|
||||||
|
);
|
||||||
|
let twap_oracle_program_id = AmmConfig::try_from(&config.account.data)
|
||||||
|
.expect("Create price observations: AMM Program must be initialized before use")
|
||||||
|
.twap_oracle_program_id;
|
||||||
|
|
||||||
|
// The clock must be the canonical 1-block LEZ system clock; otherwise a caller could seed the
|
||||||
|
// feed with a forged base timestamp.
|
||||||
|
assert_eq!(
|
||||||
|
clock.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||||
|
"Create price observations: clock account must be the canonical 1-block LEZ clock account"
|
||||||
|
);
|
||||||
|
|
||||||
|
// The pool is the price source. Verify it is a genuine AMM pool PDA so we only ever authorize
|
||||||
|
// a real pool as the source.
|
||||||
|
let pool_def = PoolDefinition::try_from(&pool.account.data)
|
||||||
|
.expect("Create price observations: AMM Program expects a valid Pool Definition Account");
|
||||||
|
assert_eq!(
|
||||||
|
pool.account_id,
|
||||||
|
compute_pool_pda(
|
||||||
|
amm_program_id,
|
||||||
|
pool_def.definition_token_a_id,
|
||||||
|
pool_def.definition_token_b_id,
|
||||||
|
),
|
||||||
|
"Create price observations: Pool Account ID does not match PDA"
|
||||||
|
);
|
||||||
|
|
||||||
|
// The initial tick comes from the pool's authoritative CurrentTickAccount, not from the
|
||||||
|
// caller. Verifying its PDA ties it to this exact pool, so the seed tick cannot be forged.
|
||||||
|
assert_eq!(
|
||||||
|
current_tick_account.account_id,
|
||||||
|
compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id),
|
||||||
|
"Create price observations: current tick Account ID does not match PDA"
|
||||||
|
);
|
||||||
|
let initial_tick = CurrentTickAccount::try_from(¤t_tick_account.account.data)
|
||||||
|
.expect("Create price observations: AMM Program expects a valid CurrentTickAccount")
|
||||||
|
.tick;
|
||||||
|
|
||||||
|
// Verify the observations account is the expected TWAP PDA for this (pool, window) pair and
|
||||||
|
// reject if it already exists.
|
||||||
|
assert_eq!(
|
||||||
|
price_observations.account_id,
|
||||||
|
compute_price_observations_pda(twap_oracle_program_id, pool.account_id, window_duration),
|
||||||
|
"Create price observations: price observations Account ID does not match PDA"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
price_observations.account,
|
||||||
|
Account::default(),
|
||||||
|
"Create price observations: price observations account already exists"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Authorize the pool as the price source so the oracle ties the feed to this pool. The AMM
|
||||||
|
// proves control of the pool PDA via its seed.
|
||||||
|
let mut pool_price_source = pool.clone();
|
||||||
|
pool_price_source.is_authorized = true;
|
||||||
|
|
||||||
|
let chained_call = ChainedCall::new(
|
||||||
|
twap_oracle_program_id,
|
||||||
|
vec![price_observations.clone(), pool_price_source, clock.clone()],
|
||||||
|
&twap_oracle_core::Instruction::CreatePriceObservations {
|
||||||
|
initial_tick,
|
||||||
|
window_duration,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_pda_seeds(vec![compute_pool_pda_seed(
|
||||||
|
pool_def.definition_token_a_id,
|
||||||
|
pool_def.definition_token_b_id,
|
||||||
|
)]);
|
||||||
|
|
||||||
|
let post_states = vec![
|
||||||
|
AccountPostState::new(config.account.clone()),
|
||||||
|
AccountPostState::new(pool.account.clone()),
|
||||||
|
AccountPostState::new(current_tick_account.account.clone()),
|
||||||
|
AccountPostState::new(price_observations.account.clone()),
|
||||||
|
AccountPostState::new(clock.account.clone()),
|
||||||
|
];
|
||||||
|
|
||||||
|
(post_states, vec![chained_call])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use amm_core::compute_pool_pda_seed;
|
||||||
|
use nssa_core::account::{Account, AccountId, Data, Nonce};
|
||||||
|
use twap_oracle_core::compute_current_tick_account_pda;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const AMM_PROGRAM_ID: ProgramId = [42; 8];
|
||||||
|
const TOKEN_PROGRAM_ID: ProgramId = [15; 8];
|
||||||
|
const TWAP_ORACLE_PROGRAM_ID: ProgramId = [77; 8];
|
||||||
|
/// 24-hour window in milliseconds.
|
||||||
|
const WINDOW_24H: u64 = 24 * 60 * 60 * 1_000;
|
||||||
|
/// The authoritative tick stored in the pool's `CurrentTickAccount`.
|
||||||
|
const CURRENT_TICK: i32 = -1_234;
|
||||||
|
|
||||||
|
fn token_a_id() -> AccountId {
|
||||||
|
AccountId::new([3; 32])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn token_b_id() -> AccountId {
|
||||||
|
AccountId::new([4; 32])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pool_id() -> AccountId {
|
||||||
|
compute_pool_pda(AMM_PROGRAM_ID, token_a_id(), token_b_id())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_init() -> AccountWithMetadata {
|
||||||
|
AccountWithMetadata {
|
||||||
|
account: Account {
|
||||||
|
program_owner: AMM_PROGRAM_ID,
|
||||||
|
balance: 0,
|
||||||
|
data: Data::from(&AmmConfig {
|
||||||
|
token_program_id: TOKEN_PROGRAM_ID,
|
||||||
|
twap_oracle_program_id: TWAP_ORACLE_PROGRAM_ID,
|
||||||
|
authority: AccountId::new([9; 32]),
|
||||||
|
}),
|
||||||
|
nonce: Nonce(0),
|
||||||
|
},
|
||||||
|
is_authorized: false,
|
||||||
|
account_id: compute_config_pda(AMM_PROGRAM_ID),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pool() -> AccountWithMetadata {
|
||||||
|
AccountWithMetadata {
|
||||||
|
account: Account {
|
||||||
|
program_owner: AMM_PROGRAM_ID,
|
||||||
|
balance: 0,
|
||||||
|
data: Data::from(&PoolDefinition {
|
||||||
|
definition_token_a_id: token_a_id(),
|
||||||
|
definition_token_b_id: token_b_id(),
|
||||||
|
vault_a_id: AccountId::new([5; 32]),
|
||||||
|
vault_b_id: AccountId::new([6; 32]),
|
||||||
|
liquidity_pool_id: AccountId::new([7; 32]),
|
||||||
|
liquidity_pool_supply: 5_000,
|
||||||
|
reserve_a: 5_000,
|
||||||
|
reserve_b: 2_500,
|
||||||
|
fees: amm_core::FEE_TIER_BPS_30,
|
||||||
|
}),
|
||||||
|
nonce: Nonce(0),
|
||||||
|
},
|
||||||
|
is_authorized: false,
|
||||||
|
account_id: pool_id(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_tick_account() -> AccountWithMetadata {
|
||||||
|
AccountWithMetadata {
|
||||||
|
account: Account {
|
||||||
|
program_owner: TWAP_ORACLE_PROGRAM_ID,
|
||||||
|
balance: 0,
|
||||||
|
data: Data::from(&CurrentTickAccount {
|
||||||
|
tick: CURRENT_TICK,
|
||||||
|
last_updated: 1_700_000_000_000,
|
||||||
|
}),
|
||||||
|
nonce: Nonce(0),
|
||||||
|
},
|
||||||
|
is_authorized: false,
|
||||||
|
account_id: compute_current_tick_account_pda(TWAP_ORACLE_PROGRAM_ID, pool_id()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn price_observations_uninit() -> AccountWithMetadata {
|
||||||
|
AccountWithMetadata {
|
||||||
|
account: Account::default(),
|
||||||
|
is_authorized: false,
|
||||||
|
account_id: compute_price_observations_pda(
|
||||||
|
TWAP_ORACLE_PROGRAM_ID,
|
||||||
|
pool_id(),
|
||||||
|
WINDOW_24H,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clock() -> AccountWithMetadata {
|
||||||
|
AccountWithMetadata {
|
||||||
|
account: Account::default(),
|
||||||
|
is_authorized: false,
|
||||||
|
account_id: CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn call() -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||||
|
create_price_observations(
|
||||||
|
config_init(),
|
||||||
|
pool(),
|
||||||
|
current_tick_account(),
|
||||||
|
price_observations_uninit(),
|
||||||
|
clock(),
|
||||||
|
WINDOW_24H,
|
||||||
|
AMM_PROGRAM_ID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── happy path ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn returns_five_post_states_unchanged() {
|
||||||
|
let (post_states, _) = call();
|
||||||
|
assert_eq!(post_states.len(), 5);
|
||||||
|
assert_eq!(*post_states[0].account(), config_init().account);
|
||||||
|
assert_eq!(*post_states[1].account(), pool().account);
|
||||||
|
assert_eq!(*post_states[2].account(), current_tick_account().account);
|
||||||
|
assert_eq!(
|
||||||
|
*post_states[3].account(),
|
||||||
|
price_observations_uninit().account
|
||||||
|
);
|
||||||
|
assert_eq!(*post_states[4].account(), clock().account);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn seeds_chained_call_with_tick_from_current_tick_account() {
|
||||||
|
let (_, chained_calls) = call();
|
||||||
|
assert_eq!(chained_calls.len(), 1);
|
||||||
|
|
||||||
|
// The chained call must carry the tick read from the CurrentTickAccount, not a
|
||||||
|
// caller-supplied value, and authorize the pool as the price source.
|
||||||
|
let mut pool_authorized = pool();
|
||||||
|
pool_authorized.is_authorized = true;
|
||||||
|
let expected = ChainedCall::new(
|
||||||
|
TWAP_ORACLE_PROGRAM_ID,
|
||||||
|
vec![price_observations_uninit(), pool_authorized, clock()],
|
||||||
|
&twap_oracle_core::Instruction::CreatePriceObservations {
|
||||||
|
initial_tick: CURRENT_TICK,
|
||||||
|
window_duration: WINDOW_24H,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_pda_seeds(vec![compute_pool_pda_seed(token_a_id(), token_b_id())]);
|
||||||
|
|
||||||
|
assert_eq!(chained_calls[0], expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── precondition violations ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "AMM config Account ID does not match PDA")]
|
||||||
|
fn wrong_config_pda_panics() {
|
||||||
|
let mut config = config_init();
|
||||||
|
config.account_id = AccountId::new([0; 32]);
|
||||||
|
create_price_observations(
|
||||||
|
config,
|
||||||
|
pool(),
|
||||||
|
current_tick_account(),
|
||||||
|
price_observations_uninit(),
|
||||||
|
clock(),
|
||||||
|
WINDOW_24H,
|
||||||
|
AMM_PROGRAM_ID,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "AMM Program must be initialized before use")]
|
||||||
|
fn uninitialized_config_panics() {
|
||||||
|
let config = AccountWithMetadata {
|
||||||
|
account: Account::default(),
|
||||||
|
is_authorized: false,
|
||||||
|
account_id: compute_config_pda(AMM_PROGRAM_ID),
|
||||||
|
};
|
||||||
|
create_price_observations(
|
||||||
|
config,
|
||||||
|
pool(),
|
||||||
|
current_tick_account(),
|
||||||
|
price_observations_uninit(),
|
||||||
|
clock(),
|
||||||
|
WINDOW_24H,
|
||||||
|
AMM_PROGRAM_ID,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "clock account must be the canonical 1-block LEZ clock account")]
|
||||||
|
fn non_canonical_clock_panics() {
|
||||||
|
let mut clock = clock();
|
||||||
|
clock.account_id = AccountId::new([9; 32]);
|
||||||
|
create_price_observations(
|
||||||
|
config_init(),
|
||||||
|
pool(),
|
||||||
|
current_tick_account(),
|
||||||
|
price_observations_uninit(),
|
||||||
|
clock,
|
||||||
|
WINDOW_24H,
|
||||||
|
AMM_PROGRAM_ID,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "Pool Account ID does not match PDA")]
|
||||||
|
fn forged_pool_account_panics() {
|
||||||
|
let mut pool = pool();
|
||||||
|
pool.account_id = AccountId::new([8; 32]);
|
||||||
|
create_price_observations(
|
||||||
|
config_init(),
|
||||||
|
pool,
|
||||||
|
current_tick_account(),
|
||||||
|
price_observations_uninit(),
|
||||||
|
clock(),
|
||||||
|
WINDOW_24H,
|
||||||
|
AMM_PROGRAM_ID,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A caller cannot substitute a current-tick account they control to forge the seed tick: the
|
||||||
|
/// account ID must match the pool's current-tick PDA.
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "current tick Account ID does not match PDA")]
|
||||||
|
fn forged_current_tick_account_panics() {
|
||||||
|
let mut current_tick = current_tick_account();
|
||||||
|
current_tick.account_id = AccountId::new([2; 32]);
|
||||||
|
create_price_observations(
|
||||||
|
config_init(),
|
||||||
|
pool(),
|
||||||
|
current_tick,
|
||||||
|
price_observations_uninit(),
|
||||||
|
clock(),
|
||||||
|
WINDOW_24H,
|
||||||
|
AMM_PROGRAM_ID,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "AMM Program expects a valid CurrentTickAccount")]
|
||||||
|
fn uninitialized_current_tick_account_panics() {
|
||||||
|
let current_tick = AccountWithMetadata {
|
||||||
|
account: Account::default(),
|
||||||
|
is_authorized: false,
|
||||||
|
account_id: compute_current_tick_account_pda(TWAP_ORACLE_PROGRAM_ID, pool_id()),
|
||||||
|
};
|
||||||
|
create_price_observations(
|
||||||
|
config_init(),
|
||||||
|
pool(),
|
||||||
|
current_tick,
|
||||||
|
price_observations_uninit(),
|
||||||
|
clock(),
|
||||||
|
WINDOW_24H,
|
||||||
|
AMM_PROGRAM_ID,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "price observations Account ID does not match PDA")]
|
||||||
|
fn wrong_observations_pda_panics() {
|
||||||
|
let mut observations = price_observations_uninit();
|
||||||
|
observations.account_id = AccountId::new([1; 32]);
|
||||||
|
create_price_observations(
|
||||||
|
config_init(),
|
||||||
|
pool(),
|
||||||
|
current_tick_account(),
|
||||||
|
observations,
|
||||||
|
clock(),
|
||||||
|
WINDOW_24H,
|
||||||
|
AMM_PROGRAM_ID,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "price observations account already exists")]
|
||||||
|
fn already_existing_observations_panics() {
|
||||||
|
let mut observations = price_observations_uninit();
|
||||||
|
observations.account.data = Data::try_from(vec![1u8; 8]).expect("fits in Data");
|
||||||
|
create_price_observations(
|
||||||
|
config_init(),
|
||||||
|
pool(),
|
||||||
|
current_tick_account(),
|
||||||
|
observations,
|
||||||
|
clock(),
|
||||||
|
WINDOW_24H,
|
||||||
|
AMM_PROGRAM_ID,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn different_windows_produce_distinct_observation_pdas() {
|
||||||
|
let window_7d = 7 * 24 * 60 * 60 * 1_000u64;
|
||||||
|
assert_ne!(
|
||||||
|
compute_price_observations_pda(TWAP_ORACLE_PROGRAM_ID, pool_id(), WINDOW_24H),
|
||||||
|
compute_price_observations_pda(TWAP_ORACLE_PROGRAM_ID, pool_id(), window_7d),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,10 +7,11 @@ use nssa_core::{
|
|||||||
/// Initializes the AMM Program by creating its singleton configuration account.
|
/// Initializes the AMM Program by creating its singleton configuration account.
|
||||||
///
|
///
|
||||||
/// The config account is a PDA derived from the constant `"CONFIG"` seed
|
/// The config account is a PDA derived from the constant `"CONFIG"` seed
|
||||||
/// (`compute_config_pda(amm_program_id)`) and stores `token_program_id` (the Token Program the
|
/// (`compute_config_pda(amm_program_id)`) and stores the program IDs the AMM issues chained calls
|
||||||
/// AMM issues every chained call to) and `authority` (the admin allowed to change configuration
|
/// to — `token_program_id` (the Token Program) and `twap_oracle_program_id` (the TWAP oracle) —
|
||||||
/// later via `update_config`). Its existence is the Program's "initialized" flag: the
|
/// plus `authority` (the admin allowed to change configuration later via `update_config`). Its
|
||||||
/// chained-call instructions read the Token Program ID from it and reject calls until it exists.
|
/// existence is the Program's "initialized" flag: the chained-call instructions read these
|
||||||
|
/// program IDs from it and reject calls until it exists.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
/// Panics if:
|
/// Panics if:
|
||||||
@ -19,6 +20,7 @@ use nssa_core::{
|
|||||||
pub fn initialize(
|
pub fn initialize(
|
||||||
config: AccountWithMetadata,
|
config: AccountWithMetadata,
|
||||||
token_program_id: ProgramId,
|
token_program_id: ProgramId,
|
||||||
|
twap_oracle_program_id: ProgramId,
|
||||||
authority: AccountId,
|
authority: AccountId,
|
||||||
amm_program_id: ProgramId,
|
amm_program_id: ProgramId,
|
||||||
) -> Vec<AccountPostState> {
|
) -> Vec<AccountPostState> {
|
||||||
@ -36,6 +38,7 @@ pub fn initialize(
|
|||||||
let mut config_post = config.account.clone();
|
let mut config_post = config.account.clone();
|
||||||
config_post.data = Data::from(&AmmConfig {
|
config_post.data = Data::from(&AmmConfig {
|
||||||
token_program_id,
|
token_program_id,
|
||||||
|
twap_oracle_program_id,
|
||||||
authority,
|
authority,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -54,6 +57,7 @@ mod tests {
|
|||||||
|
|
||||||
const AMM_PROGRAM_ID: ProgramId = [42; 8];
|
const AMM_PROGRAM_ID: ProgramId = [42; 8];
|
||||||
const TOKEN_PROGRAM_ID: ProgramId = [15; 8];
|
const TOKEN_PROGRAM_ID: ProgramId = [15; 8];
|
||||||
|
const TWAP_ORACLE_PROGRAM_ID: ProgramId = [77; 8];
|
||||||
|
|
||||||
fn authority() -> AccountId {
|
fn authority() -> AccountId {
|
||||||
AccountId::new([9; 32])
|
AccountId::new([9; 32])
|
||||||
@ -72,6 +76,7 @@ mod tests {
|
|||||||
let post_states = initialize(
|
let post_states = initialize(
|
||||||
config_uninit(),
|
config_uninit(),
|
||||||
TOKEN_PROGRAM_ID,
|
TOKEN_PROGRAM_ID,
|
||||||
|
TWAP_ORACLE_PROGRAM_ID,
|
||||||
authority(),
|
authority(),
|
||||||
AMM_PROGRAM_ID,
|
AMM_PROGRAM_ID,
|
||||||
);
|
);
|
||||||
@ -83,16 +88,18 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stores_token_program_id_and_authority() {
|
fn stores_program_ids_and_authority() {
|
||||||
let post_states = initialize(
|
let post_states = initialize(
|
||||||
config_uninit(),
|
config_uninit(),
|
||||||
TOKEN_PROGRAM_ID,
|
TOKEN_PROGRAM_ID,
|
||||||
|
TWAP_ORACLE_PROGRAM_ID,
|
||||||
authority(),
|
authority(),
|
||||||
AMM_PROGRAM_ID,
|
AMM_PROGRAM_ID,
|
||||||
);
|
);
|
||||||
let config = AmmConfig::try_from(&post_states[0].account().data)
|
let config = AmmConfig::try_from(&post_states[0].account().data)
|
||||||
.expect("post state must contain a valid AmmConfig");
|
.expect("post state must contain a valid AmmConfig");
|
||||||
assert_eq!(config.token_program_id, TOKEN_PROGRAM_ID);
|
assert_eq!(config.token_program_id, TOKEN_PROGRAM_ID);
|
||||||
|
assert_eq!(config.twap_oracle_program_id, TWAP_ORACLE_PROGRAM_ID);
|
||||||
assert_eq!(config.authority, authority());
|
assert_eq!(config.authority, authority());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,7 +108,13 @@ mod tests {
|
|||||||
fn wrong_config_account_id_panics() {
|
fn wrong_config_account_id_panics() {
|
||||||
let mut wrong = config_uninit();
|
let mut wrong = config_uninit();
|
||||||
wrong.account_id = AccountId::new([0; 32]);
|
wrong.account_id = AccountId::new([0; 32]);
|
||||||
initialize(wrong, TOKEN_PROGRAM_ID, authority(), AMM_PROGRAM_ID);
|
initialize(
|
||||||
|
wrong,
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
TWAP_ORACLE_PROGRAM_ID,
|
||||||
|
authority(),
|
||||||
|
AMM_PROGRAM_ID,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -110,9 +123,16 @@ mod tests {
|
|||||||
let mut initialized = config_uninit();
|
let mut initialized = config_uninit();
|
||||||
initialized.account.data = Data::from(&AmmConfig {
|
initialized.account.data = Data::from(&AmmConfig {
|
||||||
token_program_id: TOKEN_PROGRAM_ID,
|
token_program_id: TOKEN_PROGRAM_ID,
|
||||||
|
twap_oracle_program_id: TWAP_ORACLE_PROGRAM_ID,
|
||||||
authority: authority(),
|
authority: authority(),
|
||||||
});
|
});
|
||||||
initialized.account.nonce = Nonce(0);
|
initialized.account.nonce = Nonce(0);
|
||||||
initialize(initialized, TOKEN_PROGRAM_ID, authority(), AMM_PROGRAM_ID);
|
initialize(
|
||||||
|
initialized,
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
TWAP_ORACLE_PROGRAM_ID,
|
||||||
|
authority(),
|
||||||
|
AMM_PROGRAM_ID,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
pub use amm_core as core;
|
pub use amm_core as core;
|
||||||
|
|
||||||
pub mod add;
|
pub mod add;
|
||||||
|
pub mod create_price_observations;
|
||||||
pub mod initialize;
|
pub mod initialize;
|
||||||
pub mod new_definition;
|
pub mod new_definition;
|
||||||
pub mod remove;
|
pub mod remove;
|
||||||
|
|||||||
@ -30,6 +30,7 @@ use crate::{
|
|||||||
|
|
||||||
const TOKEN_PROGRAM_ID: ProgramId = [15; 8];
|
const TOKEN_PROGRAM_ID: ProgramId = [15; 8];
|
||||||
const AMM_PROGRAM_ID: ProgramId = [42; 8];
|
const AMM_PROGRAM_ID: ProgramId = [42; 8];
|
||||||
|
const TWAP_ORACLE_PROGRAM_ID: ProgramId = [77; 8];
|
||||||
const MALICIOUS_TOKEN_PROGRAM_ID: ProgramId = [99; 8];
|
const MALICIOUS_TOKEN_PROGRAM_ID: ProgramId = [99; 8];
|
||||||
|
|
||||||
struct BalanceForTests;
|
struct BalanceForTests;
|
||||||
@ -628,6 +629,7 @@ impl AccountWithMetadataForTests {
|
|||||||
balance: 0u128,
|
balance: 0u128,
|
||||||
data: Data::from(&AmmConfig {
|
data: Data::from(&AmmConfig {
|
||||||
token_program_id: TOKEN_PROGRAM_ID,
|
token_program_id: TOKEN_PROGRAM_ID,
|
||||||
|
twap_oracle_program_id: TWAP_ORACLE_PROGRAM_ID,
|
||||||
authority: AccountId::new([9; 32]),
|
authority: AccountId::new([9; 32]),
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
|
|||||||
@ -23,6 +23,7 @@ pub fn update_config(
|
|||||||
config: AccountWithMetadata,
|
config: AccountWithMetadata,
|
||||||
authority: AccountWithMetadata,
|
authority: AccountWithMetadata,
|
||||||
token_program_id: Option<ProgramId>,
|
token_program_id: Option<ProgramId>,
|
||||||
|
twap_oracle_program_id: Option<ProgramId>,
|
||||||
new_authority: Option<AccountId>,
|
new_authority: Option<AccountId>,
|
||||||
amm_program_id: ProgramId,
|
amm_program_id: ProgramId,
|
||||||
) -> Vec<AccountPostState> {
|
) -> Vec<AccountPostState> {
|
||||||
@ -47,6 +48,9 @@ pub fn update_config(
|
|||||||
if let Some(token_program_id) = token_program_id {
|
if let Some(token_program_id) = token_program_id {
|
||||||
config_data.token_program_id = token_program_id;
|
config_data.token_program_id = token_program_id;
|
||||||
}
|
}
|
||||||
|
if let Some(twap_oracle_program_id) = twap_oracle_program_id {
|
||||||
|
config_data.twap_oracle_program_id = twap_oracle_program_id;
|
||||||
|
}
|
||||||
if let Some(new_authority) = new_authority {
|
if let Some(new_authority) = new_authority {
|
||||||
config_data.authority = new_authority;
|
config_data.authority = new_authority;
|
||||||
}
|
}
|
||||||
@ -69,6 +73,8 @@ mod tests {
|
|||||||
const AMM_PROGRAM_ID: ProgramId = [42; 8];
|
const AMM_PROGRAM_ID: ProgramId = [42; 8];
|
||||||
const TOKEN_PROGRAM_ID: ProgramId = [15; 8];
|
const TOKEN_PROGRAM_ID: ProgramId = [15; 8];
|
||||||
const NEW_TOKEN_PROGRAM_ID: ProgramId = [16; 8];
|
const NEW_TOKEN_PROGRAM_ID: ProgramId = [16; 8];
|
||||||
|
const TWAP_ORACLE_PROGRAM_ID: ProgramId = [77; 8];
|
||||||
|
const NEW_TWAP_ORACLE_PROGRAM_ID: ProgramId = [78; 8];
|
||||||
|
|
||||||
fn admin_id() -> AccountId {
|
fn admin_id() -> AccountId {
|
||||||
AccountId::new([9; 32])
|
AccountId::new([9; 32])
|
||||||
@ -81,6 +87,7 @@ mod tests {
|
|||||||
balance: 0,
|
balance: 0,
|
||||||
data: Data::from(&AmmConfig {
|
data: Data::from(&AmmConfig {
|
||||||
token_program_id: TOKEN_PROGRAM_ID,
|
token_program_id: TOKEN_PROGRAM_ID,
|
||||||
|
twap_oracle_program_id: TWAP_ORACLE_PROGRAM_ID,
|
||||||
authority: admin_id(),
|
authority: admin_id(),
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
@ -112,11 +119,30 @@ mod tests {
|
|||||||
admin_authorized(),
|
admin_authorized(),
|
||||||
Some(NEW_TOKEN_PROGRAM_ID),
|
Some(NEW_TOKEN_PROGRAM_ID),
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
AMM_PROGRAM_ID,
|
AMM_PROGRAM_ID,
|
||||||
);
|
);
|
||||||
let config = updated_config(&post_states);
|
let config = updated_config(&post_states);
|
||||||
assert_eq!(config.token_program_id, NEW_TOKEN_PROGRAM_ID);
|
assert_eq!(config.token_program_id, NEW_TOKEN_PROGRAM_ID);
|
||||||
// Authority is unchanged.
|
// TWAP oracle program and authority are unchanged.
|
||||||
|
assert_eq!(config.twap_oracle_program_id, TWAP_ORACLE_PROGRAM_ID);
|
||||||
|
assert_eq!(config.authority, admin_id());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn updates_twap_oracle_program_id() {
|
||||||
|
let post_states = update_config(
|
||||||
|
config_init(),
|
||||||
|
admin_authorized(),
|
||||||
|
None,
|
||||||
|
Some(NEW_TWAP_ORACLE_PROGRAM_ID),
|
||||||
|
None,
|
||||||
|
AMM_PROGRAM_ID,
|
||||||
|
);
|
||||||
|
let config = updated_config(&post_states);
|
||||||
|
assert_eq!(config.twap_oracle_program_id, NEW_TWAP_ORACLE_PROGRAM_ID);
|
||||||
|
// Token program and authority are unchanged.
|
||||||
|
assert_eq!(config.token_program_id, TOKEN_PROGRAM_ID);
|
||||||
assert_eq!(config.authority, admin_id());
|
assert_eq!(config.authority, admin_id());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,6 +153,7 @@ mod tests {
|
|||||||
config_init(),
|
config_init(),
|
||||||
admin_authorized(),
|
admin_authorized(),
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
Some(new_admin),
|
Some(new_admin),
|
||||||
AMM_PROGRAM_ID,
|
AMM_PROGRAM_ID,
|
||||||
);
|
);
|
||||||
@ -143,6 +170,7 @@ mod tests {
|
|||||||
config_init(),
|
config_init(),
|
||||||
admin_authorized(),
|
admin_authorized(),
|
||||||
Some(NEW_TOKEN_PROGRAM_ID),
|
Some(NEW_TOKEN_PROGRAM_ID),
|
||||||
|
None,
|
||||||
Some(new_admin),
|
Some(new_admin),
|
||||||
AMM_PROGRAM_ID,
|
AMM_PROGRAM_ID,
|
||||||
);
|
);
|
||||||
@ -158,10 +186,12 @@ mod tests {
|
|||||||
admin_authorized(),
|
admin_authorized(),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
AMM_PROGRAM_ID,
|
AMM_PROGRAM_ID,
|
||||||
);
|
);
|
||||||
let config = updated_config(&post_states);
|
let config = updated_config(&post_states);
|
||||||
assert_eq!(config.token_program_id, TOKEN_PROGRAM_ID);
|
assert_eq!(config.token_program_id, TOKEN_PROGRAM_ID);
|
||||||
|
assert_eq!(config.twap_oracle_program_id, TWAP_ORACLE_PROGRAM_ID);
|
||||||
assert_eq!(config.authority, admin_id());
|
assert_eq!(config.authority, admin_id());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,6 +203,7 @@ mod tests {
|
|||||||
authority.clone(),
|
authority.clone(),
|
||||||
Some(NEW_TOKEN_PROGRAM_ID),
|
Some(NEW_TOKEN_PROGRAM_ID),
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
AMM_PROGRAM_ID,
|
AMM_PROGRAM_ID,
|
||||||
);
|
);
|
||||||
assert_eq!(post_states.len(), 2);
|
assert_eq!(post_states.len(), 2);
|
||||||
@ -193,6 +224,7 @@ mod tests {
|
|||||||
admin_authorized(),
|
admin_authorized(),
|
||||||
Some(NEW_TOKEN_PROGRAM_ID),
|
Some(NEW_TOKEN_PROGRAM_ID),
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
AMM_PROGRAM_ID,
|
AMM_PROGRAM_ID,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -210,6 +242,7 @@ mod tests {
|
|||||||
admin_authorized(),
|
admin_authorized(),
|
||||||
Some(NEW_TOKEN_PROGRAM_ID),
|
Some(NEW_TOKEN_PROGRAM_ID),
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
AMM_PROGRAM_ID,
|
AMM_PROGRAM_ID,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -225,6 +258,7 @@ mod tests {
|
|||||||
not_admin,
|
not_admin,
|
||||||
Some(NEW_TOKEN_PROGRAM_ID),
|
Some(NEW_TOKEN_PROGRAM_ID),
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
AMM_PROGRAM_ID,
|
AMM_PROGRAM_ID,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -240,6 +274,7 @@ mod tests {
|
|||||||
unsigned,
|
unsigned,
|
||||||
Some(NEW_TOKEN_PROGRAM_ID),
|
Some(NEW_TOKEN_PROGRAM_ID),
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
AMM_PROGRAM_ID,
|
AMM_PROGRAM_ID,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,9 @@ amm_core = { workspace = true }
|
|||||||
token_core = { workspace = true }
|
token_core = { workspace = true }
|
||||||
ata_core = { workspace = true }
|
ata_core = { workspace = true }
|
||||||
stablecoin_core = { workspace = true }
|
stablecoin_core = { workspace = true }
|
||||||
|
twap_oracle_core = { workspace = true }
|
||||||
token-methods = { path = "../token/methods" }
|
token-methods = { path = "../token/methods" }
|
||||||
amm-methods = { path = "../amm/methods" }
|
amm-methods = { path = "../amm/methods" }
|
||||||
ata-methods = { path = "../ata/methods" }
|
ata-methods = { path = "../ata/methods" }
|
||||||
stablecoin-methods = { path = "../stablecoin/methods" }
|
stablecoin-methods = { path = "../stablecoin/methods" }
|
||||||
|
twap-oracle-methods = { path = "../twap_oracle/methods" }
|
||||||
|
|||||||
@ -11,6 +11,7 @@ use nssa::{
|
|||||||
error::NssaError,
|
error::NssaError,
|
||||||
program_deployment_transaction::{self, ProgramDeploymentTransaction},
|
program_deployment_transaction::{self, ProgramDeploymentTransaction},
|
||||||
public_transaction, PrivateKey, PublicKey, PublicTransaction, V03State,
|
public_transaction, PrivateKey, PublicKey, PublicTransaction, V03State,
|
||||||
|
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||||
};
|
};
|
||||||
use nssa_core::account::{Account, AccountId, Data, Nonce};
|
use nssa_core::account::{Account, AccountId, Data, Nonce};
|
||||||
use token_core::{TokenDefinition, TokenHolding};
|
use token_core::{TokenDefinition, TokenHolding};
|
||||||
@ -47,10 +48,29 @@ impl Ids {
|
|||||||
amm_methods::AMM_ID
|
amm_methods::AMM_ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn twap_oracle_program() -> nssa_core::program::ProgramId {
|
||||||
|
twap_oracle_methods::TWAP_ORACLE_ID
|
||||||
|
}
|
||||||
|
|
||||||
fn config() -> AccountId {
|
fn config() -> AccountId {
|
||||||
amm_core::compute_config_pda(Self::amm_program())
|
amm_core::compute_config_pda(Self::amm_program())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn token_a_definition() -> AccountId {
|
fn token_a_definition() -> AccountId {
|
||||||
AccountId::new([3; 32])
|
AccountId::new([3; 32])
|
||||||
}
|
}
|
||||||
@ -301,12 +321,27 @@ impl Accounts {
|
|||||||
balance: 0_u128,
|
balance: 0_u128,
|
||||||
data: Data::from(&amm_core::AmmConfig {
|
data: Data::from(&amm_core::AmmConfig {
|
||||||
token_program_id: Ids::token_program(),
|
token_program_id: Ids::token_program(),
|
||||||
|
twap_oracle_program_id: Ids::twap_oracle_program(),
|
||||||
authority: Ids::admin(),
|
authority: Ids::admin(),
|
||||||
}),
|
}),
|
||||||
nonce: Nonce(0),
|
nonce: Nonce(0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
fn user_a_holding() -> Account {
|
||||||
Account {
|
Account {
|
||||||
program_owner: Ids::token_program(),
|
program_owner: Ids::token_program(),
|
||||||
@ -933,6 +968,14 @@ fn deploy_programs(state: &mut V03State) {
|
|||||||
amm_message,
|
amm_message,
|
||||||
))
|
))
|
||||||
.expect("amm program deployment must succeed");
|
.expect("amm program deployment must succeed");
|
||||||
|
|
||||||
|
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn state_for_amm_tests() -> V03State {
|
fn state_for_amm_tests() -> V03State {
|
||||||
@ -1189,6 +1232,7 @@ fn execute_remove_liquidity(
|
|||||||
fn execute_initialize(state: &mut V03State) {
|
fn execute_initialize(state: &mut V03State) {
|
||||||
let instruction = amm_core::Instruction::Initialize {
|
let instruction = amm_core::Instruction::Initialize {
|
||||||
token_program_id: Ids::token_program(),
|
token_program_id: Ids::token_program(),
|
||||||
|
twap_oracle_program_id: Ids::twap_oracle_program(),
|
||||||
authority: Ids::admin(),
|
authority: Ids::admin(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1206,6 +1250,32 @@ fn execute_initialize(state: &mut V03State) {
|
|||||||
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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)
|
||||||
|
}
|
||||||
|
|
||||||
fn fungible_balance(account: &Account) -> u128 {
|
fn fungible_balance(account: &Account) -> u128 {
|
||||||
let holding = TokenHolding::try_from(&account.data).expect("expected token holding");
|
let holding = TokenHolding::try_from(&account.data).expect("expected token holding");
|
||||||
let TokenHolding::Fungible {
|
let TokenHolding::Fungible {
|
||||||
@ -1264,11 +1334,13 @@ fn execute_update_config(
|
|||||||
state: &mut V03State,
|
state: &mut V03State,
|
||||||
signer: &PrivateKey,
|
signer: &PrivateKey,
|
||||||
token_program_id: Option<nssa_core::program::ProgramId>,
|
token_program_id: Option<nssa_core::program::ProgramId>,
|
||||||
|
twap_oracle_program_id: Option<nssa_core::program::ProgramId>,
|
||||||
new_authority: Option<AccountId>,
|
new_authority: Option<AccountId>,
|
||||||
) -> Result<(), NssaError> {
|
) -> Result<(), NssaError> {
|
||||||
let signer_id = AccountId::from(&PublicKey::new_from_private_key(signer));
|
let signer_id = AccountId::from(&PublicKey::new_from_private_key(signer));
|
||||||
let instruction = amm_core::Instruction::UpdateConfig {
|
let instruction = amm_core::Instruction::UpdateConfig {
|
||||||
token_program_id,
|
token_program_id,
|
||||||
|
twap_oracle_program_id,
|
||||||
new_authority,
|
new_authority,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1308,6 +1380,7 @@ fn amm_update_config_changes_token_program_id_and_authority() {
|
|||||||
&mut state,
|
&mut state,
|
||||||
&Keys::admin(),
|
&Keys::admin(),
|
||||||
Some(new_token_program),
|
Some(new_token_program),
|
||||||
|
None,
|
||||||
Some(new_admin),
|
Some(new_admin),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@ -1323,7 +1396,7 @@ fn amm_update_config_rejects_non_admin() {
|
|||||||
|
|
||||||
// user_a is not the admin; even though they sign, the update is rejected and the config is
|
// user_a is not the admin; even though they sign, the update is rejected and the config is
|
||||||
// left unchanged.
|
// left unchanged.
|
||||||
let result = execute_update_config(&mut state, &Keys::user_a(), Some([123u32; 8]), None);
|
let result = execute_update_config(&mut state, &Keys::user_a(), Some([123u32; 8]), None, None);
|
||||||
assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_))));
|
assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_))));
|
||||||
|
|
||||||
let config = config_data(&state);
|
let config = config_data(&state);
|
||||||
@ -1337,18 +1410,109 @@ fn amm_update_config_authority_handoff_revokes_old_admin() {
|
|||||||
let new_admin = Ids::user_a();
|
let new_admin = Ids::user_a();
|
||||||
|
|
||||||
// Admin hands off control to user_a.
|
// Admin hands off control to user_a.
|
||||||
execute_update_config(&mut state, &Keys::admin(), None, Some(new_admin)).unwrap();
|
execute_update_config(&mut state, &Keys::admin(), None, None, Some(new_admin)).unwrap();
|
||||||
assert_eq!(config_data(&state).authority, new_admin);
|
assert_eq!(config_data(&state).authority, new_admin);
|
||||||
|
|
||||||
// The original admin can no longer update.
|
// The original admin can no longer update.
|
||||||
let result = execute_update_config(&mut state, &Keys::admin(), Some([123u32; 8]), None);
|
let result = execute_update_config(&mut state, &Keys::admin(), Some([123u32; 8]), None, None);
|
||||||
assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_))));
|
assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_))));
|
||||||
|
|
||||||
// The new admin can.
|
// The new admin can.
|
||||||
execute_update_config(&mut state, &Keys::user_a(), Some([124u32; 8]), None).unwrap();
|
execute_update_config(&mut state, &Keys::user_a(), Some([124u32; 8]), None, None).unwrap();
|
||||||
assert_eq!(config_data(&state).token_program_id, [124u32; 8]);
|
assert_eq!(config_data(&state).token_program_id, [124u32; 8]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn amm_creates_price_observations_on_twap_oracle() {
|
||||||
|
let mut state = state_for_amm_tests();
|
||||||
|
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 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());
|
||||||
|
assert_eq!(feed.last_recorded_tick, current_tick);
|
||||||
|
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()),
|
||||||
|
Accounts::pool_definition_init()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn amm_create_price_observations_rejects_existing_account() {
|
||||||
|
let mut state = state_for_amm_tests();
|
||||||
|
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();
|
||||||
|
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;
|
||||||
|
|
||||||
|
// No current-tick account was created, so there is no authoritative tick to seed from.
|
||||||
|
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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn amm_remove_liquidity() {
|
fn amm_remove_liquidity() {
|
||||||
let mut state = state_for_amm_tests();
|
let mut state = state_for_amm_tests();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user