For the complete documentation index, see llms.txt. This page is also available as Markdown.

Price feeds

How assets are priced — one primary feed per asset, divergence monitors, feed types, and the custom price feeds.

Every asset registered in a NAV Calculator has exactly one primary price feed and zero or more monitor feeds. The primary feed converts a raw token balance into a value in the quote currency (USD by default, 8 decimals) — it is the only feed that prices NAV. Monitor feeds are read only to detect divergence; they never price the asset. An asset is marked stale when its primary feed is stale (or the L2 sequencer is down).

Source: PriceFeedLib.sol

Diagram key: Input/source is light blue, Transform is violet, Staleness gate is amber, Output is green, Off-chain is dashed grey.
Diagram key — the same colours are used across every price-feed and adapter diagram.

How prices are resolved

When pricing an asset, the NAV Calculator reads its primary feed and, in the same call, compares that price against the asset's monitor feeds to compute a divergence signal. Consumers can inspect both with getPriceData:

The key fields of the returned PriceFeedData:

Field
Description

price

Price from the primary feed (0 if the primary is stale)

decimals

Decimal places in price (pinned per asset; all feeds for an asset share decimals)

stale

true if the primary feed is stale, or the L2 sequencer is down

healthyFeedCount

How many pricing feeds passed the staleness gates this read (1 in the intended one-primary configuration)

irregular

true if the primary diverged from the monitors beyond tolerance, or (stablecoins) strayed from the $1 peg

divergenceBps

Worst-case deviation between the primary and any healthy monitor, in basis points (0 when no healthy monitor)

monitorFeedCount

How many monitor feeds passed the staleness gates this read

irregular is a pricing-quality signal, not a validity flag. Even when irregular is true, price is still populated from the primary feed — the NAV Calculator is a read-only view that never reverts on divergence. It is the offchain subscription/redemption processor that decides how to react (pause, widen a band, alert). See Primary feed and divergence monitors.

The value of a (non-debt) position is:

normalised into the quote asset's decimals.

Example (illustrative values; WETH = 0xC02a…6Cc2, a Chainlink primary with one Redstone monitor):


Primary feed and divergence monitors

Each asset is priced by exactly one deterministic primary feed. Any additional trustworthy source is registered as a monitor feed — read on every NAV call to check that the primary is still telling the truth, but never used to value the asset.

Why one primary, not the freshest feed

Pricing from whichever of several redundant feeds is freshest tracks market noise instead of redeemable value. When an asset has both a fundamental exchange-rate feed (its redeemable value) and a market composite (Redstone/API3), the market feed updates more often and therefore wins. On a leveraged fund the resulting NAV swings are amplified by gross leverage, with no position change behind them.

One deterministic primary as the sole pricing feed removes that noise, and the redundant sources serve as divergence-only monitors — so a market wobble raises an alert instead of moving the share price.

The divergence and irregular signal

On each read, over the asset's healthy monitor feeds (each an independent <asset>/USD, 8-decimal feed, gated on its own heartbeat), the NAV Calculator computes:

  • divergenceBps — the worst-case (furthest) single-monitor deviation from the primary: max |primary − monitorᵢ| / primary, in basis points. Using the furthest monitor (not an average) means two monitors straddling the primary can't mask a genuine one-sided split.

  • irregular — set true when divergenceBps exceeds the asset's divergenceToleranceBps, or (for USD-pegged stablecoins only) when the primary strays from $1 beyond the asset's pegToleranceBps.

The median monitor price and a primaryAboveMonitors direction flag are exposed separately (via getPriceDivergence) for directionality — neither feeds the irregular flag itself. Both divergence and peg checks are suppressed while the primary is stale (a stale primary is already flagged via stale).

A monitor past its own heartbeat self-excludes rather than contributing a stale rate to the comparison — and because monitorFeedCount reports how many passed, that degradation is observable rather than silent: an asset whose count drops has lost a cross-check, even though irregular stays false. This is what makes it safe to keep a monitor whose upstream feed may be retired: the worst case is losing the signal, never a wrong one.

Tolerances are per-asset config, default 0 (= that check disabled). The values in use:

Kind

divergenceToleranceBps

pegToleranceBps

Fundamental-vs-market (Custom exchange-rate primary + market monitor) — wstETH, weETH, ezETH, rETH, ETHx, osETH, stETH …

250 (2.5%)

Market-vs-market (Chainlink primary + a second market feed) — ETH/WETH, USDe …

50 (0.5%)

USD-$1 stablecoins, monitored (peg guard plus a market-divergence monitor) — mainnet USDC & USDT, Base USDC …

50

100 (1%)

USD-$1 stablecoins, peg-only (no second market feed on that chain, so divergence is disabled) — Base USDT, all Gnosis stablecoins …

0

100 (1%)

A stablecoin's $1 peg guard is independent of its divergence monitor: where a chain has no second market feed for it — e.g. RedStone publishes no USDT/USD on Base, and the Gnosis stablecoins carry no market monitor — the asset runs peg-only (divergenceToleranceBps = 0, pegToleranceBps = 100). It still raises irregular when the Chainlink primary strays past 1% from $1, just without a market cross-check.

pegToleranceBps is the system's definition of "pegged", not merely a setting on pegged assets. An asset is pegged exactly when it carries a non-zero pegToleranceBps, and that value is readable on-chain per asset — so peggedness is derived from one authority rather than recorded a second time somewhere else and kept in step. There is deliberately no separate stored peg flag: a second copy would be able to disagree with this one, and the only defence against that would be a cross-check that exists solely because the copy does.

The practical consequence for a consumer classifying an asset: read the tolerance, do not maintain your own list. A yield-bearing wrapper is not pegged for exactly this reason — its redemption rate carries its price off $1, so it holds no peg tolerance, so it does not read as pegged.

Transitional fallback. The primary lives in the asset's priceFeeds list (priceFeeds[0]); monitors live in a separate monitorFeeds list. The legacy freshest-wins selector is retained only for the migration window where an asset might still carry a second pricing feed — with the intended one-primary configuration the selector runs once and simply returns the primary. Monitor feeds must report 8 decimals (MonitorFeedDecimalsMismatch); pricing feeds for one asset must share decimals (enforced on addPriceFeed).


Supported feed types

PriceType enum:

Value
Type
Notes

0

Chainlink

Chainlink AggregatorV3Interface push feed

1

Redstone

Redstone Classic Push — AggregatorV3-compatible, read like Chainlink

2

API3

API3 dAPI via Api3ReaderProxyV1 (18-dec proxy scaled to 8-dec at read)

3

Custom

A custom ICustomPriceFeed adapter with internal staleness logic

Custom composite adapters (ICustomPriceFeed) are deployed for assets without a direct USD feed. Each prices its asset as rate × base/USD — a fundamental on-chain exchange rate (e.g. wstETH's stEthPerToken()) times the base asset's USD price (ETH/USD for the LSTs, a stablecoin/USD for the ERC4626 vaults). These Custom feeds are the intended primary for their asset; any market-based source is registered as a monitor, not folded into the price.

  • The rate leg is a live on-chain exchange rate pinned in the adapter (a protocol getRate() / convertToAssets() / stEthPerToken() call), with no market oracle in the pricing path.

  • The base/USD leg is not a hardcoded oracle — it is read from the NAV's own primary feed for the base asset, via getPriceDataNoDivergence(baseAsset), so the whole system uses one price source per base asset (see Base/USD from the NAV's own selector).

  • getLatestPrice() returns (price, stale) and is the primary staleness gate, encoding the adapter's own logic (OR of its rate-leg heartbeats and the NAV's stale bit for the base asset).

  • latestRoundData() supplies updatedAt for the secondary heartbeat gate.

See the per-asset pages below for each adapter's composition.

Choosing a heartbeat: the provider's cadence is a floor, not a target

A heartbeat registered at exactly the provider's declared cadence has zero margin, and will go stale at the end of essentially every publish cycle. A "24-hour" feed does not publish at 86,400 s; it publishes tens of seconds past the nominal target. Registered at 86400, the asset is therefore priced $0 and listed in stalePriceAssets in the gap before each update lands — not as a rare event but once per cycle, indefinitely.

The margins in use:

Provider cadence
Registered

~24 h (86,400 s)

90000

~23 h (82,800 s)

86400

Zero margin only bites heartbeat-driven feeds. Where an asset's deviation threshold fires first, the feed updates long before its heartbeat and the heartbeat never binds — which is why the rule applies to pegged assets (stablecoins, pegged rates, forex pairs) and not to volatile ones. A zero-margin registration on a volatile asset is not by itself a defect, so this is not a pattern to apply across every feed uniformly.

Both heartbeat axes are checked in preflight, off-chain: this one, comparing each feed to the cadence its publisher declares, and the derived-vs-base rule below. Because a provider's declared cadence is off-chain metadata, the check reports a zero-margin feed that has not been measured as a warning to measure, and reserves a hard failure for a recorded exemption that its own evidence contradicts.

This is an absolute rule about a feed and its provider, and it composes with the relative rule in the next section (a derived feed's heartbeat may never be tighter than its base's). Raising a base asset's heartbeat for margin therefore forces its derived feeds up too, or the relative invariant breaks — which is exactly why raising the GHO and WXDAI primaries pulled sGHO and Gnosis sDAI up with them.


Base/USD from the NAV's own selector

A custom feed prices its asset as rate × base/USD. The base/USD term — ETH/USD for the LSTs, a stablecoin/USD for the ERC4626 vaults — is read from the NAV Calculator's own price selector, getPriceData(baseAsset), rather than from a hardcoded Chainlink oracle embedded in the feed.

Why. The NAV prices the base asset itself (e.g. WETH) through its primary feed. If a custom feed embedded its own Chainlink ETH/USD while the NAV valued WETH off a different oracle, the two ETH/USD terms would not cancel in ETH-quoted NAV: a divergence between the two ETH/USD sources could move NAV by ≈ spread × grossLeverage with zero position change. Sourcing the base term from the NAV's own primary makes the cancellation structural — the derived asset and the base asset's own leg always use the identical base/USD value in the same block.

The base term is read via getPriceDataNoDivergence(baseAsset) — the base asset's primary price, skipping the monitor/divergence computation on this hot path. The base asset keeps its own divergence monitors for alerting; the derived assets simply follow the base primary. A derived asset reads stale whenever the NAV reports its base asset's primary feed stale (past its heartbeat).

An L2 sequencer outage does not zero the base leg. computeNav prices every direct-fed position at its last value during a sequencer outage and surfaces the outage only through the top-level NAV.sequencerDown flag, not per position. If custom feeds zeroed on sequencer-down, custom-priced positions would crater to $0 while direct ones stayed live — an inconsistent, understated NAV. So the base leg is marked stale only on genuine base-feed staleness, matching the direct-feed path; the L2 sequencer stays a single top-level signal.

The shared helper is NavBaseUsdLib.baseUsd(nav, baseAsset): it reads getPriceDataNoDivergence(baseAsset), normalises to 8 decimals, marks the base stale on genuine base-feed staleness (a non-positive price or a reverting read; not on sequencer-down alone — see above), and fails to stale (never reverts the calling feed). ETH+ (a basket RToken, no base oracle) is unaffected.


Custom price feeds

Custom composite feeds (PriceType.Custom, implementing ICustomPriceFeed) are deployed for assets that lack a direct USD feed. They are grouped into four families below; each links to its own page with the full composition, constructor, and staleness rules.

LSTs & LRTs

Asset
Composition / source
Chains

wstETH

stEthPerToken() × ETH/USD (stETH/ETH ≡ 1.0)

Ethereum

stETH

1.0 × ETH/USD (stETH/ETH ≡ 1.0)

Ethereum

weETH

getRate() × ETH/USD (eETH/ETH ≡ 1.0)

Ethereum

eETH

ETH/USD (eETH/ETH ≡ 1.0)

Ethereum

rETH

getExchangeRate() × ETH/USD

Ethereum

rsETH

rate provider × ETH/USD

Ethereum

ezETH

Renzo reporter rate × ETH/USD

Ethereum

cbETH

rate provider × ETH/USD

not deployed

ETHx

Stader getExchangeRate() × ETH/USD

Ethereum

ankrETH

ETH/USD ÷ ratio()

Ethereum

osToken (osETH / osGNO)

StakeWise convertToAssets() × underlying/USD

Ethereum, Gnosis

These LST/LRT feeds are fundamental primaries — they read only the protocol's on-chain exchange rate (no embedded market oracle). The market leg lives in a separate monitor: wstETH/weETH watch market <asset>/USD feeds, while ezETH, ETHx, osETH, rETH and stETH are watched by a cross-rate monitor (ETH/USD × <asset>/ETH). Where that <asset>/ETH leg is an independent market feed, a depeg raises irregular instead of moving NAV.

ezETH carries two cross-rate monitors, and only one of them is a market feed. Its original monitor's ezETH/ETH leg became fundamental (RedStone's multi-feed ezETH_FUNDAMENTAL) when RedStone sunset the Classic-Push market feed — leaving a check that compared one fundamental rate against another, which cannot see the market price diverge. A second monitor with a genuine market leg was added alongside it rather than replacing it, so the depeg check fires again.

Keeping both is safe because divergenceBps is measured against the worst single monitor, not the median: the market monitor dominates instead of being averaged away by the fundamental one sitting near 0 bps. Their heartbeats differ deliberately — each is derived from its own feed's measured cadence, not copied from the other.

stETH is priced fundamentally, at a flat 1.0 against ETH — the same claim wstETH's stEthPerToken() and eETH's constant rate encode, and it uses the same unit-rate contract as eETH. It carries two monitors: the market stETH/ETH cross-rate and a direct Chainlink stETH/USD feed. Pricing stETH off a market feed while wstETH was fundamental meant the two disagreed by the full discount during a depeg — 1,000 stETH and the economically identical ~870 wstETH valued 6% apart at a 0.94 print — so legs that should net inside one portfolio (Aave wstETH collateral against a Curve stETH/ETH LP) showed phantom P&L with no position change. It also made the divergence signal read backwards: wstETH flagged irregular during a depeg while stETH, agreeing with itself, flagged nothing.

Yield-bearing stables

Asset
Composition / source
Chains

sUSDS

convertToAssets() × USDS/USD

Ethereum

sUSDe

convertToAssets() × USDe/USD

Ethereum

sGHO

convertToAssets() × GHO/USD

Ethereum

syrupUSDC

Maple rate × USDC/USD

Ethereum

RWIV

convertToAssets() × USDC/USD

Ethereum

sXDAI

savings rate × xDAI/USD

Gnosis

These float; they are not $1 assets. Each prices as a growing redemption rate times its base stablecoin, so its USD price rises above $1 as the rate accrues — which is what makes it not pegged, and why none of them carries a pegToleranceBps. The $1 guard belongs to the base stablecoin (USDS, USDe, GHO, USDC, xDAI) that each of these feeds reads through the NAV. The "stables" grouping here describes the feed shape, not a peg.

Derived & cross-rate

Asset
Composition / source
Chains

lsETH & OETH (CrossRate)

X/USD × Y/X cross-rate of two CL feeds

Ethereum

LDO

cross-rate composition

Ethereum

ETH+

Reserve RToken basket value

not deployed; ETH+ is priced by a direct Chainlink feed

NXM / wNXM

Nexus Mutual RAMM internal price × ETH/USD

Ethereum

L2 variants

Asset
Composition / source
Chains

wstETH (L2)

CL wstETH/ETH × ETH/USD

Arbitrum, Base, Gnosis

weETH (L2)

CL weETH/ETH × ETH/USD

Arbitrum, Base

rsETH (L2)

rate provider × ETH/USD

Arbitrum

ezETH (L2)

Renzo reporter rate × ETH/USD

Arbitrum

cbETH (L2)

CL cbETH/ETH × ETH/USD

Base

syrupUSDC (L2)

Maple rate × USDC/USD

Arbitrum

Governing-leg freshness

A composite has no single honest (updatedAt, heartbeat): it depends on two Chainlink legs — the rate leg with its own heartbeat, plus the base/USD leg with a different one — and the oldest leg and the tightest-heartbeat leg are usually different feeds. Reporting the oldest timestamp against the tightest heartbeat produces a permanent false warning for feeds with a slow leg; pairing it with the loosest heartbeat hides a genuinely stale fast leg.

So getPriceData(asset) reports the governing leg: the underlying Chainlink leg with the greatest age / heartbeat ratio — the input closest to, or furthest past, its own staleness deadline — and surfaces that leg's (updatedAt, chainlinkHeartbeat). The pair is a real, matched pair from one actual oracle, and reduces to the obvious answer for a single-leg feed. Each custom feed exposes it as governingFeedFreshness() → (updatedAt, heartbeat):

Feed shape
Governing leg

Single Chainlink leg (BaseCustomPriceFeed, ERC4626VaultRatePriceFeed, and the bespoke ankrETH)

The base/USD leg, read from the base asset's own NAV selector via NavBaseUsdLib.baseFreshness

Two Chainlink legs (TwoFeedChainlinkPriceFeed, cross-rate feeds)

Whichever of the rate leg or the base leg has the larger age / heartbeat ratio; falls back to the base leg if the rate feed is unreadable

ETH+ (Reserve basket — built, not deployed)

min(lastSave()) across the active basket, against the Reserve-protocol heartbeat

No trackable Chainlink leg

(0, 0) — reported as no timestamp

Because the reported heartbeat is the governing leg's, it overrides the registered config heartbeat for custom feeds — so a custom-priced asset's reported heartbeat is typically tighter than its registration value.

Two further consequences worth knowing: which leg governs changes between reads as the ratios cycle, so a two-leg feed's reported heartbeat can alternate between its legs' values; and the feed's own latestRoundData().updatedAt is unchanged — it still returns the conservative oldest-leg timestamp, which is what feed selection, ranking and the outer heartbeat gate use. The staleness rule itself is unchanged: a custom feed backed by n Chainlink legs is stale iff any leg is stale.

The helpers are NavBaseUsdLib.baseFreshness and ICustomPriceFeed.


L2 sequencer uptime check

On Layer 2 chains the NAV Calculator also checks the Chainlink L2 Sequencer Uptime Feed; if the sequencer is down or within its grace period, the chain's reading is flagged via NAV.sequencerDown. See Stale prices & sequencer for the full staleness and sequencer model.


Inspecting an asset's feeds

Pricing (primary) feeds, monitor feeds, and the per-asset tolerances are each introspectable, and getPriceDivergence returns the live divergence read:

Example (illustrative values; WETH = 0xC02a…6Cc2 — one Chainlink primary, one Redstone monitor):


Admin operations

Feed configuration is privileged (governance Safe, MANAGER role). An asset carries one pricing feed, plus optional monitor feeds and tolerances:

Action
Call

Register an asset with its (primary) feed

registerAsset(asset, priceFeed, priceType, heartbeat)

Add / remove a pricing feed

addPriceFeed(...) · removePriceFeed(asset, priceFeed) · removePriceFeedAt(asset, index)

Set the divergence / peg tolerances

setDivergenceTolerance(asset, bps) · setPegTolerance(asset, bps)

Add / remove a monitor feed

addMonitorFeed(...) · removeMonitorFeed(asset, priceFeed) · removeMonitorFeedAt(asset, index)

Remove the asset and all its feeds

unregisterAsset(asset)

addPriceFeed requires the new feed's decimals to match the asset's existing pricing feeds (PriceFeedDecimalsMismatch) and rejects a feed already registered (PriceFeedAlreadyRegistered). addMonitorFeed requires a divergence tolerance to be set first (DivergenceToleranceRequired), the monitor to report 8 decimals (MonitorFeedDecimalsMismatch), and rejects duplicates (MonitorFeedAlreadyRegistered). Full per-function detail, roles, and reverts are on the NAV Calculator's Admin / Manager API.

Last updated