Oracles
RAI Dollar uses two independent oracle systems for different jobs:
PriceFeed(per branch), collateral USD pricing for liquidations, redemptions, and ICR/TCR. Chainlink primary with composite logic for derivatives.MarketOracle(singleton), RD/USD market price feedingRateParControl. Balancer hook-based TWAP/TWAL over finalized observations.
PriceFeed stack
Each collateral branch has its own price feed. The base contract is MainnetPriceFeedBase (Solidity 0.8.24); concrete feeds extend it for the specific collateral.
Concrete feeds
| Feed | Asset | Composition |
|---|---|---|
WETHPriceFeed | WETH | Chainlink ETH/USD directly. |
WSTETHPriceFeed | wstETH | Composite, Chainlink stETH/USD or ETH/USD × Lido canonical rate, with deviation check. |
RETHPriceFeed | rETH | Composite, Chainlink ETH/USD × rETH canonical rate. |
WBTCPriceFeed | WBTC | Composite, Chainlink BTC/USD × WBTC/BTC. |
Source modes
Each feed maintains a priceSource state machine:
enum PriceSource {
primary, // normal Chainlink primary
ETHUSDxCanonical, // fallback: ETH/USD × canonical rate
lastGoodPrice // last good cached price (terminal)
}
Mode transitions happen automatically. If Chainlink goes stale (older than the feed's stalenessThreshold), deviates too much, or fails an integrity check, the feed transitions to ETHUSDxCanonical (where it can, LST feeds have an ETH/USD path to fall back on) or to lastGoodPrice (terminal).
When the feed enters a fallback state, the branch's BorrowerOperations.shutdownFromOracleFailure() becomes callable, triggering branch shutdown (see Shutdown).
Deviation checks for derivatives
WSTETHPriceFeed enforces STETH_USD_DEVIATION_THRESHOLD = 1% between the Chainlink stETH/USD and the Chainlink ETH/USD × Lido canonical rate. If they diverge more than 1% on a redemption call, the feed uses the lower of the two, protecting the protocol from claiming over-priced collateral.
Key constants
| Constant | Value | Purpose |
|---|---|---|
TARGET_DIGITS | 18 | Output precision |
TIMEOUT (legacy PriceFeed.sol) | 4 hours | Max Chainlink staleness |
MAX_PRICE_DEVIATION_FROM_PREVIOUS_ROUND (legacy) | 50% | Sanity check |
MAX_PRICE_DIFFERENCE_BETWEEN_ORACLES (legacy) | 5% | Cross-source check |
STETH_USD_DEVIATION_THRESHOLD | 1% | wstETH redemption guard |
Read interface
fetchPrice(bool _isRedemption) returns (uint256 price, bool oracleFailure)
viewLatestPrice(bool _isRedemption) returns (uint256) // view-only
previewWillShutdown(bool _isRedemption) returns (bool)
isOracleFallbackActive() returns (bool)
fetchPrice is mutating, it advances the state machine on each call. View-only callers (off-chain dashboards, helper contracts) use viewLatestPrice.
The _isRedemption flag selects a slightly different validation path for redemption (more conservative for derivatives).
The RD Balancer pool
The protocol designates one Balancer pool as its canonical RD market-price venue. This pool is the single source of truth for the controller's RD/USD signal, and it's also where LP staking earns rewards (see LP staking).
Properties:
- Asset pair. RD against a major USD-denominated asset (typically a top stablecoin). The exact pair is set at deploy time and is on Deployed addresses.
- Pool type. A Balancer stable pool, chosen because RD's target is $1 and the curve concentrates depth around parity, which is where the controller cares about accurate price signals.
- MarketOracle integration.
MarketOracleis attached to this pool as a Balancer hook, so every swap and liquidity event writes a fresh observation to the oracle's internal array. No keeper poke is required during normal pool activity. - Price the protocol uses. A TWAP over
quotePeriod, read only from finalized observations (windows ending at least 60 seconds in the past, see below). The protocol takes the median-style time-weighted price across the window, not the spot price at any single block. - Liquidity the protocol watches. A parallel TWAL (time-weighted average liquidity) over
twalPeriod. If the pool's depth drops below safe thresholds, the controller can downweight the price signal or refuse to act on it.
Why a Balancer stable pool specifically:
- The stable-pool curve concentrates liquidity near $1, which produces a much tighter price signal at parity than a constant-product pool would.
- Balancer's hook interface lets the protocol attach an oracle directly into the pool's swap lifecycle, so the oracle update is atomic with the price movement that triggered it. No off-chain TWAP relayer, no cross-contract oracle service.
- The pool is the same venue everyone trades on; the controller is reading the same price the market is producing, not a derived feed.
The combination of stable-pool depth and hook-driven observations means the controller's signal is automatically maintained by organic trading. A keeper can also call an explicit checkpoint during quiet periods to keep observations fresh, but routine activity does that on its own.
MarketOracle
Single instance protocol-wide. Built as a Balancer hook (BaseHooks, IHooks) that runs on the RD Balancer pool described above. It implements a Uniswap-V3-style observations array and exposes TWAP/TWAL reads.
What it produces
getResultWithValidity(): TWAP price of RD in the paired asset overquotePeriod.getLiquidityWithValidity(): TWAL (time-weighted average liquidity) overtwalPeriod.
Both return a price/liquidity value and a validity boolean indicating whether the read is fresh enough to trust. RateParControl reads both and refuses to act on validity = false.
Finalized observations
The oracle reads only finalized intervals: windows whose end is no newer than:
min(now - _MIN_PRICE_AGE, latest_written_observation_time)
with _MIN_PRICE_AGE = 60 seconds. This prevents same-block manipulation: a flash-loaner moving the pool price in the same block as the read can't poison the controller, because the controller only consumes intervals that ended at least 60 seconds in the past.
Sample window construction
function _finalizedWindow(uint32 period) internal view returns (
uint32[] secondsAgos,
uint32 sampleDt,
bool ok
)
periodmust be greater than_MIN_PRICE_AGE(otherwise no finalized window exists).- The window slides back so its end is on or before
now - _MIN_PRICE_AGE. - If the most recent observation is older than
_MIN_PRICE_AGE, the window slides back further to cover only what has been observed.
The result is a sample window that never extrapolates from a stale observation: older designs that read secondsAgos = [period, 0] directly would extrapolate the latest tick forward to "now", which is unsafe if no one has poked the pool recently.
Hooks
MarketOracle implements three Balancer hooks (onBeforeSwap, onBeforeAddLiquidity, onBeforeRemoveLiquidity). Each before-hook samples the current pool state and writes a new observation to the array. So the oracle is updated automatically by pool activity; no keeper is required for organic flow.
For very quiet periods, an explicit checkpoint call is available, keepers can poke it to keep the observation array fresh.
Why TWAP + TWAL
- TWAP prevents single-block manipulation by averaging over time. Standard.
- TWAL (time-weighted average liquidity) is used to detect pool draining. If liquidity has been declining steadily, the controller can downweight the price signal, a thin pool produces a noisier TWAP, and the controller should react less aggressively. This is part of what makes the controller robust to attacks on the pool itself.
How the two oracles connect
PriceFeedis the collateral truth. Liquidations and redemptions use it to value the trove.MarketOracleis the stablecoin truth. The controller uses it to decide what rate and par should be.
They never overlap. PriceFeed is a Chainlink-derived USD price for collateral; MarketOracle is a Balancer-derived TWAP of RD in some paired asset (typically a major stablecoin). The protocol does not use the MarketOracle for collateral pricing.
Failure modes
| Failure | Affected component | Protocol response |
|---|---|---|
| Chainlink stale | One branch's PriceFeed | Falls back per state machine; previewWillShutdown=true triggers callable shutdown |
| Chainlink wildly off | One branch's PriceFeed | Deviation check rejects; same fallback path |
| LST canonical rate diverges | wstETH / rETH feed | Conservative-side selection on redemption |
| Balancer pool drained | MarketOracle | TWAL drops; controller can refuse to act on the price |
| Balancer pool not poked | MarketOracle | Finalized-observation window slides back; eventually validity = false |
| Both oracles fail | Affected branch | Shutdown via shutdownFromOracleFailure(); redemption proceeds on the discount schedule |
The protocol prefers degraded operation over false confidence. If the data isn't trustworthy, the controller stops acting and the affected branch can be wound down via redemption.
Deep dive
- Risks: oracle-specific risks and mitigations.
- Peg & PI controller: how the controller consumes
MarketOracle.