From c528d85a2b60ddfa76f1217ae1bd3e683c85939a Mon Sep 17 00:00:00 2001 From: r4bbit <445106+0x-r4bbit@users.noreply.github.com> Date: Tue, 2 Jun 2026 18:07:45 +0200 Subject: [PATCH] feat(twap-oracle): implement PublishPrice with tick-to-price conversion and tail extrapolation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add PublishPrice — a permissionless instruction that computes the TWAP over a PriceObservations buffer, extrapolated to the current time, and writes it to the consumer-facing OraclePriceAccount. The stored body averages [t1, t2] (t1 = oldest valid entry, t2 = most recent), needing no boundary search since each buffer is calibrated to one window_duration. The final segment from t2 to `now` is extrapolated from the live tick in the CurrentTickAccount (added as a fourth account), mirroring Uniswap's OracleLibrary.consult. This keeps the published timestamp = now truthful: an unchanged price yields a fresh stamp and the correct value, and a republish picks up a since-reported move instead of freezing the pre-move average. The live tick is only credited since it was written, so the tail is split at the current tick's last_updated: boundary = clamp(current_tick.last_updated, t2.ts, now) clamped_tick = last_recorded_tick + clamp(current_tick - last_recorded_tick, ±MAX_TICK_DELTA) cum_now = t2.tick_cumulative + last_recorded_tick * (boundary - t2.ts) // before the live tick took effect + clamped_tick * (now - boundary) // live tick, only since last_updated twap_tick = (cum_now - t1.tick_cumulative) / (now - t1.ts) // floor (div_euclid) Splitting at last_updated stops a tick written moments before publish from being smeared across a stale gap and inflating a supposedly fresh TWAP. The live-tick segment is clamped against last_recorded_tick by MAX_TICK_DELTA — the same bound RecordTick applies — capping how far a current-tick move can shift the result. A zero-length tail (now == t2.ts) leaves the pure stored-window average. If fewer than two observations exist the call is a silent no-op, leaving the price account at timestamp = 0 (the uninitialized signal consumers reject). While young, the TWAP covers the available span, which may be shorter than the window. The TWAP tick is converted to a price ratio via the Uniswap v3 sqrtPriceX96 representation (pure integer, zkVM-safe), stored as a Q64.64 in OraclePriceAccount.price — source-agnostic, no tick framing leaks into the standard. Out-of-range ticks clamp; ratios above 2^64 saturate at u128::MAX. Adds PRICE_FRACTIONAL_BITS = 64; removes the placeholder TWAP_PRICE_BIAS encoding. Closes #117 --- artifacts/twap_oracle-idl.json | 39 + programs/twap_oracle/core/src/lib.rs | 146 +++ .../methods/guest/src/bin/twap_oracle.rs | 31 + programs/twap_oracle/src/lib.rs | 1 + programs/twap_oracle/src/publish_price.rs | 917 ++++++++++++++++++ 5 files changed, 1134 insertions(+) create mode 100644 programs/twap_oracle/src/publish_price.rs diff --git a/artifacts/twap_oracle-idl.json b/artifacts/twap_oracle-idl.json index d8ea7a4..51bc2d4 100644 --- a/artifacts/twap_oracle-idl.json +++ b/artifacts/twap_oracle-idl.json @@ -105,6 +105,45 @@ } ] }, + { + "name": "publish_price", + "accounts": [ + { + "name": "price_observations", + "writable": false, + "signer": false, + "init": false + }, + { + "name": "oracle_price_account", + "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": [ + { + "name": "price_source_id", + "type": "account_id" + }, + { + "name": "window_duration", + "type": "u64" + } + ] + }, { "name": "record_tick", "accounts": [ diff --git a/programs/twap_oracle/core/src/lib.rs b/programs/twap_oracle/core/src/lib.rs index d4c5a9b..b4d70a3 100644 --- a/programs/twap_oracle/core/src/lib.rs +++ b/programs/twap_oracle/core/src/lib.rs @@ -94,6 +94,40 @@ pub enum Instruction { /// tick on-chain. price: u128, }, + /// Computes the TWAP over `window_duration` from the [`PriceObservations`] ring buffer and + /// writes the result to the [`OraclePriceAccount`]. + /// + /// Permissionless — anyone may call this. Returns all accounts unchanged (no-op) if the + /// ring buffer holds fewer than two observations. Once at least two observations exist the + /// TWAP is computed over the available history, which may be shorter than `window_duration` + /// while the buffer is young. + /// + /// The resulting TWAP tick is converted to a `Q64.64` price ratio via [`tick_to_oracle_price`] + /// and stored in [`OraclePriceAccount::price`]. The stored value is a plain price ratio, not a + /// tick encoding, so consumers read it directly (`price / 2^PRICE_FRACTIONAL_BITS`) and the + /// account stays source-agnostic — a non-tick source (e.g. RedStone, Pyth) writes the same + /// field. + /// + /// Required accounts (in order): + /// 1. Price observations account — initialized PDA derived from + /// `compute_price_observations_pda(self_program_id, price_source_id, window_duration)`. + /// 2. Oracle price account — initialized PDA derived from + /// `compute_oracle_price_account_pda(self_program_id, price_source_id, window_duration)`. + /// 3. Current tick account — initialized PDA derived from + /// `compute_current_tick_account_pda(self_program_id, price_source_id)`. Supplies the live + /// tick used to extend the average from the newest stored observation up to `now`. + /// 4. Clock account — read-only; supplies the publication timestamp. + PublishPrice { + /// ID of the price source; used to verify all three PDAs. + price_source_id: AccountId, + /// Duration of the TWAP window in milliseconds. Used only to verify the price + /// observations and oracle price account PDAs (the current tick account PDA does not + /// depend on it). No boundary search is performed: the oldest valid observation is the + /// ring buffer's natural start (entry 0 while partially filled, else the slot at + /// `write_index`), because each observations account is calibrated to one window via its + /// sampling guard. + window_duration: u64, + }, /// Records the current tick from a [`CurrentTickAccount`] into a [`PriceObservations`] /// ring buffer. /// @@ -418,6 +452,53 @@ fn integer_sqrt(n: alloy_primitives::U256) -> alloy_primitives::U256 { c } +// ────────────────────────────────────────────────────────────────────────────── +// TWAP price encoding +// ────────────────────────────────────────────────────────────────────────────── + +/// Number of fractional bits in the [`OraclePriceAccount::price`] fixed-point value. +/// +/// The price is stored as a `Q64.64` ratio: `OraclePriceAccount::price / 2^PRICE_FRACTIONAL_BITS` +/// is the amount of `quote_asset` one unit of `base_asset` is worth. A consumer multiplies a +/// token amount by the price with `(amount * price) >> PRICE_FRACTIONAL_BITS`. +pub const PRICE_FRACTIONAL_BITS: u32 = 64; + +/// Converts a TWAP tick into the `Q64.64` fixed-point price stored in +/// [`OraclePriceAccount::price`]. +/// +/// The price is `1.0001^tick`, computed via the Uniswap v3 `sqrtPriceX96` representation +/// (pure-integer, no floating point) and then squared back to a plain ratio: +/// +/// ```text +/// sqrtPriceX96 = sqrt(1.0001^tick) * 2^96 +/// price = sqrtPriceX96^2 / 2^128 = 1.0001^tick * 2^64 (Q64.64) +/// ``` +/// +/// `sqrtPriceX96^2` is computed with `full_math::mul_div` using a 512-bit intermediate, so it +/// never overflows for any valid tick. The tick is clamped to `[MIN_TICK, MAX_TICK]` and the +/// result saturates at `u128::MAX` for the (practically unreachable) ticks above ~443 636 whose +/// ratio would exceed `2^64`. +/// +/// See `docs/twap-oracle-tick-to-price-conversion.md` for the full derivation. +#[must_use] +pub fn tick_to_oracle_price(tick: i32) -> u128 { + use alloy_primitives::U256; + use uniswap_v3_math::tick_math::{MAX_TICK, MIN_TICK}; + + // 2^128, used to bring sqrtPriceX96^2 (a Q128.128 square) down to Q64.64. + // Built from limbs (little-endian u64 words) to avoid arithmetic operators on U256. + const TWO_POW_128: U256 = U256::from_limbs([0, 0, 1, 0]); + + let clamped_tick = tick.clamp(MIN_TICK, MAX_TICK); + let sqrt_price_x96 = uniswap_v3_math::tick_math::get_sqrt_ratio_at_tick(clamped_tick) + .expect("clamped tick is within [MIN_TICK, MAX_TICK]"); + let price_q64_64 = + uniswap_v3_math::full_math::mul_div(sqrt_price_x96, sqrt_price_x96, TWO_POW_128) + .expect("1.0001^tick * 2^64 fits in U256 for any valid tick"); + + u128::try_from(price_q64_64).unwrap_or(u128::MAX) +} + // ────────────────────────────────────────────────────────────────────────────── // Current tick account // ────────────────────────────────────────────────────────────────────────────── @@ -622,4 +703,69 @@ mod tests { "root of a ~2^256 value should be ~2^128" ); } + + // ── tick_to_oracle_price ────────────────────────────────────────────────────── + + #[test] + fn tick_zero_is_unit_price() { + // 1.0001^0 = 1.0 → exactly 2^64 in Q64.64. + assert_eq!(tick_to_oracle_price(0), ONE_Q64_64); + } + + #[test] + fn positive_tick_is_above_unit() { + assert!(tick_to_oracle_price(1) > ONE_Q64_64); + assert!(tick_to_oracle_price(10_000) > ONE_Q64_64); + } + + #[test] + fn negative_tick_is_below_unit() { + assert!(tick_to_oracle_price(-1) < ONE_Q64_64); + assert!(tick_to_oracle_price(-10_000) < ONE_Q64_64); + } + + #[test] + fn price_is_monotonic_in_tick() { + let mut prev = tick_to_oracle_price(-50_000); + for tick in (-49_000..=50_000).step_by(1_000) { + let cur = tick_to_oracle_price(tick); + assert!(cur > prev, "price must increase with tick at {tick}"); + prev = cur; + } + } + + #[test] + fn tick_10000_matches_known_ratio() { + // 1.0001^10000 ≈ 2.71814. Check the ratio in milli-units (× 1000) lands in [2717, 2719] + // using integer math only — `price * 1000 / 2^64` ≈ 2718. + let price = tick_to_oracle_price(10_000); + let ratio_milli = price + .checked_mul(1_000) + .and_then(|scaled| scaled.checked_div(ONE_Q64_64)) + .expect("price * 1000 fits in u128"); + assert!( + (2_717..=2_719).contains(&ratio_milli), + "got {ratio_milli} / 1000" + ); + } + + #[test] + fn extreme_positive_tick_saturates() { + // 1.0001^MAX_TICK far exceeds 2^64, so the Q64.64 value saturates at u128::MAX. + let price = tick_to_oracle_price(uniswap_v3_math::tick_math::MAX_TICK); + assert_eq!(price, u128::MAX); + } + + #[test] + fn ticks_beyond_bounds_are_clamped() { + // Ticks outside [MIN_TICK, MAX_TICK] must not panic; they clamp to the bound. + assert_eq!( + tick_to_oracle_price(i32::MAX), + tick_to_oracle_price(uniswap_v3_math::tick_math::MAX_TICK) + ); + assert_eq!( + tick_to_oracle_price(i32::MIN), + tick_to_oracle_price(uniswap_v3_math::tick_math::MIN_TICK) + ); + } } diff --git a/programs/twap_oracle/methods/guest/src/bin/twap_oracle.rs b/programs/twap_oracle/methods/guest/src/bin/twap_oracle.rs index a32df74..05b05e7 100644 --- a/programs/twap_oracle/methods/guest/src/bin/twap_oracle.rs +++ b/programs/twap_oracle/methods/guest/src/bin/twap_oracle.rs @@ -105,6 +105,37 @@ mod twap_oracle { Ok(spel_framework::SpelOutput::execute(post_states, vec![])) } + /// Computes the TWAP from the price observations ring buffer (extrapolated to `now` using the + /// current tick) and writes it to the price account. + /// + /// Expected accounts: + /// 1. `price_observations` — initialized PDA owned by this oracle program. + /// 2. `oracle_price_account` — initialized PDA owned by this oracle program. + /// 3. `current_tick_account` — initialized PDA owned by this oracle program; supplies the live + /// tick used to extend the average from the newest stored observation up to `now`. + /// 4. `clock` — read-only LEZ clock account. + #[instruction] + pub fn publish_price( + ctx: ProgramContext, + price_observations: AccountWithMetadata, + oracle_price_account: AccountWithMetadata, + current_tick_account: AccountWithMetadata, + clock: AccountWithMetadata, + price_source_id: AccountId, + window_duration: u64, + ) -> SpelResult { + let post_states = twap_oracle_program::publish_price::publish_price( + price_observations, + oracle_price_account, + current_tick_account, + clock, + price_source_id, + window_duration, + ctx.self_program_id, + ); + Ok(spel_framework::SpelOutput::execute(post_states, vec![])) + } + /// Records the current tick into a price observations ring buffer. /// /// Expected accounts: diff --git a/programs/twap_oracle/src/lib.rs b/programs/twap_oracle/src/lib.rs index a01c5fe..9f7e825 100644 --- a/programs/twap_oracle/src/lib.rs +++ b/programs/twap_oracle/src/lib.rs @@ -5,5 +5,6 @@ pub use twap_oracle_core as core; pub mod create_current_tick_account; pub mod create_oracle_price_account; pub mod create_price_observations; +pub mod publish_price; pub mod record_tick; pub mod update_current_tick; diff --git a/programs/twap_oracle/src/publish_price.rs b/programs/twap_oracle/src/publish_price.rs new file mode 100644 index 0000000..5b6b810 --- /dev/null +++ b/programs/twap_oracle/src/publish_price.rs @@ -0,0 +1,917 @@ +use clock_core::{ClockAccountData, CLOCK_01_PROGRAM_ACCOUNT_ID}; +use nssa_core::{ + account::{AccountId, AccountWithMetadata, Data}, + program::{AccountPostState, ProgramId}, +}; +use twap_oracle_core::{ + compute_current_tick_account_pda, compute_oracle_price_account_pda, + compute_price_observations_pda, tick_to_oracle_price, CurrentTickAccount, OraclePriceAccount, + PriceObservations, MAX_TICK_DELTA, OBSERVATIONS_CAPACITY, +}; + +/// Computes the TWAP over the span from the oldest stored observation up to `now` and writes the +/// result to the [`OraclePriceAccount`]. +/// +/// The *body* of the average comes from the [`PriceObservations`] ring buffer (oldest valid entry +/// `t1` through newest stored entry `t2`). The *final segment* from `t2` to `now` is extrapolated +/// from the [`CurrentTickAccount`], mirroring Uniswap's `OracleLibrary.consult`, which always +/// projects the accumulator to the present rather than reading only stored points. +/// +/// Projecting to `now` is what makes the published `timestamp = now` truthful even when no tick has +/// been recorded for a while: a price that has simply not changed yields a *fresh* timestamp and +/// the correct (unchanged) value, instead of a stale window stamped with a current time. If the +/// price *did* move since `t2` and the move was reported to the current tick account, the tail +/// carries it forward (clamped) rather than publishing a frozen pre-move average. +/// +/// The tail is split at the current tick's `last_updated`: the live tick is only known to have held +/// since then, so it is integrated only over `[last_updated, now]`, while `[t2, last_updated]` +/// carries the tick stored at t2 (`last_recorded_tick`). Without this split a tick written moments +/// before publish would be smeared across the whole unrecorded gap, letting a late spot move +/// dominate a long stale tail and inflating a supposedly fresh TWAP. The live-tick segment is +/// clamped against `last_recorded_tick` by [`MAX_TICK_DELTA`] — the same anti-manipulation bound +/// `RecordTick` applies — bounding how far an attacker who can move the current tick can shift the +/// published average. +/// +/// Each observations account is calibrated to a specific `window_duration` via its sampling guard +/// (`min_interval = window_duration / OBSERVATIONS_CAPACITY`), so the oldest valid entry is always +/// the natural start of the window — no boundary search is needed. +/// +/// Returns all accounts unchanged when fewer than two observations are available. The price account +/// stays at `timestamp = 0` (uninitialized signal) until there is something to publish. +/// +/// The publication timestamp is taken from `clock`, which must be [`CLOCK_01_PROGRAM_ACCOUNT_ID`]. +/// It becomes the consumer-facing freshness signal on the [`OraclePriceAccount`], so a forged or +/// caller-controlled clock could make a stale price look current; it must never be caller-supplied. +/// +/// # Panics +/// Panics if: +/// - `price_observations.account_id` does not match +/// `compute_price_observations_pda(oracle_program_id, price_source_id, window_duration)`. +/// - `oracle_price_account.account_id` does not match +/// `compute_oracle_price_account_pda(oracle_program_id, price_source_id, window_duration)`. +/// - `current_tick_account.account_id` does not match +/// `compute_current_tick_account_pda(oracle_program_id, price_source_id)`. +/// - `clock.account_id` is not [`CLOCK_01_PROGRAM_ACCOUNT_ID`]. +/// - Any account is not a valid initialised account of its respective type. +pub fn publish_price( + price_observations: AccountWithMetadata, + oracle_price_account: AccountWithMetadata, + current_tick_account: AccountWithMetadata, + clock: AccountWithMetadata, + price_source_id: AccountId, + window_duration: u64, + oracle_program_id: ProgramId, +) -> Vec { + assert_eq!( + price_observations.account_id, + compute_price_observations_pda(oracle_program_id, price_source_id, window_duration), + "PublishPrice: price observations account ID does not match expected PDA" + ); + assert_eq!( + oracle_price_account.account_id, + compute_oracle_price_account_pda(oracle_program_id, price_source_id, window_duration), + "PublishPrice: oracle price account ID does not match expected PDA" + ); + assert_eq!( + current_tick_account.account_id, + compute_current_tick_account_pda(oracle_program_id, price_source_id), + "PublishPrice: current tick account ID does not match expected PDA" + ); + assert_eq!( + clock.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID, + "PublishPrice: clock account must be the canonical 1-block LEZ clock account" + ); + + let clock_data = ClockAccountData::from_bytes(clock.account.data.as_ref()); + let now = clock_data.timestamp; + + let observations = PriceObservations::try_from(&price_observations.account.data) + .expect("PublishPrice: price observations account must be initialized"); + let mut price_account = OraclePriceAccount::try_from(&oracle_price_account.account.data) + .expect("PublishPrice: oracle price account must be initialized"); + let current_tick_data = CurrentTickAccount::try_from(¤t_tick_account.account.data) + .expect("PublishPrice: current tick account must be initialized"); + + // No-op: need at least two observations to compute a TWAP. + if observations.total_entries < 2 { + return vec![ + AccountPostState::new(price_observations.account.clone()), + AccountPostState::new(oracle_price_account.account.clone()), + AccountPostState::new(current_tick_account.account.clone()), + AccountPostState::new(clock.account.clone()), + ]; + } + + let capacity = + usize::try_from(OBSERVATIONS_CAPACITY).expect("OBSERVATIONS_CAPACITY fits in usize"); + + // t2: the most recent observation. + let t2_index = if observations.write_index == 0 { + capacity + .checked_sub(1) + .expect("OBSERVATIONS_CAPACITY is non-zero") + } else { + usize::try_from( + observations + .write_index + .checked_sub(1) + .expect("write_index > 0"), + ) + .expect("write_index - 1 fits in usize") + }; + + // t1: the oldest valid observation. Once the buffer is full, the oldest entry sits at + // write_index (the slot about to be overwritten next). Before that, entries start at 0. + let is_full = observations.total_entries >= u64::from(OBSERVATIONS_CAPACITY); + let t1_index = if is_full { + usize::try_from(observations.write_index).expect("write_index fits in usize") + } else { + 0 + }; + + let t1 = observations + .entries + .get(t1_index) + .expect("t1_index is within bounds"); + let t2 = observations + .entries + .get(t2_index) + .expect("t2_index is within bounds"); + + // Extrapolate the accumulator from the newest stored observation (t2) to `now`. The live tick + // is only known to have held since `current_tick_data.last_updated`; before that, the best + // estimate for the unrecorded gap is the tick stored at t2 (`last_recorded_tick`). Splitting + // the tail at that boundary stops a tick written moments before publish from being smeared + // across the whole gap — which would let a late price move dominate a long stale tail. The + // live-tick segment is clamped against last_recorded_tick by MAX_TICK_DELTA — the same bound + // RecordTick applies. With no elapsed tail (`now == t2.timestamp`) both segments are empty and + // the TWAP is exactly the stored-window average. + let current_tick = current_tick_data.tick; + let delta = current_tick.saturating_sub(observations.last_recorded_tick); + let clamped_delta = delta.clamp(-MAX_TICK_DELTA, MAX_TICK_DELTA); + let clamped_tick = observations + .last_recorded_tick + .saturating_add(clamped_delta); + + // `now` comes from the canonical clock and is monotonic, so it is never before the newest + // stored observation. The boundary is clamped into `[t2.timestamp, now]`: a `last_updated` at + // or before t2 means the live tick provably held for the whole tail (pre-boundary segment + // empty); one at or after `now` means the live tick has accrued no elapsed time yet + // (post-boundary segment empty). The clamp also keeps a degenerate clock from underflowing the + // saturating subtractions into a huge tail. + let boundary = current_tick_data.last_updated.clamp(t2.timestamp, now); + let pre_ms = boundary.saturating_sub(t2.timestamp); + let post_ms = now.saturating_sub(boundary); + let pre_ms_i64 = i64::try_from(pre_ms).expect("pre_ms fits in i64"); + let post_ms_i64 = i64::try_from(post_ms).expect("post_ms fits in i64"); + + // Pre-boundary segment carries `last_recorded_tick` (the tick at t2); post-boundary segment + // carries the clamped live tick. + let pre_cumulative = i64::from(observations.last_recorded_tick) + .checked_mul(pre_ms_i64) + .expect("pre-boundary tail cumulative fits in i64"); + let post_cumulative = i64::from(clamped_tick) + .checked_mul(post_ms_i64) + .expect("post-boundary tail cumulative fits in i64"); + let cum_now = t2 + .tick_cumulative + .checked_add(pre_cumulative) + .and_then(|acc| acc.checked_add(post_cumulative)) + .expect("extrapolated tick_cumulative fits in i64"); + + // Average over [t1, now]. `now > t1.timestamp` because `now >= t2.timestamp > t1.timestamp` + // (the sampling guard makes stored timestamps strictly increasing), so the divisor is positive. + let elapsed_ms = now.checked_sub(t1.timestamp).expect("now >= t1.timestamp"); + let elapsed_ms_i64 = i64::try_from(elapsed_ms).expect("elapsed_ms fits in i64"); + let cumulative_diff = cum_now + .checked_sub(t1.tick_cumulative) + .expect("tick_cumulative difference fits in i64"); + // Floor division (round toward −∞), matching Uniswap's `OracleLibrary.consult`. The divisor is + // always positive, so `div_euclid` is exactly the floor. Plain truncating division would round + // toward zero, biasing negative TWAPs upward by one tick. + let twap_tick_i64 = cumulative_diff + .checked_div_euclid(elapsed_ms_i64) + .expect("elapsed_ms is non-zero"); + let twap_tick = i32::try_from(twap_tick_i64).expect("TWAP tick fits in i32"); + + price_account.price = tick_to_oracle_price(twap_tick); + price_account.timestamp = now; + + let mut oracle_price_account_post = oracle_price_account.account.clone(); + oracle_price_account_post.data = Data::from(&price_account); + + vec![ + AccountPostState::new(price_observations.account.clone()), + AccountPostState::new(oracle_price_account_post), + AccountPostState::new(current_tick_account.account.clone()), + AccountPostState::new(clock.account.clone()), + ] +} + +#[cfg(test)] +mod tests { + use nssa_core::account::{Account, AccountId, Nonce}; + use twap_oracle_core::{ + compute_current_tick_account_pda, compute_oracle_price_account_pda, + compute_price_observations_pda, tick_to_oracle_price, CurrentTickAccount, ObservationEntry, + OraclePriceAccount, PriceObservations, MAX_TICK_DELTA, OBSERVATIONS_CAPACITY, + }; + + use super::*; + + const ORACLE_PROGRAM_ID: ProgramId = [77u32; 8]; + const CLOCK_PROGRAM_ID: ProgramId = [88u32; 8]; + const WINDOW_24H: u64 = 24 * 60 * 60 * 1_000; + + fn price_source_id() -> AccountId { + AccountId::new([1u8; 32]) + } + fn base_asset_id() -> AccountId { + AccountId::new([10u8; 32]) + } + fn quote_asset_id() -> AccountId { + AccountId::new([11u8; 32]) + } + + fn clock_account_with_id(timestamp: u64, account_id: AccountId) -> AccountWithMetadata { + let data = ClockAccountData { + block_id: 0, + timestamp, + } + .to_bytes(); + AccountWithMetadata { + account: Account { + program_owner: CLOCK_PROGRAM_ID, + balance: 0, + data: Data::try_from(data).expect("ClockAccountData fits in Data"), + nonce: Nonce(0), + }, + is_authorized: false, + account_id, + } + } + + fn clock_account_with_timestamp(timestamp: u64) -> AccountWithMetadata { + clock_account_with_id(timestamp, CLOCK_01_PROGRAM_ACCOUNT_ID) + } + + /// Builds a [`CurrentTickAccount`] at the canonical PDA for [`price_source_id`]. + fn make_current_tick_account(tick: i32, last_updated: u64) -> AccountWithMetadata { + let stored = CurrentTickAccount { tick, last_updated }; + AccountWithMetadata { + account: Account { + program_owner: ORACLE_PROGRAM_ID, + balance: 0, + data: Data::from(&stored), + nonce: Nonce(0), + }, + is_authorized: false, + account_id: compute_current_tick_account_pda(ORACLE_PROGRAM_ID, price_source_id()), + } + } + + /// Builds a [`PriceObservations`] from `(timestamp_ms, tick_cumulative)` pairs written in + /// order starting at index 0. `write_index` is set to `entries_data.len()`. + fn make_price_observations( + entries_data: &[(u64, i64)], + last_recorded_tick: i32, + ) -> AccountWithMetadata { + let capacity = + usize::try_from(OBSERVATIONS_CAPACITY).expect("OBSERVATIONS_CAPACITY fits in usize"); + let mut entries = vec![ObservationEntry::default(); capacity]; + for (i, &(timestamp, tick_cumulative)) in entries_data.iter().enumerate() { + *entries.get_mut(i).expect("i < capacity") = ObservationEntry { + timestamp, + tick_cumulative, + }; + } + let write_index = u32::try_from(entries_data.len()).expect("entry count fits in u32"); + let total_entries = u64::try_from(entries_data.len()).expect("entry count fits in u64"); + let obs = PriceObservations { + price_source_id: price_source_id(), + write_index, + total_entries, + last_recorded_tick, + entries, + }; + AccountWithMetadata { + account: Account { + program_owner: ORACLE_PROGRAM_ID, + balance: 0, + data: Data::from(&obs), + nonce: Nonce(0), + }, + is_authorized: false, + account_id: compute_price_observations_pda( + ORACLE_PROGRAM_ID, + price_source_id(), + WINDOW_24H, + ), + } + } + + fn make_oracle_price_account() -> AccountWithMetadata { + let account = OraclePriceAccount { + base_asset: base_asset_id(), + quote_asset: quote_asset_id(), + price: 0, + timestamp: 0, + source_id: price_source_id(), + confidence_interval: 0, + }; + AccountWithMetadata { + account: Account { + program_owner: ORACLE_PROGRAM_ID, + balance: 0, + data: Data::from(&account), + nonce: Nonce(0), + }, + is_authorized: false, + account_id: compute_oracle_price_account_pda( + ORACLE_PROGRAM_ID, + price_source_id(), + WINDOW_24H, + ), + } + } + + // ── happy path ──────────────────────────────────────────────────────────── + + #[test] + fn returns_four_post_states() { + let post_states = publish_price( + make_price_observations(&[(0, 0), (WINDOW_24H, 0)], 0), + make_oracle_price_account(), + make_current_tick_account(0, WINDOW_24H), + clock_account_with_timestamp(WINDOW_24H), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + assert_eq!(post_states.len(), 4); + } + + #[test] + fn twap_tick_computed_and_stored_as_price() { + // Constant tick = 100 over 24 h, published exactly at the newest observation (zero tail) → + // twap = 100 + let cumulative = 100_i64 + .checked_mul(i64::try_from(WINDOW_24H).expect("fits")) + .expect("100 * WINDOW_24H fits in i64"); + let post_states = publish_price( + make_price_observations(&[(0, 0), (WINDOW_24H, cumulative)], 100), + make_oracle_price_account(), + make_current_tick_account(100, WINDOW_24H), + clock_account_with_timestamp(WINDOW_24H), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + let account = OraclePriceAccount::try_from(&post_states[1].account().data) + .expect("valid OraclePriceAccount"); + assert_eq!(account.price, tick_to_oracle_price(100)); + } + + #[test] + fn negative_twap_tick_stored_correctly() { + // Constant tick = -50 over 24 h (zero tail) → twap = -50 + let cumulative = (-50_i64) + .checked_mul(i64::try_from(WINDOW_24H).expect("fits")) + .expect("-50 * WINDOW_24H fits in i64"); + let post_states = publish_price( + make_price_observations(&[(0, 0), (WINDOW_24H, cumulative)], -50), + make_oracle_price_account(), + make_current_tick_account(-50, WINDOW_24H), + clock_account_with_timestamp(WINDOW_24H), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + let account = OraclePriceAccount::try_from(&post_states[1].account().data) + .expect("valid OraclePriceAccount"); + assert_eq!(account.price, tick_to_oracle_price(-50)); + } + + /// When the average tick is negative and the division has a remainder, the result must round + /// down (toward −∞), like Uniswap's oracle. The cumulative here is `-(50·W + 1)` over a span of + /// `W` (zero tail), so the exact average is just below −50 and must floor to **−51**. + /// Truncating division would instead round toward zero and give −50 — one tick too high. + #[test] + fn negative_twap_with_remainder_floors_toward_negative_infinity() { + let elapsed_i64 = i64::try_from(WINDOW_24H).expect("fits"); + let cumulative_diff = 50_i64 + .checked_mul(elapsed_i64) + .and_then(|v| v.checked_add(1)) + .and_then(i64::checked_neg) + .expect("-(50 * WINDOW_24H + 1) fits in i64"); + let post_states = publish_price( + make_price_observations(&[(0, 0), (WINDOW_24H, cumulative_diff)], -50), + make_oracle_price_account(), + make_current_tick_account(-50, WINDOW_24H), + clock_account_with_timestamp(WINDOW_24H), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + let account = OraclePriceAccount::try_from(&post_states[1].account().data) + .expect("valid OraclePriceAccount"); + assert_eq!( + account.price, + tick_to_oracle_price(-51), + "must floor to -51" + ); + assert_ne!( + account.price, + tick_to_oracle_price(-50), + "truncating toward zero (-50) would be wrong" + ); + } + + #[test] + fn zero_twap_tick_stored_correctly() { + let post_states = publish_price( + make_price_observations(&[(0, 0), (WINDOW_24H, 0)], 0), + make_oracle_price_account(), + make_current_tick_account(0, WINDOW_24H), + clock_account_with_timestamp(WINDOW_24H), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + let account = OraclePriceAccount::try_from(&post_states[1].account().data) + .expect("valid OraclePriceAccount"); + assert_eq!(account.price, tick_to_oracle_price(0)); + } + + #[test] + fn timestamp_set_to_clock_now() { + let now = WINDOW_24H + .checked_mul(2) + .expect("WINDOW_24H * 2 fits in u64"); + let post_states = publish_price( + make_price_observations(&[(0, 0), (WINDOW_24H, 0)], 0), + make_oracle_price_account(), + make_current_tick_account(0, WINDOW_24H), + clock_account_with_timestamp(now), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + let account = OraclePriceAccount::try_from(&post_states[1].account().data) + .expect("valid OraclePriceAccount"); + assert_eq!(account.timestamp, now); + } + + #[test] + fn other_price_account_fields_preserved() { + let post_states = publish_price( + make_price_observations(&[(0, 0), (WINDOW_24H, 0)], 0), + make_oracle_price_account(), + make_current_tick_account(0, WINDOW_24H), + clock_account_with_timestamp(WINDOW_24H), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + let account = OraclePriceAccount::try_from(&post_states[1].account().data) + .expect("valid OraclePriceAccount"); + assert_eq!(account.base_asset, base_asset_id()); + assert_eq!(account.quote_asset, quote_asset_id()); + assert_eq!(account.source_id, price_source_id()); + assert_eq!(account.confidence_interval, 0); + } + + #[test] + fn price_observations_account_is_not_modified() { + let observations = make_price_observations(&[(0, 0), (WINDOW_24H, 0)], 0); + let post_states = publish_price( + observations.clone(), + make_oracle_price_account(), + make_current_tick_account(0, WINDOW_24H), + clock_account_with_timestamp(WINDOW_24H), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + assert_eq!(*post_states[0].account(), observations.account); + } + + #[test] + fn current_tick_account_is_not_modified() { + let current_tick = make_current_tick_account(123, WINDOW_24H); + let post_states = publish_price( + make_price_observations(&[(0, 0), (WINDOW_24H, 0)], 0), + make_oracle_price_account(), + current_tick.clone(), + clock_account_with_timestamp(WINDOW_24H), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + assert_eq!(*post_states[2].account(), current_tick.account); + } + + #[test] + fn clock_account_is_not_modified() { + let clock = clock_account_with_timestamp(WINDOW_24H); + let post_states = publish_price( + make_price_observations(&[(0, 0), (WINDOW_24H, 0)], 0), + make_oracle_price_account(), + make_current_tick_account(0, WINDOW_24H), + clock.clone(), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + assert_eq!(*post_states[3].account(), clock.account); + } + + #[test] + fn twap_uses_oldest_and_newest_entries() { + // Three observations: tick 0 for first half, tick 200 for second half, published at the + // newest observation (zero tail). t1 = entry[0] (oldest), t2 = entry[2] (newest). + // Average over full span = (0 * half + 200 * half) / full = 100. + let half = WINDOW_24H.checked_div(2).expect("fits"); + let half_i64 = i64::try_from(half).expect("fits"); + let full_i64 = i64::try_from(WINDOW_24H).expect("fits"); + let cumulative_at_half = 0_i64; + let cumulative_at_full = 200_i64.checked_mul(half_i64).expect("fits"); + let post_states = publish_price( + make_price_observations( + &[ + (0, 0), + (half, cumulative_at_half), + (WINDOW_24H, cumulative_at_full), + ], + 200, + ), + make_oracle_price_account(), + make_current_tick_account(200, WINDOW_24H), + clock_account_with_timestamp(WINDOW_24H), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + let account = OraclePriceAccount::try_from(&post_states[1].account().data) + .expect("valid OraclePriceAccount"); + let expected_tick = cumulative_at_full.checked_div(full_i64).expect("non-zero"); + assert_eq!( + account.price, + tick_to_oracle_price(i32::try_from(expected_tick).expect("tick fits in i32")) + ); + } + + // ── tail extrapolation to `now` ───────────────────────────────────────────── + + /// The motivating case: the price has not changed and no tick has been recorded for a full + /// window, but the current tick account still reports the same tick. Publishing must extend the + /// average all the way to `now`, yielding the (unchanged) price and a *fresh* `now` timestamp — + /// not a stale window. The newest stored observation is one whole window old. + #[test] + fn extrapolates_constant_price_to_now_with_fresh_timestamp() { + let tick = 100_i32; + let t2_time = WINDOW_24H; + let cumulative_at_t2 = i64::from(tick) + .checked_mul(i64::try_from(t2_time).expect("fits")) + .expect("fits"); + let now = t2_time.checked_mul(2).expect("fits"); // one window past the last observation + let post_states = publish_price( + make_price_observations(&[(0, 0), (t2_time, cumulative_at_t2)], tick), + make_oracle_price_account(), + make_current_tick_account(tick, t2_time), + clock_account_with_timestamp(now), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + let account = OraclePriceAccount::try_from(&post_states[1].account().data) + .expect("valid OraclePriceAccount"); + // Constant tick extended through the tail → average is still exactly the constant tick. + assert_eq!(account.price, tick_to_oracle_price(tick)); + // The timestamp is `now` and it is honest: the average genuinely runs to `now`. + assert_eq!(account.timestamp, now); + } + + /// Proves the tail is actually integrated (not just relabeled): stored history is flat at tick + /// 0 over `[0, W]`, then the price moved to tick 100 and the move reached the current tick + /// account but was *not* yet recorded into observations. Publishing at `2W` must blend + /// `0 over [0, W]` with `100 over [W, 2W]` → average 50. Without extrapolation this would be 0. + #[test] + fn tail_segment_extrapolated_with_current_tick() { + let t2_time = WINDOW_24H; + let now = t2_time.checked_mul(2).expect("fits"); + let post_states = publish_price( + make_price_observations(&[(0, 0), (t2_time, 0)], 0), + make_oracle_price_account(), + make_current_tick_account(100, t2_time), + clock_account_with_timestamp(now), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + let account = OraclePriceAccount::try_from(&post_states[1].account().data) + .expect("valid OraclePriceAccount"); + // (0 * W + 100 * W) / 2W = 50 + assert_eq!(account.price, tick_to_oracle_price(50)); + assert_ne!( + account.price, + tick_to_oracle_price(0), + "ignoring the tail (old behavior) would publish 0" + ); + assert_eq!(account.timestamp, now); + } + + /// A large spot jump in the unrecorded tail is clamped to `MAX_TICK_DELTA`, bounding how much + /// an attacker who can move the current tick can shift the published average. History is + /// flat at tick 0 over `[0, W]`; the current tick jumps to `10 * MAX_TICK_DELTA`; + /// publishing at `2W` integrates the *clamped* tick over the tail → average `MAX_TICK_DELTA + /// / 2`. + #[test] + fn tail_extrapolation_clamps_large_tick_jump() { + let t2_time = WINDOW_24H; + let now = t2_time.checked_mul(2).expect("fits"); + let huge = MAX_TICK_DELTA.checked_mul(10).expect("fits in i32"); + let post_states = publish_price( + make_price_observations(&[(0, 0), (t2_time, 0)], 0), + make_oracle_price_account(), + make_current_tick_account(huge, t2_time), + clock_account_with_timestamp(now), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + let account = OraclePriceAccount::try_from(&post_states[1].account().data) + .expect("valid OraclePriceAccount"); + // (0 * W + MAX_TICK_DELTA * W) / 2W = MAX_TICK_DELTA / 2 + let expected_tick = MAX_TICK_DELTA.checked_div(2).expect("non-zero"); + assert_eq!(account.price, tick_to_oracle_price(expected_tick)); + assert_eq!(account.timestamp, now); + } + + /// A downward move in the tail is integrated and floors toward −∞: history flat at tick 0 over + /// `[0, W]`, current tick `-100`, published at `2W` → `(0·W + (−100)·W) / 2W = −50`. + #[test] + fn negative_tail_segment_extrapolated() { + let t2_time = WINDOW_24H; + let now = t2_time.checked_mul(2).expect("fits"); + let post_states = publish_price( + make_price_observations(&[(0, 0), (t2_time, 0)], 0), + make_oracle_price_account(), + make_current_tick_account(-100, t2_time), + clock_account_with_timestamp(now), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + let account = OraclePriceAccount::try_from(&post_states[1].account().data) + .expect("valid OraclePriceAccount"); + assert_eq!(account.price, tick_to_oracle_price(-50)); + assert_eq!(account.timestamp, now); + } + + /// A spot move reported to the current tick account *just before* publish must not be smeared + /// across the whole unrecorded tail. History is flat at tick 0 over `[0, W]`; the tail runs + /// `[W, 2W]` but the current tick only moved to 100 at `2W - 1` ms. The move may be integrated + /// only over its 1 ms of validity, leaving the average effectively at tick 0 — not the tick 50 + /// that ignoring `last_updated` and extrapolating 100 over the full tail would produce. + #[test] + fn tail_extrapolation_respects_current_tick_last_updated() { + let t2_time = WINDOW_24H; + let now = t2_time.checked_mul(2).expect("fits"); + let tick_update_time = now.checked_sub(1).expect("now > 0"); + let post_states = publish_price( + make_price_observations(&[(0, 0), (t2_time, 0)], 0), + make_oracle_price_account(), + make_current_tick_account(100, tick_update_time), + clock_account_with_timestamp(now), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + let account = OraclePriceAccount::try_from(&post_states[1].account().data) + .expect("valid OraclePriceAccount"); + assert_eq!(account.price, tick_to_oracle_price(0)); + } + + /// Publishing exactly at the newest observation leaves no tail, so the live tick does not + /// affect the result even if it has moved: the published price is purely the stored-window + /// average. + #[test] + fn zero_tail_ignores_current_tick() { + let cumulative = 100_i64 + .checked_mul(i64::try_from(WINDOW_24H).expect("fits")) + .expect("fits"); + let post_states = publish_price( + make_price_observations(&[(0, 0), (WINDOW_24H, cumulative)], 100), + make_oracle_price_account(), + // Current tick wildly different, but the tail is zero-length so it is irrelevant. + make_current_tick_account(5_000, WINDOW_24H), + clock_account_with_timestamp(WINDOW_24H), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + let account = OraclePriceAccount::try_from(&post_states[1].account().data) + .expect("valid OraclePriceAccount"); + assert_eq!(account.price, tick_to_oracle_price(100)); + } + + // ── no-op: insufficient history ─────────────────────────────────────────── + + #[test] + fn noop_when_only_one_observation() { + let initial = make_oracle_price_account(); + let post_states = publish_price( + make_price_observations(&[(0, 0)], 0), + initial.clone(), + make_current_tick_account(0, 0), + clock_account_with_timestamp(WINDOW_24H), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + assert_eq!(*post_states[1].account(), initial.account); + } + + #[test] + fn noop_leaves_price_account_timestamp_at_zero() { + let post_states = publish_price( + make_price_observations(&[(0, 0)], 0), + make_oracle_price_account(), + make_current_tick_account(0, 0), + clock_account_with_timestamp(WINDOW_24H), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + let account = OraclePriceAccount::try_from(&post_states[1].account().data) + .expect("valid OraclePriceAccount"); + assert_eq!(account.timestamp, 0); + } + + #[test] + fn noop_returns_four_post_states() { + let post_states = publish_price( + make_price_observations(&[(0, 0)], 0), + make_oracle_price_account(), + make_current_tick_account(0, 0), + clock_account_with_timestamp(WINDOW_24H), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + assert_eq!(post_states.len(), 4); + } + + // ── clock validation ────────────────────────────────────────────────────── + + /// The coarser-cadence clock accounts (10-block, 50-block) are rejected: the published + /// timestamp must come from the canonical 1-block clock. + #[test] + #[should_panic(expected = "clock account must be the canonical 1-block LEZ clock account")] + fn non_canonical_clock_account_id_panics() { + use clock_core::CLOCK_10_PROGRAM_ACCOUNT_ID; + publish_price( + make_price_observations(&[(0, 0), (WINDOW_24H, 0)], 0), + make_oracle_price_account(), + make_current_tick_account(0, WINDOW_24H), + clock_account_with_id(WINDOW_24H, CLOCK_10_PROGRAM_ACCOUNT_ID), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + } + + /// An attacker cannot supply an account they control — even one whose data deserializes as a + /// valid [`ClockAccountData`] with a forged timestamp — in place of the system clock. The + /// published timestamp is the consumer-facing freshness signal, so a forged clock could make a + /// stale price look current. + #[test] + #[should_panic(expected = "clock account must be the canonical 1-block LEZ clock account")] + fn forged_clock_account_panics() { + publish_price( + make_price_observations(&[(0, 0), (WINDOW_24H, 0)], 0), + make_oracle_price_account(), + make_current_tick_account(0, WINDOW_24H), + clock_account_with_id(WINDOW_24H, AccountId::new([7u8; 32])), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + } + + // ── precondition violations ─────────────────────────────────────────────── + + #[test] + #[should_panic(expected = "price observations account ID does not match expected PDA")] + fn wrong_price_observations_id_panics() { + let mut wrong = make_price_observations(&[(0, 0), (WINDOW_24H, 0)], 0); + wrong.account_id = AccountId::new([0u8; 32]); + publish_price( + wrong, + make_oracle_price_account(), + make_current_tick_account(0, WINDOW_24H), + clock_account_with_timestamp(WINDOW_24H), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + } + + #[test] + #[should_panic(expected = "oracle price account ID does not match expected PDA")] + fn wrong_oracle_price_account_id_panics() { + let mut wrong = make_oracle_price_account(); + wrong.account_id = AccountId::new([0u8; 32]); + publish_price( + make_price_observations(&[(0, 0), (WINDOW_24H, 0)], 0), + wrong, + make_current_tick_account(0, WINDOW_24H), + clock_account_with_timestamp(WINDOW_24H), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + } + + #[test] + #[should_panic(expected = "current tick account ID does not match expected PDA")] + fn wrong_current_tick_account_id_panics() { + let mut wrong = make_current_tick_account(0, WINDOW_24H); + wrong.account_id = AccountId::new([0u8; 32]); + publish_price( + make_price_observations(&[(0, 0), (WINDOW_24H, 0)], 0), + make_oracle_price_account(), + wrong, + clock_account_with_timestamp(WINDOW_24H), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + } + + #[test] + #[should_panic(expected = "price observations account must be initialized")] + fn uninitialized_price_observations_panics() { + let uninit = AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: compute_price_observations_pda( + ORACLE_PROGRAM_ID, + price_source_id(), + WINDOW_24H, + ), + }; + publish_price( + uninit, + make_oracle_price_account(), + make_current_tick_account(0, WINDOW_24H), + clock_account_with_timestamp(WINDOW_24H), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + } + + #[test] + #[should_panic(expected = "oracle price account must be initialized")] + fn uninitialized_oracle_price_account_panics() { + let uninit = AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: compute_oracle_price_account_pda( + ORACLE_PROGRAM_ID, + price_source_id(), + WINDOW_24H, + ), + }; + publish_price( + make_price_observations(&[(0, 0), (WINDOW_24H, 0)], 0), + uninit, + make_current_tick_account(0, WINDOW_24H), + clock_account_with_timestamp(WINDOW_24H), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + } + + #[test] + #[should_panic(expected = "current tick account must be initialized")] + fn uninitialized_current_tick_account_panics() { + let uninit = AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: compute_current_tick_account_pda(ORACLE_PROGRAM_ID, price_source_id()), + }; + publish_price( + make_price_observations(&[(0, 0), (WINDOW_24H, 0)], 0), + make_oracle_price_account(), + uninit, + clock_account_with_timestamp(WINDOW_24H), + price_source_id(), + WINDOW_24H, + ORACLE_PROGRAM_ID, + ); + } +}