Stale prices & sequencer
How the NAV Calculator flags unreliable readings: stale feeds, quote-asset fallback, and the L2 sequencer check.
How the NAV Calculator signals that a reading is unreliable — stale price feeds, an unhealthy quote asset, or a down L2 sequencer (hard signals that must block pricing), plus a soft divergence signal that flags a price the primary and monitor feeds disagree on. A reading carrying any hard signal must not be used for subscription or redemption pricing.
Source: PriceFeedLib.sol
Stale prices and sequencer status
NAV v2 does not collapse staleness into a single flag. Instead:
NAV.stalePriceAssets— assets whose primary feed was stale at read time. An empty array means every priced asset had a fresh primary. A registered asset that has a live position but no usable price feed reaching the NAV summation is treated the same way: it is priced as 0, marked stale, surfaced here, and summed as a zero-value leg — rather than reverting the entire NAV/positions read. (The explicit query paths —getPriceData/calculateValue— still revertPriceFeedNotSetfor a feedless asset, so misconfiguration stays visible to tooling.)NAV.sequencerDown—trueif the L2 sequencer uptime feed is stale or within its grace period (L2 chains only; alwaysfalseon Mainnet).NAV.quoteAssetStale—trueif a non-USD quote asset's own feed was stale;valuethen falls back to USD even thoughquoteAssetis non-zero. Callers must check this flag before treatingvalueas denominated in the requested currency. The same signal is surfaced beyond theNAVstruct: eachPositioncarries aquoteAssetStalebool, andcalculateValuereturns it alongsidevalue— so a stale-quote USD fallback is flagged on those paths too, not only on the NAV reading.
If stalePriceAssets.length > 0, sequencerDown == true, or quoteAssetStale == true, the NAV reading should not be used for subscription or redemption pricing. Use healthCheck() to poll chain health without computing a full NAV.
Price divergence (the irregular signal)
Alongside the hard signals above, the NAV Calculator emits a soft divergence signal per asset. Each asset is priced by a single primary feed; any additional trustworthy source is registered as a monitor feed that is read only to check the primary, never to price NAV.
NAV.irregularPriceAssets— assets whose primary price diverged from the worst-case (furthest) healthy monitor beyond the asset'sdivergenceToleranceBps, or (USD-pegged stablecoins) strayed from$1beyond itspegToleranceBps. EachPositioncarries anirregularbool,getPriceDataexposesirregular/divergenceBps/monitorFeedCount, andgetPriceDivergencereturns the full read (primary, monitor median, worst-case bps, direction).The check is suppressed while the primary is stale — a stale primary is already reported via
stalePriceAssets.
irregular is a quality signal, not a validity flag: value is still populated from the primary feed and the NAV Calculator never reverts on divergence. Unlike the hard signals, a non-empty irregularPriceAssets does not by itself invalidate the reading — it tells the offchain subscription/redemption processor to investigate, widen its band, or alert before the divergence grows into a stale/blocking condition. See Price feeds for the tolerances in use.
When no monitor answered (monitorsUnhealthyPriceAssets)
A clean irregular flag can mean two very different things: the primary was checked and agreed with its monitors, or it was checked by nothing that answered. The divergence computation is guarded on there being at least one healthy monitor, so when every monitor for an asset is stale, reverting, or reporting a non-positive price, the arithmetic never runs — divergenceBps is 0 and irregular is false for want of data, not for want of divergence. From outside, the two cases were the same bytes.
NAV.monitorsUnhealthyPriceAssets— assets that have monitor feeds configured but where none was readable at query time. For these, absence fromirregularPriceAssetscarries no information.
Three boundaries matter, because each one is a plausible misreading:
It is not the "no monitors configured" case. That is static configuration, not a runtime failure, and it is deliberately excluded — an asset with nothing configured never appears here however long it goes unchecked. Derive that case off-chain from
getMonitorFeedCount(asset) == 0alone. Do not additionally requiredivergenceToleranceBps == 0: that conjunction is unsatisfiable for any asset that ever had a monitor, because setting a tolerance to0is rejected while removing a monitor leaves the tolerance untouched. An AND-recipe would silently never flag an asset whose monitors were all removed.Assets whose own primary is unusable are excluded — stale feed, sequencer down, or a primary whose 8-decimal normalisation degenerates to zero. In each of those the check was suppressed by the primary,
stalePriceAssetsalready reports the staleness cases, and naming the monitors would point an operator at the wrong contract.It flags the loss of the divergence check, not of all checks. The
$1peg guard is monitor-independent, so a stablecoin with a non-zeropegToleranceBpscan appear here and still be peg-checked.
Scope: assets the account holds positions in, exactly like stalePriceAssets and irregularPriceAssets. The array is built from the assets the account's positions touched, not from the whole registry, so an empty array means "monitors healthy or no position held". Reconciling it against the registry-wide per-asset view (getPriceDivergence(asset).monitorFeedCount) will show differences that are this scoping rather than a defect.
Two related surfaces deliberately do not carry the signal, because NAVCalculator is close enough to the EIP-170 contract-size limit that widening them was not affordable: healthCheck() — which does iterate the whole registry — and Position.irregular on getAccountPositions, which retains the original ambiguity with no NAV struct alongside to join against. Use getAccountNavVerbose, whose VerboseNAV embeds the NAV, when you need positions and the third array together.
This is not a rare path. Most monitored assets carry exactly one monitor, and many of those have no peg backstop either — so for them "every monitor dead" means one feed drifting past its heartbeat, an ordinary event rather than a correlated outage. It is also the shape a provider sunsetting a feed produces: no primary on any chain uses a monitor-only provider, so a sunset degrades the cross-check to "checked by nothing that answered" — visible here — rather than moving the NAV.
L2 sequencer uptime check
On Layer 2 chains (Arbitrum, Base, Optimism, Gnosis), the NAV Calculator additionally checks the Chainlink L2 Sequencer Uptime Feed. If the sequencer is down or within its grace period after recovery, NAV.sequencerDown is true and the entire chain's NAV reading should not be used for pricing.
Example (illustrative):
NAV.sequencerDown is always false on Ethereum Mainnet — there is no sequencer feed to check. The uptime feed and grace period are configured by the admin (setChainlinkL2SequencerUptimeFeed, setChainlinkL2SequencerGracePeriod).
Related
NAV Calculator — the
NAVstruct fields andhealthCheck().Price feeds — primary feed, divergence monitors, and per-feed staleness rules.
Last updated