Skip to content

Implement base TWAP oracle program #113

@0x-r4bbit

Description

@0x-r4bbit

This issue tracks the implementation of the base TWAP oracle program for LEZ (RFP-019). It covers the canonical price account standard,
core account types, and the fundamental instructions needed to register pools, update accumulators, and publish prices.

Background

The TWAP oracle serves two roles on LEZ:

  • Only pricing path for LEZ-native assets (LGS, reflexive stablecoin) that have no off-chain price source
  • Defence-in-depth layer for wrapped external assets alongside external feeds (RFP-020)

It reads price accumulator data from AMM pool accounts (see #111 ) and exposes prices through a
canonical account standard that all oracle sources — on-chain TWAP, RedStone (RFP-020), future Pyth — write to identically. Consumer
programs integrate once against the standard and remain agnostic to the underlying source.

Accounts to implement

Registered Feed Account (PDA per pool)

Created when a pool is registered. Fixed-size. Stores:

  • Pool account ID (the AMM PoolDefinition to read from)
  • base_asset, quote_asset (canonical asset identifiers — token mint addresses for LEZ-resident tokens)
  • MAX_TICK_DELTA for this pool (governable, defaults to global config value) We need to clarify if we'll have a global config or a hardcoded value
  • Last recorded tick (used for truncation delta computation)

Observation Account (PDA per pool)

The ring buffer of accumulator snapshots. Variable-size. Each slot stores (timestamp: u64, tick_cumulative: i64).
We need to clarify if the buffer size can be increased to allow for expandable snapshot storage

Price Account (PDA per asset pair)

The canonical output that consumer programs read. Derived from (base_asset, quote_asset) so consumers can locate it without knowing the
underlying pool. Defined as a standalone SPEL IDL artefact. Fields:

  • base_asset: AccountId
  • quote_asset: AccountId
  • price: u128 (fixed-point)
  • timestamp: u64
  • source_id: AccountId
  • confidence_interval: u128 (zero for TWAP — no confidence interval available)

Instructions

register_pool
Creates the Registered Feed Account, Observation Account (cardinality 1?), and Price Account for a given AMM pool. Called via chained call
from the AMM's NewDefinition instruction so every pool is automatically registered at creation — no manual operator step required. See note
on coupling below.

update_accumulator
Reads current_tick and last_observation_timestamp from the pool account. Applies per-block
tick-delta truncation:

delta = current_tick - last_recorded_tick
clamped_delta = clamp(delta, -MAX_TICK_DELTA, +MAX_TICK_DELTA)
tick_cumulative += clamped_delta * elapsed_seconds

Writes a new (timestamp, tick_cumulative) slot into the Observation Account ring buffer.

publish_price
Computes the TWAP over a given window from two Observation Account snapshots:

twap_tick = (tick_cumulative[t2] - tick_cumulative[t1]) / (t2 - t1)
price = 1.0001 ^ twap_tick

Validates the result is non-zero and non-negative. Writes to the Price Account. Can be called in the same transaction as
update_accumulator.

query_price
Not sure if this makes sense. Data is queried from accounts, not from programs, so this likely needs to be a combo of publish_price and then reading the price account
Read-only. Accepts a max_age parameter. Returns an error if:

  • Price is stale (timestamp older than max_age)
  • Price is zero or negative
  • (base_asset, quote_asset) does not match the caller's expectation

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions