> ## Documentation Index
> Fetch the complete documentation index at: https://xoxno.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Stellar lending oracles

> How the controller prices assets with Reflector and RedStone, applies tolerance bands and staleness checks, and stays fail-closed when feeds disagree.

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](/stellar-lending/dev/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

| Provider           | Role    | Reads                                                             |
| ------------------ | ------- | ----------------------------------------------------------------- |
| Reflector (SEP-40) | Primary | Spot price, or a TWAP over up to `MAX_TWAP_RECORDS = 12` records. |
| RedStone           | Anchor  | Spot 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:

```text theme={"system"}
ratio_bps = primary · RAY / anchor   →   expressed in BPS
```

It then compares the deviation against two operator-set bands, `first_tolerance` and `last_tolerance`:

| Deviation                        | Result                                                                                                             |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Within the first band            | Use the **primary** price.                                                                                         |
| Between the first and last bands | Use the **midpoint** `(anchor + primary) / 2`.                                                                     |
| Beyond the last band             | Reject 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](/stellar-lending/dev/risk-parameters#live-mainnet-values).

## 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.

| Flow                           | Policy           | Disabled market | Stale source | Unsafe deviation | Degraded dual source | Sanity violation |
| ------------------------------ | ---------------- | --------------- | ------------ | ---------------- | -------------------- | ---------------- |
| Borrow / withdraw-with-debt    | `RiskIncreasing` | no              | no           | no               | no                   | no               |
| Liquidation                    | `Liquidation`    | no              | no           | no               | no                   | no               |
| Supply / repay (active market) | `RiskDecreasing` | no              | yes          | yes              | yes                  | yes              |
| Repay on a disabled market     | `Repay`          | yes             | yes          | yes              | yes                  | yes              |
| Views                          | `View`           | yes             | yes          | yes              | yes                  | yes              |

`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:

| Path                                                                                                    | Effect                                                                                                                                             |
| ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| 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_tolerance` → `set_oracle_tolerance(asset, tolerance)`                   | Updates the four-band `OraclePriceFluctuation`.                                                                                                    |

Emergency disable is the only controller `ORACLE` role entrypoint:

| Endpoint                              | Effect                                                                                                                         |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `disable_token_oracle(caller, asset)` | Moves a market from `Active` to `Disabled`, blocking oracle-dependent risk-increasing operations. Emits `OracleDisabledEvent`. |

See [Governance ABI](/stellar-lending/dev/governance-abi) for proposal signatures and [Configuration](/stellar-lending/dev/configuration) for validation rules.

## Next

<CardGroup cols={2}>
  <Card title="Markets" icon="store" href="/stellar-lending/dev/markets">
    The market lifecycle that oracle configuration activates.
  </Card>

  <Card title="Risk parameters" icon="sliders" href="/stellar-lending/dev/risk-parameters">
    Per-market tolerance bands, staleness windows, and sanity bounds.
  </Card>

  <Card title="Security model" icon="shield-halved" href="/stellar-lending/dev/security-model">
    The fail-closed posture in the wider trust model.
  </Card>
</CardGroup>
