Oracle System
The Panoptic Protocol employs a sophisticated internal oracle system designed to provide stable, manipulation-resistant price data for all risk calculations. Rather than relying on a single price point, the oracle combines multiple exponential moving averages (EMAs) and a median filter to create robust price feeds.
Overviewβ
The oracle system serves several critical functions:
- Solvency Calculations: Determining at which price(s) to evaluate account health
- Safe Mode Detection: Identifying periods of extreme volatility
- Force Exercise Pricing: Providing fair prices for involuntary position closures
- Liquidation Protection: Preventing manipulation-based liquidations
EMA Architectureβ
The oracle maintains four distinct EMAs, each with a different time horizon:
| EMA Type | Period | Purpose |
|---|---|---|
| Spot EMA | 3 minutes (180s) | Tracks rapid price movements |
| Fast EMA | 10 minutes (600s) | Primary solvency reference |
| Slow EMA | 1 hour (3,600s) | Volatility baseline |
| Eons EMA | 6 hours (21,600s) | Long-term trend anchor |
These EMAs are packed efficiently into a single storage slot (OraclePack) using bit manipulation, where the periods are encoded as:
EMA_PERIODS = 180 + (600 << 24) + (3600 << 48) + (21600 << 72)
EMA Update Formulaβ
EMAs are updated using a linear approximation of exponential decay:
newEMA = oldEMA + (timeDelta Γ (newTick - oldEMA)) / period
This approximates exp(-x) β 1-x for computational efficiency.
Convergence Cap (75% Rule)β
To prevent excessive convergence after periods of inactivity, the timeDelta is capped at 75% of each EMA's period:
// Applied in cascade from longest to shortest period
if (timeDelta > (3 * EMA_PERIOD_EONS) / 4) timeDelta = (3 * EMA_PERIOD_EONS) / 4;
// ... then slowEMA, fastEMA, spotEMA
This cascading cap ensures that:
- EMAs never converge more than 75% toward the new tick in a single update
- Longer-period EMAs cap the timeDelta first, affecting shorter periods
- Extended oracle staleness doesn't cause sudden price jumps when updates resume
Core Oracle Functionsβ
getOracleTicksβ
getOracleTicksThis function computes and returns all oracle tick values from the current Uniswap pool state:
function getOracleTicks(
int24 currentTick,
OraclePack _oraclePack
) external view returns (
int24 spotTick, // Fast oracle tick (10-minute EMA)
int24 medianTick, // Slow oracle tick (median of 8 observations)
int24 latestTick, // Most recent observation
OraclePack oraclePack // Updated oracle state
)
Return Values:
- spotTick: The 10-minute EMA, used as the primary price reference for normal solvency checks
- medianTick: The median of 8 stored price observations, providing outlier resistance
- latestTick: The most recent tick observation stored in the internal oracle
- oraclePack: The potentially updated oracle state (if a new observation was warranted)
twapEMAβ
twapEMACalculates a heavily smoothed, weighted average price that is highly resistant to manipulation:
function twapEMA(OraclePack oraclePack) external pure returns (int24)
The blended TWAP is computed with the following weightings:
- 60% from Fast EMA (10 minutes)
- 30% from Slow EMA (1 hour)
- 10% from Eons EMA (6 hours)
twapEMA = (6 Γ fastEMA + 3 Γ slowEMA + 1 Γ eonsEMA) / 10
This weighted combination provides a price feed that:
- Responds to genuine price movements (via the fast EMA component)
- Resists short-term manipulation (via slow and eons components)
- Provides stability during volatile periods
computeInternalMedianβ
computeInternalMedianUpdates the internal observation buffer and returns the current median:
function computeInternalMedian(
OraclePack oraclePack,
int24 currentTick
) external view returns (
int24 medianTick,
OraclePack updatedOraclePack
)
The function:
- Computes the median from the current queue (average of rank 3 and rank 4 values)
- Checks if a new epoch has begun (64-second boundary crossed)
- If new epoch: clamps the new tick, inserts into sorted queue, updates all EMAs
- Returns the median tick and updated oracle pack (or empty pack if no update)
Epoch Check:
currentEpoch = (block.timestamp >> 6) & 0xFFFFFF;
differentEpoch = currentEpoch != recordedEpoch;
timeDelta = int256(currentEpoch - recordedEpoch) * 64; // Convert to seconds
Insert Operation: When inserting, the function:
- Clamps the new tick to prevent manipulation
- Rebases the reference tick if residuals exceed threshold
- Finds the correct sorted position using the order map
- Shifts rankings to accommodate the new observation
- Updates all four EMAs with the clamped tick
Solvency Tick Selectionβ
The getSolvencyTicks function determines which price points to use when evaluating account solvency:
function getSolvencyTicks(
int24 currentTick,
OraclePack _oraclePack
) external view returns (int24[] memory, OraclePack)
Normal Operationβ
Under normal market conditions (low deviation between oracle values), solvency is checked against a single tick:
- The spotTick (10-minute EMA)
High Deviation Modeβ
When oracle values diverge significantly, the function returns four ticks for more comprehensive solvency checking:
- spotTick
- medianTick
- latestTick
- currentTick
Deviation Threshold:
The switch to multi-tick checking occurs when the Euclidean norm of deviations exceeds MAX_TICKS_DELTA (953 ticks, ~10%):
(spotTick - medianTick)Β² + (latestTick - medianTick)Β² + (currentTick - medianTick)Β² > MAX_TICKS_DELTAΒ²
This approach:
- Maintains efficiency during normal operation (single solvency check)
- Provides conservative protection during volatile periods (must pass all four checks)
- Uses Euclidean distance which is more sensitive than checking each deviation individually
Observation Queueβ
The oracle maintains an 8-slot observation queue for computing the median tick. This queue is:
- Sorted: Observations are kept in sorted order for efficient median computation
- Time-gated: New observations are only inserted when sufficient time has passed (epoch change)
- Packed: All 8 observations fit within the
OraclePackstorage structure
OraclePack Bit Layoutβ
The entire oracle state is packed into a single 256-bit word:
Bits 255-232 (24 bits): epoch - 64-second epoch timestamp
Bits 231-208 (24 bits): orderMap - Sorted order of 8 observations (8 Γ 3-bit indices)
Bits 207-186 (22 bits): spotEMA - 3-minute EMA tick
Bits 185-164 (22 bits): fastEMA - 10-minute EMA tick
Bits 163-142 (22 bits): slowEMA - 1-hour EMA tick
Bits 141-120 (22 bits): eonsEMA - 6-hour EMA tick
Bits 119-118 (2 bits): lockMode - Guardian override (0-3)
Bits 117-96 (22 bits): referenceTick - Base tick for residual calculation
Bits 95-0 (96 bits): residuals - 8 Γ 12-bit signed residuals
Epoch-Based Timekeepingβ
The oracle uses 64-second epochs instead of raw timestamps:
currentEpoch = (block.timestamp >> 6) & 0xFFFFFF // 64-second intervals
This provides:
- Gas efficiency: Smaller storage values
- Y2K38 avoidance: 24-bit epochs support ~34 million years
- Rate limiting: Natural 64-second minimum between observations
Residual Storageβ
Observations are stored as 12-bit signed residuals relative to a reference tick:
actualTick = referenceTick + residual
This compression allows 8 observations to fit in 96 bits (12 bits Γ 8).
Order Mapβ
The 24-bit order map maintains sorted order without physically reordering data:
orderMap bits: [rank7][rank6][rank5][rank4][rank3][rank2][rank1][rank0]
3b 3b 3b 3b 3b 3b 3b 3b
Each 3-bit segment points to a slot index (0-7). Position indicates rank (0 = smallest, 7 = largest).
Median Calculationβ
The median is computed from the 4th and 5th ranked values (indices 3 and 4):
medianTick = referenceTick + (rank3_residual + rank4_residual) / 2
This provides a true median for even-numbered sets.
Tick Clampingβ
New observations are clamped to prevent single-block manipulation:
// Clamp newTick to be within MAX_MEDIAN_DELTA of the last observation
int24 lastTick = referenceTick + residual[0];
if (newTick > lastTick + MAX_MEDIAN_DELTA) {
clamped = lastTick + MAX_MEDIAN_DELTA;
} else if (newTick < lastTick - MAX_MEDIAN_DELTA) {
clamped = lastTick - MAX_MEDIAN_DELTA;
} else {
clamped = newTick;
}
This limits how much any single observation can move the median, requiring sustained price movement over multiple epochs to shift the median significantly.
Reference Tick Rebasingβ
When residuals exceed the 12-bit signed integer range (Β±2047), the oracle rebases:
if (lastResidual > MAX_RESIDUAL_THRESHOLD || lastResidual < -MAX_RESIDUAL_THRESHOLD) {
// Move reference tick to current median
newReferenceTick = getMedianTick(oraclePack);
// Recalculate all residuals relative to new reference
for (uint8 i = 0; i < 8; i++) {
newResidual[i] = oldResidual[i] - (newReferenceTick - oldReferenceTick);
}
}
This maintains precision during large price movements while keeping residuals within storage constraints.
The median filter provides robustness against:
- Flash loan attacks
- Single-block price manipulation
- Temporary liquidity imbalances
Price Manipulation Resistanceβ
The oracle system incorporates multiple layers of manipulation resistance:
Layer 1: EMA Smoothingβ
All price feeds use exponential moving averages, which inherently dampen sudden price movements. The longer the EMA period, the more manipulation-resistant the price feed. The 75% convergence cap ensures that even after extended inactivity, a single observation cannot dramatically shift any EMA.
Layer 2: Tick Clampingβ
New observations are clamped to be within MAX_MEDIAN_DELTA of the previous observation:
// Prevents any single observation from jumping more than MAX_MEDIAN_DELTA ticks
clampedTick = clamp(newTick, lastTick - MAX_MEDIAN_DELTA, lastTick + MAX_MEDIAN_DELTA)
This means an attacker must sustain manipulation over multiple 64-second epochs to significantly affect the median.
Layer 3: Multi-Period Validationβ
By comparing EMAs of different periods, the system can detect when short-term prices diverge abnormally from longer-term trends. This triggers safe mode, which requires solvency at multiple price points.
Layer 4: Median Filteringβ
The 8-observation median eliminates the impact of extreme outliers. With tick clamping, an attacker would need to:
- Manipulate prices for at least 4 consecutive epochs (256+ seconds)
- Sustain each manipulation within the delta cap
- Avoid triggering safe mode's multi-tick solvency checks
Layer 5: Conservative Fallbackβ
During high deviation periods, the system automatically switches to checking solvency at multiple price points (spot, median, latest, current), ensuring accounts are solvent across a range of prices.
Integration with Safe Modeβ
The oracle system directly feeds into Safe Mode detection. When oracle values diverge beyond configured thresholds, the protocol automatically applies more conservative risk parameters. See Safe Mode for details on how oracle deviations trigger protective measures.
Liquidation Price Constraintsβ
During liquidations, an additional constraint applies:
MAX_TWAP_DELTA_LIQUIDATION = 513 ticks (~5%)
This limits how far the current tick can deviate from the TWAP during liquidation, preventing attackers from:
- Temporarily moving the price to trigger liquidations
- Executing liquidations at manipulated prices that harm the liquidatee