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:
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.
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.
| Parameter | Value | Description |
|---|---|---|
| 5 min TWAP | ≥ 1,500 slots | Roughly the minimum to back a 5-min window on BSC with safety margin. |
| 10 min TWAP | ≥ 3,000 slots | Recommended for most integrators. Topaz's dynamic fee module defaults to this window. |
| 30 min TWAP | ≥ 9,000 slots | Required 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
Concentrated Liquidity →
How ticks define both ranges and the oracle's representation of price.
Pool Fees →
Where the DynamicSwapFeeModule fits into the per-swap fee calculation.
Integration Guide →
Build on top of Topaz pools, gauges, and oracles.
Contracts →
The DynamicSwapFeeModule address and other Slipstream periphery contracts.
