Safeguard Checks
Every call topublishRound() first parses the Pyth update bytes onchain via IPyth.parsePriceFeedUpdates, stores the verified spot as lastPublishedSpot[pairId], then runs four sequential validation checks against each per-tenor forward in the same transaction. If any check fails, the entire update is rejected and the previous prices remain in effect.
1. Spacing Check
| Parameter | Default | Role |
|---|---|---|
minForwardUpdateSpacing | 10 seconds | ORACLE_ADMIN_ROLE |
The spacing check uses
block.timestamp, which means the actual minimum interval depends on block times. On Sepolia (12-second blocks), the 10-second default effectively allows updates on every block.2. Move Limit Check
| Parameter | Default | Role |
|---|---|---|
maxOracleMovePerUpdateBps | 200 (2%) | ORACLE_ADMIN_ROLE |
Why 2% per update?
Why 2% per update?
EUR/USD is one of the most liquid currency pairs in the world. Under normal market conditions, it rarely moves more than 0.5% in a single day. A 2% per-update cap provides ample headroom for legitimate intraday volatility while catching grossly erroneous prices. During extreme events (central bank surprises, geopolitical shocks), the oracle admin may need to temporarily raise this limit.
3. Deviation vs Prior Check
| Parameter | Default | Role |
|---|---|---|
maxDeviationVsPriorBps | 50 (0.5%) | ORACLE_ADMIN_ROLE |
The deviation vs prior check uses the last accepted price as its reference, not the last published price. If a price update is rejected by safeguards, the reference does not change. This means the deviation window is anchored to the most recent successfully published price.
4. Anchor Deviation Check
pairForwardRateBps, not by widening the bound.
| Parameter | Default | Role |
|---|---|---|
maxAnchorDeviationBps | 150 (1.5%) | ORACLE_ADMIN_ROLE |
pairForwardRateBps | 250 (EUR/USD), 400 (USD/JPY) | ORACLE_ADMIN_ROLE |
The spot anchor is refreshed before the per-tenor safeguard runs inside
publishRound, so the very first publish on a pair is also gated by this check (no bootstrap bypass). The anchor is exclusively maintained by publishRound from Pyth-signed bytes — no setter can write it directly.5. Cache-Mode Refreshes (Weekend Trading)
publishRound is called with an empty pythUpdateData array (and msg.value == 0), the contract reuses the previously-stored lastPublishedSpot as the anchor instead of parsing fresh Pyth bytes. Each forward’s publishTimestamp is refreshed to the current block, keeping getForward.isValid true through FX market closure (Fri 5pm ET → Sun 5pm ET, plus holidays) when Pyth upstream attestations are paused. The per-tenor safeguards (maxAnchorDeviationBps, move-limit, spacing) still run against the cached anchor.
Cache mode is bounded by MAX_SPOT_CACHE_AGE (96 hours). Calls past that bound revert with SpotCacheStale — multi-day Pyth outages still pause trading rather than allowing trades against a multi-day-old anchor.
| Constant | Value | Role |
|---|---|---|
MAX_SPOT_CACHE_AGE | 96 hours | hard-coded, immutable |
Cache mode emits
SpotCacheFallbackUsed(pairId, cachedSpot, cachedSpotTimestamp, blockTimestamp) on every call. The subgraph indexes this event as SpotCacheFallbackEvent; off-chain monitors should alert when this fires outside expected FX closure windows (signals a transient Pyth outage worth investigating).6. Bootstrap-Window Relaxation
_verifyAndStoreSpot widens its accepted Pyth publish-time window to MAX_SPOT_CACHE_AGE (96h) on the first publish per pair (lastPublishedSpotTimestamp == 0). This lets a fresh deploy seed the spot anchor from the most recent historical Pyth attestation even when upstream attestations are paused (e.g. deploying on a Saturday). Subsequent calls re-engage the strict maxOracleAge window (30s default).
The same relaxation applies after clearAllForwards or unregisterPair clears the cached spot — the next publish bootstraps fresh.
Staleness Checks
Beyond the safeguards on price updates, the protocol also enforces staleness checks on price reads. A price that has not been refreshed within its allowed age is considered stale and invalid.Forward Staleness
maxForwardAge, isValid returns false. Operations requiring a valid forward price (position opens, PnL calculations, early terminations) will revert with OracleInvalid.Spot Staleness
maxOracleAge, spot-dependent operations will revert. Spot is pull-based from Pyth, so it is typically refreshed on demand.Oracle Validity
TheisOracleValid(pairId) function provides a comprehensive validity check used by the ModeController for automatic mode escalation:
Pair Enabled Check
Verifies the currency pair is registered and enabled in the oracle module. Disabled pairs are considered invalid.
Forward Published Check
Verifies that at least one forward price has been published (
lastForwardPublishTime > 0). Before the first publish, the oracle is invalid.Safeguard Parameters Summary
| Parameter | Default | Unit | Description |
|---|---|---|---|
minForwardUpdateSpacing | 10 | seconds | Min time between forward updates |
maxOracleMovePerUpdateBps | 200 | bps (2%) | Max price change per single update |
maxDeviationVsPriorBps | 50 | bps (0.5%) | Max drift from last accepted price |
maxAnchorDeviationBps | 150 | bps (1.5%) | Max drift from lastPublishedSpot × IRP carry |
pairForwardRateBps | per pair | bps | Per-pair carry rate used to scale spot to tenor |
maxForwardAge | 60 | seconds | Forward price staleness threshold |
maxOracleAge | 30 | seconds | Spot price staleness threshold |
ORACLE_ADMIN_ROLE (except maxOracleAge, which is set by the contract owner) and take effect immediately upon update.
Baseline Reset
When matured forward rounds are cleared viaclearMaturedForwards(pairId), the safeguard baselines are reset. This means:
- The spacing timer resets, allowing an immediate publish after clearing
- The move limit reference price resets to the latest accepted price
- The deviation tracking restarts from a clean state
Defense in Depth
The oracle safeguards work in concert with other protocol protections to provide defense in depth:Safeguards + Mode Escalation
Safeguards + Mode Escalation
If all safeguard checks pass but the published price is still suspicious, an operator with
DEFAULT_ADMIN_ROLE can manually escalate to DEGRADED mode via ModeController.setDegraded(reasonCode). The protocol then blocks new positions; if it remains in DEGRADED beyond degradedDurationSeconds, checkDegradedTimeout() auto-escalates to PAUSED. See Mode Escalation for details.Safeguards + Margin Requirements
Safeguards + Margin Requirements
Even if a slightly inaccurate price passes the safeguard checks, the margin system provides a buffer. Positions require 2% initial margin and are liquidated at 1% maintenance margin, so a small price error would need to exceed the margin buffer to cause harm.
Safeguards + Exposure Caps
Safeguards + Exposure Caps
Exposure caps limit the total notional at risk. Even if a manipulated price were accepted, the maximum loss to the pool is bounded by the stress-adjusted net exposure cap. See Pool Exposure Caps for the formula.
Safeguards + Snapshotted Entry Prices
Safeguards + Snapshotted Entry Prices
Each position’s entry price is immutably set at open time. A manipulated current price cannot retroactively change the entry price of existing positions. It can only affect new position openings and current PnL valuations.
Publisher Architecture
The publisher service that submits forward prices operates as an authorized off-chain agent:| Property | Value |
|---|---|
| Access | PUBLISHER_ROLE (access-controlled) |
| Price source | Pyth Hermes API for spot, interest-rate parity formula for forwards |
| Update frequency | Every maxForwardAge / 2 (default: 30 seconds) |
| Retry strategy | 3 attempts with exponential backoff |
| Batch support | publishRound() for all enabled tenors in a single transaction |
In the M2 deployment, the publisher is operated by the protocol team. It is the only entity with
PUBLISHER_ROLE. The safeguard checks provide protection even against bugs in the publisher software itself — an erroneous price calculation will be rejected by the onchain checks before it can affect any positions.