Skip to content

Exploring cascading observation windows #138

@0x-r4bbit

Description

@0x-r4bbit

This issue explores an alternative sampling design for the TWAP oracle:

Instead of every observation account sampling the shared CurrentTickAccount (instantaneous spot), a longer-window observation account would take its input ticks from a shorter-window observation account's average. For example, the 7-day feed records once per day, each recorded tick being the 24-hour average price rather than an instantaneous snapshot.

Current design (baseline)

Every (price_source, window) pair gets its own independent PriceObservations ring buffer (OBSERVATIONS_CAPACITY = 6396 entries). All windows sample the same shared CurrentTickAccount — the instantaneous spot tick written by the price source — just at different rates. The rate is set by the sampling guard:

min_interval = window_duration / OBSERVATIONS_CAPACITY

So a 24 h feed samples every ~13 s, a 7 d feed every ~94 s, a 30 d feed every ~7 min. PublishPrice then computes an exact time-weighted average over each buffer's span:

twap_tick = (tick_cumulative[t2] - tick_cumulative[t1]) / (t2 - t1)

The defining property: each window is fully orthogonal. Its TWAP is an exact, independently-auditable function of the raw source, with no dependency on any other feed.

The proposed change

Replace the input of a long-window feed. Instead of sampling instantaneous spot, the 7 d feed samples the 24 h feed's average. This produces a cascade, or a TWAP-of-TWAPs: a hierarchy in which each stage feeds the next.

Two distinct proposals hide inside this idea, and they have opposite verdicts:

  • (a) Cascade to reduce sampling frequency ("7 d records once per day"). The premise is that long windows do not need 6396 dense samples. If we are going to downsample a long window to a handful of samples, then feeding it sub-window averages is better than instantaneous snapshots.
  • (b) Cascade while keeping the same sample density. Here we gain almost nothing and add lag. An exact TWAP is replaced with a doubly-smoothed approximation for no resolution benefit.

The current design sidesteps the question entirely by never downsampling (6396 raw samples per window, always).

So the real question this idea poses is: do we want long windows to be sparse?

Pros

Pro Notes
Correct way to downsample If long feeds sample sparsely, averaged inputs beat instantaneous ones (anti-aliasing). A daily 24 h-average is a robust representative sample; a daily spot snapshot is a noisy/attackable one.
Lower keeper/write burden for long windows The 7 d feed is called daily instead of every ~94 s; the 30 d feed daily instead of every ~7 min. Fewer transactions to keep long feeds fresh.
Extra manipulation resistance A short-lived spike is already attenuated by the 24 h average before it reaches the 7 d feed — double smoothing on top of the existing MAX_TICK_DELTA truncation.
Work reuse The 24 h averaging is computed once and reused, rather than each window re-deriving from raw spot.

Cons

Con Notes
Lag amplification A TWAP already lags spot; a TWAP-of-TWAPs lags more. For a liquidation-grade oracle this is the dangerous one — collateral could be valued too high during a real sustained crash because the 7 d-of-24 h reacts doubly slowly. The long window already provides smoothing; cascading adds unintended extra lag.
Inter-feed coupling & new failure mode Today the 7 d RecordTick needs only CurrentTick plus its own buffer. Under cascade it depends on the 24 h feed's liveness. If nobody keeps the 24 h feed fresh, the 7 d feed silently consumes stale averages. The long feed is only as live as its upstream.
What does it even read? The 24 h average is not stored as a tick — it is only materialized in OraclePriceAccount as a Q64.64 price after PublishPrice. So the 7 d RecordTick must either (a) read the 24 h OraclePriceAccount and invert price→tick (lossy, needs an inverse of tick_to_oracle_price), or (b) re-run the TWAP computation from the 24 h ring buffer inline (duplicates publish_price). Both add accounts, code, and a "24 h must already be published" precondition.
Compounding rounding error Each stage does integer division (cumulative_diff / elapsed) plus, if routed through the price account, lossy tick↔price conversions. Cascading divides a division — error accumulates per stage.
Recalibrated safety properties MAX_TICK_DELTA truncation is calibrated assuming the raw tick is the input. Feeding it a pre-averaged value changes what the clamp means; the manipulation-resistance argument no longer composes cleanly and must be re-derived.
Loss of orthogonal configurability Today you can add or remove a 30 d feed without touching anything else. Cascade imposes a hierarchy: each window needs a designated parent, and changing a parent's parameters ripples downstream.
Harder to audit / reason about freshness Today each OraclePriceAccount is a direct, self-contained TWAP of the source. Under cascade a consumer of the 7 d price cannot reason about its validity or true data-age without also understanding the 24 h feed's health. The timestamp reflects publish time, not the age of the underlying spot data.
Longer cold-start The 7 d feed can produce nothing until the 24 h feed has ≥ 2 observations and has published — bootstrapping chains across stages.

Conclusion

For the canonical, liquidation-grade oracle, we keep windows independent (the current design). Each window yields an exact, independently-auditable TWAP with bounded, well-understood lag and a clean implicit-authorization model. The cascade trades all of that for additional lag and cross-feed coupling.

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions