mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +00:00
feat(twap-oracle): implement PublishPrice with tick-to-price conversion and tail extrapolation
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
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user