TopazTOPAZDocs

Concepts

Oracles & TWAP

Slipstream pools maintain their own price oracle — a rolling history of tick observations that lets the protocol (and integrators) read a manipulation-resistant TWAP. This page covers how that works, what observation cardinality is, and what BNB Chain's block time means for you.

Why an in-pool oracle

Every Slipstream pool is also a price oracle for the pair it holds. When a swap happens, the pool stores the new tick and a cumulative tick value alongside the timestamp. Integrators can ask the pool for two cumulative-tick observations and divide the difference by the elapsed time to get an exact time-weighted average tick — and from that, a time-weighted average price.

The advantage over spot price: TWAP is much harder to manipulate. Pushing spot to a different price costs only the slippage of one swap, but pushing a 10-minute TWAP requires sustaining the manipulated price for most of those 10 minutes — which means continuous arbitrage pressure from every honest actor watching, plus capital tied up the whole time.

Observations and cardinality

Each pool stores a ring buffer of observations. Each observation captures:

solidity
struct Observation {
    uint32 blockTimestamp;
    int56 tickCumulative;
    uint160 secondsPerLiquidityCumulativeX128;
    bool initialized;
}

A new pool starts with cardinality = 1: there's room for exactly one observation. That's enough for the pool to function as a swap venue but not enough to compute a meaningful TWAP. Anyone can grow the buffer by calling increaseObservationCardinalityNext(N) on the pool — the call is permissionless. Growing cardinality allocates more slots; observations are written as swaps occur, so a freshly-grown pool also needs swap activity before its history is useful.

Reading the oracle
To read TWAP, call pool.observe([secondsAgoEnd, secondsAgoStart]). The pool returns the cumulative-tick values at both times. Take their difference, divide by elapsed seconds, and you have the time-weighted average tick. Convert to price with price = 1.0001 ^ tick.

BNB Chain considerations

Following the Fermi hard fork (January 2026), BNB Chain targets ~0.45 second blocks — roughly 25× faster than Ethereum L1. The oracle stores observations with real timestamps, not block numbers, so the math still works correctly — but a fixed TWAP window covers many more blocks on BSC than on a slower chain. That means a pool needs higher observation cardinality on BNB Chain to back the same TWAP window.

ParameterValueDescription
5 min TWAP≥ 1,500 slotsRoughly the minimum to back a 5-min window on BSC with safety margin.
10 min TWAP≥ 3,000 slotsRecommended for most integrators. Topaz's dynamic fee module defaults to this window.
30 min TWAP≥ 9,000 slotsRequired for slow-moving oracles like liquidation backstops or long-window dynamic fees.

Recommendations include a buffer above the bare minimum because observations are only written on swaps. If a pool sits idle for long stretches, you may need more slots to cover the window comfortably.

How Topaz uses TWAP internally

The DynamicSwapFeeModule uses the oracle to compute a volatility-based fee surcharge. When the current pool tick diverges from the TWAP tick — which happens when prices are moving rapidly relative to the recent average — the module charges a higher fee on swaps. Formula shape:

totalFee = min(baseFee + dynamicFee, feeCap)

dynamicFee = |currentTick − twapTick| × scalingFactor / 1e6
twapTick   = TWAP over secondsAgo (default 600s / 10 min)

When the pool has insufficient observation data or the oracle call fails, the module gracefully falls back to base fee — no revert. This means a fresh pool with low cardinality automatically uses static fees until it has enough history to drive the dynamic component.

For pools where dynamic fees aren't configured (which is the protocol-wide default), this reduces to just the base fee — same behavior as a standard Uniswap V3-style pool. See Pool Fees for the full fee resolution order.

Integrator notes

If you're building on top of Topaz pools as price oracles:

  • Always check observation cardinality before relying on a TWAP. Call the pool's slot0() to read it.
  • Use a TWAP window appropriate to your application — short windows are responsive but more manipulable, long windows are robust but slow.
  • Prefer using cumulative tick values rather than spot ticks; that's what the oracle is designed for.
  • If you need a TWAP on a pool that doesn't yet have enough cardinality, grow it yourself with increaseObservationCardinalityNext().
  • BNB Chain reorgs are typically shallow but not impossible — consider waiting for several confirmations before using oracle data for high-value actions.

Continue reading