Skip to main content
How the protocol turns external price feeds into a single USD price, and why it refuses to act when those feeds are stale or disagree. There is no guarantee of oracle correctness. Prices live in a dedicated price aggregator contract. The controller reads it; the pool never sees a price. Read first: System architecture.

The shape of a configuration

Configuration is keyed by PriceKey, not by market:
Every key maps to one AssetOracle: PriceSource is one of four variants: An Aquarius LP source must be the only source on its key. Composition through Scaled.quote and LP leg keys is bounded at MAX_RESOLUTION_DEPTH = 3 (#229), and a cycle is rejected with #225 OracleCycleDetected.

Providers

FeedSource { provider, decimals, max_stale_seconds }, where provider is: Reflector references identify their asset with OracleAssetRef::Stellar(Address) or Symbol(Symbol); a String reference is rejected with #204. A Reflector feed must be USD-quoted at listing (#220). RedStone and Xoxno references carry a FeedNature of Market or Fundamental. That choice changes how staleness is judged and whether the leg-age spread bound applies — see the oracle disruption runbook.
Unsmoothed spot everywhere is rejected. If every configured source reads an unsmoothed market feed — including a single-source oracle whose only leg is one — set_oracle reverts with #38 SpotOnlyNotProductionSafe. Add a TWAP-read Reflector leg, or a Fundamental-nature feed.

XOXNO oracle (RWA)

The XOXNO oracle is a self-hosted multi-signer oracle. Registered bot wallets submit prices; the contract stores the latest per signer per feed and recomputes a median aggregate under an N-of-M threshold, keeping reads O(1). Writes are threshold-signed and verify signer-set membership, submission age, and relative skew. Reads are open and Reflector-compatible. Its full callable surface — submitting prices, the Reflector-compatible reads, and the admin knobs — is on XOXNO oracle ABI. Use it when Reflector and RedStone have no suitable Stellar feed — especially tokenized RWA listings. A listing does not imply issuer endorsement, regulatory compliance, or user eligibility. Keep submission age at or below the consuming oracle’s max_price_stale_seconds. Below threshold, aggregates clear and reads fail closed with adapter error 7 NoDataForFeed.
set_threshold, set_max_submission_age_seconds, and set_max_relative_skew_seconds store the new bound only; they do not re-derive aggregates that already exist. Follow a change with recompute_feeds(feed_ids) in batches small enough to stay inside the transaction footprint limit (roughly one ledger entry per signer plus three, per feed). feeds() enumerates the registered ids.

Mainnet RWA feeds

Feed ids must match the lending bot job pairs that submit prices.

Composition and tolerance

A two-source price is the midpoint of its two legs, or it fails. There is no band-by-band fallback, no “use the primary inside the first band”, and no degrading to a single leg. One readable leg is an incomplete observation, not a fallback price.
For a dual-source oracle:
  1. Both legs must be readable and fresh. Only one leg producing a reading is #205 UnsafePriceNotAllowed.
  2. within_tolerance_band divides the larger leg by the smaller and compares that ratio to upper_ratio_bps. Beyond it, #205.
  3. The served value is (a + b) / 2.
The band is stored as a pair of reciprocal ratios. At configuration time the contract checks that lower_ratio_bps equals BPS × BPS / upper_ratio_bps, rounded half up. At price time only upper_ratio_bps is read. Dividing the larger leg by the smaller already gives a ratio above 1 either way, so one bound covers both directions. upper_ratio_bps must be between MIN_TOLERANCE = 150 and MAX_TOLERANCE = 2500 bps, else #208. A single-source oracle serves its one leg directly, still subject to staleness, positivity, and sanity checks — and is admitted only with a tighter sanity band (MAX_SINGLE_SOURCE_SANITY_BAND_BPS = 1000, versus MAX_LP_SANITY_BAND_BPS = 8182 for LP oracles). Exceeding it is #226 SanityBandTooWideForSingleSource. independence governs whether the two legs may depend on the same provider contract. RequireDisjoint forbids it; AllowShared requires the declared set to match the actual one exactly, else #232 IndependenceNotDeclared.

Staleness and sanity

  • Staleness: a leg older than its own max_stale_seconds, or a blend older than the oracle’s max_price_stale_seconds, is #206 PriceFeedStale. Both bounds are clamped to [60, 93_600] seconds.
  • Future-dated observations do not raise #206. They are dropped outright, so the read fails as #210 NoLastPrice or #205 instead.
  • Leg-age spread: when both legs are Market nature, they must be within MAX_LEG_AGE_SPREAD_SECONDS = 3600 of each other, because the blend weights them equally. A Fundamental leg is exempt.
  • Sanity bounds: the final WAD price must sit in [min_sanity_price_wad, max_sanity_price_wad], else #223. The band is a pass/fail check on the already-blended value, never a clamp. It must be at least MIN_SANITY_BAND_BPS = 50 wide and at most MAX_REASONABLE_PRICE_WAD = $1e9 per whole token.

Fail-closed

Every valuation-dependent mutation consumes a complete price snapshot. Source failure, staleness, disagreement, or a failed sanity rule reverts the operation — the protocol halts risk-taking rather than acting on a questionable price. There is no permissive mode, no per-flow policy table, and no TWAP-to-spot fallback. Two read surfaces behave differently by design: get_market_indexes_detailed on the controller surfaces the same per-leg detail for UIs: price_wad, primary_price_wad, anchor_price_wad, stale, deviation, valid, and error_code.
A resolution error — a nested reference leg going stale, for example — zeroes every other field, including stale and deviation. So stale: false does not mean the staleness check passed. It may mean resolution failed before that check ran.An error_code field that reports the reason directly is written but not yet released; it is on an unmerged branch. Until it ships, treat valid: false as “unusable, reason unknown” rather than inferring a cause from the other flags.

Callable surface

Everything the price aggregator exposes. Reads are open; the three mutators are governance-routed.

Reads

PriceFeedRaw is { price_wad: i128, asset_decimals: u32, timestamp: u64 }. PriceStatus is { final_wad, primary_wad, secondary_wad, price_timestamp, stale, deviation, valid } — the blended price, both legs, the timestamp of the older leg, and three flags.
price_spread is not a band around the blend. It returns the two source legs sorted: (min(first, second), max(first, second)). On a single-source oracle both entries come from the same leg. Use it to see how far your two sources actually diverge, not as a confidence interval.
Pick between prices and quotes by what you want on failure. A liquidation bot sizing a position wants prices, so a bad feed aborts before it commits. A dashboard wants quotes, so one broken market does not blank the whole page.

Operator controls

The price aggregator exposes three configuration mutators, all governance-routed: All three publish UpdateAssetOracleEvent (["config", "asset_oracle"]) with the full post-change configuration. Read the current configuration with oracle(key).

Next

Oracle disruption runbook

Triage, feed nature, blast radius, and repair when an asset stops pricing.

Markets

Listing lifecycle and hub-asset rows.

Spokes

Spiko and Centrifuge risk modules.

Security model

Fail-closed posture in the trust model.