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
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:
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:
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:
Instructions
register_poolCreates 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_accumulatorReads current_tick and last_observation_timestamp from the pool account. Applies per-block
tick-delta truncation:
Writes a new (timestamp, tick_cumulative) slot into the Observation Account ring buffer.
publish_priceComputes the TWAP over a given window from two Observation Account snapshots:
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_priceNot sure if this makes sense. Data is queried from accounts, not from programs, so this likely needs to be a combo of
publish_priceand then reading the price accountRead-only. Accepts a max_age parameter. Returns an error if: