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.
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 independentPriceObservationsring buffer (OBSERVATIONS_CAPACITY = 6396entries). All windows sample the same sharedCurrentTickAccount— the instantaneous spot tick written by the price source — just at different rates. The rate is set by the sampling guard:So a 24 h feed samples every ~13 s, a 7 d feed every ~94 s, a 30 d feed every ~7 min.
PublishPricethen computes an exact time-weighted average over each buffer's span: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:
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
MAX_TICK_DELTAtruncation.Cons
RecordTickneeds onlyCurrentTickplus 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.OraclePriceAccountas aQ64.64price afterPublishPrice. So the 7 dRecordTickmust either (a) read the 24 hOraclePriceAccountand invert price→tick (lossy, needs an inverse oftick_to_oracle_price), or (b) re-run the TWAP computation from the 24 h ring buffer inline (duplicatespublish_price). Both add accounts, code, and a "24 h must already be published" precondition.cumulative_diff / elapsed) plus, if routed through the price account, lossy tick↔price conversions. Cascading divides a division — error accumulates per stage.MAX_TICK_DELTAtruncation 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.OraclePriceAccountis 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. Thetimestampreflects publish time, not the age of the underlying spot data.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.