> 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/contracts/nav-calculator.md).

# NAV Calculator

The **NAV Calculator** (`NAVCalculator`) is the core accounting contract. It registers the assets a fund can hold, the price feeds that value them, and the balance adapters that locate them, then returns the fund's NAV and full position breakdown for any account on that chain.

**Source:** [`NAVCalculator.sol`](https://github.com/karpatkey/onchain-accounting/blob/main/src/nav/NAVCalculator.sol)

| At a glance   |                                                                                          |
| ------------- | ---------------------------------------------------------------------------------------- |
| **Contract**  | `NAVCalculator`                                                                          |
| **Type**      | UUPS upgradeable proxy — one instance per chain                                          |
| **Roles**     | `DEFAULT_ADMIN_ROLE` · `MANAGER` (security council Safe)                                 |
| **Chains**    | Ethereum · Optimism · Arbitrum · Base · Gnosis · BSC · Polygon (+ 17 canonical-only)     |
| **Addresses** | [Deployment addresses](/funds/infrastructure/onchain-accounting/deployment-addresses.md) |

***

It is **fund-agnostic** — a single instance per chain prices any account. It is deployed as a UUPS proxy at a canonical CREATE2 address, identical across all chains, and is upgradeable by the security council Safe.

{% hint style="info" %}
**Roles are held by the karpatkey security council Safe** `0x8b884f80B3B839F52b6cE168f133e7a5D1f0A537` — both `DEFAULT_ADMIN_ROLE` (proxy upgrades, sequencer-feed setters) and `MANAGER` (asset, feed and adapter configuration), on every chain.
{% endhint %}

{% hint style="info" %}
Deployed addresses are listed on the [Deployment addresses](/funds/infrastructure/onchain-accounting/deployment-addresses.md) page. Always call the **proxy**; the implementation may change on upgrade.
{% endhint %}

### How it works

```
getAccountNav(account, quoteAsset)
  ├─ for each registered balance adapter:
  │    └─ adapter.getAdapterPositions(account, …) → PositionBalance[]
  ├─ for each position:
  │    ├─ (price, decimals, stale) = primary feed for balanceAsset
  │    ├─ irregular = primary vs monitor feeds beyond tolerance (signal only)
  │    └─ value = ±(amount × price) / 10^(assetDecimals)          // − when isDebt
  └─ return NAV {
         value,               // total in quote asset units (USD = 8 decimals)
         quoteAsset,
         timestamp,
         stalePriceAssets,    // assets whose primary feed was stale
         irregularPriceAssets,// assets whose primary diverged from monitors / peg
         sequencerDown,       // L2 sequencer status
         quoteAssetStale,     // quote-asset feed stale → value falls back to USD
         monitorsUnhealthyPriceAssets  // monitors configured, none readable → no divergence check ran
     }
```

### Solidity interface

#### Structs

The interface defines the position model, the NAV snapshot, and the price-feed snapshot. Expand each group for the full definitions.

<details>

<summary><strong>Position model — <code>Asset</code>, <code>PositionBalance</code>, <code>Position</code>, <code>VerbosePosition</code>, <code>VerboseNAV</code></strong></summary>

```solidity
struct Asset {
    address asset;
    string  symbol;
    uint8   decimals;
}

/// Balance-only data returned by adapters (no pricing).
struct PositionBalance {
    Asset        balanceAsset;       // token the amount is denominated in
    address      balanceAdapter;     // adapter that reported it (provenance, not identity)
    uint256      amount;             // magnitude (always positive); sign implied by isDebt
    bool         isDebt;             // true = subtract from NAV
    bytes32      protocolBrand;      // keccak256 of the version-stripped brand slug (e.g. "aave"); grouping only
    bytes32      protocolId;         // keccak256 of the ecosystem slug (e.g. "aave-v3"); roll-up only
    bytes32      protocolSubId;      // keccak256 of the product slug (e.g. "aave-v3-lending"); IN the identity key, selects the positionId schema
    bytes        positionId;         // ABI-encoded static identifier; decode schema set by protocolSubId
    PositionKind positionKind;       // Wallet / Supplied / Borrowed / Collateral / Staking / Rewards / Fees
    bool         isLocked;           // true = not withdrawable now (pending/cooling); mutable per-leg attribute, NOT in the identity key
    bytes32      positionInstanceId; // ephemeral per-item handle (NFT id, withdrawal-request id, exit ticket); bytes32(0) if none; NOT in the identity key
}

/// Position enriched with pricing.
struct Position {
    Asset        balanceAsset;
    address      balanceAdapter;
    uint256      amount;
    bool         isDebt;
    Asset        quoteAsset;
    int256       value;              // value in quote asset (negative for debt)
    int256       price;
    uint8        priceDecimals;
    bool         stale;
    bool         quoteAssetStale;     // true if the quote-asset feed was stale; value then falls back to USD (8 dp)
    bytes32      protocolBrand;
    bytes32      protocolId;
    bytes32      protocolSubId;
    bytes        positionId;
    PositionKind positionKind;
    bool         isLocked;            // mutable per-leg attribute, NOT in the identity key
    bytes32      positionInstanceId;  // ephemeral per-item handle; NOT in the identity key
    bool         irregular;           // primary price diverged from monitors / peg (signal only)
    bool         quoteAssetIrregular; // quote-asset primary diverged from its monitors (false for USD quote)
}

/// Position plus its protocol name and labels (returned by the verbose read methods).
struct VerbosePosition {
    Position position;
    string   protocolName;         // e.g. "Aave V3", "Morpho" — from the adapter's IPositionDescribable.protocolName()
    string[] labels;               // display breadcrumb below the protocol name — from the adapter's positionLabels(positionId); full breadcrumb = [protocolName, ...labels]
}

/// NAV snapshot plus its verbose positions.
struct VerboseNAV {
    NAV               nav;
    VerbosePosition[] positions;
}
```

</details>

<details>

<summary><strong>NAV snapshot — <code>NAV</code></strong></summary>

```solidity
struct NAV {
    int256  value;                 // total in quote asset units (can be negative if net debt)
    Asset   quoteAsset;            // quote currency; address(0) = USD
    uint64  timestamp;             // block timestamp of the reading
    Asset[] stalePriceAssets;      // assets whose primary feed was stale
    bool    sequencerDown;         // true if the L2 sequencer is down / in grace period (L2 only)
    bool    quoteAssetStale;       // true if the quote-asset feed was stale; value then falls back to USD
    Asset[] irregularPriceAssets;  // assets whose primary diverged from monitors / peg (signal only)
    bool    quoteAssetIrregular;   // quote-asset primary diverged from its monitors (false for USD quote)
    Asset[] monitorsUnhealthyPriceAssets;  // monitors configured but none readable → divergence check did not run
}
```

New fields are **appended**, never inserted mid-struct, so an in-place UUPS upgrade cannot shift an existing field's ABI offset. `monitorsUnhealthyPriceAssets` is the most recent append.

</details>

<details>

<summary><strong>Partial NAV snapshot — <code>PartialNAV</code></strong></summary>

Returned by `getAccountNavForAdapters`, for paging a NAV read that no longer fits in a single `eth_call`. It mirrors `NAV`'s first eight fields **in order**, then appends the adapters the slice actually covered:

```solidity
struct PartialNAV {
    int256  value;                 // this slice's contribution only
    Asset   quoteAsset;
    uint64  timestamp;
    Asset[] stalePriceAssets;      // only assets THIS slice touched
    bool    sequencerDown;         // per slice
    bool    quoteAssetStale;       // per slice
    Asset[] irregularPriceAssets;  // only assets THIS slice touched
    bool    quoteAssetIrregular;   // per slice
    address[] adaptersCovered;
    Asset[] monitorsUnhealthyPriceAssets;  // only assets THIS slice touched
}
```

**The positional mirror stops at those eight fields.** `monitorsUnhealthyPriceAssets` sits *after* `adaptersCovered` here, while in `NAV` it is the last field — so the two structs no longer line up past the prefix. That is deliberate: keeping `adaptersCovered` at the offset a live off-chain reassembler already decodes matters more than the documentation convenience of a positional mirror. Decode each struct against its own ABI; never assume the mirror extends to new fields.

It is a **distinct type** from `NAV`, deliberately: a partial result must not be assignable or decodable where a complete NAV is expected. Both reads run the same underlying `computeNav`, with the validated slice in place of the full adapter set, so a complete read and the sum of its slices agree because they are literally the same code path.

{% hint style="warning" %}
**Only `value` composes by addition.** The health signals do **not**. `stalePriceAssets`, `irregularPriceAssets` and `monitorsUnhealthyPriceAssets` cover only the assets that slice touched, and the three booleans are reported per slice. A caller that reassembles the total but checks the flags on one slice — the last one, or whichever it kept — silently drops every staleness, depeg and sequencer-down signal the other slices carried, and ends up with a NAV that looks healthy. Union the asset arrays and OR the booleans across **every** slice.

**Pass a caller-supplied adapter set, never an index range.** The adapter list is maintained with swap-and-pop, so removing an adapter moves the last one down into the freed slot. A caller paging `[0,10)` then `[10,20)` across a governance transaction therefore **silently skips** the moved adapter — both pages look well-formed and the total is short by its positions. Because elements only ever move down, the hazard is a missed adapter, never a double-counted one.

A slice is rejected outright rather than returning a quietly wrong `value`: `EmptyAdapterSlice()`, `AdapterNotRegistered(address)` for anything absent from `getAllAdapters()`, and `DuplicateAdapterInSlice(address)` for a repeat.
{% endhint %}

**A meta-adapter is the atomic unit.** Naming one in a slice includes all of its governed instances; there is no paging within a single adapter, since the instance set is itself swap-and-pop mutable and an instance range would carry the identical hazard. If one meta-adapter alone exceeds the budget, pagination cannot help.

</details>

<details>

<summary><strong>Price-feed snapshot — <code>PriceFeedData</code></strong></summary>

```solidity
struct PriceFeedData {
    address priceFeed;             // the primary (pricing) feed
    IPrices.PriceType priceType;   // Chainlink / Redstone / API3 / Custom
    int256  price;                 // 0 when stale
    uint8   decimals;
    uint256 chainlinkHeartbeat;    // Custom feeds: the governing leg's heartbeat, not the registered value
    uint256 updatedAt;             // Custom feeds: the governing Chainlink leg's timestamp
    bool    stale;                 // true if the primary feed is stale or sequencer down
    bool    sequencerDown;
    uint256 healthyFeedCount;      // pricing feeds that passed all staleness gates this read
    bool    irregular;             // primary diverged from monitors / peg beyond tolerance (signal only)
    uint256 divergenceBps;         // worst-case primary-vs-monitor deviation, in bps (0 if no healthy monitor)
    uint256 monitorFeedCount;      // monitor feeds that passed all staleness gates this read
}
```

For a **Custom** feed — a composite of a rate and a base/USD leg — `(updatedAt, chainlinkHeartbeat)` report the **governing leg**: the underlying Chainlink leg with the greatest `age / heartbeat` ratio, i.e. the one closest to its own staleness deadline. The pair is therefore a real, matched pair from one actual oracle rather than a mix of two, and it reduces to the single leg's values for a single-leg feed. Which leg governs can change between reads as the ratios cycle, so a two-leg feed's reported heartbeat may alternate between its legs' values. See [Price feeds](/funds/infrastructure/onchain-accounting/price-feeds.md#governing-leg-freshness).

{% hint style="warning" %}
**`(updatedAt, chainlinkHeartbeat)` are advisory; `stale` is the authoritative verdict.** The pair describes only the governing oracle 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`. Always gate on `stale` / `sequencerDown`; never recompute staleness from the pair alone.
{% endhint %}

</details>

{% hint style="warning" %}
The stable reconciliation key is `keccak256(abi.encode(chainId, protocolSubId, positionId, balanceAsset.asset, positionKind))` (see [Position identity](/funds/infrastructure/onchain-accounting/concepts/position-identity.md)). It keys on **`protocolSubId`** — the product-level slug — not `protocolId`; `protocolBrand` and `protocolId` are display/grouping layers only. `isLocked` marks a leg that is not withdrawable right now (the pending/cooling portion of a withdrawal queue or cooldown) but is **excluded** from the key — it is a mutable per-leg attribute (a leg flips `true→false` as it finalizes), so a withdrawal queue's claimable and locked legs share one key and **sum**. `positionInstanceId` (the ephemeral per-item handle — an NFT id, a withdrawal-request id, an exit ticket) is likewise **excluded**, so the legs of one anchor sum and identity survives item churn. Labels are **not** on the position — they are computed lazily by the **verbose read methods** (`getAccountPositionsVerbose` / `getAccountNavVerbose`), which call each adapter's [`IPositionDescribable`](/funds/infrastructure/onchain-accounting/concepts/position-identity.md#human-readable-labels) (`protocolName()` + `positionLabels(positionId)`) and render the full breadcrumb `[protocolName, ...labels]`. These live on the NAV Calculator itself (there is no separate lens contract).
{% endhint %}

#### Read functions

| Function                                                      | Purpose                                                                                                                                          |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `getAccountNav`                                               | Total chain-local NAV for an account in a quote currency (`address(0)` = USD)                                                                    |
| `getAccountNavForAdapters`                                    | NAV over a **caller-supplied subset** of adapters, for paging a read too large for one call (returns `PartialNAV`)                               |
| `getAccountPositions`                                         | One priced `Position` per balance entry reported for an account                                                                                  |
| `getAccountPositionsForAsset`                                 | Priced positions for a single asset across all adapters that report it                                                                           |
| `getAccountNavVerbose`                                        | `getAccountNav` plus per-position `protocolName` + `labels[]` (returns `VerboseNAV`)                                                             |
| `getAccountPositionsVerbose`                                  | `getAccountPositions` enriched with `protocolName` + `labels[]` (returns `VerbosePosition[]`)                                                    |
| `getAccountPositionsForAssetVerbose`                          | Single-asset verbose positions (returns `VerbosePosition[]`)                                                                                     |
| `healthCheck`                                                 | Sequencer status + currently-stale **and** currently-irregular assets, without a full NAV                                                        |
| `getPriceData`                                                | Detailed data for an asset's primary feed + divergence signal (`price`, `stale`, `irregular` …)                                                  |
| `getPriceDataNoDivergence`                                    | Same as `getPriceData` but skips the monitor read (used on the base/USD hot path)                                                                |
| `getPriceDivergence`                                          | Live primary-vs-monitor divergence read (median, worst-case bps, `irregular`, direction)                                                         |
| `hasPositions`                                                | Whether an account has any non-zero position                                                                                                     |
| `getAssetsWithPositions`                                      | Assets for which an account holds a non-zero position                                                                                            |
| `calculateValue`                                              | Value of a set of (asset, amount) pairs in a quote currency                                                                                      |
| `getAssetInfo`                                                | One asset's [classification](/funds/infrastructure/onchain-accounting/concepts/asset-classification.md) **and** display labels, in a single call |
| `getRegisteredAsset`                                          | Resolves **one** asset to its `(address, symbol, decimals)` in O(1), plus a `found` flag                                                         |
| `getRegisteredAssets` · `isAssetRegistered` · `getAssetCount` | Asset-registry introspection                                                                                                                     |
| `usdDecimals` · `version`                                     | USD value decimals (e.g. 8); implementation version                                                                                              |

<details>

<summary><strong>Full read-function signatures</strong></summary>

```solidity
/// Total chain-local NAV for an account in the given quote currency (address(0) = USD).
function getAccountNav(address account, address quoteAsset)
    external view returns (NAV memory nav);

/// NAV over a caller-supplied subset of registered adapters. Pass the SAME quoteAsset to every
/// slice. Only `value` sums; union the asset arrays and OR the booleans across all slices.
function getAccountNavForAdapters(address account, address quoteAsset, address[] calldata adapters)
    external view returns (PartialNAV memory partialNav);

/// One Position per balance entry reported for an account, with pricing.
function getAccountPositions(address account, address quoteAsset)
    external view returns (Position[] memory positions);

/// Positions for a single asset across all adapters that report it.
function getAccountPositionsForAsset(address account, address asset, address quoteAsset)
    external view returns (Position[] memory positions);

/// Verbose variants — positions enriched with protocolName + labels[] (see IPositionDescribable).
function getAccountNavVerbose(address account, address quoteAsset)
    external view returns (VerboseNAV memory verboseNav);
function getAccountPositionsVerbose(address account, address quoteAsset)
    external view returns (VerbosePosition[] memory positions);
function getAccountPositionsForAssetVerbose(address account, address asset, address quoteAsset)
    external view returns (VerbosePosition[] memory positions);

/// Sequencer status, all currently-stale, and all currently-irregular price assets, without a full NAV.
/// NOTE: deliberately carries no monitorsUnhealthyPriceAssets — see the note under the NAV struct.
function healthCheck()
    external view returns (bool sequencerDown, address[] memory stalePriceAssets, address[] memory irregularPriceAssets);

/// Detailed data for an asset's primary feed + divergence signal (price, decimals, stale, irregular, …).
function getPriceData(address underlyingAsset)
    external view returns (PriceFeedData memory priceFeedData);

/// Same as getPriceData but skips the monitor/divergence read (used by custom feeds on the base/USD hot path).
function getPriceDataNoDivergence(address underlyingAsset)
    external view returns (PriceFeedData memory priceFeedData);

/// Live primary-vs-monitor divergence read for an asset.
function getPriceDivergence(address underlyingAsset)
    external view
    returns (
        int256  primaryPrice,        // 8-dp primary price (0 if the primary is stale)
        int256  monitorMedian,       // median of the healthy monitor prices
        uint256 divergenceBps,       // worst-case primary-vs-monitor deviation, in bps
        bool    irregular,           // divergence beyond tolerance, or (stablecoins) off the $1 peg
        uint256 monitorFeedCount,    // healthy monitor feeds this read
        bool    primaryStale,        // primary feed stale or sequencer down (suppresses the signal)
        bool    primaryAboveMonitors // primaryPrice > monitorMedian (directionality)
    );

/// One asset's classification AND display labels, forwarded to the AssetKindRegistry this NAV
/// points at. `labels` may be empty — a real answer, not a gap. Reverts AssetNotClassified(asset)
/// if unclassified, and reverts if no registry is set.
/// Declared on NAVCalculator itself, NOT on INAVCalculator.
function getAssetInfo(address asset)
    external view returns (PegKind, AccrualKind, string[] memory labels);

/// The registry this NAV forwards classification reads to.
function assetKindRegistry() external view returns (address);

/// Monitor (divergence-only) feeds and per-asset tolerances.
function getMonitorFeeds(address asset) external view returns (IPrices.PriceFeedConfig[] memory);
function getMonitorFeedCount(address asset) external view returns (uint256);
function getAssetPricing(address asset) external view returns (IPrices.AssetPricing memory);

/// Whether an account has any non-zero position.
function hasPositions(address account) external view returns (bool has);

/// Assets for which an account holds at least one non-zero position.
function getAssetsWithPositions(address account) external view returns (address[] memory assets);

/// Value of a set of (asset, amount) pairs in a quote currency.
function calculateValue(address[] memory assets, uint256[] memory amounts, address quoteAsset)
    external view returns (int256 value, bool quoteAssetStale);

/// Registry introspection.
/// Resolve ONE asset in O(1). Non-reverting: `found == false` for an unregistered asset.
/// Prefer this over getRegisteredAssets() — see the note below.
function getRegisteredAsset(address asset) external view returns (Asset memory assetInfo, bool found);
function getRegisteredAssets() external view returns (Asset[] memory assets);
function isAssetRegistered(address asset) external view returns (bool);
function getAssetCount() external view returns (uint256);
function usdDecimals() external view returns (uint8);   // e.g. 8
function version() external view returns (uint64);
```

</details>

{% hint style="warning" %}
**To resolve one asset, use `getRegisteredAsset`, not `getRegisteredAssets`.** The plural getter copies the **entire** registry across the ABI boundary, so resolving M tokens by calling it M times pays M full fetches — and the cost is super-linear, not linear, because each call allocates a fresh N-element array. At scale that is enough to exhaust an adapter's gas stipend and turn a NAV read into a **revert**, not merely a slow one. `getRegisteredAsset` is O(1).

It is also **non-reverting by design**: an unregistered asset returns `found == false` rather than reverting, because "not registered" is an ordinary outcome on an adapter's emission path — the adapter simply skips that leg. A reverting getter would turn every unregistered token an adapter happens to encounter into a failed instance read, converting a skipped leg into a dropped position.
{% endhint %}

#### Examples

Illustrative calls and returns. Values are example data (not live); addresses are abbreviated. The account is a portfolio Safe, `USDC` = `0xA0b8…eB48` (6 dp), `WETH` = `0xC02a…6Cc2` (18 dp), and the USD quote asset is `address(0)`.

```
getAccountNav(0x1F98…E984, address(0))
→ NAV {
    value:                1284530000000,   // 12,845.30 USD  (value / 1e8)
    quoteAsset:           { 0x0000…0000, "USD", 8 },
    timestamp:            1718901234,
    stalePriceAssets:     [],              // all primary feeds fresh
    sequencerDown:        false,
    quoteAssetStale:      false,
    irregularPriceAssets: [],              // no primary diverged from its monitors / peg
    quoteAssetIrregular:  false,
    monitorsUnhealthyPriceAssets: []       // every configured monitor answered — the line above means something
  }
```

```
getAccountPositions(0x1F98…E984, address(0))
→ [
    Position {
      balanceAsset:   { 0xA0b8…eB48, "USDC", 6 },
      balanceAdapter: 0x081D…dC39,        // ERC20Default (wallet)
      amount:         5000000000,         // 5,000 USDC
      isDebt:         false,
      quoteAsset:     { 0x0000…0000, "USD", 8 },
      value:          500000000000,       // 5,000.00 USD
      price:          100000000,          // $1.00 (8 dp)
      priceDecimals:  8,
      stale:          false,
      protocolBrand:  keccak256("wallet"),
      protocolId:     keccak256("wallet"),
      protocolSubId:  keccak256("wallet"),
      positionId:     abi.encode(0xA0b8…eB48),
      positionKind:   0,                   // Wallet
      isLocked:       false,
      positionInstanceId: bytes32(0),
      irregular:      false,
      quoteAssetIrregular: false
    },
    Position {
      balanceAsset:   { 0xC02a…6Cc2, "WETH", 18 },
      balanceAdapter: 0x9aBc…7e58,        // Morpho Markets (meta)
      amount:         2000000000000000000, // 2.0 WETH supplied
      isDebt:         false,
      value:          784530000000,       // 7,845.30 USD
      price:          392265000000,       // $3,922.65 (8 dp)
      priceDecimals:  8,
      stale:          false,
      protocolBrand:  keccak256("morpho"),
      protocolId:     keccak256("morpho"),
      protocolSubId:  keccak256("morpho-markets"),
      positionId:     abi.encode(0x3a85…f21d),  // bytes32 marketId
      positionKind:   1,                   // Supplied
      isLocked:       false,
      positionInstanceId: bytes32(0),
      irregular:      false,
      quoteAssetIrregular: false
    }
  ]
```

```
getAccountPositionsForAsset(0x1F98…E984, 0xC02a…6Cc2, address(0))   // WETH only
→ [ Position { …WETH supplied on Morpho, value 7,845.30 USD… } ]
```

```
getPriceData(0xC02a…6Cc2)   // WETH: Chainlink primary + Redstone monitor
→ PriceFeedData {
    priceFeed:          0x5f4e…8419,   // Chainlink ETH/USD (primary)
    priceType:          0,             // Chainlink
    price:              392265000000,  // $3,922.65 (8 dp)
    decimals:           8,
    chainlinkHeartbeat: 3600,
    updatedAt:          1718901200,
    stale:              false,
    sequencerDown:      false,
    healthyFeedCount:   1,             // 1 primary feed fresh
    irregular:          false,         // within the 50-bps divergence tolerance
    divergenceBps:      11,            // primary vs Redstone monitor
    monitorFeedCount:   1              // 1 monitor feed fresh
  }
```

```
getAccountPositionsVerbose(0x1F98…E984, address(0))   // same positions, with labels
→ [
    VerbosePosition {
      position:     { …WETH supplied on Morpho… },
      protocolName: "Morpho",
      labels:       ["Market", "wstETH/USDC"]
    },
    …
  ]
```

```
healthCheck()
→ (false, [], [])                     // sequencer up, no stale assets, no irregular assets
→ (false, [0x6B17…1d0F], [])          // example: DAI primary feed stale
→ (false, [], [0xae78…86CA])          // example: wstETH primary diverged from its monitors

hasPositions(0x1F98…E984)             → true
getAssetsWithPositions(0x1F98…E984)   → [0xA0b8…eB48, 0xC02a…6Cc2]   // USDC, WETH

calculateValue([0xC02a…6Cc2], [1000000000000000000], address(0))    // 1 WETH in USD
→ (392265000000, false)               // 3,922.65 USD; quote-asset feed not stale
```

```
getRegisteredAssets()
→ [ {0xEeee…EEeE,"ETH",18}, {0xA0b8…eB48,"USDC",6}, {0xC02a…6Cc2,"WETH",18}, … ]

getAssetCount()                       → 37
isAssetRegistered(0xC02a…6Cc2)        → true
usdDecimals()                         → 8
version()                             → 2
```

### Stale prices and sequencer status

The `NAV` struct carries hard reliability signals — `stalePriceAssets`, `sequencerDown`, and `quoteAssetStale` — that must **block** a pricing read, plus two soft signals: `irregularPriceAssets` (and `quoteAssetIrregular`), which flags a price the primary and monitor feeds disagree on, and `monitorsUnhealthyPriceAssets`, which flags assets where **no monitor answered at all**, so the disagreement check never ran. See [Stale prices & sequencer](/funds/infrastructure/onchain-accounting/concepts/stale-prices-and-sequencer.md) for the full model, and use `healthCheck()` to poll chain health without computing a full NAV — noting that `healthCheck` does not carry the third array.

***

## Admin / Manager API

Configuration is privileged and held by the security council Safe. Most mutations require the **`MANAGER`** role; the sequencer-feed setters and proxy upgrades require **`DEFAULT_ADMIN_ROLE`**. Both roles are held by the security council Safe. All batch calls are **all-or-nothing** — if any item fails its check, the whole transaction reverts.

#### Assets & price feeds (MANAGER)

An asset enters the system with its primary feed via `registerAsset`, carries **one pricing feed** plus optional **monitor** feeds and tolerances, and leaves via `unregisterAsset`. See [Price feeds](/funds/infrastructure/onchain-accounting/price-feeds.md) for the primary-feed + divergence-monitor model.

| Function                                                                    | What it does · key reverts                                                                                                                                                                                                                                                                                                                                                          |
| --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `registerAsset(asset, priceFeed, priceType, heartbeat)`                     | Registers a new asset **and** its primary price feed in one call; reads `symbol`/`decimals` from the ERC-20 (or the native-token constant) and adds the asset to the default (wallet) adapter's inclusion list. Reverts `InvalidArguments` for an address with no code (the native-token sentinel aside), so a typo'd or not-yet-deployed token cannot be registered.               |
| `addPriceFeed(asset, priceFeed, priceType, heartbeat)`                      | Adds another **pricing** feed to an already-registered asset (transitional; production keeps one). Reverts `PriceFeedAlreadyRegistered` (same feed twice) or `PriceFeedDecimalsMismatch` (a new feed's decimals must equal the existing feeds'); `heartbeat` must be non-zero.                                                                                                      |
| `removePriceFeed(asset, priceFeed)`                                         | Removes one pricing feed by address. Reverts `PriceFeedNotFound`, or `CannotRemoveLastFeed` if it is the only pricing feed left.                                                                                                                                                                                                                                                    |
| `removePriceFeedAt(asset, index)`                                           | Same, by array index. Reverts `PriceFeedIndexOutOfBounds` or `CannotRemoveLastFeed`.                                                                                                                                                                                                                                                                                                |
| `setDivergenceTolerance(asset, bps)`                                        | Sets the primary-vs-monitor divergence tolerance. Must be in `(0, 10000]`; `0` **reverts** `InvalidTolerance` — it does not disable the check. Required before any monitor feed can be added.                                                                                                                                                                                       |
| `setPegTolerance(asset, bps)`                                               | Sets the $1-peg tolerance (stablecoins). Must be in `[0, 10000]`; `0` is accepted and disables the peg check (only a value above `10000` reverts `InvalidTolerance`). **Also the switch that decides the asset's** [**peg classification**](/funds/infrastructure/onchain-accounting/concepts/asset-classification.md#the-peg-axis-is-derived-not-stored) — crossing zero flips it. |
| `setAssetKindRegistry(registry)`                                            | Points this NAV at the chain's [`AssetKindRegistry`](/funds/infrastructure/onchain-accounting/concepts/asset-classification.md). Rejects `address(0)` (`InvalidArguments`). Re-settable: the registry is a plain contract, so replacing it is a redeploy plus this call.                                                                                                            |
| `addMonitorFeed(asset, priceFeed, priceType, heartbeat)`                    | Adds a divergence-only monitor feed. Reverts `DivergenceToleranceRequired` (no tolerance set), `MonitorFeedDecimalsMismatch` (monitor must report 8 decimals), or `MonitorFeedAlreadyRegistered`.                                                                                                                                                                                   |
| `removeMonitorFeed(asset, priceFeed)` · `removeMonitorFeedAt(asset, index)` | Removes a monitor feed by address or index (`MonitorFeedNotFound`). Monitors are optional — the last one may be removed.                                                                                                                                                                                                                                                            |
| `unregisterAsset(asset)`                                                    | Fully removes the asset: clears **all** its pricing and monitor feeds, removes it from the default-adapter list, and drops it from the asset registry. Use only after the asset is no longer held anywhere.                                                                                                                                                                         |

#### Balance adapters (MANAGER)

Plain (single-scope) adapters only — meta-adapters use the next group. See [Balance adapters](/funds/infrastructure/onchain-accounting/balance-adapters.md).

| Function                                    | What it does · key reverts                                                                                                                                                                                                                                           |
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `addBalanceAdapters(address[] adapters)`    | Batch-registers **plain** adapters into the global registry. Reverts `InvalidArguments` (empty list / zero address), `DuplicateBalanceAdapter`, or `MetaAdapterNotAllowed` if any adapter implements `IMetaBalanceAdapter` (those must use `addMetaBalanceAdapter`). |
| `removeBalanceAdapters(address[] adapters)` | Batch-removes adapters from the registry. Reverts if any is not registered or is the built-in `ERC20Default` adapter (which cannot be removed).                                                                                                                      |

#### Meta-adapters & their instances (MANAGER)

A meta-adapter is registered **with** its initial instance set; the set is then grown or shrunk incrementally. See [Meta balance adapters](/funds/infrastructure/onchain-accounting/meta-balance-adapters.md).

| Function                                              | What it does · key reverts                                                                                                                                                                                                                                                            |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `addMetaBalanceAdapter(adapter, bytes32[] instances)` | Registers a meta-adapter **and** seeds its instance set atomically. The adapter must implement `IMetaBalanceAdapter` (else `NotMetaAdapter` / `InvalidBalanceAdapter`). Reverts `DuplicateBalanceAdapter` or `DuplicateMetaInstance`. An empty instance list is allowed (seed later). |
| `addMetaInstances(adapter, bytes32[] instances)`      | Adds instance coordinates to a registered meta-adapter. Reverts `DuplicateMetaInstance` (already present or repeated in the batch — a duplicate would double-count NAV), `InvalidArguments` (empty), or `NotMetaAdapter`.                                                             |
| `removeMetaInstances(adapter, bytes32[] instances)`   | Removes coordinates. Reverts `MetaInstanceNotFound`, `InvalidArguments` (empty), or `NotMetaAdapter`. Order is not preserved.                                                                                                                                                         |

Adding a market/vault/pool to coverage is thus an `addMetaInstances` transaction — never a new contract deploy.

#### Default (wallet) adapter inclusion (MANAGER)

| Function                                | What it does                                                                                                                                                     |
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `excludeAssetFromDefaultAdapter(asset)` | Stops the built-in `ERC20Default` adapter from reporting **wallet** balances for an asset, without unregistering the asset (its protocol positions still count). |
| `includeAssetInDefaultAdapter(asset)`   | Re-enables wallet-balance reporting for the asset.                                                                                                               |

#### L2 sequencer & upgrades (DEFAULT\_ADMIN\_ROLE)

| Function                                      | What it does · key reverts                                                                                                                             |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `setChainlinkL2SequencerUptimeFeed(feed)`     | Sets the Chainlink L2 sequencer uptime feed (`address(0)` on L1). Admin-gated because disabling sequencer detection on an L2 has upgrade-level impact. |
| `setChainlinkL2SequencerGracePeriod(seconds)` | Sets the post-recovery grace period. Reverts `UptimeFeedNotSet` if no uptime feed is configured.                                                       |
| *UUPS upgrade*                                | `upgradeToAndCall(...)` (UUPSUpgradeable) is authorized by `DEFAULT_ADMIN_ROLE`; the implementation can change, the proxy address never does.          |

<details>

<summary><strong>Admin / Manager function signatures</strong></summary>

```solidity
// Assets & price feeds (MANAGER)
function registerAsset(address asset, address priceFeed, IPrices.PriceType priceType, uint256 chainlinkHeartbeat) external;
function addPriceFeed(address asset, address priceFeed, IPrices.PriceType priceType, uint256 chainlinkHeartbeat) external;
function removePriceFeed(address asset, address priceFeed) external;
function removePriceFeedAt(address asset, uint256 index) external;
function unregisterAsset(address underlyingAsset) external;

// Divergence monitors & tolerances (MANAGER)
function setDivergenceTolerance(address asset, uint32 divergenceToleranceBps) external;
function setPegTolerance(address asset, uint32 pegToleranceBps) external;
function addMonitorFeed(address asset, address priceFeed, IPrices.PriceType priceType, uint256 chainlinkHeartbeat) external;
function removeMonitorFeed(address asset, address priceFeed) external;
function removeMonitorFeedAt(address asset, uint256 index) external;

// Default (wallet) adapter inclusion (MANAGER)
function excludeAssetFromDefaultAdapter(address asset) external;
function includeAssetInDefaultAdapter(address asset) external;

// Balance adapters (MANAGER)
function addBalanceAdapters(address[] calldata adapters) external;          // plain adapters only
function removeBalanceAdapters(address[] calldata adapters) external;
function addMetaBalanceAdapter(address adapter, bytes32[] calldata instances) external;  // meta-adapter + seed
function addMetaInstances(address adapter, bytes32[] calldata instances) external;
function removeMetaInstances(address adapter, bytes32[] calldata instances) external;

// L2 sequencer (DEFAULT_ADMIN_ROLE)
function setChainlinkL2SequencerUptimeFeed(address newChainlinkL2UptimeFeed) external;
function setChainlinkL2SequencerGracePeriod(uint256 newSequencerGracePeriod) external;
```

</details>

{% hint style="info" %}
Plain adapters are registered with `addBalanceAdapters`; **meta-adapters** (one adapter covering many protocol instances) are registered with `addMetaBalanceAdapter` and have their queried instance set adjusted with `addMetaInstances` / `removeMetaInstances`. See [Balance adapters](/funds/infrastructure/onchain-accounting/balance-adapters.md).
{% endhint %}

***

## Multichain aggregation

The offchain **NAV Worker** collects per-chain NAV values and sums them:

1. Call `getAccountNav` on every chain the fund operates on.
2. Verify `stalePriceAssets` is empty, `sequencerDown` is `false`, and `quoteAssetStale` is `false` on every chain.
3. If any chain is unhealthy, the global NAV is withheld until the condition clears.
4. Otherwise, sum all per-chain `value` fields to produce the global fund NAV.
5. Treat a non-empty `irregularPriceAssets` as an **alert**, not a hard block: the value is still usable, but the divergence should be investigated before it widens into a stale/blocking condition.
6. Treat a non-empty `monitorsUnhealthyPriceAssets` as an alert on the **check itself** rather than on the price: those assets lost their cross-check this read, so an empty `irregularPriceAssets` says nothing about them. Reconcile the two lists together — an asset in the third array is unverified, not verified-clean.

{% hint style="info" %}
See [Code examples](/funds/infrastructure/onchain-accounting/code-examples.md) for TypeScript and Python examples of the single-chain and multichain read paths.
{% 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/contracts/nav-calculator.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.
