Oracle model

Every market trades on a single oracle price: the tracked asset valued in the market’s collateral token, normalized to a 1e18 scale (collateral-denominated). There is no separate “mark” vs “index” price — the same price is used to open, value, close, compute health, check liquidation, and settle. The pool reads it through the IPriceOracle.getPrice() interface. Each oracle is deployed per market + collateral pair, so settings like staleDuration and maxReasonablePrice are isolated per pair.

Oracle types

  • ChainlinkPriceOracle reads a Chainlink aggregator’s latestRoundData(), scales the answer to 1e18, and rejects non-positive answers, stale rounds (answeredInRound < roundId), prices older than staleDuration, and prices outside its configured bounds.
  • PythPriceOracle reads a Pyth price feed. It stores an immutable bytes32 feedId and reads the cached on-chain value via getPriceNoOlderThan(feedId, staleDuration) (the data source is the Pyth contract, not a “price account”). See the pull-model notes below.
  • DexPriceOracle reads any IDexPriceSource adapter (Uniswap V2 / V3 TWAP, or custom). It validates price bounds only — freshness and manipulation resistance are the adapter’s responsibility (see below).
All three scale to 1e18 and enforce a common circuit-breaker band: a price of zero, below minReasonablePrice, or above maxReasonablePrice reverts with BadPrice. Defaults are maxReasonablePrice = 1_000_000 * 1e18 and minReasonablePrice = 0 (floor disabled). The minReasonablePrice floor (enabled by the owner) rejects a feed clamped to its aggregator min-answer during a flash crash, so positions can’t settle against a wrong, pinned-low price. The staleDuration default of 600 (10 min) applies only to Chainlink and DEX oracles (BasePriceOracle); both staleDuration and the price bounds are tunable per oracle by the owner. Pyth is different — its staleness is hard-capped at 60s (see below).

Pyth specifics (pull model)

Pyth is a pull oracle: a fresh price update must be pushed to the Pyth contract (paying the ETH update fee) in the same flow before reading. getPrice() is view-only and reads the cached value, reverting if it is older than staleDuration. Even after a fresh update, a read can still revert with BadConfidence — the confidence interval is too wide relative to the price (conf × 10000 > price × maxConfidenceBps, i.e. conf / price exceeds maxConfidenceBps expressed in basis points). Each oracle sets maxConfidenceBps, so a successful price update does not guarantee a successful read. Pyth staleness is hard-capped at 60s: DEFAULT_PYTH_STALE_DURATION = MAX_PYTH_STALE_DURATION = 60, and setStaleDuration reverts with PythStaleDurationTooLong for any value above 60s. The 10-min BasePriceOracle default does not apply to Pyth. As a result a Pyth-backed market needs a price update no older than 60s, so refresh the feed in (or near) the same transaction as the trade.

DEX specifics (no staleness check)

DexPriceOracle does not apply a time-based staleness check the way Chainlink and Pyth do — it reads the current adapter output and validates only price bounds. Any freshness or anti-manipulation guarantee (such as a TWAP window) must come from the approved adapter. DEX price sources must also be on the factory’s approved list before an oracle can use them.

Factory and registry

OracleFactory is the owner-gated factory and registry. It deploys a dedicated oracle per market/collateral pair, keying it on marketKey => collateralToken, and owns the deployed oracles. DEX sources must first be approved on the factory before an oracle can be created against them.

Oracle changes: two 24h timelocks

There are two separate, independent 24-hour propose-then-accept flows. No one can set an arbitrary price directly.
  • Swapping a market’s oracle is controlled by RISK_ROLE on the pool (proposePriceOracle → wait ORACLE_CHANGE_DELAY = 24h → acceptPriceOracle). The candidate must match the market and collateral (OracleMismatch) and, when a registry is configured, be the registry-authorized oracle (UntrustedOracle). Acceptance re-validates both, so a proposal revoked or replaced during the delay cannot install a stale candidate, and it unconditionally requires a readable, non-zero price (ZeroPrice). When positions are open, acceptance also compares old and new prices: a deviation above 2% (MAX_ORACLE_ROTATION_DEVIATION = 0.02e18) reverts OracleRotationDeviationTooHigh unless the pool is already reduce-only, and an unreadable or zero old price reverts OracleRecoveryRequiresReduceOnly under the same condition. Recovering from a broken oracle is therefore an explicit two-role action: GUARDIAN_ROLE sets reduce-only first (setReduceOnly), then RISK_ROLE accepts, and reduce-only stays on until governance clears it.
  • Changing an oracle’s underlying data source is controlled by the oracle’s owner — the factory (updateDataSource → wait DATA_SOURCE_CHANGE_DELAY = 24h → acceptDataSourceUpdate), exposed via the factory’s convenience methods. The new source must be pre-approved (setDataSourceApproval, otherwise DataSourceNotApproved), and acceptance re-checks the approval and compares both sources’ normalized prices in the same block, reverting DataSourceDeviationTooHigh above 2% (MAX_DATA_SOURCE_DEVIATION = 0.02e18). This path never skips the comparison, so it only supports healthy-to-healthy rotation. Oracles use Ownable2Step for ownership handover.

Market schedule and risk phases

An oracle can be bound to a TradingCalendar: a weekly session template (including overnight sessions and weekends) plus holidays, half-days, and DST transitions. IPriceOracle.marketRiskPhase(reopenGraceDuration) then reports one of three phases: Active, Closed (returning the next scheduled open), or ReopenGrace (within the fixed grace after a scheduled open). Oracles without a calendar always report Active, so 24/7 markets are unaffected. The pool gates new risk on the phase (_checkNewRiskAllowed, exposed as marketRiskStatus()): every openPosition / openCrossMarginPosition overload, addLiquidity, and addLiquidityTo revert NewRiskMarketClosed while closed and NewRiskReopenGrace(availableAt) during the grace. Closing, reducing leverage, adding position collateral, liquidation, removing liquidity, funding accrual, and claimSettlement stay available in every phase. The grace duration is set per pool (setClosureCooldownParams, RISK_ROLE), and the manual pause and reduce-only flags still apply on top. On the price side, a Pyth oracle in a scheduled closure serves a frozen price: a stale read is tolerated while the calendar reports closed and the price age stays within maxClosureStaleness (isClosure() is true only in that state). Past that cap the read reverts Stale, and while the market is scheduled open a stale read reverts as before. Funding accrual has no closure exemption: it keeps accruing over closed intervals against the frozen price. Only the Pyth oracle implements closure handling; Chainlink and DEX oracles are closure-unaware.

Sequencer guard

BasePriceOracle.getPrice() first runs a sequencer liveness check through a SequencerGuard bound to the chain’s uptime feed. It reverts when the feed is unreadable (SequencerFeedUnavailable), when the sequencer reports down (SequencerDown), or when the sequencer restarted within the grace period (GracePeriodNotOver; default 1 hour, owner-tunable). Because every price consumer goes through getPrice(), a failed check blocks opens, closes, margin additions, leverage reductions, liquidations, and all LP actions; claimSettlement does not read the price and stays available. The guard is mandatory on Base mainnet (chain id 8453, reverting SequencerGuardRequired when unset) and optional on other chains.

Operational notes

  • Keep off-chain tooling and deployment config aligned with the oracle type and feed/feed-id chosen for the pool.
  • Treat stale or rejected prices as a risk to health checks — when a price is rejected, opening, closing, and liquidation are all unavailable until it refreshes.
  • Local setups usually use mocks so you can test the full lifecycle without a live feed; Base Sepolia defaults to a Pyth-based setup in the deployment runbook.
For the user-facing explanation of how oracle prices affect trading, see Price oracles.