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.
Oracle configuration is local to the pool
A pool oracle measures its own pair on its own chain. Do not apply a BNB pool's price, observation history or dynamic-fee settings to a same-named spoke pool. Every integration must check the chosen pool's actual observations and the deployed module configuration.
Window requirements are time-based. Block time, timestamp granularity and observation-write frequency differ by network; the old BNB sizing figures below are illustrative, not requirements to copy to a spoke. Check whether an actual requested window can be read and define a failure policy before depending on it. Multichain integration.
A pool TWAP is also different from the xTOPAZ vault's TOPAZ-per-share accounting rate. Neither alone establishes a market exit price for xTOPAZ.
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
BNB’s fast block cadence makes it important to size and verify observation history for the intended time window. The oracle uses timestamps, not block numbers. Actual writes and retained timestamps determine which windows are available; a slot count alone does not establish usable history.
| TWAP window | Observations needed | Why |
|---|---|---|
| 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. |
These historical sizing examples are not a live configuration requirement. Increasing capacity creates room for future observations, not past history. Sparse swaps reduce observation-write frequency; they do not by themselves consume more slots. Verify the oldest usable observation and the actual observe call.
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), the dynamic component is zero and the pool charges its base fee, subject to the module's default fee cap — 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.