> 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/architecture/onchain-accounting/price-feeds.md).

# Price feeds

How assets are priced: one primary feed per asset, divergence monitors, feed types, the rules every feed follows, and the full list of custom 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 token balance into a value in the quote currency (USD by default, 8 decimals) and is the only feed that sets the price. Monitor feeds are read on every call to check the primary, never to price the asset.

**Source:** `PriceFeedLib.sol`

## Reading a price

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

| Field              | Description                                                                                 |
| ------------------ | ------------------------------------------------------------------------------------------- |
| `price`            | Price from the primary feed (`0` if the primary is stale)                                   |
| `decimals`         | Decimals in `price`, pinned per asset                                                       |
| `stale`            | `true` if the primary is stale, or the L2 sequencer is down                                 |
| `irregular`        | `true` if the primary diverged from the monitors beyond tolerance, or strayed from a $1 peg |
| `divergenceBps`    | Worst-case deviation between the primary and any healthy monitor, in basis points           |
| `monitorFeedCount` | How many monitor feeds passed their staleness checks on this read                           |

A position is valued as `amount × price / 10^assetDecimals`, normalised to the quote asset's decimals. `irregular` is a quality signal, not a validity flag: `price` is still populated and the NAV Calculator never reverts on divergence. It tells the offchain subscription and redemption processor to withhold or widen pricing and alert operators. See [Stale prices & sequencer](/funds/infrastructure/architecture/onchain-accounting/concepts/stale-prices-and-sequencer.md).

## Primary feed and divergence monitors

Each asset has one deterministic primary. Pricing from whichever of several redundant feeds is freshest would track market noise instead of redeemable value, so extra sources are registered as monitors: a market wobble raises an alert instead of moving the share price.

On each read the NAV Calculator computes `divergenceBps`, the deviation of the primary from the **furthest** healthy monitor (not an average, so two monitors on either side of the primary cannot mask a gap). `irregular` is set when `divergenceBps` exceeds the asset's `divergenceToleranceBps`, or, for USD stablecoins, when the primary strays from $1 by more than `pegToleranceBps`. Both checks are suppressed while the primary is stale. A monitor past its own heartbeat drops out of the comparison, and `monitorFeedCount` shows that a cross-check was lost.

| Kind                                                                                       | `divergenceToleranceBps` | `pegToleranceBps` |
| ------------------------------------------------------------------------------------------ | ------------------------ | ----------------- |
| Fundamental primary with a market monitor (wstETH, weETH, ezETH, rETH, ETHx, osETH, stETH) | 250 (2.5%)               | none              |
| Market primary with a second market feed (ETH/WETH, USDe)                                  | 50 (0.5%)                | none              |
| USD stablecoin with a market monitor (mainnet USDC and USDT)                               | 50                       | 100 (1%)          |
| USD stablecoin, peg guard only (Base USDT, Gnosis stablecoins)                             | 0                        | 100 (1%)          |

Tolerances are per-asset configuration and default to `0`, which disables the check. An asset counts as pegged exactly when it has a non-zero `pegToleranceBps`, so consumers should read the tolerance onchain instead of keeping their own list. Yield-bearing wrappers are not pegged: their redemption rate carries the price off $1.

## Feed types

| Value | Type        | Notes                                                            |
| ----- | ----------- | ---------------------------------------------------------------- |
| 0     | `Chainlink` | Chainlink `AggregatorV3Interface` push feed                      |
| 1     | `Redstone`  | Redstone Classic Push, read like Chainlink                       |
| 2     | `API3`      | API3 dAPI via `Api3ReaderProxyV1` (18-decimal proxy scaled to 8) |
| 3     | `Custom`    | A custom `ICustomPriceFeed` adapter with its own staleness logic |

Chainlink, Redstone and API3 feeds are gated on a configurable heartbeat: a feed is stale once `block.timestamp - updatedAt` reaches it.

A **Custom** feed prices an asset that has no direct USD feed as `rate × base/USD`:

* The **rate leg** is a live onchain exchange rate (`getRate()`, `convertToAssets()`, `stEthPerToken()`), with no market oracle in the pricing path.
* The **base/USD leg** is read from the NAV's own primary feed for the base asset, through `getPriceDataNoDivergence(baseAsset)`. The derived asset and its base then always use the same base/USD value in the same block, so the two terms cancel in an ETH-quoted NAV.
* `getLatestPrice()` returns `(price, stale)`. It is stale if the rate leg is unreadable or the NAV reports the base asset stale.

## Rules every feed follows

**Heartbeat equals the provider's declared cadence.** A registered heartbeat must match the provider's declared heartbeat, not exceed it and not undercut it. Registering above it lets a dead feed read as fresh, which puts a wrong number into NAV silently. Registering below it prices a healthy asset at `$0` and lists it in `stalePriceAssets`, which is loud. The first error is silent and affects correctness, the second is visible and affects liveness, so the policy refuses to trade the first for the second. Chainlink's heartbeat is a round-initiation commitment, and measured across 43 feeds the answer lands 2 to 96 seconds after it, independent of cadence. Any margin would be a bet that the ambiguity resolves benignly. A pre-deploy check flags registrations in both directions.

**A wrong heartbeat is corrected in place.** Calling `addOrModifyPriceFeed` (or `addMonitorFeed`) again with a feed the asset already carries rewrites only that feed's heartbeat and emits `PriceFeedHeartbeatUpdated` (or `MonitorFeedHeartbeatUpdated`). The feed address, type, decimals and position stay as they are, so the correction cannot change which oracle prices the asset. It skips the admission and live-answer checks that adding a feed runs. The contract only checks that the new heartbeat is non-zero, so run the cadence preflight before each update, read the value back afterwards, and subscribe alerting to those events.

**A derived feed is never tighter than its base.** It inherits the base's staleness verdict, so a tighter heartbeat cannot prevent a stale price and can only produce a false stale verdict. A looser one is safe.

**Base assets use direct feeds.** WETH, GNO and the stablecoins are priced by Chainlink, API3 or Redstone feeds, never by a NAV-sourced Custom feed, otherwise `getPriceData(baseAsset)` would recurse. Registration rejects the one-hop case (`CustomFeedBaseCircular`, `CustomFeedBaseUnreadable`). Longer cycles are not caught onchain, so the pre-deploy configuration check fails if any base is priced by a Custom primary.

**A Custom feed is bound to its asset.** Registration asks the feed which asset it prices (`underlyingAssetSupported()`) and reverts `CustomFeedAssetMismatch` on a mismatch. This covers `registerAsset`, `addOrModifyPriceFeed`, `addMonitorFeed` and the read-only `checkPriceOracleSupport`. Without it, a feed for one asset registered against another would price every position at the wrong price with no flag.

**The base is not verified onchain.** The binding covers the asset, not its base. Registration rejects a base equal to the asset itself but cannot tell whether a base is the right one, so a wrong base returns a plausible, wrong number. The exception is the ten ERC-4626 vault-rate feeds, whose constructor pins the base to `IERC4626(vault).asset()` and reverts `BaseAssetMismatch`. Check the base at deploy time. ERC-4626 rate feeds also need vaults whose `totalAssets` is not `balanceOf`-based, since a donation could inflate that rate.

**External rate oracles are checked before deploy.** A feed built on an external rate oracle (for example the `Y/X` leg of a cross-rate feed) is checked against its declared configuration on three axes: the pair named by `description()`, a plausibility band on the current rate, and readability. The check fails closed: an instance without a declared guard stops the deploy script. Redstone Classic Push returns the same `description()` for every pair, so for those feeds the pair assertion is replaced by an explicit `$UNVERIFIABLE` entry with a written reason, and the band and readability checks still apply.

## Governing-leg freshness

A composite feed depends on two Chainlink legs with different heartbeats. `getPriceData` reports the **governing leg**: the one with the highest `age / heartbeat` ratio, together with that leg's own `(updatedAt, heartbeat)`. Each Custom feed exposes it as `governingFeedFreshness()`. A single-leg feed reports its base leg, a two-leg feed whichever of the two is closest to its deadline, and a feed with no trackable Chainlink leg reports `(0, 0)`. The reported heartbeat overrides the registered one, so it is usually tighter. The pair is advisory: `stale` is the authoritative verdict, and consumers must gate on `stale` and `sequencerDown` instead of recomputing staleness from the pair.

## L2 sequencer uptime

On Layer 2 chains the NAV Calculator also checks the Chainlink sequencer uptime feed. If the sequencer is down or within its grace period, the chain's reading is flagged through `NAV.sequencerDown`. A sequencer outage does not zero the base leg of Custom feeds; it is surfaced once, at the top level. See [Stale prices & sequencer](/funds/infrastructure/architecture/onchain-accounting/concepts/stale-prices-and-sequencer.md).

## Custom price feeds

Every Custom feed is listed below with the contract it is deployed from.

### LSTs and LRTs

Fundamental primaries: they read only the protocol's exchange rate. The market leg sits in a monitor, so a depeg raises `irregular` instead of moving NAV.

| Asset                   | Price                                                             | Chains           | Contract                               | Notes                                                                                                      |
| ----------------------- | ----------------------------------------------------------------- | ---------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| wstETH                  | `stEthPerToken()` × ETH/USD (stETH/ETH ≡ 1.0)                     | Ethereum         | `wstETH_CustomPriceFeed.sol`           | Two market monitors (Redstone, Chainlink wstETH/USD), 250 bps tolerance.                                   |
| stETH                   | `1.0` × ETH/USD (stETH/ETH ≡ 1.0); market feed demoted to monitor | Ethereum         | `eETH_CustomPriceFeed.sol`             | Primary reuses the eETH unit-rate contract. Monitors: market stETH/ETH cross-rate and Chainlink stETH/USD. |
| wARS                    | **inverted** Chainlink `USD / ARS`                                | Ethereum, Arc    | `InvertedChainlinkCustomPriceFeed.sol` | Inverts a Chainlink USD/ARS feed. Registered directly, it would over-report by about 2.5 million times.    |
| weETH                   | `getRate()` × ETH/USD (eETH/ETH ≡ 1.0)                            | Ethereum         | `weETH_CustomPriceFeed.sol`            | Redstone weETH/USD monitor, 250 bps tolerance.                                                             |
| eETH                    | ETH/USD (eETH/ETH ≡ 1.0)                                          | Ethereum         | `eETH_CustomPriceFeed.sol`             | No monitor on mainnet.                                                                                     |
| rETH                    | `getExchangeRate()` × ETH/USD                                     | Ethereum         | `rETH_CustomPriceFeed.sol`             | Cross-rate monitor.                                                                                        |
| rsETH                   | rate provider × ETH/USD                                           | Ethereum         | `rsETH_CustomPriceFeed.sol`            |                                                                                                            |
| ezETH                   | Renzo reporter rate × ETH/USD                                     | Ethereum         | `ezETH_CustomPriceFeed.sol`            | Two cross-rate monitors, one with a market leg.                                                            |
| cbETH                   | rate provider × ETH/USD                                           | None             | `cbETH_CustomPriceFeed.sol`            | Not deployed.                                                                                              |
| ETHx                    | Stader `getExchangeRate()` × ETH/USD                              | Ethereum         | `ETHx_CustomPriceFeed.sol`             | Cross-rate monitor.                                                                                        |
| ankrETH                 | ETH/USD ÷ `ratio()`                                               | Ethereum         | `ankrETH_CustomPriceFeed.sol`          |                                                                                                            |
| osToken (osETH / osGNO) | StakeWise `convertToAssets()` × underlying/USD                    | Ethereum, Gnosis | `osToken_CustomPriceFeed.sol`          | One instance per token: osETH (ETH/USD) and osGNO (GNO/USD). Cross-rate monitor on osETH.                  |

### Yield-bearing stables

These float and are not $1 assets. Each prices as a growing redemption rate times its base stablecoin, so none carries a peg tolerance. The $1 guard belongs to the base stablecoin.

| Asset     | Price                          | Chains   | Contract                        | Notes                                        |
| --------- | ------------------------------ | -------- | ------------------------------- | -------------------------------------------- |
| sUSDS     | `convertToAssets()` × USDS/USD | Ethereum | `sUSDS_CustomPriceFeed.sol`     |                                              |
| sUSDe     | `convertToAssets()` × USDe/USD | Ethereum | `sUSDe_CustomPriceFeed.sol`     |                                              |
| sGHO      | `convertToAssets()` × GHO/USD  | Ethereum | `sGHO_CustomPriceFeed.sol`      |                                              |
| syrupUSDC | Maple rate × USDC/USD          | Ethereum | `syrupUSDC_CustomPriceFeed.sol` | Rate from the ERC-4626 vault.                |
| RWIV      | `convertToAssets()` × USDC/USD | Ethereum | `RWIV_CustomPriceFeed.sol`      |                                              |
| sXDAI     | savings rate × xDAI/USD        | Gnosis   | `sXDAI_CustomPriceFeed.sol`     |                                              |
| spUSDC    | `convertToAssets()` × USDC/USD | Ethereum | `spUSDC_CustomPriceFeed.sol`    |                                              |
| spUSDT    | `convertToAssets()` × USDT/USD | Ethereum | `spUSDT_CustomPriceFeed.sol`    |                                              |
| spETH     | `convertToAssets()` × WETH/USD | Ethereum | `spETH_CustomPriceFeed.sol`     | Base is WETH, so the price tracks ETH.       |
| sDAI      | savings rate × DAI/USD         | Ethereum | `sDAI_CustomPriceFeed.sol`      | Gnosis uses [sXDAI](#yield-bearing-stables). |

### Derived and cross-rate

| Asset             | Price                                      | Chains   | Contract                       | Notes                                                                                                                                                                               |
| ----------------- | ------------------------------------------ | -------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| lsETH (CrossRate) | X/USD × Y/X cross-rate of two CL feeds     | Ethereum | `CrossRateCustomPriceFeed.sol` | lsETH uses a Chainlink LsETH/ETH leg. The OETH primary was retired on 12 August 2026. The same contract backs the cross-rate monitors for stETH, rETH, ezETH, ETHx, osETH and LBTC. |
| LDO               | cross-rate composition                     | Ethereum | `LDO_CustomPriceFeed.sol`      |                                                                                                                                                                                     |
| ETH+              | Reserve RToken basket value                | None     | `ETHPlus_CustomPriceFeed.sol`  | Built but not deployed. ETH+ is priced by a direct Chainlink feed.                                                                                                                  |
| NXM / wNXM        | Nexus Mutual RAMM internal price × ETH/USD | Ethereum | `NXM_CustomPriceFeed.sol`      | One instance per token (NXM and wNXM).                                                                                                                                              |

### L2 variants

| Asset          | Price                         | Chains                 | Contract                           | Notes                          |
| -------------- | ----------------------------- | ---------------------- | ---------------------------------- | ------------------------------ |
| wstETH (L2)    | CL wstETH/ETH × ETH/USD       | Arbitrum, Base, Gnosis | `wstETH_L2_CustomPriceFeed.sol`    |                                |
| weETH (L2)     | CL weETH/ETH × ETH/USD        | Arbitrum, Base         | `weETH_L2_CustomPriceFeed.sol`     |                                |
| rsETH (L2)     | rate provider × ETH/USD       | Arbitrum               | `rsETH_L2_CustomPriceFeed.sol`     |                                |
| ezETH (L2)     | Renzo reporter rate × ETH/USD | Arbitrum               | `ezETH_L2_CustomPriceFeed.sol`     |                                |
| cbETH (L2)     | CL cbETH/ETH × ETH/USD        | Base                   | `cbETH_L2_CustomPriceFeed.sol`     |                                |
| syrupUSDC (L2) | Maple rate × USDC/USD         | Arbitrum               | `syrupUSDC_L2_CustomPriceFeed.sol` | Chainlink syrupUSDC/USDC rate. |

## Inspecting an asset's feeds

```solidity
getPriceFeeds(asset)        // primary feed config
getMonitorFeeds(asset)      // monitor feed configs
getAssetPricing(asset)      // divergenceToleranceBps, pegToleranceBps
getPriceDivergence(asset)   // primaryPrice, monitorMedian, divergenceBps, irregular,
                            // monitorFeedCount, primaryStale, primaryAboveMonitors
```

`PriceFeedConfig` holds `priceFeed`, `decimals`, `priceType` and `chainlinkHeartbeat`. The monitor median and `primaryAboveMonitors` give direction only and do not feed `irregular`.

## Admin operations

Feed configuration is privileged. It requires the `MANAGER` role, held by the NAV timelock and the Security Council Safe (the Governance Safe on Robinhood Chain), and by `AssetKindRegistry`, which uses it to register, classify and label an asset in one transaction.

| Action                                                | Call                                                                                                |
| ----------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| Register an asset with its primary feed               | `registerAsset(asset, priceFeed, priceType, heartbeat)`                                             |
| Add or remove a pricing feed, or change its heartbeat | `addOrModifyPriceFeed(...)`, `removePriceFeed(asset, priceFeed)`, `removePriceFeedAt(asset, index)` |
| Set the divergence or peg tolerance                   | `setDivergenceTolerance(asset, bps)`, `setPegTolerance(asset, bps)`                                 |
| Add or remove a monitor feed, or change its heartbeat | `addMonitorFeed(...)`, `removeMonitorFeed(asset, priceFeed)`, `removeMonitorFeedAt(asset, index)`   |
| Remove the asset and all its feeds                    | `unregisterAsset(asset)`                                                                            |

An asset must keep at least one pricing feed (`removePriceFeed` reverts with `CannotRemoveLastFeed`), and `unregisterAsset` should only run once the asset has left every portfolio. `addOrModifyPriceFeed` requires the feed's live `decimals` to match the asset's existing pricing feed (`PriceFeedDecimalsMismatch`), also on a heartbeat update. For a feed already registered it updates the heartbeat, and reverts `PriceFeedAlreadyRegistered` only if the heartbeat is unchanged and `PriceFeedTypeImmutable` if `priceType` differs. `addMonitorFeed` needs a divergence tolerance to be set first, requires 8 decimals, and upserts the same way. Per-function detail is on the [NAV Calculator](/funds/infrastructure/architecture/onchain-accounting/contracts/nav-calculator.md) page.


---

# 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 by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

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

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

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.
