mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-26 14:41:12 +00:00
feat(twap-oracle): implement RecordTick instruction
Add RecordTick — a permissionless instruction that reads the current tick from a CurrentTickAccount and advances a PriceObservations ring buffer. Authorization is implicit: both PDAs are verified against price_source_id, so the tick can only have been written by whoever controls that price source. A sampling guard silently no-ops if less than `window_duration / OBSERVATIONS_CAPACITY`` ms have elapsed, allowing keepers to call blindly on every block. Tick-delta truncation clamps the per-observation delta to `MAX_TICK_DELTA (9 116)` before advancing tick_cumulative, with last_recorded_tick tracking the untruncated position for the next delta. Also switches ObservationEntry.tick_cumulative to use elapsed milliseconds rather than seconds. Closes #116
This commit is contained in:
@@ -25,7 +25,7 @@ pub enum Instruction {
|
||||
/// Duration of the TWAP window this feed serves, in milliseconds.
|
||||
///
|
||||
/// Together with `OBSERVATIONS_CAPACITY` this determines the minimum sampling interval
|
||||
/// enforced by `RecordPrice`: `min_interval = window_duration / OBSERVATIONS_CAPACITY`.
|
||||
/// enforced by `RecordTick`: `min_interval = window_duration / OBSERVATIONS_CAPACITY`.
|
||||
/// It is also part of the PDA seed, so each window gets a distinct account.
|
||||
window_duration: u64,
|
||||
},
|
||||
@@ -94,12 +94,44 @@ pub enum Instruction {
|
||||
/// tick on-chain.
|
||||
price: u128,
|
||||
},
|
||||
/// Records the current tick from a [`CurrentTickAccount`] into a [`PriceObservations`]
|
||||
/// ring buffer.
|
||||
///
|
||||
/// Permissionless — anyone may call this. Both PDAs are verified against `price_source_id`,
|
||||
/// so the tick can only have been written by whoever controls that price source.
|
||||
///
|
||||
/// A sampling guard silently skips the write if less than
|
||||
/// `window_duration / OBSERVATIONS_CAPACITY` milliseconds have elapsed since the last
|
||||
/// observation. Callers may call this on every block without concern — the guard handles
|
||||
/// downsampling on-chain.
|
||||
///
|
||||
/// 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. Current tick account — initialized PDA derived from
|
||||
/// `compute_current_tick_account_pda(self_program_id, price_source_id)`.
|
||||
/// 3. Clock account — read-only; supplies the current timestamp.
|
||||
RecordTick {
|
||||
/// ID of the price source; used to verify both PDAs.
|
||||
price_source_id: AccountId,
|
||||
/// Duration of the TWAP window in milliseconds; used to verify the
|
||||
/// [`PriceObservations`] PDA and to compute the sampling guard interval.
|
||||
window_duration: u64,
|
||||
},
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// Price feed
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Maximum tick delta injected into the accumulator per observation.
|
||||
///
|
||||
/// Matches the Uniswap v4 truncated oracle hook reference value (~2.39× price move per block).
|
||||
/// An attacker who moves the pool by more than this in one block still only injects
|
||||
/// `MAX_TICK_DELTA` ticks into the cumulative — they must sustain the manipulation across
|
||||
/// many blocks while arbitrage erodes their position.
|
||||
pub const MAX_TICK_DELTA: i32 = 9_116;
|
||||
|
||||
/// Number of entries in each price feed.
|
||||
///
|
||||
/// 6 396 is the maximum that fits within the `DATA_MAX_LENGTH = 100 KiB` runtime ceiling.
|
||||
@@ -123,7 +155,7 @@ pub struct ObservationEntry {
|
||||
/// Running sum of `tick × elapsed_ms` up to this entry.
|
||||
///
|
||||
/// Grows without bound over time, which is why this is `i64` rather than `i32`.
|
||||
/// The TWAP over any window `[t1, t2]` is computed as
|
||||
/// The TWAP over any window `[t1, t2]` (timestamps in milliseconds) is computed as
|
||||
/// `(tick_cumulative[t2] - tick_cumulative[t1]) / (t2 - t1)`.
|
||||
pub tick_cumulative: i64,
|
||||
}
|
||||
@@ -135,7 +167,7 @@ pub struct ObservationEntry {
|
||||
/// The window duration is not stored here — it is implicit in the PDA address. Any caller
|
||||
/// that locates this account already knows the window duration used to derive it.
|
||||
/// Only the account that controls `price_source_id` (proven via `is_authorized = true` at call
|
||||
/// time) may append new entries via `RecordPrice`.
|
||||
/// time) may append new entries via `RecordTick`.
|
||||
#[account_type]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
pub struct PriceObservations {
|
||||
|
||||
Reference in New Issue
Block a user