> For the complete documentation index, see [llms.txt](https://docs.kpk.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kpk.io/funds/infrastructure/onchain-accounting/price-feeds.md).

# 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`](https://github.com/karpatkey/onchain-accounting/blob/main/src/prices/PriceFeedLib.sol)

<figure><img src="/files/gQagfplUSMnAUF2AdUqq" alt="Diagram key: Input/source is light blue, Transform is violet, Staleness gate is amber, Output is green, Off-chain is dashed grey."><figcaption><p>Diagram key — the same colours are used across every price-feed and adapter diagram.</p></figcaption></figure>

***

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

```solidity
function getPriceData(address underlyingAsset)
    external view returns (PriceFeedData memory);
```

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                                                     |

{% hint style="info" %}
`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](#primary-feed-and-divergence-monitors).
{% endhint %}

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

```
positionValue = (amount × price) / 10^(assetDecimals)
```

normalised into the quote asset's decimals.

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

```
getPriceData(0xC02a…6Cc2)
→ PriceFeedData {
    priceFeed: 0x5f4e…8419, priceType: 0 /* Chainlink */, price: 392265000000,
    decimals: 8, chainlinkHeartbeat: 3600, updatedAt: 1718901200,
    stale: false, sequencerDown: false, healthyFeedCount: 1,
    irregular: false, divergenceBps: 11, monitorFeedCount: 1
  }
```

***

## 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`](/funds/infrastructure/onchain-accounting/contracts/nav-calculator.md#read-functions)) 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.

{% hint style="info" %}
**`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.
{% endhint %}

{% hint style="warning" %}
`irregular` never changes `price` or `value`, and the NAV Calculator **never reverts** on divergence — it is a read-only view. A `true` value tells the offchain [subscription/redemption processor](/funds/infrastructure/onchain-accounting/contracts/nav-calculator.md) to withhold or widen pricing and alert operators. See [Stale prices & sequencer](/funds/infrastructure/onchain-accounting/concepts/stale-prices-and-sequencer.md) for how it sits alongside `stale`, `sequencerDown`, and `quoteAssetStale`.
{% endhint %}

{% hint style="info" %}
**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`).
{% endhint %}

***

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

{% tabs %}
{% tab title="Chainlink / Redstone / API3" %}
AggregatorV3-shaped feeds. Each is gated on a configurable **heartbeat** (`block.timestamp − updatedAt ≥ heartbeat → stale`). Redstone and API3 are read through the same `latestRoundData` path as Chainlink.
{% endtab %}

{% tab title="Custom" %}
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](#primary-feed-and-divergence-monitors), 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](#base-usd-from-the-navs-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.
{% endtab %}
{% endtabs %}

### 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&#x20;*****heartbeat-driven*****&#x20;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](#base-usd-from-the-navs-own-selector) 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.

{% hint style="info" %}
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.
{% endhint %}

***

## 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](#primary-feed-and-divergence-monitors). 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.

{% hint style="info" %}
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).

{% hint style="warning" %}
**Never register a derived feed with a heartbeat tighter than its own base asset's.** Because the derived feed inherits the base's staleness verdict regardless, a tighter heartbeat cannot prevent a stale price — it can only **manufacture a stale verdict while the base is still fresh**, zeroing the asset's price and listing it in `stalePriceAssets` for no reason. A single-primary asset has no second feed to rescue that read.

The invariant is guarded pre-deploy. Matching the base is the *floor*, not the whole answer — the base's own value must itself carry [margin over its provider's cadence](#choosing-a-heartbeat-the-providers-cadence-is-a-floor-not-a-target), since matching a base that is registered at zero margin reproduces the base's own blackout in the derived feed.

The **reverse** direction is safe and intentional: several derived feeds sit at `86400` over a WETH base at `3600`. A looser derived heartbeat cannot over-report, precisely because the base's own verdict is inherited either way — so tightening those would only add a needless blackout window.
{% endhint %}

An **L2 sequencer outage does&#x20;*****not*****&#x20;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.
{% endhint %}

{% hint style="warning" %}
**Circularity invariant.** A `baseAsset` (WETH, GNO, the stablecoins) MUST be priced by **direct** feeds (Chainlink / API3 / Redstone) — never by a NAV-sourced custom feed — otherwise `getPriceData(baseAsset)` would recurse. The shared helper is [`NavBaseUsdLib`](https://github.com/karpatkey/onchain-accounting/blob/main/src/prices/NavBaseUsdLib.sol).

**The one-hop case is rejected on-chain at registration.** Registering a Custom feed makes the NAV probe the feed's `BASE_ASSET()`; if that base *is* the asset being registered, registration reverts `CustomFeedBaseCircular`. A feed that does not answer `BASE_ASSET()` at all — or answers with something that is not a clean address word — is rejected too (`CustomFeedBaseUnreadable`), so the check fails closed rather than waving through what it cannot read. It runs on the same validation path as the [asset-binding check](#custom-price-feeds), so it covers registration, adding a feed, adding a monitor, and the read-only pre-flight query.

**Multi-hop cycles are not caught on-chain, deliberately.** A feed for A based on B and a feed for B based on A both register cleanly, *in either order*: the check is handed only `(asset, feed)` and never reads the registry. That is not an oversight — the only on-chain alternative is an ordering-dependent registry scan, and such a scan **cannot answer "no"**, because a base with no custom feed today can acquire one tomorrow. A check that cannot fail is worse than no check.

Instead the cycle is made **unconstructible in configuration**, and that is now asserted rather than assumed: of the **41** configured Custom `(asset, feed)` pairs across all seven chains — 31 primaries and 10 monitors — **none** has a base priced by a Custom primary. Every base resolves to a Chainlink-only asset: WETH, USDC, GHO, USDS, USDe, GNO, WXDAI. The pre-deploy config check treats a violation as an **error**, not a warning.
{% endhint %}

{% hint style="danger" %}
**A wrong base is worse than a cyclic one, and nothing on-chain catches it.** A cycle at least announces itself in gas — a poisoned read has been measured at \~153× the cost of a healthy one. A base that is merely the **wrong asset** has no such signature: no gas anomaly, no `stale` flag, no `irregular` flag. It returns a plausible, wrong number at an ordinary read.

The registration guard rejects a base that is the asset *itself*; it cannot tell whether a base is the *right* asset. **35 of the 41 pairs depend on the configured base being correct.** The exception is the six ERC-4626 vault-rate feeds — [sUSDe](/funds/infrastructure/onchain-accounting/price-feeds/susde.md), [sUSDS](/funds/infrastructure/onchain-accounting/price-feeds/susds.md), [syrupUSDC](/funds/infrastructure/onchain-accounting/price-feeds/syrupusdc.md), [RWIV](/funds/infrastructure/onchain-accounting/price-feeds/rwiv.md), [sGHO](/funds/infrastructure/onchain-accounting/price-feeds/sgho.md), [sXDAI](/funds/infrastructure/onchain-accounting/price-feeds/sxdai.md) — whose constructor pins the base to `IERC4626(vault).asset()` and reverts `BaseAssetMismatch` on anything else.
{% endhint %}

The shared helper is [`NavBaseUsdLib.baseUsd(nav, baseAsset)`](https://github.com/karpatkey/onchain-accounting/blob/main/src/prices/NavBaseUsdLib.sol): 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.

{% hint style="warning" %}
**A Custom feed is bound on-chain to the asset it prices.** Registration asks the feed which asset it prices (`underlyingAssetSupported()`) and reverts `CustomFeedAssetMismatch(asset, feed, feedAsset)` if it is not the asset being registered. This covers all three entry points — `registerAsset`, `addPriceFeed` and `addMonitorFeed` — and the read-only `checkPriceOracleSupport`, so tooling can pre-flight the check without a transaction.

Without it, registering feed A for asset B would succeed silently and every B position would be priced at A's price indefinitely: a plausible number, with `stale = false`, `irregular = false`, and no on-chain trace. The exposure is real because feeds are resolved from config by **string key**, across near-identical names — `ezETH` / `ezETHCrossRate`, `osETH` / `osETHCrossRate`, `ETHx` / `ETHxCrossRate`, `rETH` / `rETHCrossRate`, `NXM` / `wNXM`, `weETH_L2` / `weETHCrossRate`.

The check covers the *asset* axis only. A feed's **`baseAsset`** is not bound the same way, so a feed deployed against the wrong base (the osToken feed is built against WETH on mainnet and GNO on Gnosis) would still pass. Verify the base at deploy time.
{% endhint %}

{% hint style="warning" %}
**The rate-feed axis is guarded pre-deploy.** A feed constructed with an external rate oracle — the `Y/X` leg of a [cross-rate](/funds/infrastructure/onchain-accounting/price-feeds/cross-rate.md) feed, for instance — has a third way to be wrong: the right asset, the right base, and the wrong oracle. Confirming the address *is* an aggregator never establishes **which pair** it reports, so each such instance is checked against its declared config before deployment on three axes: **pair identity** from `description()`, a **plausibility band** on the current rate, and **contract-readability**.

The band is not redundant with the pair check. It catches what `description()` cannot — a feed with no description, a renamed one, or one whose description is itself wrong — and rules out cross-class confusion outright, since feeds for different asset classes do not trade in overlapping ranges. The readability check exists because an access-controlled aggregator *reverts* when read, which would otherwise surface as an opaque constructor failure.

**The guard fails closed.** An instance that takes a rate-feed-shaped constructor parameter but declares no guard block **reverts the deploy script** rather than being silently skipped, so the guard cannot quietly stop covering the next feed anyone adds.

Coverage is deliberately **uneven by provider**, and declared rather than assumed. Where a provider's `description()` names the pair, identity is asserted exactly. Where it cannot — RedStone Classic-Push returns the constant string `"Redstone Price Feed"` on *every* feed for *every* pair, so an exact-match assertion is unimplementable for that family rather than merely weak — the config sets `description: "$UNVERIFIABLE"` **with a mandatory written reason**, and the deploy reverts if the reason is missing. The band and readability checks still apply in full; only the pair assertion is dropped, and it is dropped visibly.
{% endhint %}

#### LSTs & LRTs

| Asset                                                                                      | Composition / source                                               | Chains           |
| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | ---------------- |
| [wstETH](/funds/infrastructure/onchain-accounting/price-feeds/wsteth.md)                   | `stEthPerToken()` × ETH/USD (stETH/ETH ≡ 1.0)                      | Ethereum         |
| [stETH](/funds/infrastructure/onchain-accounting/price-feeds/steth.md)                     | `1.0` × ETH/USD (stETH/ETH ≡ 1.0) — market feed demoted to monitor | Ethereum         |
| [wARS](/funds/infrastructure/onchain-accounting/price-feeds/wars.md)                       | **inverted** Chainlink `USD / ARS`                                 | Ethereum         |
| [weETH](/funds/infrastructure/onchain-accounting/price-feeds/weeth.md)                     | `getRate()` × ETH/USD (eETH/ETH ≡ 1.0)                             | Ethereum         |
| [eETH](/funds/infrastructure/onchain-accounting/price-feeds/eeth.md)                       | ETH/USD (eETH/ETH ≡ 1.0)                                           | Ethereum         |
| [rETH](/funds/infrastructure/onchain-accounting/price-feeds/reth.md)                       | `getExchangeRate()` × ETH/USD                                      | Ethereum         |
| [rsETH](/funds/infrastructure/onchain-accounting/price-feeds/rseth.md)                     | rate provider × ETH/USD                                            | Ethereum         |
| [ezETH](/funds/infrastructure/onchain-accounting/price-feeds/ezeth.md)                     | Renzo reporter rate × ETH/USD                                      | Ethereum         |
| [cbETH](/funds/infrastructure/onchain-accounting/price-feeds/cbeth.md)                     | rate provider × ETH/USD                                            | — *not deployed* |
| [ETHx](/funds/infrastructure/onchain-accounting/price-feeds/ethx.md)                       | Stader `getExchangeRate()` × ETH/USD                               | Ethereum         |
| [ankrETH](/funds/infrastructure/onchain-accounting/price-feeds/ankreth.md)                 | ETH/USD ÷ `ratio()`                                                | Ethereum         |
| [osToken (osETH / osGNO)](/funds/infrastructure/onchain-accounting/price-feeds/ostoken.md) | StakeWise `convertToAssets()` × underlying/USD                     | Ethereum, Gnosis |

{% hint style="info" %}
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](#primary-feed-and-divergence-monitors): wstETH/weETH watch market `<asset>/USD` feeds, while ezETH, ETHx, osETH, rETH and stETH are watched by a [cross-rate](/funds/infrastructure/onchain-accounting/price-feeds/cross-rate.md) monitor (`ETH/USD × <asset>/ETH`). Where that `<asset>/ETH` leg is an independent **market** feed, a depeg raises `irregular` instead of moving NAV.

{% hint style="info" %}
**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.
{% endhint %}

**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.
{% endhint %}

#### Yield-bearing stables

| Asset                                                                          | Composition / source           | Chains   |
| ------------------------------------------------------------------------------ | ------------------------------ | -------- |
| [sUSDS](/funds/infrastructure/onchain-accounting/price-feeds/susds.md)         | `convertToAssets()` × USDS/USD | Ethereum |
| [sUSDe](/funds/infrastructure/onchain-accounting/price-feeds/susde.md)         | `convertToAssets()` × USDe/USD | Ethereum |
| [sGHO](/funds/infrastructure/onchain-accounting/price-feeds/sgho.md)           | `convertToAssets()` × GHO/USD  | Ethereum |
| [syrupUSDC](/funds/infrastructure/onchain-accounting/price-feeds/syrupusdc.md) | Maple rate × USDC/USD          | Ethereum |
| [RWIV](/funds/infrastructure/onchain-accounting/price-feeds/rwiv.md)           | `convertToAssets()` × USDC/USD | Ethereum |
| [sXDAI](/funds/infrastructure/onchain-accounting/price-feeds/sxdai.md)         | savings rate × xDAI/USD        | Gnosis   |

{% hint style="info" %}
**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-divergence-and-irregular-signal). 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.
{% endhint %}

{% hint style="warning" %}
**ERC4626 rate legs must be manipulation-resistant.** The `convertToAssets()` feeds (`ERC4626VaultRatePriceFeed`) read the vault's share→asset rate live, so the vault's `totalAssets` accounting must **not** be `balanceOf`-based — a `balanceOf` vault can be inflated by a donation, skewing the rate. The six shipped vaults use manipulation-resistant accounting; any new ERC4626 rate feed must reject `balanceOf`-based vaults before wiring, and its `baseAsset_` must equal the vault's `asset()` (enforced on-chain — `BaseAssetMismatch`).
{% endhint %}

#### Derived & cross-rate

| Asset                                                                                          | Composition / source                       | Chains                                                      |
| ---------------------------------------------------------------------------------------------- | ------------------------------------------ | ----------------------------------------------------------- |
| [lsETH & OETH (CrossRate)](/funds/infrastructure/onchain-accounting/price-feeds/cross-rate.md) | X/USD × Y/X cross-rate of two CL feeds     | Ethereum                                                    |
| [LDO](/funds/infrastructure/onchain-accounting/price-feeds/ldo.md)                             | cross-rate composition                     | Ethereum                                                    |
| [ETH+](/funds/infrastructure/onchain-accounting/price-feeds/eth-plus.md)                       | Reserve RToken basket value                | — *not deployed; ETH+ is priced by a direct Chainlink feed* |
| [NXM / wNXM](/funds/infrastructure/onchain-accounting/price-feeds/nxm.md)                      | Nexus Mutual RAMM internal price × ETH/USD | Ethereum                                                    |

#### L2 variants

| Asset                                                                                  | Composition / source          | Chains                 |
| -------------------------------------------------------------------------------------- | ----------------------------- | ---------------------- |
| [wstETH (L2)](/funds/infrastructure/onchain-accounting/price-feeds/wsteth-l2.md)       | CL wstETH/ETH × ETH/USD       | Arbitrum, Base, Gnosis |
| [weETH (L2)](/funds/infrastructure/onchain-accounting/price-feeds/weeth-l2.md)         | CL weETH/ETH × ETH/USD        | Arbitrum, Base         |
| [rsETH (L2)](/funds/infrastructure/onchain-accounting/price-feeds/rseth-l2.md)         | rate provider × ETH/USD       | Arbitrum               |
| [ezETH (L2)](/funds/infrastructure/onchain-accounting/price-feeds/ezeth-l2.md)         | Renzo reporter rate × ETH/USD | Arbitrum               |
| [cbETH (L2)](/funds/infrastructure/onchain-accounting/price-feeds/cbeth-l2.md)         | CL cbETH/ETH × ETH/USD        | Base                   |
| [syrupUSDC (L2)](/funds/infrastructure/onchain-accounting/price-feeds/syrupusdc-l2.md) | 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+](/funds/infrastructure/onchain-accounting/price-feeds/eth-plus.md) (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.

{% hint style="warning" %}
**The pair is advisory; `stale` is the authoritative verdict.** `(updatedAt, chainlinkHeartbeat)` describe only the governing leg's push freshness. `stale` additionally folds in the rate answer's validity, the L2 sequencer state, and the feed's own configured heartbeat gate — so the pair can read fresh (`age < heartbeat`) while `stale` is `true`. Consumers must gate on `stale` / `sequencerDown` and never recompute staleness from the pair alone.
{% endhint %}

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`](https://github.com/karpatkey/onchain-accounting/blob/main/src/prices/NavBaseUsdLib.sol) and [`ICustomPriceFeed`](https://github.com/karpatkey/onchain-accounting/blob/main/src/prices/ICustomPriceFeed.sol).

***

## 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](/funds/infrastructure/onchain-accounting/concepts/stale-prices-and-sequencer.md) 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:

```solidity
// Pricing (primary) feeds
function getPriceFeeds(address asset) external view returns (PriceFeedConfig[] memory);
function getPriceFeedCount(address asset) external view returns (uint256);
function getPriceFeedAt(address asset, uint256 index) external view returns (PriceFeedConfig memory);

// Monitor (divergence-only) feeds — reuse the same PriceFeedConfig struct
function getMonitorFeeds(address asset) external view returns (PriceFeedConfig[] memory);
function getMonitorFeedCount(address asset) external view returns (uint256);

// Per-asset tolerances
function getAssetPricing(address asset) external view returns (AssetPricing memory);

// Live divergence read
function getPriceDivergence(address underlyingAsset) external view returns (
    int256 primaryPrice, int256 monitorMedian, uint256 divergenceBps,
    bool irregular, uint256 monitorFeedCount, bool primaryStale, bool primaryAboveMonitors
);

struct PriceFeedConfig {
    address   priceFeed;
    uint8     decimals;
    PriceType priceType;
    uint256   chainlinkHeartbeat;  // staleness window in seconds
}

struct AssetPricing {
    uint32 divergenceToleranceBps;  // stored 0 = divergence check off; unset default only — setDivergenceTolerance rejects 0 (InvalidTolerance), takes (0, 10000]
    uint32 pegToleranceBps;         // stored 0 = $1 peg check off (stablecoins only); setPegTolerance accepts 0, takes [0, 10000]
}
```

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

```
getPriceFeedCount(0xC02a…6Cc2)     → 1
getPriceFeeds(0xC02a…6Cc2)
→ [ PriceFeedConfig { 0x5f4e…8419, 8, 0 /* Chainlink */, 3600 } ]

getMonitorFeedCount(0xC02a…6Cc2)   → 1
getMonitorFeeds(0xC02a…6Cc2)
→ [ PriceFeedConfig { 0x67F6…6Dc4, 8, 1 /* Redstone */, 86400 } ]

getAssetPricing(0xC02a…6Cc2)       → AssetPricing { 50, 0 }   // 0.5% divergence, no peg check

getPriceDivergence(0xC02a…6Cc2)
→ (392265000000, 392308000000, 11, false, 1, false, false)   // 11 bps apart, within tolerance
```

***

## Admin operations

Feed configuration is privileged (security council 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)`                                                                            |

```solidity
function registerAsset(address asset, address priceFeed, PriceType priceType, uint256 chainlinkHeartbeat) external;
function addPriceFeed(address asset, address priceFeed, PriceType priceType, uint256 chainlinkHeartbeat) external;
function removePriceFeed(address asset, address priceFeed) external;     // reverts on the last pricing feed
function removePriceFeedAt(address asset, uint256 index) external;

function setDivergenceTolerance(address asset, uint32 divergenceToleranceBps) external;  // (0, 10000]; 0 reverts InvalidTolerance
function setPegTolerance(address asset, uint32 pegToleranceBps) external;                // [0, 10000]; 0 disables the peg guard
function addMonitorFeed(address asset, address priceFeed, PriceType priceType, uint256 chainlinkHeartbeat) external;
function removeMonitorFeed(address asset, address priceFeed) external;
function removeMonitorFeedAt(address asset, uint256 index) external;
```

`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](/funds/infrastructure/onchain-accounting/contracts/nav-calculator.md#admin-manager-api).

{% hint style="warning" %}
An asset must always keep at least one **pricing** feed: `removePriceFeed` reverts with `CannotRemoveLastFeed`. Monitor feeds are optional — the last one **can** be removed. Fully removing pricing for an asset is done via `unregisterAsset`, which should only happen after the asset has been withdrawn from every portfolio.
{% endhint %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.kpk.io/funds/infrastructure/onchain-accounting/price-feeds.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
