Skip to main content
How the protocol turns external price feeds into a single trusted USD price, and how it refuses to act when those feeds are stale or disagree. Read first: System architecture. The controller is the only contract that reads prices. It resolves a primary feed, optionally cross-checks an anchor, normalizes everything to USD-WAD (10^18), and applies a failure policy chosen by the calling flow’s risk direction. The pool never sees a price.

Providers

ProviderRoleReads
Reflector (SEP-40)PrimarySpot price, or a TWAP over up to MAX_TWAP_RECORDS = 12 records.
RedStoneAnchorSpot price with dual timestamps (observed and published).
The Reflector base asset must be USD at listing — the controller rejects a non-USD-quoted configuration. Every feed’s native decimals are normalized to WAD at the edge; the controller discovers oracle decimals from the feed contract, never from token decimals.

Oracle strategy

Each market picks one resolution strategy (OracleStrategy):
  • Single — read the primary feed only. No cross-check.
  • PrimaryWithAnchor — read the primary and an anchor, then reconcile them through tolerance bands (below). Every current mainnet market uses this.
A feed identifies its asset through an OracleAssetRef: Stellar(Address) for an asset-address feed, Symbol(Symbol) for a symbol feed, or String(String).

Tolerance bands

Under PrimaryWithAnchor, the controller measures how far the primary price has drifted from the anchor and decides which price to trust (tolerance.rs). It computes the ratio in BPS:
ratio_bps = primary · RAY / anchor   →   expressed in BPS
It then compares the deviation against two operator-set bands, first_tolerance and last_tolerance:
DeviationResult
Within the first bandUse the primary price.
Between the first and last bandsUse the midpoint (anchor + primary) / 2.
Beyond the last bandReject with #205 UnsafePriceNotAllowed under a strict policy; fall back to the anchor under a permissive policy.
The bands are bounded: first_tolerance ∈ [50, 5000] BPS, last_tolerance ∈ [150, 5000] BPS, and last_tolerance > first_tolerance. On mainnet, USDC, XLM, EURC, BTC, and ETH use 200 / 500; USTRY uses 300 / 800; CETES and TESOURO use 400 / 1000. See Risk parameters.

Staleness and sanity

Beyond deviation, every price passes freshness and bounds checks (observation.rs):
  • Staleness: a feed older than the market’s max_price_stale_seconds is stale. That window is clamped to [60, 86_400] seconds; values outside the range are pulled to the nearest bound.
  • Future skew: a feed timestamped more than 60 seconds ahead of the ledger clock (MAX_FUTURE_SKEW_SECONDS) is rejected unconditionally with #206 PriceFeedStale — no policy tolerates it.
  • Sanity bounds: the final WAD price must fall within the market’s [min_sanity_price_wad, max_sanity_price_wad]. A value outside the band is a sanity violation, tolerated only by the lenient policies below.

Oracle policies

Every flow selects an OraclePolicy before reading prices (policy.rs). The policy decides which leniencies are acceptable for that flow’s risk direction. The strict policies — used by every flow that increases risk or moves collateral — reject all of them.
FlowPolicyDisabled marketStale sourceUnsafe deviationDegraded dual sourceSanity violation
Borrow / withdraw-with-debtRiskIncreasingnonononono
LiquidationLiquidationnonononono
Supply / repay (active market)RiskDecreasingnoyesyesyesyes
Repay on a disabled marketRepayyesyesyesyesyes
ViewsViewyesyesyesyesyes
Liquidation deliberately mirrors RiskIncreasing (it rejects every loosening) so seizure accounting can never read a degraded price; the two are kept as distinct variants for intent and auditing.

Fail-closed

The oracle path is fail-closed by design:
  • There are no try_ wrappers in the critical price path. A trap inside a provider reverts the whole transaction rather than being swallowed.
  • A missing primary price hard-panics — the protocol never substitutes zero or a default.
  • Risk-increasing and liquidation flows reject staleness, out-of-band deviation, and any degraded dual source. A risk-increasing operation never proceeds on a single degraded source.
  • A TWAP-to-spot fallback is allowed only under permissive policies, and it emits OracleTwapDegradedEvent so the degradation is observable.
The asymmetry is intentional: flows that reduce risk (supply, repay) stay available even on a degraded oracle so a user can always rescue a position, while flows that increase risk revert rather than act on an untrusted price.

Operator controls

Oracle configuration is owner-gated on the controller and reached through governance in production:
PathEffect
Governance propose(AdminOperation::ConfigureMarketOracle)set_market_oracle_config(asset, config)Sets strategy, primary, anchor, staleness, tolerances, and sanity bounds; creates or updates AssetOracle(asset). Emits UpdateAssetOracleEvent.
Governance propose_edit_oracle_toleranceset_oracle_tolerance(asset, tolerance)Updates the four-band OraclePriceFluctuation.
Emergency disable is the only controller ORACLE role entrypoint:
EndpointEffect
disable_token_oracle(caller, asset)Moves a market from Active to Disabled, blocking oracle-dependent risk-increasing operations. Emits OracleDisabledEvent.
See Governance ABI for proposal signatures and Configuration for validation rules.

Next

Markets

The market lifecycle that oracle configuration activates.

Risk parameters

Per-market tolerance bands, staleness windows, and sanity bounds.

Security model

The fail-closed posture in the wider trust model.