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 (answer > 0), stale rounds (answeredInRound >= roundId), prices older than staleDuration, and prices outside 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 common bounds: a zero price reverts (BadPrice), and a price above maxReasonablePrice reverts. Defaults are maxReasonablePrice = 1_000_000 * 1e18 and staleDuration = 600 (10 min); both are tunable per oracle by the owner.

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 / price > maxConfidenceBps). Each oracle sets maxConfidenceBps, so a successful price update does not guarantee a successful read.

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 new oracle must match the market and collateral (OracleMismatch otherwise).
  • 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. Oracles use Ownable2Step for ownership handover.

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.