Skip to content

feat(twap-oracle): implement RecordTick instruction#136

Open
0x-r4bbit wants to merge 4 commits into
mainfrom
feat/record-tick
Open

feat(twap-oracle): implement RecordTick instruction#136
0x-r4bbit wants to merge 4 commits into
mainfrom
feat/record-tick

Conversation

@0x-r4bbit

Copy link
Copy Markdown
Collaborator

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

0x-r4bbit added 3 commits May 29, 2026 09:32
Adds the CreatePriceObservations instruction to the TWAP oracle program.
The instruction initialises a PriceObservations PDA for a given price
source account and time window, writing the initial tick and timestamp
as the first entry.

Key design decisions:

- Per-window accounts: each (price_source, window_duration) pair maps to
  a distinct PriceObservations PDA. The window duration is baked into the
  PDA seed so a single price source can support multiple TWAP windows
  (24h, 7d, 30d) at independent sampling rates without sharing a buffer.

- window_duration not stored on struct: it is implicit in the PDA address.
  Any reader that located the account already knows the window duration
  used to derive it. Storing it would be redundant.

- Authorization is implicit: the PriceObservations PDA is derived from
  the price source account ID, so is_authorized = true on the price source
  proves the caller controls it without a redundant authority field.

- Impersonation is prevented by the PDA check: passing a controlled price
  source with a victim's observations account ID fails immediately because
  the computed PDA (from the attacker's source) does not match.

Closes #126
Adds the CreateOraclePriceAccount instruction to the TWAP oracle program.
The instruction initialises a canonical OraclePriceAccount PDA for a given
price source and time window. The account starts with price = 0 and
timestamp = 0 — a deliberately invalid sentinel state that signals "not
yet published". Consumers are expected to reject any account whose
timestamp is zero or stale, so the transient invalid state requires no
special on-chain enforcement.

- PDA mirrors PriceObservations: derived from (oracle_program_id,
  price_source_id, window_duration) with a distinct seed constant, so
  each (source, window) pair maps to a distinct oracle price account that
  cannot collide with its corresponding observations account.

- source_id is not a parameter: it is always set to price_source.account_id.
  Accepting it as a free parameter would allow callers to register a price
  account that claims to represent a source it does not control. Deriving
  it from the authorized price source account closes that vector entirely.

- Authorization follows the same model as CreatePriceObservations:
  is_authorized = true on the price source proves the caller controls it;
  the PDA check ensures the supplied oracle price account address is the
  one derived from that specific source and window.

- price = 0 / timestamp = 0 is the correct initial state: coupling account
  creation to first publication would require the observation account to
  already hold a full window of ticks, blocking registration for up to the
  window duration. Consumers must validate oracle prices regardless, so the
  zero sentinel falls naturally out of the staleness check they already own.

Closes #129
…ntTick

Add CurrentTickAccount — an oracle-owned PDA (one per price source) that holds
the latest raw tick written by the price source and a timestamp. The price source
calls UpdateCurrentTick after each price-changing operation; anyone can then call
RecordTick (upcoming) to advance the PriceObservations accumulator without
requiring the price source to be present. PDA is derived from price_source_id
only (no window) since a single current tick serves all time windows.
@0x-r4bbit 0x-r4bbit requested review from 3esmit and gravityblast June 2, 2026 09:42
@0x-r4bbit 0x-r4bbit changed the base branch from feat/tick_account to main June 2, 2026 09:43
@0x-r4bbit 0x-r4bbit closed this Jun 2, 2026
@0x-r4bbit 0x-r4bbit reopened this Jun 2, 2026
@0x-r4bbit

Copy link
Copy Markdown
Collaborator Author

This is based on #131

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements the TWAP oracle’s on-chain data model and crank flow by introducing CurrentTickAccount + PriceObservations PDAs and a permissionless RecordTick instruction that advances the observations ring buffer using the LEZ clock timestamp. It also updates the IDL and guest entrypoint to expose the new instructions and removes the previous noop placeholder.

Changes:

  • Add account types + PDA helpers for PriceObservations, CurrentTickAccount, and OraclePriceAccount (including source_id: AccountId).
  • Implement instructions to create/init PDAs (create_*), write ticks (update_current_tick), and permissionlessly sample (record_tick) with sampling guard + tick-delta truncation.
  • Update guest method entrypoint and regenerate artifacts/locks accordingly.

Reviewed changes

Copilot reviewed 14 out of 20 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
programs/twap_oracle/src/update_current_tick.rs New instruction helper to update CurrentTickAccount from an authorized price source.
programs/twap_oracle/src/record_tick.rs New permissionless crank to advance the PriceObservations ring buffer and accumulator.
programs/twap_oracle/src/noop.rs Removes placeholder noop instruction implementation.
programs/twap_oracle/src/lib.rs Exposes newly added instruction modules.
programs/twap_oracle/src/create_price_observations.rs New initializer for PriceObservations PDA.
programs/twap_oracle/src/create_oracle_price_account.rs New initializer for canonical OraclePriceAccount PDA.
programs/twap_oracle/src/create_current_tick_account.rs New initializer for CurrentTickAccount PDA.
programs/twap_oracle/methods/guest/src/bin/twap_oracle.rs Wires new instructions into the guest entrypoint (replacing noop).
programs/twap_oracle/methods/guest/Cargo.toml Adds clock_core dependency for guest build.
programs/twap_oracle/methods/guest/Cargo.lock Lockfile updates from new dependency and transitive bumps.
programs/twap_oracle/core/src/lib.rs Adds instruction enum variants, account structs, constants, and PDA derivation helpers.
programs/twap_oracle/core/Cargo.toml Enables workspace lints and adds risc0-zkvm dependency.
programs/twap_oracle/Cargo.toml Adds clock_core dependency and enables workspace lints.
programs/token/methods/guest/Cargo.lock Transitive dependency version bumps.
programs/stablecoin/methods/guest/Cargo.lock Transitive dependency version bumps.
programs/ata/methods/guest/Cargo.lock Transitive dependency version bumps.
programs/amm/methods/guest/Cargo.lock Transitive dependency version bumps.
Cargo.lock Workspace lockfile updates (deps + new clock_core references).
artifacts/twap_oracle-idl.json Regenerated IDL reflecting new instructions/accounts/types.
artifacts/stablecoin-idl.json Regenerated IDL reflecting updated shared account/type definitions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +94 to +105
// Tick-delta truncation.
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);

// Advance cumulative (tick × elapsed milliseconds).
let elapsed_ms_i64 = i64::try_from(elapsed_ms).expect("elapsed_ms fits in i64");
let new_cumulative = i64::from(clamped_delta)
.checked_mul(elapsed_ms_i64)
.and_then(|product| last_cumulative.checked_add(product))
.expect("tick_cumulative fits in i64");

Comment on lines +62 to +65
let min_interval = window_duration
.checked_div(u64::from(OBSERVATIONS_CAPACITY))
.expect("OBSERVATIONS_CAPACITY is non-zero");
let last_index = if observations.write_index == 0 {
Comment on lines +49 to +51
let clock_data = ClockAccountData::from_bytes(clock.account.data.as_ref());
let now = clock_data.timestamp;

Comment on lines +27 to +30
/// Together with `OBSERVATIONS_CAPACITY` this determines the minimum sampling interval
/// enforced by `RecordPrice`: `min_interval = window_duration / OBSERVATIONS_CAPACITY`.
/// It is also part of the PDA seed, so each window gets a distinct account.
window_duration: u64,
Comment on lines +155 to +159
/// 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`.
#[account_type]
Comment on lines +37 to +39
let clock_data = ClockAccountData::from_bytes(clock.account.data.as_ref());
stored.tick = tick;
stored.last_updated = clock_data.timestamp;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement accumulator update

2 participants