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:
r4bbit
2026-06-22 09:47:45 +02:00
parent 1d9e3dcb49
commit 4e4338945d
13 changed files with 1554 additions and 69 deletions
+27 -7
View File
@@ -7,10 +7,11 @@ use nssa_core::{
/// Initializes the AMM Program by creating its singleton configuration account.
///
/// 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
/// AMM issues every chained call to) and `authority` (the admin allowed to change configuration
/// later via `update_config`). Its existence is the Program's "initialized" flag: the
/// chained-call instructions read the Token Program ID from it and reject calls until it exists.
/// (`compute_config_pda(amm_program_id)`) and stores the program IDs the AMM issues chained calls
/// to — `token_program_id` (the Token Program) and `twap_oracle_program_id` (the TWAP oracle) —
/// plus `authority` (the admin allowed to change configuration later via `update_config`). Its
/// 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 if:
@@ -19,6 +20,7 @@ use nssa_core::{
pub fn initialize(
config: AccountWithMetadata,
token_program_id: ProgramId,
twap_oracle_program_id: ProgramId,
authority: AccountId,
amm_program_id: ProgramId,
) -> Vec<AccountPostState> {
@@ -36,6 +38,7 @@ pub fn initialize(
let mut config_post = config.account.clone();
config_post.data = Data::from(&AmmConfig {
token_program_id,
twap_oracle_program_id,
authority,
});
@@ -54,6 +57,7 @@ mod tests {
const AMM_PROGRAM_ID: ProgramId = [42; 8];
const TOKEN_PROGRAM_ID: ProgramId = [15; 8];
const TWAP_ORACLE_PROGRAM_ID: ProgramId = [77; 8];
fn authority() -> AccountId {
AccountId::new([9; 32])
@@ -72,6 +76,7 @@ mod tests {
let post_states = initialize(
config_uninit(),
TOKEN_PROGRAM_ID,
TWAP_ORACLE_PROGRAM_ID,
authority(),
AMM_PROGRAM_ID,
);
@@ -83,16 +88,18 @@ mod tests {
}
#[test]
fn stores_token_program_id_and_authority() {
fn stores_program_ids_and_authority() {
let post_states = initialize(
config_uninit(),
TOKEN_PROGRAM_ID,
TWAP_ORACLE_PROGRAM_ID,
authority(),
AMM_PROGRAM_ID,
);
let config = AmmConfig::try_from(&post_states[0].account().data)
.expect("post state must contain a valid AmmConfig");
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());
}
@@ -101,7 +108,13 @@ mod tests {
fn wrong_config_account_id_panics() {
let mut wrong = config_uninit();
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]
@@ -110,9 +123,16 @@ mod tests {
let mut initialized = config_uninit();
initialized.account.data = Data::from(&AmmConfig {
token_program_id: TOKEN_PROGRAM_ID,
twap_oracle_program_id: TWAP_ORACLE_PROGRAM_ID,
authority: authority(),
});
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,
);
}
}