> This is part 2 of 2 of the full documentation (pages 101–131 of 131).
> The content is paginated: fetch every part to see all of it.
> Previous part: https://docs.kpk.io/llms-full.txt
> Page index: https://docs.kpk.io/llms.txt

# 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
This site has an agentic ask interface you may use to query the documentation dynamically 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/index.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.

---

# Fluid Vaults

One immutable meta-adapter that reports an account's collateral and debt across any Fluid T1 vault.

One immutable meta-adapter that reports an account's collateral and debt across any Fluid T1 vault. Per vault it emits a `Collateral` leg (supply) and a `Borrowed` leg (debt), aggregated over the account's Fluid NFT positions in that vault.

* **Type:** Meta-adapter
* **protocolSubId:** `keccak256("fluid-vaults")`
* **Instance key:** the Fluid T1 vault address, widened to `bytes32`.
* **Source:** `FluidVaultsMetaBalanceAdapter.sol`

{% hint style="warning" %}
Only Fluid **T1** vaults are supported; Smart vaults (T2/T3/T4) have a different, longer `constantsView` layout. This is **enforced onchain**, not merely by convention: the token read is gated on the VaultResolver's `getVaultType(vault)` and fails closed, so a mis-configured non-T1 instance reads back as no tokens and reports nothing, no legs, no underlying assets. The guard matters because adding an instance is a `MANAGER` config transaction rather than a deploy, and non-T1 vaults are the majority of those live on mainnet.
{% endhint %}

## Positions returned

Up to two legs per vault:

| Leg        | PositionKind | isDebt  | Description                                            |
| ---------- | ------------ | ------- | ------------------------------------------------------ |
| Collateral | `Collateral` | `false` | Total supplied collateral, in the vault's supply token |
| Borrow     | `Borrowed`   | `true`  | Total outstanding debt, in the vault's borrow token    |

A leg is dropped when its amount is zero, when the token is not registered in NAVCalculator, or when filtered out by `assetFilter`. If the account **has debt** in the vault and the borrow token is unregistered, the vault reports **nothing at all**, the collateral leg included, rather than emitting collateral with no offsetting debt, which would over-report NAV.

The NFT aggregation is likewise **complete or nothing**: if a position read runs out of gas mid-loop, the vault raises `FluidPositionGasExhausted(vault, nftId, stipend, consumed)` rather than returning the partial sum. Each NFT contributes both a supply and a borrow, so silently omitting one moves equity by `supply − borrow`, which is an *over*-report for an underwater position, not merely a conservative under-report. Making the truncation visible is the only answer that is safe in both directions. See [Never report collateral without its debt](/funds/infrastructure/architecture/onchain-accounting/balance-adapters#how-an-adapter-is-built).

## Balance calculation

<figure><picture><source srcset="/files/8u3gfRhagTu4pOIW61SX" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2FPc6r22eFzNZOjmnsc81I%2Fmeta-fluid-vaults.png?alt=media&amp;token=326d585d-5155-4e39-8a9d-5877742a7963" alt="Flowchart of the Fluid Vaults balance adapter: read calls derive position legs into PositionBalance entries."></picture><figcaption><p>Fluid Vaults adapter: how the underlying balances reported to the NAV Calculator are derived.</p></figcaption></figure>

The supply/borrow tokens come from the vault's `constantsView()` (`supplyToken`, `borrowToken`). Amounts are aggregated across the account's Fluid NFT positions via the VaultResolver (logic in `FluidVaultLib`):

```
nftIds = VaultResolver.positionsNftIdOfUser(account)
for each nftId where VaultResolver.vaultByNftId(nftId) == vault:
    pos = VaultResolver.positionByNftId(nftId)
    if pos.isLiquidated: skip
    collateral += pos.supply
    debt       += pos.borrow
```

`collateral` is reported in supply-token decimals, `debt` in borrow-token decimals. Liquidated positions are skipped.

## Identity

* **positionId:** `abi.encode(address vault)`
* **positionKind:** `Collateral` (supply leg), `Borrowed` (debt leg)

There is no `labels` field on the position. The adapter implements `positionLabels(positionId)`, which returns `["Vault", "<collateral>/<debt> #<vaultId>"]`, e.g. `["Vault", "wstETH/USDC #4"]`; surfaced only by the verbose reads (`getAccountPositionsVerbose` / `getAccountNavVerbose`); the full breadcrumb is `[protocolName, ...labels]`.

A Fluid vault is **not** an ERC20, `name()`, `symbol()` and `version()` all revert on it, so the leaf is built from the vault's token pair, exactly as the Morpho markets adapter does. Fluid's own per-vault id is appended because **the pair alone is not unique**: mainnet carries many more live T1 vaults than distinct pairs, and every configured vault has a live same-pair twin, so labelling by pair alone would reproduce the indistinguishability the leaf exists to remove. Symbols resolve from the NAV's registered-asset record, which maps the ERC-7528 native sentinel to the chain's native symbol, since several T1 vaults use it as one side, falling back to a direct ERC20 read and then to `"?"`, so an unreadable side renders as `"?/USDC"`.

## Constructor

```solidity
constructor(address vaultResolver_, address navCalculator_)
```

| Parameter        | Description                                                                                                       |
| ---------------- | ----------------------------------------------------------------------------------------------------------------- |
| `vaultResolver_` | Fluid VaultResolver address (chain-level constant) used for NFT enumeration and per-NFT reads. Must be a contract |
| `navCalculator_` | NAVCalculator address, used for the registered-asset registry. Must be a contract                                 |

## Registration

Register the adapter with `addMetaBalanceAdapter(adapter, instances)`, then add or remove vaults with `addMetaInstances(adapter, instances)` / `removeMetaInstances(adapter, instances)`. The instance coordinate is the T1 vault address widened to `bytes32`.


# Compound V3

One immutable meta-adapter that reports an account's position in any Compound V3 (Comet) market, base-token supply or borrow, every collateral leg, and unclaimed COMP rewards.

One immutable meta-adapter that reports an account's position in *any* Compound V3 (Comet) market, base-token supply or borrow, every collateral leg, and unclaimed COMP rewards. Which comets it queries is a governed instance set on the NAV Calculator; the adapter holds no per-comet state. Read math is delegated to `CompoundV3CometLib`.

* **Type:** Meta-adapter
* **protocolSubId:** `keccak256("compound-v3-comets")`
* **Instance key:** the comet (market) address, widened to `bytes32`
* **Source:** `CompoundV3CometsMetaBalanceAdapter.sol`

## Positions returned

| Leg         | PositionKind | isDebt  | Description                                          |
| ----------- | ------------ | ------- | ---------------------------------------------------- |
| Base supply | `Supplied`   | `false` | Positive base position, present value                |
| Base borrow | `Borrowed`   | `true`  | Negative base position, present value                |
| Collateral  | `Collateral` | `false` | One leg per collateral asset with a non-zero balance |
| Rewards     | `Rewards`    | `false` | Unclaimed reward token (COMP)                        |

The base leg is supply or borrow depending on the sign of the net base position (never both). Each leg is dropped if its amount is zero, its asset is unregistered, or it is excluded by `assetFilter`. A multi-role asset (e.g. COMP as both collateral and reward) emits one leg per role.

Where the net base position is a **borrow** and the base asset is unregistered, the comet reports **nothing**, collateral and rewards included, rather than emitting credit legs with no offsetting debt. See [Never report collateral without its debt](/funds/infrastructure/architecture/onchain-accounting/balance-adapters#how-an-adapter-is-built).

## Balance calculation

<figure><picture><source srcset="/files/wIY0CZhSnKNun9przEEO" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2FxART8yonnoGaLfOtpIxd%2Fmeta-compound-v3.png?alt=media&amp;token=2bd9a8a1-28b5-4ca1-b57e-2aa5d97134ee" alt="Flowchart of the Compound V3 balance adapter: read calls derive position legs into PositionBalance entries."></picture><figcaption><p>Compound V3 adapter: how the underlying balances reported to the NAV Calculator are derived.</p></figcaption></figure>

`CompoundV3CometLib.accountData(comet, rewards, rewardToken, account)` returns the net base, collateral balances, and unclaimed reward, accruing interest and reward indices to `block.timestamp` (it re-derives Comet's accrual rather than mutating state).

```
baseNet = userBasic(account).principal * (principal >= 0 ? baseSupplyIndex : baseBorrowIndex) / BASE_INDEX_SCALE
          (indices first advanced by the elapsed-time accrual)
  baseNet > 0 → Supplied, amount =  baseNet
  baseNet < 0 → Borrowed, amount = -baseNet

collateralBalances[i] = comet.collateralBalanceOf(account, asset[i])   // per collateral asset

rewardOwed = accrued − rewardsClaimed(comet, account), floored at 0
```

Reward accrual mirrors Comet's tracking-index math, handling the optional 4th `multiplier`/`rescaleFactor` field returned by some chains' `rewardConfig` (detected via returndata length). The library is a leg-for-leg, fork-tested port of the per-comet adapter.

## Identity

* **positionId:** `abi.encode(address comet)`; the comet address (all legs of a comet share it)
* **positionKind:** `Supplied` · `Borrowed` · `Collateral` · `Rewards`

There is no `labels` field on the position. The adapter implements `positionLabels(positionId)`, which returns `["Market", "<comet symbol()>"]` (e.g. `"cUSDCv3"`) from the comet's onchain symbol; surfaced only by the verbose reads (`getAccountPositionsVerbose` / `getAccountNavVerbose`); the full breadcrumb is `[protocolName, ...labels]`.

## Constructor

```solidity
constructor(address compoundRewards_, address rewardToken_, address navCalculator_)
```

| Parameter          | Description                                                                                |
| ------------------ | ------------------------------------------------------------------------------------------ |
| `compoundRewards_` | The chain's CometRewards contract (shared by every comet on the chain; must be a contract) |
| `rewardToken_`     | The reward token, COMP or its bridged form (must be non-zero)                              |
| `navCalculator_`   | NAV Calculator contract (asset registry + instance set)                                    |

`compoundRewards_` and `rewardToken_` are chain-level constants pinned once here, so they apply across every comet in the instance set.

## Registration

Meta-adapter. Register once and seed its comets with `addMetaBalanceAdapter(adapter, instances)`, then adjust with `addMetaInstances(adapter, instances)` / `removeMetaInstances(adapter, instances)` (MANAGER-gated). Each instance coordinate is a **comet address widened to `bytes32`** (decoded via `_toAddress`). Adding a new comet is a config call, no new deploy.


# StakeWise V3

One immutable meta-adapter that reports an account's StakeWise V3 vault positions across any vault on a chain.

One immutable meta-adapter that reports an account's StakeWise V3 vault positions across any vault on a chain. Per vault it emits two gross legs: a `Staking` leg (vault shares converted to the underlying) and a `Borrowed` leg (minted osToken debt converted to the underlying).

* **Type:** Meta-adapter
* **protocolSubId:** `keccak256("stakewise-v3-vaults")`
* **Instance key:** the StakeWise V3 vault address, widened to `bytes32`.
* **Source:** `StakeWiseV3VaultsMetaBalanceAdapter.sol`

## Positions returned

Up to two legs per vault, both denominated in the underlying staking asset:

| Leg          | PositionKind | isDebt  | Description                                                       |
| ------------ | ------------ | ------- | ----------------------------------------------------------------- |
| Staking      | `Staking`    | `false` | Vault shares converted to the underlying staking asset            |
| osToken debt | `Borrowed`   | `true`  | Minted osToken position converted to the underlying staking asset |

Both legs are dropped when their amount is zero. The whole instance returns empty if the underlying is unregistered in NAVCalculator or filtered out by `assetFilter`.

## Balance calculation

<figure><picture><source srcset="/files/v3uJVsjE7v4FyTpexdIP" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2FSvNHj24HRZ4yynjZeu7U%2Fmeta-stakewise-v3.png?alt=media&amp;token=98ecf31e-fa38-42d6-ac6b-d076200f87b7" alt="Flowchart of the StakeWise V3 balance adapter: read calls derive position legs into PositionBalance entries."></picture><figcaption><p>StakeWise V3 adapter: how the underlying balances reported to the NAV Calculator are derived.</p></figcaption></figure>

Both legs are denominated in `UNDERLYING_TOKEN` (the chain-level staking asset, e.g. the ETH sentinel on mainnet, GNO on Gnosis):

```
vaultAssets = vault.convertToAssets(vault.getShares(account))                            // Staking
mintedOs    = OS_TOKEN_VAULT_CONTROLLER.convertToAssets(vault.osTokenPositions(account))  // Borrowed
```

The osToken debt is denominated in osToken **shares**, whose exchange rate is governed by the `OsTokenVaultController`, a different object from the staking vault's own share→asset rate. The `Borrowed` leg therefore converts through the controller (the same rate object the osToken price feed uses); converting through the vault's rate would mis-scale the debt.

The two legs are gross (not netted): the staking principal and the osToken debt are reported separately.

## Identity

* **positionId:** `abi.encode(address vault)`
* **positionKind:** `Staking` (vault shares), `Borrowed` (osToken debt)
* **Labels:** the adapter implements `positionLabels(positionId)`, returning a two-tier leaf; both tiers read live and fail-open, surfaced only on the verbose read path:

  1. the vault's ERC20 `name()`: `["Vault", "Stakers Union GNO"]`. Only StakeWise's `…Erc20Vault` implementation variants are ERC20s and answer it;
  2. otherwise `"<underlying symbol> <vault address>"`, `["Vault", "GNO 0x4b4406ed…"]`, where every instance configured today lands. The symbol comes from the NAV's asset registry rather than an ERC20 `symbol()` read, because the mainnet underlying is the ETH sentinel `0xEeee…EEeE`, which has no code; an unregistered underlying degrades to the bare address.

  Tier 2 is a coordinate rendering, not a name, the non-ERC20 vault variants expose no human-readable identity onchain at all (`name()`, `symbol()`, `contractURI()` and `metadataIpfsHash()` all revert; `vaultId()` is a *type* hash that collides across vaults), and the names StakeWise's own UI shows come from offchain IPFS metadata. Treat the leaf as descriptive, never as an identifier: operator names collide; three separate Gnosis vaults are all named `"SEED"`. Key on `positionId`.

The reconciliation key is `keccak256(abi.encode(chainId, protocolSubId, positionId, balanceAsset.asset, positionKind))`, the `Staking` and `Borrowed` legs reconcile independently via `positionKind`.

## Constructor

```solidity
constructor(address underlyingToken_, address osTokenVaultController_, address navCalculator_)
```

| Parameter                 | Description                                                                                                                                                                                     |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `underlyingToken_`        | Underlying staking asset all configured vaults are denominated in (chain-level constant; e.g. ETH sentinel on mainnet, GNO on Gnosis). Must be non-zero                                         |
| `osTokenVaultController_` | StakeWise `OsTokenVaultController` for this chain, converts osToken-debt shares to the underlying for the `Borrowed` leg (the same rate object the osToken price feed uses). Must be a contract |
| `navCalculator_`          | NAVCalculator address, used for the registered-asset registry. Must be a contract                                                                                                               |

## Registration

Register the adapter with `addMetaBalanceAdapter(adapter, instances)`, then add or remove vaults with `addMetaInstances(adapter, instances)` / `removeMetaInstances(adapter, instances)`. The instance coordinate is the vault address widened to `bytes32`.


# Balancer V3 Pools

Reports an account's wallet-held (unstaked) Balancer V3 pool liquidity across any pool.

Reports an account's **wallet-held (unstaked)** Balancer V3 pool liquidity across any pool. It decomposes the account's BPT (pool token) balance pro-rata into the pool's underlying tokens, the AMM-supplied LP only. Gauge-staked LP and rewards are the separate [Balancer V3 Gauges](/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters/balancer-v3-gauges) adapter.

* **Type:** Meta-adapter
* **protocolSubId:** `keccak256("balancer-v3-pools")`
* **Instance key:** the pool (BPT) address, widened to bytes32.
* **Source:** `BalancerV3PoolsMetaBalanceAdapter.sol`

## Positions returned

One leg per registered underlying pool token with a non-zero share:

| Leg    | PositionKind | isDebt  | Description                                                                |
| ------ | ------------ | ------- | -------------------------------------------------------------------------- |
| Supply | `Supplied`   | `false` | Pro-rata share of one underlying pool token, from the account's wallet BPT |

Tokens not registered in NAVCalculator are skipped, as are zero-amount and asset-filtered tokens. ERC4626 pool tokens are unwrapped to their `asset()` so the underlying is reported.

## Balance calculation

<figure><picture><source srcset="/files/W9kVKX7XGpHhABI3Mn8T" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2Fsgv4TZKf5eTGQv33ODrW%2Fmeta-balancer-v3-pools.png?alt=media&amp;token=8f64147b-5d90-4843-941f-93e6034df7f0" alt="Flowchart of the Balancer V3 Pools balance adapter: read calls derive position legs into PositionBalance entries."></picture><figcaption><p>Balancer V3 Pools adapter: how the underlying balances reported to the NAV Calculator are derived.</p></figcaption></figure>

For BPT balance `walletBpt = pool.balanceOf(account)` (returns nothing if zero), the adapter iterates the pool tokens in index order and computes each one's amount with `BalancerV3PoolLib.poolBalanceOfIndexed(pool, walletBpt, i)`. Keying on the pool-token **index** (rather than resolving by underlying address) ensures every index contributes exactly one leg, so a pool holding the same underlying at two indices (e.g. two distinct ERC4626 wrappers of the same asset) is decomposed correctly rather than double-counting one index and dropping the duplicate:

```
tokenBalance = balancesLiveScaled18[i] * walletBpt / pool.totalSupply()
```

The 18-decimal live-scaled share is then converted back to raw token units:

```
raw = tokenBalance * 1e18 / (decimalScalingFactors[i] * tokenRates[i])
```

A transiently-zero scaling factor or token rate yields a `0` leg (rather than a divide-by-zero that would revert the whole instance). If the pool token is an ERC4626 wrapper, `raw` is treated as wrapped-token shares and unwrapped via `previewRedeem(shares)` to the underlying asset.

## Identity

* **positionId:** `abi.encode(address pool)`
* **positionKind:** `Supplied`

There is no `labels` field on the position. The adapter implements `positionLabels(positionId)`, which returns `["AMM Liquidity Pool", "<sym0>/<sym1>/…"]` from the pool's underlying token symbols; surfaced only by the verbose reads (`getAccountPositionsVerbose` / `getAccountNavVerbose`); the full breadcrumb is `[protocolName, ...labels]`.

## Constructor

```solidity
constructor(address navCalculator_)
```

| Parameter        | Description                                                                                   |
| ---------------- | --------------------------------------------------------------------------------------------- |
| `navCalculator_` | NAVCalculator address (must be a contract); source of the asset registry and the instance set |

## Registration

Registered as a meta-adapter via `addMetaBalanceAdapter(adapter)`, then per-pool instances are added with `addMetaInstances`. Each instance coordinate is the pool (BPT) address widened to bytes32.


# Balancer V3 Gauges

Reports an account's gauge-staked Balancer V3 LP plus claimable rewards across any gauge.

Reports an account's **gauge-staked** Balancer V3 LP plus claimable rewards across any gauge. The staked LP is decomposed pro-rata into the pool's underlying tokens (valued via the gauge's `lp_token` pool), and BAL plus the gauge's extra reward tokens are reported as accrued rewards. Wallet-held (unstaked) LP is the separate [Balancer V3 Pools](/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters/balancer-v3-pools) adapter.

* **Type:** Meta-adapter
* **protocolSubId:** `keccak256("balancer-v3-gauges")`
* **Instance key:** the gauge address, widened to bytes32.
* **Source:** `BalancerV3GaugesMetaBalanceAdapter.sol`

## Positions returned

| Leg             | PositionKind | isDebt  | Description                                                                     |
| --------------- | ------------ | ------- | ------------------------------------------------------------------------------- |
| Staking         | `Staking`    | `false` | Pro-rata share of one underlying pool token, from the account's gauge-staked LP |
| Rewards (BAL)   | `Rewards`    | `false` | Claimable BAL, minted via the Balancer Minter                                   |
| Rewards (extra) | `Rewards`    | `false` | Claimable amount of each extra gauge reward token                               |

Tokens not registered in NAVCalculator are skipped, as are zero amounts and asset-filtered tokens.

## Balance calculation

<figure><picture><source srcset="/files/utI3Yfo1aiIuutPNSZiq" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2FTXLDfltgiTZpOphyYG0q%2Fmeta-balancer-v3-gauges.png?alt=media&amp;token=8e56e2dc-6f9c-4967-bc17-656ccbb040ee" alt="Flowchart of the Balancer V3 Gauges balance adapter: read calls derive position legs into PositionBalance entries."></picture><figcaption><p>Balancer V3 Gauges adapter: how the underlying balances reported to the NAV Calculator are derived.</p></figcaption></figure>

**Staking legs** use `staked = gauge.balanceOf(account)` and the same `BalancerV3PoolLib.poolBalanceOfIndexed` decomposition as the Pools adapter (live-scaled-18 share → raw units, ERC4626 unwrap via `previewRedeem`), applied to the gauge `lp_token` pool. As in the Pools adapter, the tokens are iterated in index order and each leg is keyed on the pool-token index, so a pool holding the same underlying at two indices is decomposed correctly; a transiently-zero scaling factor or token rate yields a `0` leg.

**BAL rewards** (`claimableBal`) emulate the gauge inflation checkpoint offchain:

```
claimable = max(integrateFraction(account) - minter.minted(account, gauge), 0)
```

`integrateFraction` is recomputed by walking weekly boundaries (`WEEK = 7 * 86400`) from the last period to `block.timestamp`, accumulating `integrate_inv_supply` from the inflation rate, capped relative weight, and `working_supply`. It tries the mainnet model (`calculateIntegrateFraction`, up to 500 weeks, with a future-epoch rate switch via the token admin) and falls back to the L2 model (`calculateIntegrateFractionL2`, up to 256 weeks, per-week `inflation_rate(week)`). A killed gauge zeroes the rate. Any revert yields 0.

**Extra rewards** come from `gauge.claimable_reward(account, token)` for each `reward_tokens(i)` (BAL excluded here, since it is handled via the minter).

## Identity

* **positionId:** `abi.encode(address gauge)`
* **positionKind:** `Staking` · `Rewards`

There is no `labels` field on the position. The adapter implements `positionLabels(positionId)`, which returns `["Staking Gauge", "<sym0>/<sym1>/…"]` from the staked pool's underlying token symbols; surfaced only by the verbose reads (`getAccountPositionsVerbose` / `getAccountNavVerbose`); the full breadcrumb is `[protocolName, ...labels]`.

## Constructor

```solidity
constructor(address balancerMinter_, address navCalculator_)
```

| Parameter         | Description                                                                                            |
| ----------------- | ------------------------------------------------------------------------------------------------------ |
| `balancerMinter_` | Balancer Minter contract (must be a contract); resolves the BAL token, token admin, and minted amounts |
| `navCalculator_`  | NAVCalculator address (must be a contract); source of the asset registry and the instance set          |

## Registration

Registered as a meta-adapter via `addMetaBalanceAdapter(adapter)`, then per-gauge instances are added with `addMetaInstances`. Each instance coordinate is the gauge address widened to bytes32.


# Balancer V2 Pools

Reports an account's wallet-held (unstaked) Balancer V2 pool liquidity across any pool.

Reports an account's **wallet-held (unstaked)** Balancer V2 pool liquidity across any pool. It decomposes the account's BPT balance pro-rata into the pool's underlying tokens by reading the Balancer V2 Vault's live balances, the AMM-supplied LP only. Gauge-staked LP and rewards are the separate [Balancer V2 Gauges](/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters/balancer-v2-gauges) adapter.

* **Type:** Meta-adapter
* **protocolSubId:** `keccak256("balancer-v2-pools")`
* **Instance key:** the pool's **raw `bytes32` poolId** (used directly, not an address). The BPT (ERC20) address is the top 160 bits of the poolId.
* **Source:** `BalancerV2PoolsMetaBalanceAdapter.sol`

## Positions returned

One leg per registered underlying pool token with a non-zero share:

| Leg    | PositionKind | isDebt  | Description                                                                |
| ------ | ------------ | ------- | -------------------------------------------------------------------------- |
| Supply | `Supplied`   | `false` | Pro-rata share of one underlying pool token, from the account's wallet BPT |

The pool's own pre-minted BPT (composable-stable pools) is excluded from the token list. Unregistered, zero-amount, and asset-filtered tokens are skipped.

## Balance calculation

<figure><picture><source srcset="/files/mGDlPwwklvrfZpbokz5s" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2FzKIZ1ppaHn2qZ5TJ8MJx%2Fmeta-balancer-v2-pools.png?alt=media&amp;token=29fc5955-4654-4cc7-8011-3a900d8cfc81" alt="Flowchart of the Balancer V2 Pools balance adapter: read calls derive position legs into PositionBalance entries."></picture><figcaption><p>Balancer V2 Pools adapter: how the underlying balances reported to the NAV Calculator are derived.</p></figcaption></figure>

Balancer V2 holds every pool's tokens in a single Vault. For BPT balance `walletBpt = BPT.balanceOf(account)` (returns nothing if zero), `BalancerV2PoolLib.shares` reads the Vault's `getPoolTokens(poolId)` balances and computes each token's pro-rata amount:

```
amount[i] = balances[i] * walletBpt / effectiveSupply
```

`effectiveSupply` is `getActualSupply()` when the pool exposes it (composable-stable: excludes pre-minted BPT, includes due protocol fees) and falls back to `totalSupply()` for legacy weighted/stable pools.

## Identity

* **positionId:** `abi.encode(bytes32 poolId)`
* **positionKind:** `Supplied`

There is no `labels` field on the position. The adapter implements `positionLabels(positionId)`, which returns `["AMM Liquidity Pool", "<sym0>/<sym1>/…"]` from the pool's underlying token symbols; surfaced only by the verbose reads (`getAccountPositionsVerbose` / `getAccountNavVerbose`); the full breadcrumb is `[protocolName, ...labels]`.

## Constructor

```solidity
constructor(address vault_, address navCalculator_)
```

| Parameter        | Description                                                                                                  |
| ---------------- | ------------------------------------------------------------------------------------------------------------ |
| `vault_`         | Balancer V2 Vault address (must be a contract); the chain-level singleton that custodies every pool's tokens |
| `navCalculator_` | NAVCalculator address (must be a contract); source of the asset registry and the instance set                |

## Registration

Registered as a meta-adapter via `addMetaBalanceAdapter(adapter, instances)`, with the instance set adjusted by `addMetaInstances` / `removeMetaInstances`. Instance coordinates are passed as `bytes32` and stored as-is; for Balancer V2 each coordinate is the **raw `bytes32` poolId** (not an address widened to `bytes32`, as most meta-adapters use). The adapter recovers the BPT from the poolId's top 160 bits when it needs the ERC-20.


# Balancer V2 Gauges

Reports an account's gauge-staked Balancer V2 LP plus claimable rewards across any gauge.

Reports an account's **gauge-staked** Balancer V2 LP plus claimable rewards across any gauge. The staked LP is decomposed pro-rata into the pool's underlying tokens via the Vault's pool balances, and BAL plus the gauge's extra reward tokens are reported as accrued rewards. Wallet-held (unstaked) LP is the separate [Balancer V2 Pools](/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters/balancer-v2-pools) adapter.

* **Type:** Meta-adapter
* **protocolSubId:** `keccak256("balancer-v2-gauges")`
* **Instance key:** the gauge address, widened to bytes32.
* **Source:** `BalancerV2GaugesMetaBalanceAdapter.sol`

## Positions returned

| Leg             | PositionKind | isDebt  | Description                                                                     |
| --------------- | ------------ | ------- | ------------------------------------------------------------------------------- |
| Staking         | `Staking`    | `false` | Pro-rata share of one underlying pool token, from the account's gauge-staked LP |
| Rewards (BAL)   | `Rewards`    | `false` | Claimable BAL, minted via the Balancer Minter                                   |
| Rewards (extra) | `Rewards`    | `false` | Claimable amount of each extra gauge reward token                               |

Unregistered, zero-amount, and asset-filtered tokens are skipped.

## Balance calculation

<figure><picture><source srcset="/files/2H6ns7jRklDkXB2Lv6BZ" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2FPPGPLGaMiiBYLoBY5c4P%2Fmeta-balancer-v2-gauges.png?alt=media&amp;token=85fc5d4d-7371-4040-bafd-3568d5f06df9" alt="Flowchart of the Balancer V2 Gauges balance adapter: read calls derive position legs into PositionBalance entries."></picture><figcaption><p>Balancer V2 Gauges adapter: how the underlying balances reported to the NAV Calculator are derived.</p></figcaption></figure>

**Staking legs** use `staked = gauge.balanceOf(account)`. The gauge's `lp_token` BPT resolves its `getPoolId()`, and `BalancerV2PoolLib.shares` reads the Vault balances to compute each token's pro-rata amount:

```
amount[i] = balances[i] * staked / effectiveSupply
```

where `effectiveSupply` is `getActualSupply()` (composable-stable) or `totalSupply()` (legacy). The poolId read is skipped entirely when stake is zero.

**BAL rewards** and **extra rewards** use the exact same logic as the [Balancer V3 Gauges](/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters/balancer-v3-gauges) adapter, V2 and V3 gauges share the BalancerMinter inflation design:

```
claimableBAL = max(integrateFraction(account) - minter.minted(account, gauge), 0)
```

`integrateFraction` is recomputed offchain by walking weekly boundaries (`WEEK = 7 * 86400`) accumulating `integrate_inv_supply`, trying the mainnet model (up to 500 weeks, future-epoch rate switch via the token admin) and falling back to the L2 per-week-rate model (up to 256 weeks); a killed gauge zeroes the rate. Extra rewards come from `claimable_reward(account, token)` per `reward_tokens(i)` (BAL excluded).

## Identity

* **positionId:** `abi.encode(address gauge)`
* **positionKind:** `Staking` · `Rewards`

There is no `labels` field on the position. The adapter implements `positionLabels(positionId)`, which returns `["Staking Gauge", "<sym0>/<sym1>/…"]` from the staked pool's underlying token symbols; surfaced only by the verbose reads (`getAccountPositionsVerbose` / `getAccountNavVerbose`); the full breadcrumb is `[protocolName, ...labels]`.

## Constructor

```solidity
constructor(address vault_, address balancerMinter_, address navCalculator_)
```

| Parameter         | Description                                                                                            |
| ----------------- | ------------------------------------------------------------------------------------------------------ |
| `vault_`          | Balancer V2 Vault address (must be a contract); the chain-level singleton holding pool balances        |
| `balancerMinter_` | Balancer Minter contract (must be a contract); resolves the BAL token, token admin, and minted amounts |
| `navCalculator_`  | NAVCalculator address (must be a contract); source of the asset registry and the instance set          |

## Registration

Registered as a meta-adapter via `addMetaBalanceAdapter(adapter)`, then per-gauge instances are added with `addMetaInstances`. Each instance coordinate is the gauge address widened to bytes32.


# Curve Pools

Reports an account's wallet-held (unstaked) Curve LP position across any configured pool, decomposed pro-rata into the pool's underlying coins.

Reports an account's **wallet-held (unstaked)** Curve LP position across any configured pool, decomposed pro-rata into the pool's underlying coins. Gauge-staked LP and rewards are the separate [Curve Gauges](/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters/curve-gauges) adapter.

* **Type:** Meta-adapter
* **protocolSubId:** `keccak256("curve-pools")`
* **Instance key:** the pool address, widened to bytes32.
* **Source:** `CurvePoolsMetaBalanceAdapter.sol`

## Positions returned

One leg per registered pool coin with a non-zero wallet share:

| Leg    | PositionKind | isDebt  | Description                                                   |
| ------ | ------------ | ------- | ------------------------------------------------------------- |
| Supply | `Supplied`   | `false` | Pro-rata share of one pool coin, from the account's wallet LP |

Unregistered, zero-amount, and asset-filtered coins are skipped. For metapools, `coins()` enumerates only the meta-coins; base-pool coins are not recursed into (the base LP token has its own price feed). Lending pools report wrapped-token balances.

## Balance calculation

<figure><picture><source srcset="/files/ezsbK9iXOHQwqOXieRMQ" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2FqghJPvz6XBtBsIzspF5e%2Fmeta-curve-pools.png?alt=media&amp;token=9541a3fc-c87a-4375-ac6a-01f3482d5f7d" alt="Flowchart of the Curve Pools balance adapter: read calls derive position legs into PositionBalance entries."></picture><figcaption><p>Curve Pools adapter: how the underlying balances reported to the NAV Calculator are derived.</p></figcaption></figure>

The LP token is resolved from the pool by `CurvePoolLib.getLpToken`, the pool itself for classic StableSwap / TriCryptoNG (self-LP), or a separate ERC20 via `lp_token()` for StableSwap-NG factory / TwoCryptoNG pools.

{% hint style="warning" %}
**Legacy StableSwap pools are not decomposed here, deliberately.** On the oldest generation (3pool, whose LP is the separate 3Crv) `lp_token()` reverts, so resolution falls through to "the pool is its own LP token", which is false for these pools, and `balanceOf` on the pool itself reverts. Wallet-held LP for such a pool therefore reports **nothing**.

Unlike the [gauge](/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters/curve-gauges) and [Convex](/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters/convex) staking paths, this is **not** resolved through Curve's registry, and the gap is intentional. Resolving it would flip this adapter from emitting nothing to emitting full `Supplied` coin legs, which double-counts for any operator who worked around the under-report the obvious way, by registering the LP token as an asset so the [default adapter](/funds/infrastructure/architecture/onchain-accounting/balance-adapters/erc20-default) reports it. That would be an over-report, the one direction this family must never take, so the under-report is held until all three consumers can be changed together.
{% endhint %}

With \`walletLp = lpToken.balanceOf(account)\` and \`totalSupply\` (ERC20 \`totalSupply()\`, falling back to the StableSwap-NG \`total\_supply()\` storage variant), each coin amount is:

```
amount[i] = poolBalance[i] * walletLp / totalSupply
```

`poolBalance[i]` is read via `balances(uint256)`, falling back to `balances(int128)` for Vyper 0.1.x pools. Coin enumeration similarly tries `coins(uint256)` then `coins(int128)`, up to `MAX_COINS = 8`.

## Identity

* **positionId:** `abi.encode(address pool)`
* **positionKind:** `Supplied`

There is no `labels` field on the position. The adapter implements `positionLabels(positionId)`, which returns `["AMM Liquidity Pool", "TOKEN0/TOKEN1/…"]` from the onchain coin symbols; surfaced only by the verbose reads (`getAccountPositionsVerbose` / `getAccountNavVerbose`); the full breadcrumb is `[protocolName, ...labels]`.

## Constructor

```solidity
constructor(address navCalculator_)
```

| Parameter        | Description                                                                                   |
| ---------------- | --------------------------------------------------------------------------------------------- |
| `navCalculator_` | NAVCalculator address (must be a contract); source of the asset registry and the instance set |

## Registration

Registered as a meta-adapter via `addMetaBalanceAdapter(adapter)`, then per-pool instances are added with `addMetaInstances`. Each instance coordinate is the pool address widened to bytes32.


# Curve Gauges

Reports an account's gauge-staked Curve LP position across any configured gauge, plus all claimable rewards (CRV and extra reward tokens).

Reports an account's **gauge-staked** Curve LP position across any configured gauge, plus all claimable rewards (CRV and extra reward tokens). The staked LP is decomposed pro-rata into the pool's underlying coins using the same logic as the Pools adapter. Wallet-held (unstaked) LP is the separate [Curve Pools](/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters/curve-pools) adapter.

* **Type:** Meta-adapter
* **protocolSubId:** `keccak256("curve-gauges")`
* **Instance key:** the gauge address, widened to bytes32.
* **Source:** `CurveGaugesMetaBalanceAdapter.sol`

## Positions returned

| Leg             | PositionKind | isDebt  | Description                                                           |
| --------------- | ------------ | ------- | --------------------------------------------------------------------- |
| Staking         | `Staking`    | `false` | Pro-rata share of one pool coin, from the account's staked LP         |
| Rewards (CRV)   | `Rewards`    | `false` | Claimable CRV (lower-bound estimate); only when `CURVE_MINTER` is set |
| Rewards (extra) | `Rewards`    | `false` | Claimable amount of each extra reward token (V2+ gauges)              |

Unregistered, zero-amount, and asset-filtered tokens are skipped.

## Balance calculation

<figure><picture><source srcset="/files/aRq3tsZftgWoOfsGuny5" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2FACSmae2MK3aDZ7btoTfi%2Fmeta-curve-gauges.png?alt=media&amp;token=8a3ea617-6fd6-431e-bfda-5e92dc4c009f" alt="Flowchart of the Curve Gauges balance adapter: read calls derive position legs into PositionBalance entries."></picture><figcaption><p>Curve Gauges adapter: how the underlying balances reported to the NAV Calculator are derived.</p></figcaption></figure>

**Staking legs:** with `stakedLp = gauge.balanceOf(account)`, the gauge's `lp_token()` resolves the pool (`CurvePoolLib.getPool`: self-LP if it has `coins()`, else `minter()`, else Curve's own registry). Each coin amount is:

```
amount[i] = poolBalance[i] * stakedLp / totalSupply
```

{% hint style="info" %}
**Legacy StableSwap pools resolve via Curve's registry, and the answer is validated rather than trusted.** The two heuristics above both miss the oldest generation, on 3Crv, `coins()`, `coins(int128)` and `minter()` all revert, so the pool is looked up through Curve's `AddressProvider` → registry. The reply must then answer `coins()` **and** round-trip (the registry's LP token for that pool must be the LP token asked about), because a wrong or hostile answer would decompose the position against *another pool's* balances; an over-report, the one direction this family must never take. Every hop is a bounded, guarded staticcall, so a chain without an AddressProvider degrades to the previous behaviour instead of reverting.

If the pool still cannot be resolved, the adapter reports the **staked LP token itself** as one leg rather than dropping the principal. Reverting would not help: the meta base absorbs a non-gas revert and substitutes empty legs, producing the same silent zero by another route, so the visibility has to live in the returned data. Only *structural* failure triggers this; a filtered or unregistered coin does not.
{% endhint %}

**CRV rewards** use a view-safe lower bound:

```
crvLb = max(integrate_fraction(account) - minter.minted(account, gauge), 0)
```

This is stale between checkpoints (deposit / withdraw / claim) and reads 0 for recently-transferred gauge tokens; for an exact value, `gauge.claimable_tokens(account)` must be called as a state mutation (eth\_call). The CRV leg is emitted only when `CURVE_MINTER != address(0)` (mainnet) and CRV is registered.

**Extra rewards** come from `claimable_reward(account, token)` for each `reward_tokens(i)` up to `MAX_COINS = 8` (CRV is skipped here, since it is handled via the Minter). V3+ gauges update these at most hourly, so values may be stale.

## Identity

* **positionId:** `abi.encode(address gauge)`
* **positionKind:** `Staking` · `Rewards`

There is no `labels` field on the position. The adapter implements `positionLabels(positionId)`, which returns `["Staking Gauge", "TOKEN0/TOKEN1/…"]` from the onchain coin symbols; surfaced only by the verbose reads (`getAccountPositionsVerbose` / `getAccountNavVerbose`); the full breadcrumb is `[protocolName, ...labels]`.

## Constructor

```solidity
constructor(address minter_, address crvToken_, address navCalculator_)
```

| Parameter        | Description                                                                                   |
| ---------------- | --------------------------------------------------------------------------------------------- |
| `minter_`        | Curve Minter for CRV emission; pass `address(0)` to disable CRV tracking (L2 / non-mainnet)   |
| `crvToken_`      | CRV token address; `address(0)` to skip CRV                                                   |
| `navCalculator_` | NAVCalculator address (must be a contract); source of the asset registry and the instance set |

On L2 chains (Arbitrum, Optimism, Base, Polygon) pass `minter_ = address(0)`; LP decomposition is always attempted regardless of the minter.

## Registration

Registered as a meta-adapter via `addMetaBalanceAdapter(adapter)`, then per-gauge instances are added with `addMetaInstances`. Each instance coordinate is the gauge address widened to bytes32.


# Convex

The Convex adapter reports an account's Convex-staked Curve LP positions across any configured Convex pool, plus all claimable rewards (CRV, CVX, and extra tokens).

The Convex adapter (`ConvexCurveLPStakingMetaBalanceAdapter`) reports an account's Convex-staked Curve LP positions across any configured Convex pool, plus all claimable rewards (CRV, CVX, and extra tokens). A single immutable meta-adapter serves every pool; the active set of Convex rewarders lives on NAVCalculator.

* **Type:** Meta-adapter
* **protocolSubId:** `keccak256("convex-curve-lp-staking")`
* **Instance key:** the Convex `BaseRewardPool` (rewarder) address, widened to bytes32. The underlying Curve LP token and pool are resolved onchain via `rewarder.pid()` → `Booster.poolInfo(pid).lptoken` → `CurvePoolLib.getPool(lpToken)`.
* **Source:** `ConvexCurveLPStakingMetaBalanceAdapter.sol`

## Positions returned

Per rewarder instance:

| Leg                                      | PositionKind | isDebt  | Description                                                                                                                                                         |
| ---------------------------------------- | ------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Staked LP coin (one per Curve pool coin) | `Staking`    | `false` | Pro-rata share of each Curve pool coin backing the staked LP                                                                                                        |
| Staked LP token (fallback)               | `Staking`    | `false` | The LP token itself, emitted **instead** of the per-coin legs when the Curve pool cannot be resolved structurally, so the principal is reported rather than dropped |
| CRV reward                               | `Rewards`    | `false` | Claimable CRV (main reward token)                                                                                                                                   |
| CVX reward                               | `Rewards`    | `false` | CVX derived from CRV via the cliff schedule (skipped if `CVX_TOKEN == address(0)`)                                                                                  |
| Extra reward (up to 8)                   | `Rewards`    | `false` | Each enumerated extra reward token; stash wrappers unwrapped to the underlying                                                                                      |

Legs with zero amount, unregistered assets, or assets excluded by `assetFilter` are dropped.

## Balance calculation

<figure><picture><source srcset="/files/fWEdh0aHRWAouHCgCXYU" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2F675QIhe8CQvujVsXU2eF%2Fmeta-convex.png?alt=media&amp;token=c848e22c-e7d2-48db-a49e-beba84e33d0c" alt="Flowchart of the Convex balance adapter: read calls derive position legs into PositionBalance entries."></picture><figcaption><p>Convex adapter: how the underlying balances reported to the NAV Calculator are derived.</p></figcaption></figure>

**Staking legs**: `stakedLp = rewarder.balanceOf(account)`. For each Curve pool coin `i`:

```
amount = poolBalance[i] × stakedLp / lpToken.totalSupply()
```

(`CurvePoolLib.proRataAmount`, using `getPoolBalance`, `getTotalSupply`, `getCoins`.)

**CRV reward:**

```
crvEarned = rewarder.earned(account)
```

This is exact and view-safe; Convex accounts for all pending CRV in `earned()`, so it is not a lower bound.

**CVX reward**, derived from `crvEarned` via the Convex cliff emission schedule:

```
cliff      = CVX.totalSupply() / 100_000e18           // reductionPerCliff
remaining  = 1_000 - cliff                            // totalCliffs = 1000
cvxAmount  = crvEarned × remaining / 1_000
cvxAmount  = min(cvxAmount, 100_000_000e18 - supply)  // cap at max supply
```

Returns 0 once `cliff >= 1000`.

**Extra reward legs**, for each extra rewarder up to 8: `amount = extraRewarder.earned(account)`. The reward token is unwrapped from any stash wrapper (`stash.token()` → underlying). Tokens equal to CRV are skipped (already reported); CVX via a stash wrapper is skipped (already derived in the CVX leg).

## Identity

* **positionId:** `abi.encode(address rewarder)`, one positionId per rewarder instance (all legs of an instance share it).
* **positionKind:** `Staking` for LP coins, `Rewards` for CRV / CVX / extra tokens.

There is no `labels` field on the position. The adapter implements `positionLabels(positionId)`, which returns the 3-segment breadcrumb `["Staking", "Curve LP", "TOKEN0/TOKEN1/…"]`, the third segment being the underlying Curve pool's coin symbols, derived onchain from the rewarder's LP token (collapses to `["Staking", "Curve LP"]` if unreadable). It is surfaced only by the verbose reads (`getAccountPositionsVerbose` / `getAccountNavVerbose`); the full breadcrumb is `[protocolName, ...labels]`.

## Constructor

```solidity
constructor(
    address booster_,
    address cvxToken_,
    address navCalculator_
)
```

| Parameter        | Description                                                                                                               |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `booster_`       | Convex Booster contract (resolves `lptoken` from a pool ID; mainnet `0xF403C135812408BFbE8713b5A23a04b3D48AAE31`)         |
| `cvxToken_`      | CVX token address (mainnet `0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B`); pass `address(0)` to skip CVX computation on L2 |
| `navCalculator_` | NAVCalculator contract for asset-registry access                                                                          |

## Registration

Register the adapter once with `addMetaBalanceAdapter`, then enable each pool with `addMetaInstances`. The instance coordinate is the Convex `BaseRewardPool` (rewarder) address widened to bytes32. The adapter holds no instance state; it reads its active set from `NAV_CALCULATOR.getMetaInstances(address(this))`.


# Uniswap V2

The Uniswap V2 adapter reports an account's V2 LP across any pair as the dilution-adjusted pro-rata share of both reserves, emitted as one Supplied leg per token.

The Uniswap V2 adapter reports an account's V2 LP across any pair as the dilution-adjusted pro-rata share of both reserves, emitted as one `Supplied` leg per token. A single immutable meta-adapter serves every pair; the active pair set lives on NAVCalculator.

* **Type:** Meta-adapter
* **protocolSubId:** `keccak256("uniswap-v2")`
* **Instance key:** the Uniswap V2 pair address, widened to bytes32.
* **Source:** `UniswapV2PoolsMetaBalanceAdapter.sol`

## Positions returned

Per pair instance, up to two legs:

| Leg          | PositionKind | isDebt  | Description                          |
| ------------ | ------------ | ------- | ------------------------------------ |
| token0 share | `Supplied`   | `false` | Account's pro-rata share of reserve0 |
| token1 share | `Supplied`   | `false` | Account's pro-rata share of reserve1 |

Legs with zero amount, unregistered assets, or assets excluded by `assetFilter` are dropped.

## Balance calculation

<figure><picture><source srcset="/files/AfcICO5unZAwLVoZVsiH" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2FVxzvm2OQZAsO8v5jWyyQ%2Fmeta-uniswap-v2.png?alt=media&amp;token=3dd0f05f-4a77-4e01-986b-7df04c44a335" alt="Flowchart of the Uniswap V2 balance adapter: read calls derive position legs into PositionBalance entries."></picture><figcaption><p>Uniswap V2 adapter: how the underlying balances reported to the NAV Calculator are derived.</p></figcaption></figure>

Delegated to `UniswapV2PairLib.pairAmounts(pair, account)`. The account's LP balance is converted to underlying reserves against an **effective** supply that includes the protocol fee LP the pair would mint on its next mint/burn, so the reported amounts match what an LP would actually receive on `burn`:

```
balance         = pair.balanceOf(account)
totalSupply     = pair.totalSupply()
(r0, r1)        = pair.getReserves()
effectiveSupply = totalSupply + pendingProtocolMint        // V2 _mintFee dilution

amount0 = balance × r0 / effectiveSupply
amount1 = balance × r1 / effectiveSupply
```

The dilution term mirrors V2's internal `_mintFee`, the LP the pair would mint to `feeTo` on the next burn:

```
pendingProtocolMint = totalSupply × (√k − √kLast) × numFactor / (rootKCoeff·√k + rootKLastCoeff·√kLast)
```

The three coefficients `(numFactor, rootKCoeff, rootKLastCoeff)` come from a virtual `_mintFeeConstants()` hook, so byte-compatible forks can override them. Uniswap V2 (and SushiSwap, which shares them) use `(1, 5, 1)`, i.e. `(√k − √kLast) / (5·√k + √kLast)`, capturing 1/6 of fee growth.

It is 0 when `feeTo` is unset, `kLast == 0`, or `√k <= √kLast`. `feeTo()` and `kLast()` are read defensively, so forks omitting them are treated as having no protocol fee.

{% hint style="info" %}
**An absent `feeTo()` and a malformed one are treated differently**, because this is the one defensive read whose failure direction is **upward**: dropping the dilution term shrinks `effectiveSupply` and so *raises* the reported share. A read that **fails** means a fork without protocol fees, and reporting undiluted is correct; that is why the defensive read exists. A read that **succeeds** but returns a word that is not a valid address means a malformed factory: the dilution is unknowable, so the pair is **dropped** rather than valued on a guess. Treating the second case as "no protocol fee" over-reports the position.
{% endhint %}

## Identity

* **positionId:** `abi.encode(address pair)`, one positionId per pair instance.
* **positionKind:** `Supplied` for both legs.

There is no `labels` field on the position. The adapter implements `positionLabels(positionId)`, which returns `["AMM Liquidity Pool", "sym0/sym1"]` from the pair's onchain token symbols; surfaced only by the verbose reads (`getAccountPositionsVerbose` / `getAccountNavVerbose`); the full breadcrumb is `[protocolName, ...labels]`.

## Constructor

```solidity
constructor(
    address navCalculator_
)
```

| Parameter        | Description                                      |
| ---------------- | ------------------------------------------------ |
| `navCalculator_` | NAVCalculator contract for asset-registry access |

No protocol addresses are pinned; every pair is self-describing (`token0`, `token1`, `factory`, reserves are read from the pair itself).

## Registration

Register the adapter once with `addMetaBalanceAdapter`, then enable each pair with `addMetaInstances`. The instance coordinate is the Uniswap V2 pair address widened to bytes32. The adapter holds no instance state; it reads its active set from `NAV_CALCULATOR.getMetaInstances(address(this))`.


# Uniswap V3

A meta-adapter that reports an account's Uniswap V3 concentrated-liquidity LP across every configured pool, the in-range liquidity (Supplied) and the full uncollected fees (Fees) of each NFT position

A **meta-adapter** that reports an account's **Uniswap V3 concentrated-liquidity LP**, both the in-range liquidity (`Supplied`) and the full uncollected fees (`Fees`) of every NFT position the account holds. Each position is an ERC-721 minted by the NonfungiblePositionManager (NPM), which is `ERC721Enumerable`, so the adapter discovers an account's positions entirely onchain: `balanceOf(account)` + `tokenOfOwnerByIndex`, then `positions(tokenId)`, keeping the NFTs that belong to each configured pool. A single immutable contract serves every pool; the active pool set is governed on NAVCalculator.

* **Type:** [Meta-adapter](/funds/infrastructure/architecture/onchain-accounting)
* **protocolSubId:** `keccak256("uniswap-v3")` (brand `uniswap`, ecosystem `uniswap-v3`)
* **Instance key:** the Uniswap V3 pool address, widened to bytes32.
* **Source:** `UniswapV3PoolsMetaBalanceAdapter.sol`

## Positions returned

Per NFT the account holds in a configured pool, up to four legs (token0/token1 × liquidity/fees):

| Leg              | PositionKind | isDebt  | isLocked | Description                                                                    |
| ---------------- | ------------ | ------- | -------- | ------------------------------------------------------------------------------ |
| token0 liquidity | `Supplied`   | `false` | `false`  | token0 amount of the position's in-range liquidity                             |
| token1 liquidity | `Supplied`   | `false` | `false`  | token1 amount of the position's in-range liquidity                             |
| token0 fees      | `Fees`       | `false` | `false`  | Full uncollected token0 fees (settled `tokensOwed0` + `feeGrowthInside` delta) |
| token1 fees      | `Fees`       | `false` | `false`  | Full uncollected token1 fees (settled `tokensOwed1` + `feeGrowthInside` delta) |

**Both** pool tokens must be registered in NAVCalculator or the whole instance is dropped, NAV values an LP from the sum of its two legs, so a half-priced LP would silently understate it. Each pool's fan-out is isolated by the meta base's per-instance `try/catch`, so a bad/unfeeded instance discards only that pool, never the account's whole Uniswap V3 LP set. Legs with zero amount, or assets excluded by `assetFilter`, are dropped. `isLocked` is `false` for every leg; these are live LP positions, not a withdrawal queue. The NFT `tokenId` rides in each leg's `positionInstanceId`.

Per-account discovery is bounded by `MAX_POSITIONS` (1000). The count comes from the account's **full** NPM NFT balance (enumerated before the per-pool filter) and is attacker-inflatable, NPM positions are force-mintable and freely transferable, so the cap guards against a griefer spamming dust position NFTs to gas-exhaust the NAV read. Any position at enumeration index `>= MAX_POSITIONS` is omitted (an under-report); for any real holding the cap is a no-op.

## Balance calculation

<figure><picture><source srcset="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2FcWE2qH58rMeAIqW2OQ2U%2Fmeta-uniswap-v3_D.png?alt=media&#x26;token=bc7d88da-8d1d-4a6c-a3a0-7011a8add622" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2FNsqKyFcaqZee2al5GMCi%2Fmeta-uniswap-v3.png?alt=media&#x26;token=04beef6d-953c-4891-a738-dd5b0d0e27ff" alt="Flowchart of the Uniswap V3 balance adapter: read calls derive position legs into PositionBalance entries."></picture><figcaption><p>Uniswap V3 adapter: how the underlying balances reported to the NAV Calculator are derived.</p></figcaption></figure>

For each configured pool, the adapter enumerates the account's NPM NFTs, keeps those whose `(token0, token1, fee)` matches the pool, reads `NPM.positions(tokenId)` and the pool's live `slot0`, and derives the legs through `UniswapV3PoolLib.positionAmounts(...)`:

```
(liq0, liq1, fee0, fee1) = UniswapV3PoolLib.positionAmounts(
    pool, position, sqrtPriceX96, tick    // sqrtPriceX96, tick from pool.slot0()
)
```

**Liquidity legs (`Supplied`)**, the in-range token0/token1 amounts of the position's `liquidity` over its tick range, computed from the pool's **live `slot0`** price (`sqrtPriceX96`, `tick`), i.e. the instantaneous onchain withdrawable split, not an oracle reconstruction.

{% hint style="info" %}
**Why live `slot0` is safe here.** Under the **async NAV-consumer model**, a NAV reading is not settled against in the same transaction, so transiently imbalancing the pool to skew the reported amounts cannot be monetised, arbitrage restores the pool within the block, and the consumer never acts on the manipulated mid-read value. Reporting the real instantaneous split keeps the adapter consistent with how the Balancer/Curve LP adapters value pro-rata holdings.
{% endhint %}

**Fee legs (`Fees`)**, the **full uncollected** fees: the settled `tokensOwed0` / `tokensOwed1` credited at the position's last interaction **plus** the `feeGrowthInside` delta accrued since, computed from the pool's current tick and the position's tick range.

## Identity

* **positionId:** `abi.encode(address pool)`, the pool address (the instance coordinate). Each `(token0, token1, fee)` triple is its own deployed pool, so the pool address uniquely identifies pair + fee tier; all of an account's NFTs in the same pool share one positionId and **sum**.
* **positionInstanceId:** `bytes32(tokenId)`, the NFT id, the ephemeral per-item handle (excluded from the identity key). It is **not** in `positionId` and **not** in `labels`.
* **positionKind:** `Supplied` for liquidity legs, `Fees` for fee legs.
* **labels:** there is no `labels` field on the position. The adapter implements `positionLabels(positionId)`, which returns `["AMM Liquidity Pool", "<sym0>/<sym1> <fee>%"]` (e.g. `["AMM Liquidity Pool", "USDC/WETH 0.3%"]`); token symbols + fee tier read live from the pool, surfaced only by the verbose reads.

## Constructor

```solidity
constructor(
    address positionManager_,
    address navCalculator_
)
```

| Parameter          | Description                                                                                                                                         |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `positionManager_` | The per-chain canonical NonfungiblePositionManager (NPM) address, the chain-level constant, same for every V3 pool on the chain. Must be a contract |
| `navCalculator_`   | NAVCalculator contract for asset-registry access. Must be a contract                                                                                |

The pool addresses are **not** pinned; they are the governed instance set on NAVCalculator.

{% hint style="info" %}
**Licensing.** This adapter links the GPL-2.0-or-later Uniswap V3 math libraries (`TickMath`, `LiquidityAmounts`, `FixedPoint128` via `UniswapV3PoolLib`), so the combined work is GPL-2.0-or-later, hence the file's `GPL-2.0-or-later` SPDX identifier rather than the repo's default BUSL-1.1.
{% endhint %}

## Registration

Register the adapter once with `addMetaBalanceAdapter(adapter, pools)`, then enable each pool with `addMetaInstances(adapter, pools)` and drop coverage with `removeMetaInstances`. The instance coordinate is the Uniswap V3 pool address widened to bytes32. The adapter **holds no instance state; it** reads its active set from `NAV_CALCULATOR.getMetaInstances(address(this))`. See [Meta balance adapters](/funds/infrastructure/architecture/onchain-accounting).

## Chains

| Chain        | Adapter                                                                                                                  | NonfungiblePositionManager                               | Pools configured |
| ------------ | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | ---------------- |
| Ethereum (1) | [`0x2F7e8f78FD8EE977F6D53752b03A79c77012Ba52`](https://etherscan.io/address/0x2F7e8f78FD8EE977F6D53752b03A79c77012Ba52)  | `0xC36442b4a4522E871399CD717aBDD847Ab11FE88` (canonical) | 4                |
| Gnosis (100) | [`0xaC47D9Cbe8B3EDc9c9d1a6c8092054299bBC1adE`](https://gnosisscan.io/address/0xaC47D9Cbe8B3EDc9c9d1a6c8092054299bBC1adE) | `0xAE8fbE656a77519a7490054274910129c9244FA3`             | 7                |

The adapter takes the NPM as its only protocol argument, so its CREATE2 address is a pure function of that address: **one shared address across every chain hosting the canonical NPM, and a different one wherever the NPM differs.** Gnosis is the second case, hence two rows above rather than one. See [Deployment addresses](https://docs.kpk.io/funds/infrastructure/onchain-accounting/deployment-addresses#adapters-sharing-an-address-only-where-the-protocol-does).

{% hint style="danger" %}
**Gnosis hosts three Uniswap V3 deployments; identify the NPM by its `factory()`, never by `symbol()`.** All three NPMs answer `symbol() == "UNI-V3-POS"` and `supportsInterface(ERC721Enumerable) == true`, and all three were deployed by the same address, so neither the symbol nor the interface distinguishes them. Two are dead (`totalSupply == 0`): `0xd6053798…7537` behind factory `0x581e2A6e…`, and `0x49B53C35…2C71` behind factory `0x963Df249…`. The live one is `0xAE8fbE65…4FA3`, whose `factory()` is `0xe32F7dD7e3f098D518ff19A22d5f028e076489B1`, the address the [Gnosis analytics docs](https://docs.analytics.gnosis.io/protocols/dexes/uniswap-v3/) publish.
{% endhint %}

### Choosing the pool set

**Both tokens of a pool must be registered assets, or the whole instance is dropped**, NAV values an LP from the sum of its two legs, so a half-priced LP would silently understate it. Beyond that, the set should stay tight: `_instancePositions` re-enumerates an account's NFTs **once per configured pool**, so the read cost is `O(pools × NFTs)` and an empty pool is pure overhead on every NAV read for every account on that chain.

The Gnosis set was chosen by querying the factory for every pair × fee-tier combination of the chain's registered assets, 24 live pools, and keeping the 7 holding more than $10k, which together account for essentially all of the \~$3.4M across them. Adding one of the rest later is an `addMetaInstances` transaction, not a deploy, and the omission direction is safe: a position in an unconfigured pool is **skipped, never over-valued**.


# PancakeSwap V2

The PancakeSwap V2 adapter reports an account's V2 LP across any pair as the dilution-adjusted pro-rata share of both reserves, emitted as one Supplied leg per token.

The PancakeSwap V2 adapter reports an account's V2 LP across any pair as the dilution-adjusted pro-rata share of both reserves, emitted as one `Supplied` leg per token. PancakeSwap V2 shares the Uniswap V2 Pair/Factory ABI and pro-rata math, so this is a thin subclass of the [Uniswap V2](/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters/uniswap-v2) meta-adapter. It differs in two respects: the reported protocol slug, and its `_mintFee` protocol-fee fraction, which is **not** Uniswap's; it overrides the dilution coefficients accordingly. A single immutable meta-adapter serves every pair; the active pair set lives on NAVCalculator.

* **Type:** Meta-adapter
* **protocolSubId:** `keccak256("pancakeswap-v2")`
* **Instance key:** the PancakeSwap V2 pair address, widened to bytes32.
* **Source:** `PancakeSwapV2PoolsMetaBalanceAdapter.sol`

## Positions returned

Per pair instance, up to two legs:

| Leg          | PositionKind | isDebt  | Description                          |
| ------------ | ------------ | ------- | ------------------------------------ |
| token0 share | `Supplied`   | `false` | Account's pro-rata share of reserve0 |
| token1 share | `Supplied`   | `false` | Account's pro-rata share of reserve1 |

Legs with zero amount, unregistered assets, or assets excluded by `assetFilter` are dropped.

## Balance calculation

<figure><picture><source srcset="/files/vNIQPeqv1bgKAqqmWyNC" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2F6ztQtFYGGoCwUi4xZg4k%2Fmeta-pancakeswap-v2.png?alt=media&amp;token=3b056147-8936-41dc-8b3c-be1bca94f7a7" alt="Flowchart of the PancakeSwap V2 balance adapter: read calls derive position legs into PositionBalance entries."></picture><figcaption><p>PancakeSwap V2 adapter: how the underlying balances reported to the NAV Calculator are derived.</p></figcaption></figure>

Delegated to `UniswapV2PairLib.pairAmounts(pair, account, ...)`, same as [Uniswap V2](/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters/uniswap-v2) but with PancakeSwap's own `_mintFee` coefficients. The account's LP balance is converted to underlying reserves against an effective supply that includes the protocol fee LP the pair would mint on its next mint/burn:

```
balance         = pair.balanceOf(account)
totalSupply     = pair.totalSupply()
(r0, r1)        = pair.getReserves()
effectiveSupply = totalSupply + pendingProtocolMint        // V2 _mintFee dilution

amount0 = balance × r0 / effectiveSupply
amount1 = balance × r1 / effectiveSupply
```

with PancakeSwap V2's `_mintFee` coefficients `(numFactor, rootKCoeff, rootKLastCoeff) = (8, 17, 8)`, overriding Uniswap's `(1, 5, 1)` via the `_mintFeeConstants()` hook:

```
pendingProtocolMint = totalSupply × (√k − √kLast) × 8 / (17·√k + 8·√kLast)
```

PancakeSwap's protocol-fee fraction is larger than Uniswap's; using Uniswap's `(1, 5, 1)` constants would understate the dilution LP and slightly overstate the holder's reserve share whenever PancakeSwap's `feeTo` is set.

It is 0 when `feeTo` is unset, `kLast == 0`, or `√k <= √kLast` (read defensively).

## Identity

* **positionId:** `abi.encode(address pair)`, one positionId per pair instance (same schema as Uniswap V2).
* **positionKind:** `Supplied` for both legs.

There is no `labels` field on the position. The adapter implements `positionLabels(positionId)`, which returns `["AMM Liquidity Pool", "sym0/sym1"]` from the pair's onchain token symbols; surfaced only by the verbose reads (`getAccountPositionsVerbose` / `getAccountNavVerbose`); the full breadcrumb is `[protocolName, ...labels]`.

## Constructor

```solidity
constructor(
    address navCalculator_
)
```

| Parameter        | Description                                      |
| ---------------- | ------------------------------------------------ |
| `navCalculator_` | NAVCalculator contract for asset-registry access |

No protocol addresses are pinned; every pair is self-describing.

## Registration

Register the adapter once with `addMetaBalanceAdapter`, then enable each pair with `addMetaInstances`. The instance coordinate is the PancakeSwap V2 pair address widened to bytes32. The adapter holds no instance state; it reads its active set from `NAV_CALCULATOR.getMetaInstances(address(this))`.


# PancakeSwap V3

A meta-adapter that reports an account's PancakeSwap V3 concentrated-liquidity LP across every configured pool, the in-range liquidity (Supplied) and the full uncollected fees (Fees) of each NFT posi

A **meta-adapter** that reports an account's **PancakeSwap V3 concentrated-liquidity LP**, both the in-range liquidity (`Supplied`) and the full uncollected fees (`Fees`) of every NFT position the account holds. PancakeSwap V3 is a Uniswap V3 fork; this adapter subclasses the [Uniswap V3](/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters/uniswap-v3) meta-adapter. It differs in exactly two ways: it reports the `pancakeswap-v3` brand/product slug instead of `uniswap-v3`, and it overrides the `_slot0` read to handle PancakeSwap's ABI divergence (below). Everything else, the onchain NPM enumeration, the live-`slot0` pricing, the full-fee accounting, the per-pool fault isolation, is identical.

* **Type:** Meta-adapter
* **protocolSubId:** `keccak256("pancakeswap-v3")` (brand `pancakeswap`, ecosystem `pancakeswap-v3`)
* **Instance key:** the PancakeSwap V3 pool address, widened to bytes32.
* **Source:** `PancakeSwapV3PoolsMetaBalanceAdapter.sol`

{% hint style="info" %}
This subclass overrides the `_slot0` read because PancakeSwap declares `slot0().feeProtocol` as `uint32` vs Uniswap's `uint8`, so the strict full-tuple decode needs a PancakeSwap-specific ABI. The base reads the pool's live `slot0` to compute instantaneous in-range amounts, so this override is what lets the shared valuation path work unchanged against PancakeSwap pools. It changes how the tuple is *decoded*, not which price is used.
{% endhint %}

## Positions returned

Per NFT the account holds in a configured pool, up to four legs (token0/token1 × liquidity/fees):

| Leg              | PositionKind | isDebt  | isLocked | Description                                                                    |
| ---------------- | ------------ | ------- | -------- | ------------------------------------------------------------------------------ |
| token0 liquidity | `Supplied`   | `false` | `false`  | token0 amount of the position's in-range liquidity                             |
| token1 liquidity | `Supplied`   | `false` | `false`  | token1 amount of the position's in-range liquidity                             |
| token0 fees      | `Fees`       | `false` | `false`  | Full uncollected token0 fees (settled `tokensOwed0` + `feeGrowthInside` delta) |
| token1 fees      | `Fees`       | `false` | `false`  | Full uncollected token1 fees (settled `tokensOwed1` + `feeGrowthInside` delta) |

**Both** pool tokens must be registered in NAVCalculator or the whole instance is dropped (the liquidity split needs **both** prices). Each pool's fan-out is isolated by the meta base's per-instance `try/catch`, so a bad/unfeeded instance discards only that pool, never the account's whole LP set. Legs with zero amount, or assets excluded by `assetFilter`, are dropped. `isLocked` is `false` for every leg. The NFT `tokenId` rides in each leg's `positionInstanceId`. Per-account discovery is bounded by `MAX_POSITIONS` (1000); see [Uniswap V3](/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters/uniswap-v3#positions-returned).

## Balance calculation

<figure><picture><source srcset="/files/yBX7YNULDHIfmPVpdIet" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2F9fPjSaQvQhGUdtIEkxG7%2Fmeta-pancakeswap-v3.png?alt=media&amp;token=95a47da4-3192-49f1-bc7b-35463f80164a" alt="Flowchart of the PancakeSwap V3 balance adapter: read calls derive position legs into PositionBalance entries."></picture><figcaption><p>PancakeSwap V3 adapter: how the underlying balances reported to the NAV Calculator are derived.</p></figcaption></figure>

Identical to [Uniswap V3](/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters/uniswap-v3#balance-calculation), except for the `_slot0` decode override. For each configured pool the adapter enumerates the account's NPM NFTs, keeps those in the pool, reads `NPM.positions(tokenId)` and the pool's live `slot0`, and derives the legs via `UniswapV3PoolLib.positionAmounts(...)`.

**Liquidity legs (`Supplied`)**, the in-range token0/token1 amounts computed from the pool's **live `slot0`** price (`sqrtPriceX96`, `tick`), the instantaneous onchain withdrawable split. See the [Uniswap V3 note](/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters/uniswap-v3#balance-calculation) on why reading the live spot is safe under the async NAV-consumer model.

**Fee legs (`Fees`)**, the **full uncollected** fees: settled `tokensOwed0` / `tokensOwed1` **plus** the `feeGrowthInside` delta accrued since the position's last interaction, from the pool's current tick.

## Identity

* **positionId:** `abi.encode(address pool)`, the pool address (the instance coordinate). Each fee tier is its own pool; all of an account's NFTs in the same pool share one positionId and **sum**.
* **positionInstanceId:** `bytes32(tokenId)`, the NFT id, the ephemeral per-item handle (excluded from the identity key); not in `positionId` and not in `labels`.
* **positionKind:** `Supplied` for liquidity legs, `Fees` for fee legs.
* **labels:** there is no `labels` field on the position. The adapter implements `positionLabels(positionId)`, which returns `["AMM Liquidity Pool", "<sym0>/<sym1> <fee>%"]`; token symbols + fee tier read live from the pool, surfaced only by the verbose reads.

## Constructor

```solidity
constructor(
    address positionManager_,
    address navCalculator_
)
```

| Parameter          | Description                                                                                                             |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| `positionManager_` | The PancakeSwap V3 NonfungiblePositionManager (canonical CREATE2 address), the chain-level constant. Must be a contract |
| `navCalculator_`   | NAVCalculator contract for asset-registry access. Must be a contract                                                    |

The pool addresses are **not** pinned; they are the governed instance set on NAVCalculator. Like its base, this adapter is `GPL-2.0-or-later` (it links the Uniswap V3 math libraries).

## Registration

Register the adapter once with `addMetaBalanceAdapter(adapter, pools)`, then enable each pool with `addMetaInstances(adapter, pools)` and drop coverage with `removeMetaInstances`. The instance coordinate is the PancakeSwap V3 pool address widened to bytes32. The adapter reads its active set from `NAV_CALCULATOR.getMetaInstances(address(this))`. See Meta balance adapters.


# Wallet Yield

Seventeen immutable meta-adapters that report a wallet-held yield-bearing token (wstETH, sDAI, syrupUSDC, …) as the protocol position it is, at the same balance.

Seventeen immutable meta-adapters, one per protocol, that report a **yield-bearing token held directly in a wallet** (wstETH, weETH, sDAI, syrupUSDC and the like) as the protocol position it actually is, rather than as an anonymous wallet balance. The amount and value are exactly what the [default adapter](/funds/infrastructure/architecture/onchain-accounting/balance-adapters/erc20-default) would report; only the identity fields change. Which tokens each adapter covers is a governed instance set on the NAV Calculator.

* **Type:** Meta-adapter (one per protocol, sharing one base)
* **protocolSubId:** one per adapter; see [Adapters](#adapters) below
* **Instance key:** the token address, widened to `bytes32`
* **Source:** `WalletTokenMetaBalanceAdapterBase.sol` · `walletYield/`

## Positions returned

One position per configured token the account holds.

| Leg               | PositionKind            | isDebt  | Description                                 |
| ----------------- | ----------------------- | ------- | ------------------------------------------- |
| Wallet-held token | `Staking` or `Supplied` | `false` | The token's raw wallet balance, unconverted |

The kind is fixed per adapter and follows how the protocol exits at par: `Staking` when exiting means an unstake (a withdrawal queue, cooldown, or validator exit), `Supplied` when it means redeeming deposit or vault shares. A token returns an empty result (rather than reverting) when it is unregistered, excluded by `assetFilter`, or the balance is zero, so one bad token never zeroes out the rest of the set.

## Balance calculation

```
amount = token.balanceOf(account)   // raw balance, no conversion; the token itself is the priced asset
```

The `balanceAsset` is the **token itself**, not an underlying. These tokens are already registered and priced NAV assets, so the pricing path is untouched and the reported value is identical to the default adapter's. This is deliberately unlike the ERC-4626 vault adapters, which convert shares to an underlying because the share is not a registered asset there.

{% hint style="info" %}
**A token is reported by at most one adapter.** Claiming a token as an instance removes it from the default adapter's inclusion list in the same transaction. As a second guard, these adapters report **nothing** for a token the default adapter still includes, so if the two ever disagree the holding is under-reported rather than double-counted. An unreadable answer counts as 'still included'; a check that runs out of gas raises `DefaultAdapterProbeGasExhausted`, which the adapter's instance fan-out re-raises as `InstanceGasExhausted` instead of silently dropping the holding; see [a starved read is not an empty read](/funds/infrastructure/architecture/onchain-accounting/balance-adapters#how-an-adapter-is-built).
{% endhint %}

## Identity

* **positionId:** `abi.encode(address token)` (the token address)
* **positionKind:** `Staking` or `Supplied`, per adapter

There is no `labels` field on the position. The adapter implements `positionLabels(positionId)`, which returns `["<category>", "<token symbol>"]` (for example `["LST", "wstETH"]` or `["Savings", "sDAI"]`), collapsing to `["<category>"]` if the symbol cannot be read. It is surfaced only by the verbose reads (`getAccountPositionsVerbose` / `getAccountNavVerbose`); the full breadcrumb is `[protocolName, ...labels]`.

## Adapters

| Adapter                               | Tokens (chain)                                          | PositionKind | `protocolSubId`                | Label category |
| ------------------------------------- | ------------------------------------------------------- | ------------ | ------------------------------ | -------------- |
| `LidoStakedEthWalletMeta`             | wstETH, stETH (1) · wstETH (10, 100, 8453, 42161)       | `Staking`    | `lido-staked-eth`              | LST            |
| `EtherFiStakedEthWalletMeta`          | eETH, weETH (1) · weETH (8453, 42161)                   | `Staking`    | `etherfi-staked-eth`           | LRT            |
| `RocketPoolStakedEthWalletMeta`       | rETH (1)                                                | `Staking`    | `rocket-pool-staked-eth`       | LST            |
| `RenzoRestakedEthWalletMeta`          | ezETH (1, 42161)                                        | `Staking`    | `renzo-restaked-eth`           | LRT            |
| `KelpRestakedEthWalletMeta`           | rsETH (1, 42161)                                        | `Staking`    | `kelp-restaked-eth`            | LRT            |
| `StaderStakedEthWalletMeta`           | ETHx (1)                                                | `Staking`    | `stader-staked-eth`            | LST            |
| `StakeWiseV3OsTokenWalletMeta`        | osETH (1) · osGNO (100)                                 | `Staking`    | `stakewise-v3-os-tokens`       | LST            |
| `LiquidCollectiveStakedEthWalletMeta` | LsETH (1)                                               | `Staking`    | `liquid-collective-staked-eth` | LST            |
| `AnkrStakedEthWalletMeta`             | ankrETH (1)                                             | `Staking`    | `ankr-staked-eth`              | LST            |
| `CoinbaseStakedEthWalletMeta`         | cbETH (8453)                                            | `Staking`    | `coinbase-staked-eth`          | LST            |
| `EthenaSavingsWalletMeta`             | sUSDe (1)                                               | `Staking`    | `ethena-savings`               | Savings        |
| `ReserveBasketsWalletMeta`            | ETH+ (1)                                                | `Supplied`   | `reserve-baskets`              | LST            |
| `SkySavingsWalletMeta`                | sDAI, sUSDS (1) · sDAI (100)                            | `Supplied`   | `sky-savings`                  | Savings        |
| `AaveSavingsWalletMeta`               | sGHO (1)                                                | `Supplied`   | `aave-v3-savings`              | Savings        |
| `SparkSavingsWalletMeta`              | spETH, spUSDC, spUSDT (1)                               | `Supplied`   | `spark-savings`                | Savings        |
| `MapleSavingsWalletMeta`              | syrupUSDC, syrupUSDG, syrupUSDT (1) · syrupUSDC (42161) | `Supplied`   | `maple-savings`                | Savings        |
| `NexusMutualVaultsWalletMeta`         | RWIV (1)                                                | `Supplied`   | `nexus-mutual-vaults`          | Vault          |

Each adapter's `protocolId` is the protocol slug (`lido`, `etherfi`, `sky`, …, with `aave-v3` for Aave and `stakewise-v3` for StakeWise); see the [protocol taxonomy catalogue](/funds/infrastructure/architecture/onchain-accounting/concepts/position-identity#protocol-taxonomy-catalogue). Every one takes only the NAV proxy as a constructor argument, so each sits at the **same address on every chain** (★); see [Deployment addresses](/funds/infrastructure/architecture/onchain-accounting/deployment-addresses).

## Constructor

```solidity
constructor(address navCalculator_)
```

| Parameter        | Description                                             |
| ---------------- | ------------------------------------------------------- |
| `navCalculator_` | NAV Calculator contract (asset registry + instance set) |

The constructor also resolves the NAV's default adapter once and refuses to deploy (`DefaultAdapterUnresolvable`) if it cannot, since the double-count guard above depends on it.

## Registration

Meta-adapter. Register once and seed its tokens with `addMetaBalanceAdapter(adapter, instances)`, then adjust with `addMetaInstances(adapter, instances)` / `removeMetaInstances(adapter, instances)` (MANAGER-gated). Each instance coordinate is a **token address widened to `bytes32`**; a token that is not a registered NAV asset simply reports nothing. Removing a token releases it but does **not** hand it back to the default adapter; re-include it with `includeAssetInDefaultAdapter`, or it is reported by nobody.


# Assisted balance adapters

Adapters for positions that can't be discovered onchain, the position owner feeds the coordinates itself, onchain and permissionlessly, valued live.

A handful of positions cannot be discovered or located purely from onchain state; they live behind a non-enumerable NFT (an ether.fi withdrawal-request `WithdrawRequestNFT`, a Nexus `StakingNFT`) or an opaque exit-queue ticket (StakeWise V3) with no owner→positions view. For these, an **assisted adapter** keeps a small cache of *position coordinates*; everything else, the amounts, the pricing, is still read **live** on every NAV query.

The coordinates are fed **permissionlessly and onchain by the position owner itself**: the mutators are keyed on `msg.sender`, so a caller can only ever touch its own set. The canonical feeder is a stateless **assistant** contract the owner Safe `delegatecall`s; a thin **wrapper you use in place of the protocol's own deposit/withdraw call**. It performs that same protocol operation and, in the same transaction, records (or removes) the resulting position's coordinate on the adapter, so the otherwise-invisible position is tracked by NAV. See [The delegatecall assistant](#the-delegatecall-assistant).

* **Base class:** `AssistedCoordCache`, the single cache base for the whole family
* **Marker interface:** `IAssistedBalanceAdapter`
* **Source:** `AssistedCoordCache.sol` · `IAssistedBalanceAdapter.sol`

{% hint style="info" %}
**Permissionless; no roles.** The family has no roles and no `AccessControl`. Because the invariant is upheld by live re-verification alone (below), a role would add nothing: a caller can only ever touch its own coordinates, and a faulty feeder can only under-report. Each account feeds its own coordinates.
{% endhint %}

## Trust model

The family invariant makes the feeder **minimally trusted**: it caches **only coordinates, never amounts**.

* Every read re-values the position **live** from onchain state and **re-verifies ownership**.
* So a faulty, stale, or hostile feeder can only **under-report** (omit a position); never over-report or fabricate value.
* **Self-scoped by `msg.sender`.** The `submit` / `remove` / `clearAccount` mutators take no `account` argument and derive it from `msg.sender`, so a caller can only ever add or remove *its own* coordinates. Nobody can touch another account's set, which is why the family needs no roles at all.
* If nobody feeds an account's coordinates, its positions read as `0` until they're submitted; NAV under-counts but is never inflated.

{% hint style="success" %}
**For the portfolio Safes the cache is complete by construction, not merely conservative**, so the under-report above is the worst case in the abstract, not the operating reality. Two guarantees compose, at different levels:

The Safes' roles-modifier permits these protocol entries **only through the assistant contracts**; the protocol's own entrypoint is not in the allow-list. A position of one of these types therefore *cannot be opened without its coordinate being fed to the adapter* in the same transaction. Note the second guarantee lives in the Safe's permission configuration, not in these contracts: it holds for the governed portfolio Safes, and an arbitrary account feeding its own coordinates has only the contract-level guarantee.
{% endhint %}

| Level                              | Property                       | Enforced by                                                                    |
| ---------------------------------- | ------------------------------ | ------------------------------------------------------------------------------ |
| **Contract** (this system)         | cannot **over**-report         | the coordinate cache plus each adapter's live valuation and ownership re-check |
| **Roles** (the Safe's permissions) | cannot **under**-report either | the roles-modifier allow-list                                                  |

* Each cached coordinate is valued in **isolation**: a single position that reverts while being priced (e.g. Nexus's revert-prone `StakingViewer.getTokens`) discards **only that coordinate**, never the account's whole set for the adapter, the base wraps each coordinate's valuation in a per-item self-`staticcall` + `try/catch` (fail-open). A coordinate that instead **runs out of gas** is not absorbed: the fan-out raises `CoordinateGasExhausted(adapter, coordId, stipend, consumed)`, because a starved read is not evidence of an empty position and silently dropping it would understate the account. See [a starved read is not an empty read](/funds/infrastructure/architecture/onchain-accounting/balance-adapters#how-an-adapter-is-built).

{% hint style="info" %}
This is the one sanctioned exception to the "adapters are immutable, view-only" rule, an assisted adapter holds a per-account coordinate cache and exposes `submit`/`remove` mutators. Every other adapter ([plain](/funds/infrastructure/architecture/onchain-accounting/balance-adapters) or [meta](/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters)) is a pure `view` reader the NAV Calculator invokes with `staticcall`. The read path here is still pure `view`; only the coordinate cache is written.
{% endhint %}

## The coordinate cache: `AssistedCoordCache`

One base serves every member. A position coordinate is an opaque `bytes` blob (`abi.encode(...)`), so the same cache handles both shapes the family uses today:

| Coordinate shape                                      | `abi.encode(...)`                              | Members           |
| ----------------------------------------------------- | ---------------------------------------------- | ----------------- |
| A bare `uint256` (NFT tokenId / withdrawal requestId) | `abi.encode(id)`                               | ether.fi, Nexus   |
| A tuple                                               | `abi.encode(vault, positionTicket, timestamp)` | StakeWise V3 exit |

The base provides the `msg.sender`-keyed CRUD (blobs deduped by `keccak256`, with an O(1) swap-pop removal), the single-underlying-asset `IBalanceAdapter` getter surface, and the per-coordinate fail-open valuation fan-out. Each concrete adapter adds a **typed** `submit` / `remove` that `abi.encode`s its coordinate and implements the protocol-specific hooks: `_valueCoord` (decode + value live), `_validateCoord` (write-time hygiene), `_removable` (liveness proxy), `_coordId`, `_maxCoords`, `_maxLegsPerCoord`.

### Write semantics: fail-loud

Writes are **fail-loud and atomic**; correctness never depends on them (reads re-verify regardless), but they keep the cache clean:

| On `submit(coord)`                                                            | Reverts                                        |
| ----------------------------------------------------------------------------- | ---------------------------------------------- |
| Coordinate not currently owned by the caller                                  | `CoordinateNotOwned(account, id)`              |
| Coordinate fails the adapter's protocol validation (e.g. unregistered target) | `InvalidCoordinate(account, id)`               |
| Cache already at the per-account cap                                          | `CoordinateCacheFull(account, maxCoordinates)` |
| Coordinate already cached                                                     | *(no-op: idempotent)*                          |

Ownership is re-validated on **every** `submit`, before the idempotent early-return, so resubmitting a coordinate the caller has since transferred away is rejected loud rather than silently kept.

| On `remove(coord)`                           | Reverts                            |
| -------------------------------------------- | ---------------------------------- |
| Coordinate not in the caller's cache         | `CoordinateNotCached(account, id)` |
| Coordinate still resolves to a live position | `CoordinateStillLive(account, id)` |

`remove` is deliberately **refused while a position is live**; only a claimed/consumed coordinate can be dropped, so a live position is never silently un-reported. (This is what the assistants rely on: they claim/withdraw *first*, then remove.)

`clearAccount()` wipes **all** of the caller's coordinates unconditionally; it does **not** honour the `CoordinateStillLive` guard, so any still-live positions stop being reported until re-submitted. Like everything else in the family it only ever under-reports the caller's own NAV; it is self-service GC, not a NAV-neutral operation.

## The delegatecall assistant

**An assistant is a thin wrapper around the protocol's own entry/exit functions; the channel you use to open or close one of these positions.** Normally you would interact with the protocol directly: stake into a Nexus Mutual pool via the pool's `depositTo`, enter ether.fi's withdrawal queue via the LiquidityPool's `requestWithdraw`, enter StakeWise's exit queue via the vault's `enterExitQueue`. The catch is that the resulting position, a non-enumerable NFT, an opaque queue ticket, is then **invisible to NAV**: nothing onchain links it back to the account, so an assisted adapter has no way to find it.

The assistant closes that gap. You run the **exact same operation through the assistant instead of the protocol contract**: it forwards the identical call to the protocol *and*, in the **same transaction**, records the resulting position's coordinate on the assisted adapter, so the position is tracked and priced by NAV from the moment it opens, and untracked when it closes. Same protocol action, plus automatic bookkeeping. This pairing, a wrapper that performs the action and an adapter that reports it, is the fundamental use case of the assistant/assisted family.

Mechanically, the adapter's cache is keyed on `msg.sender`, so *whoever calls `submit`/`remove` is the account being tracked*. The assistant is a stateless module (`BalanceAdapterAssistant` base) the owner Safe **`delegatecall`s**, so it runs in the Safe's own context:

```
Manager Safe ──execTransactionWithRole──▶ Portfolio Safe ──DELEGATECALL──▶ Assistant
                                                                          │  (runs in the Safe's context:
                                                                          │   address(this) == Portfolio Safe)
                                                                          ├──▶ Protocol   (msg.sender = Safe → enters/exits, owns the position)
                                                                          └──▶ Adapter    (msg.sender = Safe → caches the coordinate for the Safe)
```

Under `delegatecall`, `address(this)` is the Safe, so the protocol sees `msg.sender = Safe` (the position owner) and the adapter's `msg.sender`-keyed cache records against that same Safe, **no roles, no custody, no asset-forwarding**. The deposit/withdrawal and the cache update happen **atomically in one transaction**, so there is no staleness window.

Each assistant contributes these pieces from its base:

* `SELF` / `ADAPTER`, its own deployed address (pinned at construction) and the adapter it feeds, both immutable (readable even under delegatecall, since they live in the assistant's bytecode).
* `onlyDelegateCall`: a *direct* (non-delegatecall) call has `address(this) == SELF` and is rejected fail-loud with the canonical `NotDelegateCall`, rather than letting the protocol revert obscurely.
* `_requireSelf(account)`: for assistants whose forwarded protocol call carries an explicit `receiver`/`owner`, asserts it equals `address(this)` (the Safe); reverts `AssistantAccountMismatch` otherwise.
* **Best-effort untrack**: a withdraw/claim entrypoint forwards to the protocol **first**, then tries to `remove` the coordinate. Because removal must never block the underlying operation, the assistant swallows the two expected cache-state reverts, `CoordinateStillLive` (a partial op left the position live → keep it tracked) and `CoordinateNotCached` (nothing to untrack), and re-throws anything else. This is safe: a coordinate left cached only ever under-reports, since reads re-verify and re-value live.

{% hint style="warning" %}
**EOAs cannot use an assistant**; an EOA cannot `delegatecall`. The intended feeders are Safe accounts, and *which* Safe may run an assistant is governed at the **Safe/Zodiac Roles layer** (a role permitting a delegatecall to the assistant), not by the adapter. The adapter itself stays permissionless.
{% endhint %}

Anyone *may* also call `submit`/`remove` directly (e.g. to register a pre-existing position, or from a bespoke integration), since the mutators are permissionless; the assistant is just the ergonomic, atomic path.

## Error taxonomy

Shared across the family, defined in `src/errors.sol`:

| Error                                          | Raised when                                                               |
| ---------------------------------------------- | ------------------------------------------------------------------------- |
| `CoordinateNotOwned(account, id)`              | `submit` given a coordinate the caller doesn't own                        |
| `CoordinateCacheFull(account, max)`            | `submit` would grow the caller's cache past its cap                       |
| `CoordinateNotCached(account, id)`             | `remove` targeted a coordinate not in the caller's cache                  |
| `CoordinateStillLive(account, id)`             | `remove` targeted a coordinate that's still a live position               |
| `InvalidCoordinate(account, id)`               | `submit` failed the adapter's write-time protocol validation              |
| `NotDelegateCall()`                            | an assistant entrypoint was called directly instead of `delegatecall`ed   |
| `AssistantAccountMismatch(expected, provided)` | an assistant was told to act for an account other than the executing Safe |

## How a read works

```
for each cached coordinate (submitted by the account itself):
    re-verify the coordinate still belongs to the account   // live on-chain check; discard if not
    value it from current protocol state                    // live amount, never cached
    emit its position leg(s)                                 // per-item; positionInstanceId carries the coordinate id
```

The cache only answers *"which positions might this account have?"*; a question that has no onchain view function for these protocols. The *value* of each position is always derived fresh, and each coordinate is valued behind its own self-`staticcall` so one reverting position never drops the rest.

## Registration

From the NAV Calculator's perspective an assisted adapter is an ordinary `IBalanceAdapter`, so it is registered like a [plain adapter](/funds/infrastructure/architecture/onchain-accounting/balance-adapters):

| Action     | Call                                         |
| ---------- | -------------------------------------------- |
| **Add**    | `addBalanceAdapters([adapter])` (MANAGER)    |
| **Remove** | `removeBalanceAdapters([adapter])` (MANAGER) |

There is no NAV-Calculator-side instance set (that is the [meta-adapter](/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters) model). The coordinate cache is kept current by the position owner's own transactions, typically via the paired assistant, delegatecalled inside the same deposit/withdraw operation.

## Catalogue

| Adapter                                                                                                                               | Why it needs assisting                                                            | Coordinate the owner submits                       | Paired assistant                                                         |
| ------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------ |
| [ether.fi Withdrawal Queue](/funds/infrastructure/architecture/onchain-accounting/assisted-balance-adapters/etherfi-withdrawal-queue) | Withdrawal `WithdrawRequestNFT`s are not owner-enumerable                         | The request `tokenId` (`submit(uint256)`)          | `EtherFiWithdrawalQueueAssistant` (`requestWithdraw` / `claimWithdraw`)  |
| [Nexus Mutual Staking](/funds/infrastructure/architecture/onchain-accounting/assisted-balance-adapters/nexus-mutual-staking)          | `StakingNFT`s are not owner-enumerable                                            | The `StakingNFT` `tokenId` (`submit(uint256)`)     | `NexusMutualStakingPoolsAssistant` (`depositTo` / `withdraw`)            |
| [StakeWise V3 Exit Queue](/funds/infrastructure/architecture/onchain-accounting/assisted-balance-adapters/stakewise-v3-exit-queue)    | Exit positions are opaque `(vault, ticket, timestamp)` tuples with no enumeration | The exit tuple (`submit(address,uint256,uint256)`) | `StakeWiseV3ExitQueueAssistant` (`enterExitQueue` / `claimExitedAssets`) |

{% hint style="info" %}
**Related, non-assisted.** Some queue/LP positions don't need assisting because they *are* discoverable onchain: [Stader's withdrawal queue](/funds/infrastructure/architecture/onchain-accounting/balance-adapters/stader-withdrawal-queue) is a [plain adapter](/funds/infrastructure/architecture/onchain-accounting/balance-adapters) (its per-account request list is enumerable via `getRequestIdsByUser`), and [Uniswap V3](/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters/uniswap-v3) / [PancakeSwap V3](/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters/pancakeswap-v3) are [meta-adapters](/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters) (the NonfungiblePositionManager is `ERC721Enumerable`).
{% endhint %}


# ether.fi Withdrawal Queue

An assisted adapter that reports the in-flight ETH owed to an account in ether.fi's withdrawal queue, fed onchain by the owner Safe.

An **assisted** adapter that reports the **in-flight ETH owed to an account in ether.fi's withdrawal queue**. Requesting a withdrawal burns eETH/weETH and mints a `WithdrawRequestNFT`; the queued ETH is then invisible to NAV, because the NFT is not `ERC721Enumerable` and exposes no owner→tokenIds view; a pure-view adapter cannot discover the positions. The owner feeds the request `tokenId` itself (`submit(uint256)`, keyed on `msg.sender`); the adapter then values it entirely from live onchain reads, re-verifying ownership on every read.

* **Type:** [Assisted adapter](/funds/infrastructure/architecture/onchain-accounting/assisted-balance-adapters), permissionless, `msg.sender`-keyed (`AssistedCoordCache`)
* **protocolSubId:** `keccak256("etherfi-withdrawal")`
* **Source:** `EtherFiWithdrawalQueueAssistedBalanceAdapter.sol`
* **Assistant:** `EtherFiWithdrawalQueueAssistant.sol`

{% hint style="info" %}
The cache stores **only tokenIds, never amounts**, so the feeder cannot fabricate value; at worst it omits an id (NAV under-counts), never over-counts. See [Assisted balance adapters](/funds/infrastructure/architecture/onchain-accounting/assisted-balance-adapters) for the family trust model.
{% endhint %}

## Positions returned

One `Supplied` leg per owned cached request, all sharing one static positionId:

| Leg                   | PositionKind | isDebt  | isLocked | Description                                           |
| --------------------- | ------------ | ------- | -------- | ----------------------------------------------------- |
| Per finalised request | `Supplied`   | `false` | `false`  | Native ETH from a finalised tokenId, withdrawable now |
| Per pending request   | `Supplied`   | `false` | `true`   | Native ETH from a pending tokenId, still finalising   |

Each owned, still-open request becomes its own leg, denominated in native ETH (carried in `balanceAsset`), with `isLocked` set from that request's state (`isLocked = !finalized`). The per-request `WithdrawRequestNFT` tokenId rides in `positionInstanceId` (= `bytes32(tokenId)`), not in the label. A zero-amount leg is omitted; the position is dropped entirely if native ETH is not registered in NAVCalculator, if there are no owned open requests, or if filtered out by `assetFilter`. The balance methods (`getAdapterBalances`) still report the summed total.

## Feed surface & assistant

The adapter exposes a small, permissionless, `msg.sender`-keyed surface (no roles, no `account` argument):

| Function                      | Purpose                                                                                                                                                  |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `submit(uint256 tokenId)`     | Track `tokenId` for the caller (idempotent). Reverts `CoordinateNotOwned` if the caller doesn't own it, `CoordinateCacheFull` past `MAX_TOKEN_IDS` (256) |
| `remove(uint256 tokenId)`     | Stop tracking `tokenId` for the caller. Reverts `CoordinateNotCached` if untracked, `CoordinateStillLive` if still owned (not yet claimed)               |
| `tokenIdsOf(address account)` | View the candidate tokenIds cached for `account` (display/debug; not ownership-filtered)                                                                 |
| `clearAccount()`              | Drop **all** of the caller's cached ids (unconditional self-service GC)                                                                                  |

`_removable` is *"no longer owned by the caller"*: ether.fi's `claimWithdraw` **burns** the NFT, so a claimed id reads as unowned and becomes removable, while a still-owned (pending) id cannot be dropped (`CoordinateStillLive`). See the [family write semantics](/funds/infrastructure/architecture/onchain-accounting/assisted-balance-adapters#write-semantics-fail-loud).

### The assistant (`EtherFiWithdrawalQueueAssistant`)

**Use the assistant in place of ether.fi's LiquidityPool.** Instead of requesting a withdrawal directly by calling the LiquidityPool's `requestWithdraw` (which would leave the resulting `WithdrawRequestNFT` invisible to NAV), you delegatecall the assistant: it forwards the **same** `requestWithdraw` and, in the same transaction, `submit`s the returned `requestId` to the adapter. Claiming works the same way through `claimWithdraw`. It is a thin wrapper over ether.fi's own withdrawal channel; same action, plus the bookkeeping that makes the queued ETH visible to NAV.

| Entrypoint                        | Does                                                                                                                                  | Adapter call        |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
| `requestWithdraw(uint256 amount)` | Approves the LiquidityPool for the Safe's eETH and calls it with `recipient = the Safe`, minting the `WithdrawRequestNFT` to the Safe | `submit(requestId)` |
| `claimWithdraw(uint256 tokenId)`  | **Claims first** (burns the NFT, ETH to the Safe), **then** untracks best-effort                                                      | `remove(tokenId)`   |

Both are `onlyDelegateCall` (a direct call reverts `NotDelegateCall`). Under delegatecall `address(this)` is the Safe, so ether.fi sees `msg.sender = Safe` (which its claim path hard-requires: `ownerOf == msg.sender`) and the adapter caches against that same Safe. The `remove` on claim is **best-effort**; it never blocks the claim: the assistant swallows the expected cache-state reverts (`CoordinateNotCached` for an id acquired outside the assistant or already cleared, `CoordinateStillLive` if somehow not yet claimed) and re-throws anything else.

<figure><picture><source srcset="/files/YsUHOFA3VNzBUm4UQi5Q" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2FHn6kNDZbu61n1FKhGwna%2Fwithdraw-request-flow.png?alt=media&amp;token=5747206e-1c82-4ff8-9c6c-2d8f238185c6" alt="Flow to request an ether.fi withdrawal: the Manager Safe drives the Portfolio Safe to delegatecall the assistant, which burns eETH via the LiquidityPool and submits the resulting requestId to the adapter."></picture><figcaption><p><strong>Requesting a withdrawal.</strong> The Safe delegatecalls the assistant, which burns eETH via the LiquidityPool (minting the <code>WithdrawRequestNFT</code>) and <code>submit</code>s the returned requestId to the adapter: all in one transaction.</p></figcaption></figure>

<figure><picture><source srcset="/files/OEtUSDmkjHZwu3RaSzD0" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2F2Lfxe7sHrpSvXs43eYyL%2Fclaim-withdraw-flow.png?alt=media&amp;token=5c500890-e419-41e6-aa9f-548c600668f7" alt="Flow to claim an ether.fi withdrawal: the Safe delegatecalls the assistant, which claims (burning the NFT, ETH to the Safe) then best-effort removes the requestId from the adapter cache."></picture><figcaption><p><strong>Claiming.</strong> The assistant claims first, burning the NFT and sending ETH to the Safe, then best-effort <code>remove</code>s the requestId from the adapter's cache.</p></figcaption></figure>

## Balance calculation

<figure><picture><source srcset="/files/yIuUDHT0PVIaOEpn2wPK" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2F0T9BO9ijzDfxbeUhHqbf%2Fget-adapter-positions-liquidity.png?alt=media&amp;token=b7d6e0ac-5f0d-4f3e-b710-214c0b0fe44a" alt="Flowchart of the ether.fi Withdrawal Queue balance adapter: cached tokenIds are re-verified and valued into a PositionBalance entry."></picture><figcaption><p>ether.fi Withdrawal Queue adapter: how the underlying balances reported to the NAV Calculator are derived.</p></figcaption></figure>

For each cached `tokenId`, the adapter re-verifies that `account` still owns the request and emits one leg valued at the ETH owed:

```
for each cached tokenId owned by account:
    if nft.isFinalized(tokenId):
        amount = nft.getClaimableAmount(tokenId); finalized = true
    else:
        amount = min(amountOfEEth, LiquidityPool.amountForShare(shareOfEEth)) - feeGwei; finalized = false   // current estimate
    emit leg { amount, isLocked = !finalized, positionInstanceId = bytes32(tokenId) }
```

A request is claimable iff `nft.isFinalized(tokenId)`; those use the protocol's `getClaimableAmount` and emit a leg with `isLocked=false`. Pending requests are valued live as `min(amountOfEEth, amountForShare(shareOfEEth)) − feeGwei`, converting the request's locked shares to ETH at the current rate, and emit a leg with `isLocked=true`. Each cached id is valued in isolation behind the base's per-coordinate self-`staticcall` (every external read try/catches to 0), so a stale, invalid, or reverting id contributes nothing and never drops the rest.

## Identity

* **positionId:** `abi.encode(withdrawRequestNFT)` (static; the ether.fi `WithdrawRequestNFT` address)
* **positionKind:** `Supplied`
* **positionInstanceId:** `bytes32(tokenId)` (the per-request `WithdrawRequestNFT` id; ephemeral, burned on claim)
* **Labels:** the adapter implements `positionLabels(positionId)`, returning `["Withdrawal Queue", "eETH"]` (surfaced only on the verbose read path; full breadcrumb = `["ether.fi", "Withdrawal Queue", "eETH"]`). The tokenId is **not** in the label; it rides in `positionInstanceId`.

Every per-request leg shares this single static positionId; legs of the same lock state are told apart by `positionInstanceId`. The owed ETH rides in `balanceAsset`. The reconciliation key is `keccak256(abi.encode(chainId, protocolSubId, positionId, balanceAsset.asset, positionKind))`; it **excludes** both `isLocked` and `positionInstanceId`, so all of an account's owned requests (claimable and pending alike) reconcile into **one coordinate** and sum to the full ETH owed; read `isLocked` per leg for the claimable-vs-pending split.

## Constructor

```solidity
constructor(
    address withdrawRequestNft_,
    address liquidityPool_,
    address underlyingToken_,
    address navCalculator_
)
```

| Parameter             | Description                                                                                               |
| --------------------- | --------------------------------------------------------------------------------------------------------- |
| `withdrawRequestNft_` | ether.fi `WithdrawRequestNFT`, source of ownership + per-request data. Must be a contract                 |
| `liquidityPool_`      | ether.fi `LiquidityPool`, `amountForShare` converts a request's shares to current ETH. Must be a contract |
| `underlyingToken_`    | Payout asset, native ETH (ERC-7528 sentinel). Must be non-zero                                            |
| `navCalculator_`      | NAVCalculator address, used for asset-registry metadata. Must be a contract                               |

There is **no** `admin_` / `assistant_` argument; the family is permissionless. The paired `EtherFiWithdrawalQueueAssistant` is deployed separately, pinned to this adapter (plus the LiquidityPool, WithdrawRequestNFT, and eETH token) at its own construction.

## Registration

Registered as a plain adapter with `addBalanceAdapters([adapter])` and removed with `removeBalanceAdapters([adapter])`. The coordinate cache is then kept current by the position owner's own transactions, typically via the assistant, delegatecalled inside the same `requestWithdraw` / `claimWithdraw`.


# StakeWise V3 Exit Queue

An assisted adapter that reports the assets owed to an account in StakeWise V3's exit queue, fed onchain by the owner Safe.

An **assisted** adapter that reports the assets owed to an account in StakeWise V3's exit queue. When shares enter the queue, their value is invisible to NAV: a position is identified only by the `positionTicket` that `enterExitQueue` returns, and there is no per-account enumeration. The owner submits each `(vault, positionTicket, timestamp)` tuple, and the adapter values it from live onchain reads.

* **Type:** Assisted adapter, permissionless and keyed on `msg.sender`
* **protocolSubId:** `keccak256("stakewise-v3-exit")`
* **Source:** `StakeWiseV3ExitQueueAssistedBalanceAdapter.sol`
* **Assistant:** `StakeWiseV3ExitQueueAssistant.sol`

{% hint style="warning" %}
This is not the [StakeWise V3](/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters/stakewise-v3) page. That meta-adapter reports active staking positions; this one reports shares already on their way out. A Fund using StakeWise needs both to see its full exposure.
{% endhint %}

## Positions returned

Up to two legs per exit ticket, split by claimability. Both are `Supplied`, not debt, denominated in the staking asset (native ETH on mainnet), with `positionInstanceId = bytes32(positionTicket)`.

| Leg       | isLocked | Description                                                         |
| --------- | -------- | ------------------------------------------------------------------- |
| Claimable | `false`  | Exited assets past the claim delay, withdrawable now                |
| Locked    | `true`   | Still-queued shares, and exited assets still inside the claim delay |

A zero leg is omitted. The position is dropped if the staking asset is not registered in the NAV Calculator, if no live tickets are cached, or if `assetFilter` excludes it.

## Feed surface

The surface is permissionless, with no roles and no `account` argument. Each coordinate is a tuple.

| Function                                                           | Purpose                                                                                                                                                        |
| ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `submit(address vault, uint256 positionTicket, uint256 timestamp)` | Track one of the caller's exit positions. Reverts if the vault is not StakeWise-registered, the tuple is not owed to the caller, or the cache holds 256 tuples |
| `remove(address vault, uint256 positionTicket, uint256 timestamp)` | Stop tracking a position once it is claimed. Reverts while the position is still owed                                                                          |
| `positionsOf(address account)`                                     | View the cached tuples for an account (display only)                                                                                                           |
| `clearAccount()`                                                   | Drop all of the caller's cached tuples                                                                                                                         |

Exit positions have no `ownerOf`, so ownership is proven by `calculateExitedAssets(account, ...) > 0`: a tuple that is not the caller's resolves to zero and is rejected on `submit`.

### The assistant

Use the assistant instead of calling the vault directly. It is delegatecalled by the Safe, forwards the same StakeWise call, and keeps the adapter in step in the same transaction.

| Entrypoint                                                            | Does                                                            | Adapter call                                     |
| --------------------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------ |
| `enterExitQueue(vault, shares)`                                       | Enters the queue with the Safe as receiver and returns a ticket | `submit(vault, positionTicket, block.timestamp)` |
| `claimExitedAssets(vault, positionTicket, timestamp, exitQueueIndex)` | Claims exited assets to the Safe                                | `remove(vault, positionTicket, timestamp)`       |

Under delegatecall the vault sees the Safe as `msg.sender`, which StakeWise's claim path requires. Removal on claim is best effort and never blocks the claim: a partial claim leaves the ticket tracked, and an untracked ticket is ignored.

<figure><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2Fgit-blob-0b1a711a0c19b58362160de670afcc05f9dfe42d%2Fadapter-stakewise-v3-exit-queue-enter.svg?alt=media" alt="Flow to enter the StakeWise exit queue through the assistant."><figcaption><p><strong>Entering the exit queue.</strong> The Safe delegatecalls the assistant, which enters the queue and submits the ticket to the adapter in one transaction.</p></figcaption></figure>

<figure><picture><source srcset="/files/tH4CApfTwCCc4Hr36pES" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2FUjQk4Zz4zYwi6i6hz87k%2Fget-adapter-positions-vault.png?alt=media&amp;token=19cd5cef-9e1d-4f9b-ae7a-f21fbf90b966" alt="Flowchart of how the adapter values cached exit tuples."></picture><figcaption><p><strong>Balance calculation.</strong> How cached exit tuples are valued into position legs.</p></figcaption></figure>

{% hint style="info" %}
StakeWise re-queues a not-yet-checkpointed remainder under a **new** `positionTicket`. The caller must `submit` the new ticket separately, using the vault's re-queue event.
{% endhint %}

## Balance calculation

<figure><picture><source srcset="/files/puGgRJYczXukkk8tOIOY" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2FU3B2S1PDEeFmjQjPonzo%2Fclaim-exited-assets-flow.png?alt=media&amp;token=4669404d-6e64-43cc-b1fa-687628e71ff8" alt="Flow to claim exited assets through the assistant."></picture><figcaption><p><strong>Claiming.</strong> The assistant claims to the Safe, then removes the ticket once it is fully consumed.</p></figcaption></figure>

For each cached tuple the adapter calls the vault with `receiver = account` and splits the position:

```
idx = vault.getExitQueueIndex(positionTicket)          // -1: not yet checkpointed
(leftShares, _, exited) = vault.calculateExitedAssets(account, positionTicket, timestamp, idx)
queued = vault.convertToAssets(leftShares)             // valued at the current rate
if block.timestamp >= timestamp + EXIT_CLAIM_DELAY:
    claimable = exited;  locked = queued
else:
    claimable = 0;       locked = exited + queued
```

A ticket that is not yet checkpointed is fully pending and is reported as locked. Each tuple is valued in isolation, so one bad tuple never drops the rest.

## Identity

* **positionId:** `abi.encode(vault)`, one per vault
* **positionKind:** `Supplied`
* **positionInstanceId:** `bytes32(positionTicket)`, shared by both legs of a ticket
* **Labels:** `["Exit Queue", "<receiptSymbol>"]`, for example `["Exit Queue", "osETH"]`, shown on the verbose reads

The reconciliation key excludes `isLocked` and `positionInstanceId`, so all of an account's tickets for a vault reconcile into one coordinate and sum. Read `isLocked` per leg for the claimable and locked split.

## Constructor

```solidity
constructor(
    address vaultsRegistry_,
    address underlyingToken_,
    string memory receiptSymbol_,
    address navCalculator_,
    uint256 exitClaimDelay_
)
```

| Parameter          | Description                                                                                    |
| ------------------ | ---------------------------------------------------------------------------------------------- |
| `vaultsRegistry_`  | StakeWise `VaultsRegistry`, which gates the vaults the cache accepts                           |
| `underlyingToken_` | Staking asset the queue pays out: native ETH on mainnet. All accepted vaults must settle in it |
| `receiptSymbol_`   | Receipt token symbol for the label (`"osETH"` on mainnet, `"osGNO"` on Gnosis). May be empty   |
| `navCalculator_`   | NAV Calculator, used for asset-registry metadata                                               |
| `exitClaimDelay_`  | StakeWise's post-checkpoint claim delay in seconds (`86400` on V3 mainnet)                     |

There is no admin or assistant argument. The assistant is deployed separately and pinned to this adapter. On mainnet every StakeWise V3 vault settles in ETH, so do not deploy against a non-ETH `underlyingToken` on a chain with a different staking asset.

## Registration

Register with `addBalanceAdapters([adapter])` and remove with `removeBalanceAdapters([adapter])`. The owner's own transactions then keep the cache current, typically through the assistant.


# Nexus Mutual Staking

An assisted adapter that reports an account's staked NXM across Nexus Mutual V2 staking pools, per StakingNFT position the owner has submitted.

An **assisted** adapter that reports an account's **staked NXM across Nexus Mutual V2 staking pools**, emitted **per `StakingNFT` position** as NXM-denominated `Staking` and `Rewards` legs. Each stake is an ERC-721 `StakingNFT`; the NFT is not owner-enumerable and the StakingViewer exposes no owner→tokenIds view, so a pure-view adapter cannot discover which positions belong to an account. The owner feeds the `StakingNFT` `tokenId` itself (`submit(uint256)`, keyed on `msg.sender`); the adapter then values it entirely from live onchain reads, re-verifying ownership on every read.

* **Type:** Assisted adapter, permissionless, `msg.sender`-keyed (`AssistedCoordCache`)
* **protocolSubId:** `keccak256("nexus-mutual-staking-pools")`
* **Source:** `NexusMutualStakingPoolsAssistedBalanceAdapter.sol`
* **Assistant:** `NexusMutualStakingPoolsAssistant.sol`

{% hint style="info" %}
The cache stores **only tokenIds, never amounts**, so the feeder cannot fabricate value; at worst it omits an id (NAV under-counts), never over-counts. See Assisted balance adapters for the family trust model.
{% endhint %}

## Positions returned

Up to two legs **per `StakingNFT`** the account holds (a holder can have several, across several pools):

| Leg     | PositionKind | isDebt  | isLocked | Description                                                                                                  |
| ------- | ------------ | ------- | -------- | ------------------------------------------------------------------------------------------------------------ |
| Stake   | `Staking`    | `false` | `false`  | `activeStake + expiredStake`, staked NXM principal (expired tranches no longer earn but remain withdrawable) |
| Rewards | `Rewards`    | `false` | `false`  | Accrued, unclaimed NXM rewards for that position                                                             |

A leg with a zero amount is skipped. Positions are denominated in **NXM** and dropped if NXM is not registered in NAVCalculator. `isLocked` is `false` for both legs.

## Feed surface & assistant

The adapter exposes a small, permissionless, `msg.sender`-keyed surface (no roles, no `account` argument):

| Function                      | Purpose                                                                                                                                                  |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `submit(uint256 tokenId)`     | Track `tokenId` for the caller (idempotent). Reverts `CoordinateNotOwned` if the caller doesn't own it, `CoordinateCacheFull` past `MAX_TOKEN_IDS` (256) |
| `remove(uint256 tokenId)`     | Stop tracking `tokenId` for the caller. Reverts `CoordinateNotCached` if untracked, `CoordinateStillLive` if the position is still live                  |
| `tokenIdsOf(address account)` | View the candidate tokenIds cached for `account` (display/debug; not ownership-filtered)                                                                 |
| `clearAccount()`              | Drop **all** of the caller's cached ids (unconditional self-service GC)                                                                                  |

{% hint style="warning" %}
**`_removable` is value-based.** Nexus does **not** burn the `StakingNFT` on withdraw, so "no longer owned" is not a reliable liveness proxy. An id is removable only once it is either no longer owned by the account **or** fully emptied (no active/expired stake and no rewards). A **partial** or rewards-only withdraw leaves a live position that stays tracked; on an unresolvable `getTokens` read the adapter errs toward *keeping* the id, so a still-staked NFT is never dropped.
{% endhint %}

### The assistant (`NexusMutualStakingPoolsAssistant`)

**Use the assistant in place of the Nexus staking-pool contract.** Instead of staking directly by calling the pool's `depositTo` (which would leave the resulting `StakingNFT` invisible to NAV), you delegatecall the assistant: it forwards the **same** `depositTo` to the pool and, in the same transaction, `submit`s the minted `tokenId` to the adapter so the stake is tracked. Withdrawing works the same way through `withdraw`. It is a thin wrapper over Nexus's own staking channel; same action, plus the bookkeeping that makes the position visible to NAV.

| Entrypoint                                                                     | Does                                                                                                                                                      | Adapter call                 |
| ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
| `depositTo(poolAddress, amount, trancheId, requestTokenId)`                    | Approves the Nexus `TokenController` for the Safe's own NXM and stakes it (`destination = the Safe`), minting/topping-up a `StakingNFT` owned by the Safe | `submit(tokenId)`            |
| `withdraw(poolAddress, tokenId, withdrawStake, withdrawRewards, trancheIds[])` | Withdraws stake and/or rewards to the Safe, then tries to `remove(tokenId)`                                                                               | `remove(tokenId)`; see below |

Both are `onlyDelegateCall`. Because the module runs in the Safe's context, the Safe holds the NXM, is the Nexus member, and owns the minted NFT, no custody or asset-forwarding. On `withdraw` the id is untracked **best-effort**; removal never blocks the withdraw itself: a partial / rewards-only withdraw leaves live stake (adapter reverts `CoordinateStillLive` → keep it tracked), and an id that isn't tracked reverts `CoordinateNotCached` (nothing to untrack); the assistant **swallows** both and re-throws anything else. So the id is effectively dropped only once the position is fully emptied.

<figure><picture><source srcset="/files/aaxRBQm2mROMhNaagCat" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2FTCBRwjic5Goan3kq2KVW%2Fdeposit-to-flow.png?alt=media&amp;token=49f6c805-b34a-4e55-b871-d1986b3cbdf9" alt="Flow to stake into Nexus: the Manager Safe drives the Portfolio Safe to delegatecall the assistant, which stakes NXM into the pool and submits the resulting StakingNFT tokenId to the adapter."></picture><figcaption><p><strong>Staking.</strong> The Safe delegatecalls the assistant, which stakes its own NXM into the pool (minting/topping-up a <code>StakingNFT</code>) and <code>submit</code>s the returned tokenId to the adapter: in one transaction.</p></figcaption></figure>

<figure><picture><source srcset="/files/V1oBNU8hdWwQVHozw7cp" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2FhsyIuQo675ZEbEDn1a9n%2Fwithdraw-staking-flow.png?alt=media&amp;token=96f52f07-3bd4-436a-9d71-6e908996e19f" alt="Flow to withdraw from Nexus: the assistant withdraws stake/rewards to the Safe then best-effort removes the tokenId: kept if still staked (CoordinateStillLive), dropped once fully emptied."></picture><figcaption><p><strong>Withdrawing.</strong> The assistant withdraws stake and/or rewards to the Safe, then best-effort <code>remove</code>s the tokenId: a partial withdraw keeps it tracked (<code>CoordinateStillLive</code>); it is dropped only once the position is fully emptied.</p></figcaption></figure>

## Balance calculation

<figure><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2Fgit-blob-64dce3fec7253c283f2744660af6a23493412218%2Fadapter-nexus-mutual-staking.svg?alt=media" alt="Flowchart of the Nexus Mutual Staking balance adapter: an assistant-submitted StakingNFT tokenId is ownership-verified, then valued via the StakingViewer into Staking and Rewards PositionBalance entries."><figcaption><p>Nexus Mutual Staking adapter: how the underlying balances reported to the NAV Calculator are derived.</p></figcaption></figure>

For each cached `tokenId`, the adapter values the position via the StakingViewer and re-verifies ownership against the StakingNFT:

```
for each cached tokenId:
    if StakingNFT.tokenInfo(tokenId).owner != account: skip      // live ownership re-check
    token = StakingViewer.getTokens([tokenId])
    staking = token.activeStake + token.expiredStake;  rewards = token.rewards
    if staking == 0 and rewards == 0: skip
    pool = StakingViewer.stakingPool(token.poolId)               // positionId
    emit Staking leg (if staking > 0) and Rewards leg (if rewards > 0)
```

Ownership is re-checked every read via `StakingNFT.tokenInfo`, so a stale or transferred id contributes nothing. Stake and rewards are read live from `StakingViewer.getTokens`; the cache holds no amounts. Each id is valued behind the base's per-coordinate self-`staticcall` (fail-open), so a revert in one position's live reads, Nexus's `getTokens` is revert-prone, discards just that id and never drops the account's whole Nexus stake.

## Identity

* **positionId:** `abi.encode(address stakingPool)` where `stakingPool = StakingViewer.stakingPool(poolId)`, the pool address. Both legs of a position share the same key. The NFT `tokenId` is **not** part of the positionId; it rides in `positionInstanceId`.
* **positionKind:** `Staking` · `Rewards`
* **positionInstanceId:** `bytes32(tokenId)` (the per-position `StakingNFT` id; ephemeral, both legs of a position carry the same instance id)
* **Labels:** the adapter implements `positionLabels(positionId)`, returning `["Staking Pools", "Pool #<poolId>"]`; the numeric pool id read live via `INexusMutualStakingPool.getPoolId()` (falling back to the pool's `"0x…"` address if that read reverts), matching the Nexus UI's pool numbering (surfaced only on the verbose read path; full breadcrumb = `["Nexus Mutual", "Staking Pools", "Pool #<poolId>"]`). The NFT id is **not** in the label; it rides in `positionInstanceId`.

An account's NFTs in the same pool share that pool's positionId. The reconciliation key is `keccak256(abi.encode(chainId, protocolSubId, positionId, balanceAsset.asset, positionKind))`, the `Staking` and `Rewards` legs reconcile independently via `positionKind` (and `positionInstanceId` is excluded, so multiple NFTs in one pool reconcile into one coordinate per kind).

## Constructor

```solidity
constructor(
    address stakingNft_,
    address stakingViewer_,
    address underlyingToken_,
    address navCalculator_
)
```

| Parameter          | Description                                                                                                                              |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `stakingNft_`      | Nexus Mutual `StakingNFT` (ERC-721), source of ownership re-verification via `tokenInfo`. Must be a contract                             |
| `stakingViewer_`   | Nexus Mutual `StakingViewer`, per-token active/expired stake + rewards, and `stakingPool(poolId)` for the positionId. Must be a contract |
| `underlyingToken_` | Asset positions are denominated in: NXM. Must be non-zero                                                                                |
| `navCalculator_`   | NAVCalculator address, used for asset-registry metadata. Must be a contract                                                              |

There is **no** `admin_` / `assistant_` argument; the family is permissionless. The paired `NexusMutualStakingPoolsAssistant` is deployed separately, pinned to this adapter (plus the NXM token and Nexus `TokenController`).

## Registration

Registered as a plain adapter with `addBalanceAdapters([adapter])` and removed with `removeBalanceAdapters([adapter])`. The coordinate cache is then kept current by the position owner's own transactions, typically via the assistant, delegatecalled inside the same `depositTo` / `withdraw`.


# Attested balance adapters

The fourth adapter family, for positions the chain can verify but not value, where every write carries a proof checked onchain.

**The position is real, funded and enforceable onchain. What is missing onchain is the information needed to value it.**

The canonical shape is an offchain allocation. The protocol custodies the assets and will honour the claim; how much each account is owed is computed offchain from data the chain does not hold, and the contract stores only a **cryptographic commitment** to the resulting dataset, typically a merkle root, alongside how much each account has already taken.

So the chain can *verify* a value but cannot *derive* one. Handed a candidate figure and a witness for it, the contract answers yes or no. Handed only an account, it cannot produce that account's figure, or even enumerate which accounts hold one. Nothing is hidden and nothing is missing; the information is simply not in a form a `view` function can reach.

* **Base class:** `AttestedValueCache`, the single cache base for the whole family
* **Marker interface:** `IAttestedBalanceAdapter`
* **Source:** `AttestedValueCache.sol` · `IAttestedBalanceAdapter.sol`

## What each family is missing

Every adapter family exists because something it needs is absent from onchain state. Which thing is absent decides the whole design:

| Family                                                                                                                                                                | Absent onchain                                                    | Consequence                                                            |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------- |
| [Plain](/funds/infrastructure/architecture/onchain-accounting/balance-adapters) / [meta](/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters) | nothing                                                           | read the value live, every time                                        |
| [Assisted](/funds/infrastructure/architecture/onchain-accounting/assisted-balance-adapters)                                                                           | the position's **location**, a non-enumerable ticket or NFT id    | cache the coordinate; value it live; re-verify ownership on every read |
| **Attested**                                                                                                                                                          | the position's **value**; the chain holds only a commitment to it | cache the value with a proof; net off whatever has already settled     |

The difference is not one of degree. An assisted adapter is missing an *address*, and a wrong one is harmless: the coordinate is re-checked against live ownership and values to nothing if it does not hold up. An attested adapter is missing a *number*, and the number **is** the report; there is no live state to check it against later. That is why this family cannot borrow the assisted rule of *cache coordinates, never amounts*, and why the amount it caches has to be proven at the moment it is written.

## Why the value has to be cached

Three constraints compose, and the family's entire shape follows from them:

1. **A value the chain can only verify must arrive from outside.** Someone has to bring both the figure and its witness.
2. **There is nowhere to put a witness at read time.** The NAV read's signature is fixed, `getAccountNav(account, quoteAsset)` reaches each adapter through `getAdapterPositions(account, assetFilter)`, and neither has a parameter that could carry proof data.
3. **So the value is stored, and its proof is checked when it is written** rather than when it is read.

That is the trade the family makes, and it is worth naming: it converts a **trust** problem into a **freshness** problem. Because every write is verified against the protocol's live commitment, the feeder is never trusted for the figure; only for keeping it current. What it can cost you is staleness, never a fabricated number.

{% hint style="info" %}
**"Merkle root" is the common case, not the family's assumption.** The base only requires that a member can answer *"is this cached element still valid?"* against some onchain **validity anchor**, a tree root, a signature's TTL, a per-asset epoch. That is why the family exposes `isAttestationLive` as a predicate rather than publishing one global epoch: the first member's scheme is not baked into the family contract.
{% endhint %}

## An attested figure is an entitlement, so report it net

An attested position usually does not vanish when it settles; it **converts** into a form NAV already sees. Merkl's claim transfers the reward tokens into the account's wallet, where the default ERC-20 adapter reports them like any other balance.

So a member must report the entitlement **net of whatever has already been converted**, by reading the settled portion live from the protocol on every read. Reporting the gross attested figure would double-count the position for the entire window between settlement and the keeper's next submission, and in the one direction the family forbids.

## Trust model

Three properties, each doing distinct work:

* **Proof-verified.** A write lands only if its proof validates against the protocol's live commitment, so a feeder can cache only an account's *real* figure; it can **under-report, never over-report**.
* **No suppression.** A submit is an **upsert**: it refreshes the elements it carries and leaves the rest untouched, so a partial or empty submission cannot wipe a balance. Removal is only via the clears, and those are **stale-gated**; `clearAttestation` reverts while the element still contributes to NAV, and `clearAccount` reverts if *any* of the account's elements is still live. A live entry can be refreshed but never silenced.
* **Stale reads as zero.** When a cached element's epoch no longer matches the live anchor, the read returns `0` rather than extrapolate across a rotation.

The family reports **credits only**. Every leg has `isDebt == false`, and the base enforces it as it collects rather than trusting members to remember; because the whole safety argument rests on it. A dropped credit leg shrinks NAV, the direction this family already tolerates; a dropped debt leg would inflate it, the one direction it forbids.

{% hint style="info" %}
**What `SUBMITTER_ROLE` buys, and what it does not.** Writes are role-gated (granted and revoked by `DEFAULT_ADMIN_ROLE`); the clears are deliberately **not**. The role does not protect the reported *amount*; the per-write proof already does that, and no caller could ever write an unattested figure. It protects the space the proof does not cover: **which** elements occupy an account's cache, and **how** a proven total is labelled.

Removal is permissionless precisely *because* it is stale-gated. Role-gating it would mean a compromised role-holder could delete valid, live allocations, a targeted under-report caused by the mechanism meant to prevent one.
{% endhint %}

## The universal interface

The family is defined by one interface, `IAttestedBalanceAdapter`, whose calls are implemented **once for the whole family**. Only the proof data is protocol-specific: each member `abi.decode`s the opaque `bytes` into its own shape.

```solidity
// Writes — gated on SUBMITTER_ROLE
function submit(address account, bytes calldata data) external;
function submitBatch(address[] calldata accounts, bytes[] calldata data) external;

// Garbage collection — permissionless, stale-gated
function clearAttestation(address account, address asset) external;
function clearAccount(address account) external;

// Cache-free read — view, trustless
function previewPositions(address account, bytes calldata data)
    external view returns (PositionBalance[] memory positions);

// Introspection — the consumer surface
function attestations(address account, address asset)
    external view returns (uint256 value, bytes32 epoch, uint64 attestedAt);
function assetsOf(address account) external view returns (address[] memory);
function isAttestationLive(address account, address asset) external view returns (bool);
function maxAssetsPerAccount() external view returns (uint256);
```

`submit` takes an explicit account; the family is **submit-on-behalf-of**, so one keeper feeds many tracked accounts, unlike the assisted family where the owner feeds its own.

**Both writes are atomic.** Any invalid element reverts the whole `submit`, leaving the prior set intact. `submitBatch` is atomic **across accounts**: one bad element reverts the entire batch and no account in the call is updated. A multi-account feeder should therefore pre-filter its data, or fall back to per-account `submit`, so that one account cannot block every other account's refresh.

`submit` and `previewPositions` take the **same payload** and share the member's single decode-and-verify hook. That is deliberate: it is what stops the write path and the preview from ever disagreeing about what a valid attestation is.

## What a zero means

**Three different states all read as a balance of `0`, and each calls for a different response.** `isAttestationLive` is what tells them apart:

| State                     | How to detect it                                    | What it means                                                                                    |
| ------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Nothing cached            | `attestations(account, asset)` returns `epoch == 0` | The account has no element for this asset. Nothing to do                                         |
| Cached, no longer current | `epoch != 0` but `isAttestationLive` is `false`     | **The feeder is behind.** The anchor rotated and the keeper has not resubmitted, page the feeder |
| Live but fully consumed   | `isAttestationLive` is `true`, balance `0`          | The entitlement was claimed. Garbage-collect the element with `clearAttestation`                 |

A non-zero `value` from `attestations` is **not** a claim that the element contributes to NAV; it is the cached figure, whatever its epoch. For the amount actually reported, read `getAdapterBalanceForAsset`. `attestedAt` is a freshness timestamp for monitoring, not an input to the balance.

The consequence is a deliberate bias: reported balances depend on **keeper freshness**, and after a rotation an account reads `0` until the keeper resubmits. That is an intentional under-count, not a fault; the alternative is reporting a figure against a dataset the chain no longer commits to.

{% hint style="warning" %}
**`isAttestationLive` can flap, and live is not the same as counted.** Where a member's validity anchor is non-monotonic, one that can return to a value it held before, the predicate can return `false` and then `true` again for the same element. See the member's page for whether its anchor behaves that way. And a live but fully-consumed element reads `0` while remaining live, which is why the table above needs all three rows.
{% endhint %}

## How a read works

```
assets = assetsOf(account)              // the per-account cache drives the read
for each asset:
    if the cached epoch != the live epoch:  value it as 0     // stale, never extrapolated
    else:  value it from the cached amount, netted off live protocol state
    member fans that value into one or more legs
    base checks every leg as it collects   // credit, right asset, right adapter, sum within value
```

Each asset is valued behind its own stipended self-`staticcall`, so one reverting asset drops only itself rather than the account's whole set.

{% hint style="info" %}
**`underlyingAssetsSupported()` returns an empty array for this entire family**, and that is not a bug. The family cannot enumerate its universe ahead of time, because which assets an account holds is discovered from proofs, not declared up front. Use the per-account `assetsOf(account)` instead; a consumer that enumerates supported assets to decide what to read will read nothing.
{% endhint %}

## Reading without the cache

`previewPositions(account, data)` values an account from **caller-supplied** proof data, with no prior submit and without touching the cache. It is the family's answer for a portal, frontend or third party that computes proofs offchain and wants a figure now.

It is a `view`: it reads no cached element and writes none, so it neither feeds the cache nor affects what the NAV returns, `getAdapterPositions` keeps reading the keeper-fed cache regardless. Integrity is identical to the write path, because every proof in `data` is verified against the live commitment, so it can only surface the account's real figure.

{% hint style="info" %}
**It fails loud, unlike the NAV read.** An invalid proof reverts rather than degrading to zero. That is the right direction for an explicit query, a caller supplying proof data wants to know the data was rejected, and the opposite of the NAV path, which must never let one bad element take down an account's whole reading. Empty `data`, or a zero account, returns an empty array; the same asset twice in one payload is rejected with `DuplicateAsset`.
{% endhint %}

## What the proof does and does not cover

{% hint style="danger" %}
**The attestation covers the proof-bound value, and nothing else a member returns.**

A member may attach auxiliary sub-position metadata, an identifier splitting one proven total into several reported legs. Where the proof commits only to the total and the contract checks only that the parts sum to it, **that metadata is submitter-controlled and is not attested.**

So a consumer must not treat returned position **identity** as proof-backed. The proof-backed figure is the **sum**. Each member's page states exactly which of its fields is attested and which is presentation, and how its legs must be aggregated.
{% endhint %}

## Error taxonomy

Declared on `IAttestedBalanceAdapter` itself, so a consumer can decode a revert without importing any member:

| Error                                  | Raised when                                                                |
| -------------------------------------- | -------------------------------------------------------------------------- |
| `NAVCalculatorTooOld(navCalculator)`   | the NAV Calculator given at construction cannot serve `getRegisteredAsset` |
| `UninitializedEpoch()`                 | the member's validity anchor read back as zero                             |
| `TooManyAttestations(account, count)`  | admitting an element would exceed the per-account cache cap                |
| `AttestationStillLive(account, asset)` | a clear targeted an element still contributing to NAV                      |
| `NothingToClear()`                     | a clear targeted an `(account, asset)` with nothing cached                 |
| `DuplicateAsset(asset)`                | the same asset appeared twice in one `previewPositions` payload            |

Four more guard the legs a member builds, because the leg builder is the one part of the read a member overrides:

| Guard                        | What it pins                                                                                                                        |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `DebtLegRejected(asset)`     | **direction**; a leg with `isDebt == true`, which the family's credits-only charter forbids                                         |
| `LegAssetMismatch(expected)` | **unit**; `balanceAsset.asset` is the pricing key, so a leg denominated in another asset would inflate quote-NAV by the price ratio |
| `LegSumExceedsValue(asset)`  | **magnitude**; legs must *partition* the attested value, never each carry all of it                                                 |
| `LegAdapterMismatch(asset)`  | **attribution**; `balanceAdapter` must be the adapter itself, the invariant reconciliation keys on                                  |

{% hint style="warning" %}
**These four fire only when a member is buggy, but their effect is consumer-visible.** They revert outside the per-asset isolation, and `PriceFeedLib` fails open on a plain revert, so the adapter's **entire** leg set silently leaves NAV for that account, not just the offending asset. The trigger can be as small as a one-wei rounding error in a member's split.

Diagnostic: if this adapter's contribution vanishes from an account's NAV with no error, call `getAdapterPositions(account, address(0))` directly; the revert NAV swallowed surfaces there. It is a diagnostic, not a data path; NAV reads add the pricing and health layer that a direct call skips. Near frame exhaustion the same fault can instead surface as `AdapterGasExhausted`, which fails the whole account's `getAccountNav` loudly; see [how an adapter is built](/funds/infrastructure/architecture/onchain-accounting/balance-adapters#how-an-adapter-is-built).
{% endhint %}

## Monitoring

| Event                | Emitted on                                                                                                              |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `AttestationCached`  | every `(account, asset)` upsert. Carries the `submitter` for **attribution**; authorisation is the role, not this field |
| `AttestationCleared` | one element garbage-collected                                                                                           |
| `AccountCleared`     | an account's whole set garbage-collected                                                                                |

**Watch the cache cap.** Compare `assetsOf(account).length` against `maxAssetsPerAccount()` to spot an account approaching its cap, whether through ordinary accumulation or through a third party occupying slots. The cap is constant per deployment, no admin can loosen it, so an account at the cap needs garbage collection, not a configuration change.

## Registration

From the NAV Calculator's perspective an attested adapter is an ordinary `IBalanceAdapter`, so it is registered like a [plain adapter](/funds/infrastructure/architecture/onchain-accounting/balance-adapters):

| Action     | Call                                         |
| ---------- | -------------------------------------------- |
| **Add**    | `addBalanceAdapters([adapter])` (MANAGER)    |
| **Remove** | `removeBalanceAdapters([adapter])` (MANAGER) |

One family-specific prerequisite: the keeper must hold `SUBMITTER_ROLE` on the adapter before it can write. A registered adapter with no submitter reports nothing; every account reads `0`, which is the *"nothing cached"* row of the table above.

## Members

| Adapter                                                                                                | Protocol | Positions                                                           |
| ------------------------------------------------------------------------------------------------------ | -------- | ------------------------------------------------------------------- |
| [Merkl Rewards](/funds/infrastructure/architecture/onchain-accounting/attested-balance-adapters/merkl) | Merkl    | Unclaimed rewards per token (`Rewards`), one leg per campaign share |

{% content-ref url="/pages/OIH6BILsiRxKiXkTGlKK" %}
[Merkl Rewards](/funds/infrastructure/architecture/onchain-accounting/attested-balance-adapters/merkl)
{% endcontent-ref %}


# Merkl Rewards

Reports an account's unclaimed Merkl rewards, held by the Distributor, valued from a proof-verified cache, per reward token and campaign share.

Reports an account's **unclaimed Merkl rewards**, per reward token.

The rewards are real and already onchain: campaign funders transfer the reward tokens to Merkl's **Distributor**, which custodies them until each account claims its share. What is not onchain is the **split**, Merkl's offchain engine derives each account's cumulative entitlement per token from campaign rules and offchain observation, publishes the resulting reward file, and pushes only a **merkle root over it** to the Distributor at intervals. The contract therefore holds the tokens, the current root, and `claimed`, how much each account has already withdrawn, but nothing that yields an account's entitlement on its own.

That is precisely the [attested](/funds/infrastructure/architecture/onchain-accounting/attested-balance-adapters) shape: the value is onchain **verifiable but not derivable**. The family page covers the model, the trust boundaries, the introspection reads and the error taxonomy; this page covers only what is specific to Merkl.

* **Type:** Attested adapter, the family's first member
* **protocolBrand / protocolId / protocolSubId:** all three are `ProtocolIds.MERKL`; Merkl has no product split to distinguish
* **positionId:** `abi.encode(DISTRIBUTOR)`, **identical for every leg of every token**
* **positionInstanceId:** the campaign coordinate, where a breakdown was submitted
* **positionKind:** `Rewards` · **protocolName():** `"Merkl"` · **labels:** `["Claimable Rewards"]`
* **Distributor:** `0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae`, Angle Labs, the same address on every supported chain
* **Admin:** the Security Council Safe holds `DEFAULT_ADMIN_ROLE` on every chain the adapter is deployed on, and grants and revokes the keeper's `SUBMITTER_ROLE`
* **Source:** `MerklRewardsAttestedBalanceAdapter.sol`

## What it reports

A token's unclaimed total is its **proof-verified cumulative minus the amount already claimed onchain**, with `claimed` read live from the Distributor on every read. One leg per cached reward token, or one per campaign share where a breakdown was submitted.

The live netting is what keeps the position from being counted twice. A claim does not destroy value; it **moves** it out of the Distributor and into the account's wallet, where the [default adapter](/funds/infrastructure/architecture/onchain-accounting/balance-adapters) already reports it as an ordinary ERC-20 balance. Because Merkl's leaves carry a *cumulative* figure rather than an incremental one, subtracting `claimed` hands off exactly the settled portion and leaves the entitlement reported only while it is genuinely still outstanding.

| Leg               | PositionKind | isDebt  | Amount                                           |
| ----------------- | ------------ | ------- | ------------------------------------------------ |
| Claimable Rewards | `Rewards`    | `false` | Proven cumulative − claimed, in the reward token |

## Reading the legs: sum them, never de-duplicate

{% hint style="danger" %}
**Every Merkl leg of one token shares an identical reconciliation key.**

`positionId` is `abi.encode(DISTRIBUTOR)`, the same value for every leg of every token, and the campaign coordinate rides in `positionInstanceId`, which is **excluded** from the stable key. Legs of *different* tokens still separate, because the key includes `balanceAsset.asset`. Legs of the *same* token do not.

So a consumer that treats a repeated key as duplicate data and keeps one row **under-reports that token by up to the per-campaign cap**. Sum the legs.

Campaign attribution is **display-grade, not audit-grade**: the proof binds only the per-token total and the contract checks only that the shares sum to it. **The per-token total, the sum of a token's legs, is the proof-backed figure.** Treat the split as presentation.
{% endhint %}

`campaignsOf(user, token)` returns the cached `CampaignShare[]` directly, for a consumer that wants the breakdown without reassembling it from legs. Same standing as the legs' campaign coordinates: display and debugging, with the shares summing to the proof-verified per-token cumulative. Empty where the token was submitted untagged.

## The proof shape

Merkl decodes the family's opaque `bytes` into `Allocation[]`, the same shape for the write path and the cache-free read:

```solidity
struct CampaignShare {
    bytes32 campaignId;
    bytes32 subCampaignId;
    uint256 cumulativeAmount;
}

struct Allocation {
    address        token;             // reward token
    uint256        cumulativeAmount;  // proof-bound total for (account, token)
    bytes32[]      proof;             // merkle proof against the Distributor's live root
    CampaignShare[] campaigns;        // optional breakdown; must SUM to cumulativeAmount
}
```

The leaf proved is `keccak256(abi.encode(account, token, cumulativeAmount))`. Typed convenience wrappers exist for keeper ergonomics, `submitProofs(user, allocations)` and `submitProofsBatch(users, allocations)`, carrying the same atomicity as the family's `submit` and `submitBatch`.

## Reading a position without the cache

`previewPositions(account, abi.encode(allocations))` values an account straight from caller-supplied proof data, the same payload the write path takes. See [reading without the cache](/funds/infrastructure/architecture/onchain-accounting/attested-balance-adapters#reading-without-the-cache) for the semantics; the Merkl-specific part is only the payload shape above. As on the write path, the **campaign split is not proof-bound**, a preview built from fabricated shares that happen to sum correctly returns those shares.

## Staleness and the dispute window

Each new root is published subject to a short dispute window; a window in which watchers verify the reward file and can post collateral to challenge it, and a challenged tree can be resolved back to its predecessor or revoked outright. `getMerkleRoot()` reflects that: during the window, or while a disputer is set, it returns the **previous** root. The root is therefore **not monotonic**; it can return to a value it held before, which is why the family's staleness check is a straight equality test and never an extrapolation.

Two operational consequences:

* **`isAttestationLive` can flap** for the same element, `false` during a rotation, `true` again if the root rolls back. A monitor that pages on a single `false` will page on dispute windows.
* **A clear performed while the root is rolled back is not undone when the root returns.** `clearAttestation` is legitimate the moment an element reads stale, so an element cleared inside the window stays gone until the keeper resubmits. The reading is an under-count that self-heals on the next feed cycle.

{% hint style="info" %}
**`AmountRegression` should never fire.** For a well-formed Merkl tree it is unreachable; the amount is committed inside the leaf, so exactly one value verifies per `(user, token)` under one root. It is a tripwire, not a protection: alert on any occurrence rather than retrying.
{% endhint %}

## Bounds

Two caps bound the per-account leg count, and it is their **product** a read budget must size against; not the token cap alone, because the campaign split is not proof-bound and so is influenceable by whoever submits. Read `MAX_TOKENS_PER_USER` and `MAX_CAMPAIGNS_PER_TOKEN` off the adapter rather than from a figure here; the product has already been tightened once. The family read `maxAssetsPerAccount()` returns the token cap, so a protocol-agnostic consumer needs no Merkl symbol to watch it.

Two consequences of the token cap that garbage collection, not configuration, resolves:

* Under upsert semantics the cache **accumulates every token ever proven**. Stale entries go only via the permissionless, stale-gated clears, so an account farming Merkl across many root epochs drifts toward the cap unless the keeper also collects garbage.
* **At the cap, a submit carrying a new token reverts**: and because a submit is atomic over its whole list, that reverts the refresh of the account's *other* tokens too. A keeper at the cap stops updating everything, not just the new token.

Per-token isolation does not make an under-budgeted read survivable: with a full cache and an expensive token valued last, the read can revert outright. Size the budget off the product.

## Failure behaviour

Writes fail closed with named errors; `InvalidProof` (proof does not verify against the live root), `AmountRegression` (a cumulative below the cached value for the *same* root), `CampaignSumMismatch` (shares do not sum to the proven total), `TooManyCampaigns` (breakdown over the cap). A rejected submit leaves the previous cached value intact rather than a partial one.

Reads never revert on a bad cache entry: a stale epoch reads `0`, and a token whose asset is unregistered on the NAV contributes nothing.

{% hint style="warning" %}
**One silent failure mode worth monitoring.** The live netting reads `claimed` off the Distributor unguarded and ABI-decodes the result, so if the Distributor, an upgradeable proxy under Angle governance, ever returns something undecodable, the read reverts. On the NAV path that means this adapter's whole leg set drops out for the account: an under-report, the safe direction, but with no error surfaced. The [family diagnostic](/funds/infrastructure/architecture/onchain-accounting/attested-balance-adapters#error-taxonomy) applies.
{% endhint %}


# Deployment addresses

Per-chain contract addresses for the Onchain Accounting System (NAV), the canonical CREATE2 layer, the chains it runs on, and per-chain feeds and adapters.

The **Onchain Accounting System (NAV)** is deployed on **25 chains**, plus Robinhood Chain (see [below](#robinhood-chain)). On **8** of them the Funds hold positions, so those carry registered assets, with custom price feeds and balance adapters as needed; the other **17 run the canonical layer (implementation + proxy) only**. Configuration is **incremental and per-chain**: coverage varies (some chains carry many assets, others only a few), and it expands as the Funds' strategies do. New custom price feeds and balance adapters are **developed and/or deployed as needed**: bringing a new protocol, asset, or chain into a strategy means shipping its feed/adapter (if one doesn't exist yet) and registering it on the NAV Calculator.

{% hint style="success" %}
**The one address every integrator calls:** the NAVCalculator proxy at [`0x54EaD2A1dB7456cA917675Ea8908ec8A997c6214`](https://etherscan.io/address/0x54EaD2A1dB7456cA917675Ea8908ec8A997c6214), the **same address on every chain**.
{% endhint %}

The tables below mirror KPK's per-chain deployment records, which the deploy scripts write as each contract lands.

## Canonical layer: identical on every chain

Every contract here has the **same address on every chain it is deployed on**: all of them on the 25 chains, and all but the timelock on Robinhood Chain. All but one are deployed deterministically via CREATE2. The exception is `ERC20Default`, which is created with plain `CREATE` inside `NAVCalculator.initialize()`; it is identical everywhere because the proxy that creates it is itself CREATE2-identical and does so at the same nonce. Same guarantee, different mechanism.

| Contract                       | Address                                                                                                                 | Notes                                                                                                                                                                                                                          |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **NAVCalculator proxy (UUPS)** | [`0x54EaD2A1dB7456cA917675Ea8908ec8A997c6214`](https://etherscan.io/address/0x54EaD2A1dB7456cA917675Ea8908ec8A997c6214) | **The address every consumer should call.** Permanent across upgrades. Verbose label reads (`getAccountNavVerbose` / `getAccountPositionsVerbose`) live here.                                                                  |
| NAVCalculator implementation   | [`0xA7243DCDF7e4D6f8FA20A9F022186C0d0778C2b6`](https://etherscan.io/address/0xA7243DCDF7e4D6f8FA20A9F022186C0d0778C2b6) | UUPS impl, immutable post-deploy.                                                                                                                                                                                              |
| `PriceFeedLib`                 | [`0x0f913658d70BB19b7B42eF9926c3df7c70fafCf2`](https://etherscan.io/address/0x0f913658d70BB19b7B42eF9926c3df7c70fafCf2) | External library linked into the implementation.                                                                                                                                                                               |
| `NAVRegistryLib`               | [`0x3D5369B0b2044d62a8f643bf9a2b8CC03775934D`](https://etherscan.io/address/0x3D5369B0b2044d62a8f643bf9a2b8CC03775934D) | External library linked into the implementation.                                                                                                                                                                               |
| `ERC20Default` balance adapter | [`0x4Bc39Dce61245F5A63189d1FA03E6D76646f1EE6`](https://etherscan.io/address/0x4Bc39Dce61245F5A63189d1FA03E6D76646f1EE6) | Created inside `NAVCalculator.initialize()` with plain `CREATE`, not CREATE2; identical on all 25 chains and cannot be removed.                                                                                                |
| `AssetKindRegistry`            | [`0xf60467a88C2b4d0f46C0c41C3Ba0C74da3d3CCD0`](https://etherscan.io/address/0xf60467a88C2b4d0f46C0c41C3Ba0C74da3d3CCD0) | Same address on every chain. Holds `MANAGER` on the NAV; the contract through which assets are registered, classified and labelled in a single transaction.                                                                    |
| `TimelockController`           | [`0xAB92EA75D07792915eB1f29259354866B880dFC2`](https://etherscan.io/address/0xAB92EA75D07792915eB1f29259354866B880dFC2) | Same address on all 25 chains. Holds `DEFAULT_ADMIN_ROLE` and `MANAGER` on the NAV, alongside the Security Council Safe; see [NAV Calculator](/funds/infrastructure/architecture/onchain-accounting/contracts/nav-calculator). |

{% hint style="info" %}
**Every contract in this table is the same on all 25 chains, `AssetKindRegistry` and the timelock included.** `AssetKindRegistry` is CREATE2-deployed from the canonical deployer, and its only constructor argument is the NAV proxy (itself CREATE2-identical fleet-wide), so the init-code hash matches everywhere and one salt yields one address. The timelock is an OpenZeppelin `TimelockController`, CREATE2-deployed with the same constructor arguments on every chain, so it lands at one address too.
{% endhint %}

### NAV-only balance adapters: also one address on every chain

Twenty-seven deployed balance adapters take the NAV proxy as their **only** constructor argument: ten protocol adapters, plus the seventeen [wallet-yield adapters](/funds/infrastructure/architecture/onchain-accounting/meta-balance-adapters/wallet-yield). The proxy is itself CREATE2-identical fleet-wide, so each of these has the same init-code hash everywhere and one salt yields one address. Each is listed with the chains it is registered on:

| Adapter                               | Address                                      | Chains                  |
| ------------------------------------- | -------------------------------------------- | ----------------------- |
| Aave V3 Instances                     | `0xa1b3c695A768A6Ce571fF699F463224CB32260C1` | 1, 10, 100, 8453, 42161 |
| Aave V3 Safety Module                 | `0xa356717027B5d4010a7449904545E5aFb355Bdc9` | 1                       |
| Aave V4 Spokes                        | `0xAd5b905ad5BB13909aE658a1910bC4cdFcD81dE3` | 1                       |
| Balancer V3 Pools                     | `0x8AE70624CB7729B0408C8f0ce8ffdac9974FE33A` | 1, 8453, 42161          |
| Curve Pools                           | `0x4c25B00401d28d5F6FC65a7Af8386b239FA5e735` | 1                       |
| Euler Vaults                          | `0x331566e8F7b48Fa683fE865403Cf4311b50A7710` | 1                       |
| Fluid fTokens                         | `0xb1a03cDD28cAA91f409C417C9f56ab53bc9d7ae9` | 1, 137, 8453, 42161     |
| Gearbox V3 Markets                    | `0xE338E5d4F7adF34107239D59828DEAD2fa2cfD86` | 1                       |
| Morpho Vaults                         | `0x72a012B30b240458C3f79fd20E5c2A907d77e54f` | 1, 5042, 42161          |
| Symbiotic Vaults                      | `0x320699dE0693d71C28f5559a68635495AdA0826c` | 1                       |
| Lido Staked ETH (wallet)              | `0x7ED4F12798ea3f26AE02a6521eE84702CE5f8f90` | 1, 10, 100, 8453, 42161 |
| ether.fi Staked ETH (wallet)          | `0x8a6fde256DD9B262596A22BC4a6c8292Fc3a5D65` | 1, 8453, 42161          |
| Rocket Pool Staked ETH (wallet)       | `0x75D1cB3040Dc0322C3f747807360A1Ec03A80eB4` | 1                       |
| Renzo Restaked ETH (wallet)           | `0x9145A559108426e1eA429A2A6441e2A875f069ec` | 1, 42161                |
| Kelp Restaked ETH (wallet)            | `0xc0A8E9FA22F28bDE931624c6A454DE649a790273` | 1, 42161                |
| Stader Staked ETH (wallet)            | `0x30c6A35827E3dC294f410073C305C8b3f91f1a4a` | 1                       |
| StakeWise V3 osToken (wallet)         | `0x818dD183121641B09752891bB7E971b0FE29e101` | 1, 100                  |
| Liquid Collective Staked ETH (wallet) | `0x1473445103673F89Af909387eB905B4328D522D3` | 1                       |
| Ankr Staked ETH (wallet)              | `0xc157f1dF063b161Ec9a89aE25aeBc6fB95b6E90e` | 1                       |
| Coinbase Staked ETH (wallet)          | `0x91d437752463B60103d515c16680d0a17C1c91F9` | 8453                    |
| Ethena Savings (wallet)               | `0x51970009da25B52048ACBBB850B11aa198bdC64B` | 1                       |
| Reserve Baskets (wallet)              | `0x11e68edfBc0449b1c863e11b866B7aF4290F8469` | 1                       |
| Sky Savings (wallet)                  | `0xb967b206445ca58445d09b6CE9878add5Bb14dd4` | 1, 100                  |
| Aave Savings (wallet)                 | `0x36A3F6424FB9AA5B1a3cDD33B2cE09C0c5035D8F` | 1                       |
| Spark Savings (wallet)                | `0xd848BE8B5f580C7F8a7f93256DD218cf06CAcd3f` | 1                       |
| Maple Savings (wallet)                | `0xDeE2BcC847DFc113ec70296C5D31e055a6cd902b` | 1, 42161                |
| Nexus Mutual Vaults (wallet)          | `0xeA71c3af5127f7fC0F791FE0bFfF9826Ea589060` | 1                       |

### Adapters sharing an address only where the protocol does

Four adapters take **one chain-specific protocol address** alongside the NAV (Merkl also takes an admin address, the same on every chain). Their address is therefore a pure function of that protocol address: a **single shared address across the chains that share it**, and a different (still deterministic) address where it differs. That is a weaker guarantee than the section above, so these are marked ☆ rather than ★ in the per-chain tables.

| Adapter            | Address                                      | Deployed on                          | Would reuse this address on                                                                             | Because                                                                                                                                                              |
| ------------------ | -------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Uniswap V3 Pools ☆ | `0x2F7e8f78FD8EE977F6D53752b03A79c77012Ba52` | **1, 10†, 137†, 42161†**             | –                                                                                                       | all four host the canonical NonfungiblePositionManager at `0xC36442…FE88`                                                                                            |
| Uniswap V3 Pools ☆ | `0xaC47D9Cbe8B3EDc9c9d1a6c8092054299bBC1adE` | **100**                              | –                                                                                                       | Gnosis hosts its own NonfungiblePositionManager at `0xAE8fbE65…4FA3`, not the canonical one, so the constructor argument differs and the CREATE2 address does too.   |
| Morpho Markets ☆   | `0x08e590BcFc753fC6383aFE327253558C962D3EB6` | **1, 8453, 56†**                     | –                                                                                                       | all three host Morpho at `0xBBBB…FFCb`                                                                                                                               |
| Morpho Markets ☆   | `0xCFf47e28B51a7f4599150f73c348E1a155A93437` | 42161                                | –                                                                                                       | Arbitrum's Morpho is a separate deployment at `0x6c247b1F…8F5e`, so it takes its own address                                                                         |
| Fluid Vaults ☆     | `0x161EB6952Ffc4319a05bd8FEBB1B90283F267005` | 1                                    | 56, 137, 8453, 9745, 42161                                                                              | Fluid deploys its VaultResolver deterministically at `0xA5C3E165…95cC` on all six                                                                                    |
| Merkl Rewards ☆    | `0xFf2D5A8bf801191eD1292E6DD6792F5D03c5aFBF` | **1, 10, 56, 100, 137, 8453, 42161** | 130, 143, 146, 196, 480, 999, 5000, 5042, 9745, 16661, 42220, 43111, 43114, 57073, 59144, 80094, 534352 | Merkl's Distributor is the same address on every chain except opBNB (204), where it is absent, so the one chain-specific argument is not chain-specific in practice. |

{% hint style="info" %}
**† means deployed but not registered on the NAV Calculator.** The contract is live and verified at the address shown, but the NAV does not call it, so it contributes nothing to any reading and costs nothing on the read path. These are pre-provisioned on chains where the Funds hold no positions in that protocol yet, so that onboarding a pool or market later is a single governance transaction rather than a deploy plus a registration. They are therefore **absent from the per-chain tables below**, which list only what the NAV actually calls.

**"Would reuse this address" is weaker still; it is not a deployment at all.** CREATE2 fixes *where* a contract lands, not *whether* it exists; nothing is deployed at those addresses on the right-hand chains today. The address is nonetheless reserved in a strong sense: it derives from the init-code hash, so the only contract that can ever occupy it is this exact bytecode bound to this NAV.
{% endhint %}

{% hint style="warning" %}
**Whether a chain belongs on the Uniswap V3 row is decided by calling the contract, never by checking for code.** Several chains carry unrelated code at `0xC36442…FE88`, and Base's real position manager is at a different address entirely, so presence of code there is not evidence of identity. Comparing bytecode hashes does not work either: the position manager stores `factory` and `WETH9` as immutables, so the identical contract hashes differently on every chain. Confirm with `name()` and `factory()`.
{% endhint %}

{% hint style="info" %}
**Every other balance adapter, and all custom price feeds, are per-chain, and correctly so.** Their constructors take chain-specific protocol addresses (SparkLend's `dataProvider`, Compound's `compoundRewards`, Balancer's `balancerMinter`, …) that genuinely differ per chain, so CREATE2 could not give them a shared address. Read those from the deployment records rather than assuming they match across chains.
{% endhint %}

{% hint style="info" %}
Always call the **proxy** address. The implementation may change on upgrade; the proxy survives those upgrades.
{% endhint %}

{% hint style="warning" %}
**"Permanent" means across implementation upgrades; it does not mean the stack can always be migrated in place.** The `ERC20Default` adapter above is minted **inside** `initialize()`, its setter is **set-once** with no admin path, and the registry refuses to remove it by name. So no upgrade, no config transaction and no admin call can repoint a live proxy at a different `ERC20Default`.

That matters because `ERC20Default` prices every wallet balance on every chain, and its bytecode is reachable from shared code that is **`internal`**, inlined at compile time rather than linked at an address. A fix in that shared code therefore only reaches the adapter when the adapter is itself recompiled, which an implementation upgrade cannot do. When such a change ships, the whole stack is deployed afresh behind a **new proxy address** rather than upgraded, and integrators move to it. Treat the proxy as stable, not as immortal, and read it from this page rather than hard-coding it.

So a redeploy is a new proxy whenever `ERC20Default`'s bytecode changes; that is the trigger, not a schedule.

The two external libraries above are **linked at an address** rather than inlined, so their addresses are pinned pre-deploy from a clean whole-program build and change whenever their own compilation output does. **A library pin is only final once the deploy tree is**: any change reaching a library's own source moves it again, including work for a feature deliberately kept *out* of the NAV, since the enabling code may edit the libraries regardless. Take the library addresses from this page or the canonical JSON after the redeploy, never from a pre-deploy figure.
{% endhint %}

## Chains

These 8 chains carry the assets registered for the Funds' strategies, priced by direct feeds and (where needed) custom price feeds, plus balance adapters. The set registered so far was chosen to cover the first Funds' initial strategies (a deliberate superset rather than an exact fit), and it grows per-chain as positions are added, so the counts below vary:

| Chain    | Chain ID | Registered assets | Custom price feeds | Registered balance adapters (excl. `ERC20Default`) |
| -------- | -------- | ----------------- | ------------------ | -------------------------------------------------- |
| Ethereum | 1        | 56                | 34                 | 47                                                 |
| Arbitrum | 42161    | 11                | 8                  | 13                                                 |
| Base     | 8453     | 11                | 4                  | 10                                                 |
| Gnosis   | 100      | 11                | 3                  | 8                                                  |
| Optimism | 10       | 6                 | 0                  | 4                                                  |
| BSC      | 56       | 6                 | 0                  | 1                                                  |
| Polygon  | 137      | 5                 | 0                  | 2                                                  |
| Arc      | 5042     | 2                 | 1                  | 1                                                  |

{% hint style="info" %}
**Registered assets** is the NAV's total priced-token count for that chain (`getAssetCount()`, read on chain), every token the NAV values, most of them via **direct** feeds. It is therefore larger than the two right-hand columns, which count only the *custom* composite price feeds and the non-`ERC20Default` balance adapters. A **0** in *custom price feeds* means that chain's assets are all priced by direct feeds (Chainlink / Redstone / API3) registered straight on the NAV; no custom composite feed is needed. See [Price feeds](/funds/infrastructure/architecture/onchain-accounting/price-feeds).

The *custom price feeds* column counts what is **deployed and recorded** in the chain's deployment record; the *balance adapters* column counts what is **registered** (read back from `getAllAdapters()`), so an adapter deployed on a chain but not registered there (marked † above) is not counted. **The custom-feed tables further down are deliberately smaller** than their column, and the difference is not an error: they list **primary** feeds by token, so the monitor feeds that watch them (`…CrossRate`, `…CrossRateMarket`) are counted here but described in [Cross-rate](/funds/infrastructure/architecture/onchain-accounting/price-feeds#derived-and-cross-rate) rather than tabulated.
{% endhint %}

{% hint style="info" %}
The NAV **implementation + proxy** (canonical layer only; no registered assets yet) are also live on **17** further chains: Berachain (80094), Ink (57073), Linea (59144), opBNB (204), Sonic (146), Unichain (130), Scroll (534352), X Layer (196), Celo (42220), Avalanche (43114), Mantle (5000), World Chain (480), Monad (143), Plasma (9745), Hemi (43111), 0G (16661), HyperEVM (999). Custom feeds and adapters are registered on each as assets land there.

Each of these still reports `getAssetCount() == 1`: `initialize()` seeds the `ERC20Default` adapter with the chain's **native token** sentinel, which cannot be removed. That single entry is the seed, not a registered strategy asset.
{% endhint %}

### Robinhood Chain

Robinhood Chain (4663) runs the same NAV proxy, implementation, libraries, and `ERC20Default` as the table above, at the same addresses. It is governed separately: there is no timelock, and `DEFAULT_ADMIN_ROLE` and `MANAGER` are held by the Robinhood Chain Governance Safe [`0x2F0d561B9a8CD981e6E73069592BDeA4e48A8bec`](https://robin.etherscan.io/address/0x2F0d561B9a8CD981e6E73069592BDeA4e48A8bec) (2-of-5). Its balance adapters and price feeds come from a separate deployment kit and are not listed on this page.

## Balance adapters (per chain)

Only adapters **currently registered on the NAV Calculator** are listed; each entry below was read back from `getAllAdapters()` on its chain, not from a deployment record.

**★ means the address is the same on every chain**: it is a property of the contract, not a claim about how many chains currently run it. Several ★ adapters are registered on Ethereum alone today; the marker says that wherever one is deployed next, it lands on the address shown. Twenty-eight entries carry it: the twenty-seven adapters taking only the NAV proxy as a constructor argument, plus `ERC20Default`. The mechanism differs slightly: the twenty-seven are CREATE2 from chain-independent init code, while `ERC20Default` is created with plain `CREATE` inside `NAVCalculator.initialize()` and is identical everywhere because the proxy that creates it is itself at the same address on every chain, at the same nonce.

**☆ is the weaker cousin**: also deterministic, but the constructor takes one chain-specific protocol address, so the adapter shares a single address only across the chains that share that address, and takes a different one where it differs.

Everything unmarked is plain `CREATE` and unique per chain, because its constructor takes chain-specific protocol addresses.

{% hint style="info" %}
Assisted adapters (ether.fi, Nexus, StakeWise exit) report **`$0` until the position owner feeds its coordinates**, permissionlessly, onchain, typically via the paired delegatecall assistant. They provide correctness-on-exit coverage, not steady-state value. The assistants themselves are action modules and are **not** registered on the NAV Calculator, so they do not appear below.
{% endhint %}

### Ethereum (1)

| Adapter                                 | Address                                                                                                                 |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `AaveSavingsWalletMeta` ★               | [`0x36A3F6424FB9AA5B1a3cDD33B2cE09C0c5035D8F`](https://etherscan.io/address/0x36A3F6424FB9AA5B1a3cDD33B2cE09C0c5035D8F) |
| `AaveUmbrella`                          | [`0x7299757E188000ca576c4197E1Ed1c53A04AC995`](https://etherscan.io/address/0x7299757E188000ca576c4197E1Ed1c53A04AC995) |
| `AaveV3InstancesMeta` ★                 | [`0xa1b3c695A768A6Ce571fF699F463224CB32260C1`](https://etherscan.io/address/0xa1b3c695A768A6Ce571fF699F463224CB32260C1) |
| `AaveV3SafetyModule` ★                  | [`0xa356717027B5d4010a7449904545E5aFb355Bdc9`](https://etherscan.io/address/0xa356717027B5d4010a7449904545E5aFb355Bdc9) |
| `AaveV4SpokesMeta` ★                    | [`0xAd5b905ad5BB13909aE658a1910bC4cdFcD81dE3`](https://etherscan.io/address/0xAd5b905ad5BB13909aE658a1910bC4cdFcD81dE3) |
| `AnkrStakedEthWalletMeta` ★             | [`0xc157f1dF063b161Ec9a89aE25aeBc6fB95b6E90e`](https://etherscan.io/address/0xc157f1dF063b161Ec9a89aE25aeBc6fB95b6E90e) |
| `BalancerV3GaugesMeta`                  | [`0x1EE1b9dA14225B8a6Dd8D87d1052eC4D37A6E362`](https://etherscan.io/address/0x1EE1b9dA14225B8a6Dd8D87d1052eC4D37A6E362) |
| `BalancerV3PoolsMeta` ★                 | [`0x8AE70624CB7729B0408C8f0ce8ffdac9974FE33A`](https://etherscan.io/address/0x8AE70624CB7729B0408C8f0ce8ffdac9974FE33A) |
| `CapSavings`                            | [`0xEc605FC2521792f2018dF92Bb5aDBf85EB417cb6`](https://etherscan.io/address/0xEc605FC2521792f2018dF92Bb5aDBf85EB417cb6) |
| `CapStablecoin`                         | [`0x121583E21C671e9B1193FE478ec9dc9D73D73a3F`](https://etherscan.io/address/0x121583E21C671e9B1193FE478ec9dc9D73D73a3F) |
| `CompoundV3CometsMeta`                  | [`0xDf85833a69B74e86e38160405a12bA7356AcdCdf`](https://etherscan.io/address/0xDf85833a69B74e86e38160405a12bA7356AcdCdf) |
| `ConvexCurveLPStakingMeta`              | [`0xB41E5A6F34FdD511716Ee455cb6047D1Fcc171F6`](https://etherscan.io/address/0xB41E5A6F34FdD511716Ee455cb6047D1Fcc171F6) |
| `CurveGaugesMeta`                       | [`0xC0c22b89E9cC80a3891dF0ceF1e81b78b865F646`](https://etherscan.io/address/0xC0c22b89E9cC80a3891dF0ceF1e81b78b865F646) |
| `CurvePoolsMeta` ★                      | [`0x4c25B00401d28d5F6FC65a7Af8386b239FA5e735`](https://etherscan.io/address/0x4c25B00401d28d5F6FC65a7Af8386b239FA5e735) |
| `ERC20Default` ★                        | [`0x4Bc39Dce61245F5A63189d1FA03E6D76646f1EE6`](https://etherscan.io/address/0x4Bc39Dce61245F5A63189d1FA03E6D76646f1EE6) |
| `EthenaCooldown`                        | [`0x3F12f2baf245eeF00F6b0C5C2173Df990FFBa796`](https://etherscan.io/address/0x3F12f2baf245eeF00F6b0C5C2173Df990FFBa796) |
| `EthenaSavingsWalletMeta` ★             | [`0x51970009da25B52048ACBBB850B11aa198bdC64B`](https://etherscan.io/address/0x51970009da25B52048ACBBB850B11aa198bdC64B) |
| `EtherFiStakedEthWalletMeta` ★          | [`0x8a6fde256DD9B262596A22BC4a6c8292Fc3a5D65`](https://etherscan.io/address/0x8a6fde256DD9B262596A22BC4a6c8292Fc3a5D65) |
| `EtherFiWithdrawalQueueAssisted`        | [`0x5c5c1A98CdF07c9D9B4664933AF98a30e36F3618`](https://etherscan.io/address/0x5c5c1A98CdF07c9D9B4664933AF98a30e36F3618) |
| `EulerVaultsMeta` ★                     | [`0x331566e8F7b48Fa683fE865403Cf4311b50A7710`](https://etherscan.io/address/0x331566e8F7b48Fa683fE865403Cf4311b50A7710) |
| `FluidFTokensMeta` ★                    | [`0xb1a03cDD28cAA91f409C417C9f56ab53bc9d7ae9`](https://etherscan.io/address/0xb1a03cDD28cAA91f409C417C9f56ab53bc9d7ae9) |
| `FluidVaultsMeta` ☆                     | [`0x161EB6952Ffc4319a05bd8FEBB1B90283F267005`](https://etherscan.io/address/0x161EB6952Ffc4319a05bd8FEBB1B90283F267005) |
| `GearboxV3MarketsMeta` ★                | [`0xE338E5d4F7adF34107239D59828DEAD2fa2cfD86`](https://etherscan.io/address/0xE338E5d4F7adF34107239D59828DEAD2fa2cfD86) |
| `KelpRestakedEthWalletMeta` ★           | [`0xc0A8E9FA22F28bDE931624c6A454DE649a790273`](https://etherscan.io/address/0xc0A8E9FA22F28bDE931624c6A454DE649a790273) |
| `KelpWithdrawalQueue`                   | [`0x136439b0CA2Ff5e9Ec8dC472222248E3A57CeF43`](https://etherscan.io/address/0x136439b0CA2Ff5e9Ec8dC472222248E3A57CeF43) |
| `LidoStakedEthWalletMeta` ★             | [`0x7ED4F12798ea3f26AE02a6521eE84702CE5f8f90`](https://etherscan.io/address/0x7ED4F12798ea3f26AE02a6521eE84702CE5f8f90) |
| `LidoWithdrawalQueue`                   | [`0xD32d7db9c94E0A08ef354D73d618B86b8215A2cF`](https://etherscan.io/address/0xD32d7db9c94E0A08ef354D73d618B86b8215A2cF) |
| `LiquidCollectiveStakedEthWalletMeta` ★ | [`0x1473445103673F89Af909387eB905B4328D522D3`](https://etherscan.io/address/0x1473445103673F89Af909387eB905B4328D522D3) |
| `MapleSavingsWalletMeta` ★              | [`0xDeE2BcC847DFc113ec70296C5D31e055a6cd902b`](https://etherscan.io/address/0xDeE2BcC847DFc113ec70296C5D31e055a6cd902b) |
| `MerklRewards` ☆                        | [`0xFf2D5A8bf801191eD1292E6DD6792F5D03c5aFBF`](https://etherscan.io/address/0xFf2D5A8bf801191eD1292E6DD6792F5D03c5aFBF) |
| `MorphoMarketsMeta` ☆                   | [`0x08e590BcFc753fC6383aFE327253558C962D3EB6`](https://etherscan.io/address/0x08e590BcFc753fC6383aFE327253558C962D3EB6) |
| `MorphoVaultsMeta` ★                    | [`0x72a012B30b240458C3f79fd20E5c2A907d77e54f`](https://etherscan.io/address/0x72a012B30b240458C3f79fd20E5c2A907d77e54f) |
| `NexusMutualStakingPoolsAssisted`       | [`0x25Be749999683381703c7346462503a5EFFFd184`](https://etherscan.io/address/0x25Be749999683381703c7346462503a5EFFFd184) |
| `NexusMutualVaultsWalletMeta` ★         | [`0xeA71c3af5127f7fC0F791FE0bFfF9826Ea589060`](https://etherscan.io/address/0xeA71c3af5127f7fC0F791FE0bFfF9826Ea589060) |
| `PolygonStakingMeta`                    | [`0x905c04819dd4DB75590663658B7cd635aD67B151`](https://etherscan.io/address/0x905c04819dd4DB75590663658B7cd635aD67B151) |
| `RenzoRestakedEthWalletMeta` ★          | [`0x9145A559108426e1eA429A2A6441e2A875f069ec`](https://etherscan.io/address/0x9145A559108426e1eA429A2A6441e2A875f069ec) |
| `ReserveBasketsWalletMeta` ★            | [`0x11e68edfBc0449b1c863e11b866B7aF4290F8469`](https://etherscan.io/address/0x11e68edfBc0449b1c863e11b866B7aF4290F8469) |
| `RocketPoolStakedEthWalletMeta` ★       | [`0x75D1cB3040Dc0322C3f747807360A1Ec03A80eB4`](https://etherscan.io/address/0x75D1cB3040Dc0322C3f747807360A1Ec03A80eB4) |
| `SkySavingsWalletMeta` ★                | [`0xb967b206445ca58445d09b6CE9878add5Bb14dd4`](https://etherscan.io/address/0xb967b206445ca58445d09b6CE9878add5Bb14dd4) |
| `SparkLend`                             | [`0x9ad8dDaAD5166f2f0Fe4e883aFe78902f8BF0CAE`](https://etherscan.io/address/0x9ad8dDaAD5166f2f0Fe4e883aFe78902f8BF0CAE) |
| `SparkSavingsWalletMeta` ★              | [`0xd848BE8B5f580C7F8a7f93256DD218cf06CAcd3f`](https://etherscan.io/address/0xd848BE8B5f580C7F8a7f93256DD218cf06CAcd3f) |
| `StaderStakedEthWalletMeta` ★           | [`0x30c6A35827E3dC294f410073C305C8b3f91f1a4a`](https://etherscan.io/address/0x30c6A35827E3dC294f410073C305C8b3f91f1a4a) |
| `StaderWithdrawalQueue`                 | [`0xCB975897AfA295C8fAEB0C33B333C77B95195094`](https://etherscan.io/address/0xCB975897AfA295C8fAEB0C33B333C77B95195094) |
| `StakeWiseV3ExitQueueAssisted`          | [`0x83375fDE808F6388c85fC0A1dA86a69492a20798`](https://etherscan.io/address/0x83375fDE808F6388c85fC0A1dA86a69492a20798) |
| `StakeWiseV3OsTokenWalletMeta` ★        | [`0x818dD183121641B09752891bB7E971b0FE29e101`](https://etherscan.io/address/0x818dD183121641B09752891bB7E971b0FE29e101) |
| `StakeWiseV3VaultsMeta`                 | [`0xE97bE4B11EE8D65A123dD99bDdec7a4fDcA6359E`](https://etherscan.io/address/0xE97bE4B11EE8D65A123dD99bDdec7a4fDcA6359E) |
| `SymbioticVaultsMeta` ★                 | [`0x320699dE0693d71C28f5559a68635495AdA0826c`](https://etherscan.io/address/0x320699dE0693d71C28f5559a68635495AdA0826c) |
| `UniswapV3PoolsMeta` ☆                  | [`0x2F7e8f78FD8EE977F6D53752b03A79c77012Ba52`](https://etherscan.io/address/0x2F7e8f78FD8EE977F6D53752b03A79c77012Ba52) |

### Optimism (10)

| Adapter                     | Address                                                                                                                            |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `AaveV3InstancesMeta` ★     | [`0xa1b3c695A768A6Ce571fF699F463224CB32260C1`](https://optimistic.etherscan.io/address/0xa1b3c695A768A6Ce571fF699F463224CB32260C1) |
| `CompoundV3CometsMeta`      | [`0xc4422B74ddb9057fcFc9DB64EF7ce166eD6862CA`](https://optimistic.etherscan.io/address/0xc4422B74ddb9057fcFc9DB64EF7ce166eD6862CA) |
| `ERC20Default` ★            | [`0x4Bc39Dce61245F5A63189d1FA03E6D76646f1EE6`](https://optimistic.etherscan.io/address/0x4Bc39Dce61245F5A63189d1FA03E6D76646f1EE6) |
| `LidoStakedEthWalletMeta` ★ | [`0x7ED4F12798ea3f26AE02a6521eE84702CE5f8f90`](https://optimistic.etherscan.io/address/0x7ED4F12798ea3f26AE02a6521eE84702CE5f8f90) |
| `MerklRewards` ☆            | [`0xFf2D5A8bf801191eD1292E6DD6792F5D03c5aFBF`](https://optimistic.etherscan.io/address/0xFf2D5A8bf801191eD1292E6DD6792F5D03c5aFBF) |

### Gnosis (100)

| Adapter                          | Address                                                                                                                  |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `AaveV3InstancesMeta` ★          | [`0xa1b3c695A768A6Ce571fF699F463224CB32260C1`](https://gnosisscan.io/address/0xa1b3c695A768A6Ce571fF699F463224CB32260C1) |
| `ERC20Default` ★                 | [`0x4Bc39Dce61245F5A63189d1FA03E6D76646f1EE6`](https://gnosisscan.io/address/0x4Bc39Dce61245F5A63189d1FA03E6D76646f1EE6) |
| `LidoStakedEthWalletMeta` ★      | [`0x7ED4F12798ea3f26AE02a6521eE84702CE5f8f90`](https://gnosisscan.io/address/0x7ED4F12798ea3f26AE02a6521eE84702CE5f8f90) |
| `MerklRewards` ☆                 | [`0xFf2D5A8bf801191eD1292E6DD6792F5D03c5aFBF`](https://gnosisscan.io/address/0xFf2D5A8bf801191eD1292E6DD6792F5D03c5aFBF) |
| `SkySavingsWalletMeta` ★         | [`0xb967b206445ca58445d09b6CE9878add5Bb14dd4`](https://gnosisscan.io/address/0xb967b206445ca58445d09b6CE9878add5Bb14dd4) |
| `StakeWiseV3ExitQueueAssisted`   | [`0xE1e163663961237C0955b665188864f0fD281020`](https://gnosisscan.io/address/0xE1e163663961237C0955b665188864f0fD281020) |
| `StakeWiseV3OsTokenWalletMeta` ★ | [`0x818dD183121641B09752891bB7E971b0FE29e101`](https://gnosisscan.io/address/0x818dD183121641B09752891bB7E971b0FE29e101) |
| `StakeWiseV3VaultsMeta`          | [`0x04f7a5Faaa9da0a163814e8C30ED7435F159A8eC`](https://gnosisscan.io/address/0x04f7a5Faaa9da0a163814e8C30ED7435F159A8eC) |
| `UniswapV3PoolsMeta` ☆           | [`0xaC47D9Cbe8B3EDc9c9d1a6c8092054299bBC1adE`](https://gnosisscan.io/address/0xaC47D9Cbe8B3EDc9c9d1a6c8092054299bBC1adE) |

### Polygon (137)

| Adapter              | Address                                                                                                                    |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `ERC20Default` ★     | [`0x4Bc39Dce61245F5A63189d1FA03E6D76646f1EE6`](https://polygonscan.com/address/0x4Bc39Dce61245F5A63189d1FA03E6D76646f1EE6) |
| `FluidFTokensMeta` ★ | [`0xb1a03cDD28cAA91f409C417C9f56ab53bc9d7ae9`](https://polygonscan.com/address/0xb1a03cDD28cAA91f409C417C9f56ab53bc9d7ae9) |
| `MerklRewards` ☆     | [`0xFf2D5A8bf801191eD1292E6DD6792F5D03c5aFBF`](https://polygonscan.com/address/0xFf2D5A8bf801191eD1292E6DD6792F5D03c5aFBF) |

### Base (8453)

| Adapter                         | Address                                                                                                                 |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `AaveV3InstancesMeta` ★         | [`0xa1b3c695A768A6Ce571fF699F463224CB32260C1`](https://basescan.org/address/0xa1b3c695A768A6Ce571fF699F463224CB32260C1) |
| `BalancerV3GaugesMeta`          | [`0xb9210b8311EA6Cee6cc9D1f8D4c29371fDA23495`](https://basescan.org/address/0xb9210b8311EA6Cee6cc9D1f8D4c29371fDA23495) |
| `BalancerV3PoolsMeta` ★         | [`0x8AE70624CB7729B0408C8f0ce8ffdac9974FE33A`](https://basescan.org/address/0x8AE70624CB7729B0408C8f0ce8ffdac9974FE33A) |
| `CoinbaseStakedEthWalletMeta` ★ | [`0x91d437752463B60103d515c16680d0a17C1c91F9`](https://basescan.org/address/0x91d437752463B60103d515c16680d0a17C1c91F9) |
| `CompoundV3CometsMeta`          | [`0xcf359A24511f6CEBE19125FE733338FCAe28Fc2E`](https://basescan.org/address/0xcf359A24511f6CEBE19125FE733338FCAe28Fc2E) |
| `ERC20Default` ★                | [`0x4Bc39Dce61245F5A63189d1FA03E6D76646f1EE6`](https://basescan.org/address/0x4Bc39Dce61245F5A63189d1FA03E6D76646f1EE6) |
| `EtherFiStakedEthWalletMeta` ★  | [`0x8a6fde256DD9B262596A22BC4a6c8292Fc3a5D65`](https://basescan.org/address/0x8a6fde256DD9B262596A22BC4a6c8292Fc3a5D65) |
| `FluidFTokensMeta` ★            | [`0xb1a03cDD28cAA91f409C417C9f56ab53bc9d7ae9`](https://basescan.org/address/0xb1a03cDD28cAA91f409C417C9f56ab53bc9d7ae9) |
| `LidoStakedEthWalletMeta` ★     | [`0x7ED4F12798ea3f26AE02a6521eE84702CE5f8f90`](https://basescan.org/address/0x7ED4F12798ea3f26AE02a6521eE84702CE5f8f90) |
| `MerklRewards` ☆                | [`0xFf2D5A8bf801191eD1292E6DD6792F5D03c5aFBF`](https://basescan.org/address/0xFf2D5A8bf801191eD1292E6DD6792F5D03c5aFBF) |
| `MorphoMarketsMeta` ☆           | [`0x08e590BcFc753fC6383aFE327253558C962D3EB6`](https://basescan.org/address/0x08e590BcFc753fC6383aFE327253558C962D3EB6) |

### Arbitrum (42161)

| Adapter                        | Address                                                                                                                |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `AaveV3InstancesMeta` ★        | [`0xa1b3c695A768A6Ce571fF699F463224CB32260C1`](https://arbiscan.io/address/0xa1b3c695A768A6Ce571fF699F463224CB32260C1) |
| `BalancerV3GaugesMeta`         | [`0xDe49FB1096Bb3B2A631183779482441c5505dAf8`](https://arbiscan.io/address/0xDe49FB1096Bb3B2A631183779482441c5505dAf8) |
| `BalancerV3PoolsMeta` ★        | [`0x8AE70624CB7729B0408C8f0ce8ffdac9974FE33A`](https://arbiscan.io/address/0x8AE70624CB7729B0408C8f0ce8ffdac9974FE33A) |
| `CompoundV3CometsMeta`         | [`0x84e2a1205e0bB6F4ECe35c5383948c4607eceEB0`](https://arbiscan.io/address/0x84e2a1205e0bB6F4ECe35c5383948c4607eceEB0) |
| `ERC20Default` ★               | [`0x4Bc39Dce61245F5A63189d1FA03E6D76646f1EE6`](https://arbiscan.io/address/0x4Bc39Dce61245F5A63189d1FA03E6D76646f1EE6) |
| `EtherFiStakedEthWalletMeta` ★ | [`0x8a6fde256DD9B262596A22BC4a6c8292Fc3a5D65`](https://arbiscan.io/address/0x8a6fde256DD9B262596A22BC4a6c8292Fc3a5D65) |
| `FluidFTokensMeta` ★           | [`0xb1a03cDD28cAA91f409C417C9f56ab53bc9d7ae9`](https://arbiscan.io/address/0xb1a03cDD28cAA91f409C417C9f56ab53bc9d7ae9) |
| `KelpRestakedEthWalletMeta` ★  | [`0xc0A8E9FA22F28bDE931624c6A454DE649a790273`](https://arbiscan.io/address/0xc0A8E9FA22F28bDE931624c6A454DE649a790273) |
| `LidoStakedEthWalletMeta` ★    | [`0x7ED4F12798ea3f26AE02a6521eE84702CE5f8f90`](https://arbiscan.io/address/0x7ED4F12798ea3f26AE02a6521eE84702CE5f8f90) |
| `MapleSavingsWalletMeta` ★     | [`0xDeE2BcC847DFc113ec70296C5D31e055a6cd902b`](https://arbiscan.io/address/0xDeE2BcC847DFc113ec70296C5D31e055a6cd902b) |
| `MerklRewards` ☆               | [`0xFf2D5A8bf801191eD1292E6DD6792F5D03c5aFBF`](https://arbiscan.io/address/0xFf2D5A8bf801191eD1292E6DD6792F5D03c5aFBF) |
| `MorphoMarketsMeta` ☆          | [`0xCFf47e28B51a7f4599150f73c348E1a155A93437`](https://arbiscan.io/address/0xCFf47e28B51a7f4599150f73c348E1a155A93437) |
| `MorphoVaultsMeta` ★           | [`0x72a012B30b240458C3f79fd20E5c2A907d77e54f`](https://arbiscan.io/address/0x72a012B30b240458C3f79fd20E5c2A907d77e54f) |
| `RenzoRestakedEthWalletMeta` ★ | [`0x9145A559108426e1eA429A2A6441e2A875f069ec`](https://arbiscan.io/address/0x9145A559108426e1eA429A2A6441e2A875f069ec) |

### BSC (56)

| Adapter          | Address                                                                                                                |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `ERC20Default` ★ | [`0x4Bc39Dce61245F5A63189d1FA03E6D76646f1EE6`](https://bscscan.com/address/0x4Bc39Dce61245F5A63189d1FA03E6D76646f1EE6) |
| `MerklRewards` ☆ | [`0xFf2D5A8bf801191eD1292E6DD6792F5D03c5aFBF`](https://bscscan.com/address/0xFf2D5A8bf801191eD1292E6DD6792F5D03c5aFBF) |

### Arc (5042)

| Adapter              | Address                                                                                                                     |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `ERC20Default` ★     | [`0x4Bc39Dce61245F5A63189d1FA03E6D76646f1EE6`](https://arc.etherscan.io/address/0x4Bc39Dce61245F5A63189d1FA03E6D76646f1EE6) |
| `MorphoVaultsMeta` ★ | [`0x72a012B30b240458C3f79fd20E5c2A907d77e54f`](https://arc.etherscan.io/address/0x72a012B30b240458C3f79fd20E5c2A907d77e54f) |

★ = same address on every chain; ☆ = shared only where the protocol address is. See [the fleet-wide adapter table](#nav-only-balance-adapters-also-one-address-on-every-chain) above.

## Custom price feeds (per chain)

These are plain `CREATE` deployments and their addresses are unique per chain; each feed's constructor takes chain-specific token and oracle addresses, so CREATE2 could not give them a shared address. Optimism (10), Polygon (137) and BSC (56) are absent below because they register no custom feeds at all; every asset they carry is priced by a direct feed registered straight on the NAV.

Every entry below was read back from `getPriceFeeds(asset)` on its chain, so this table lists the feeds the NAV **actually uses**, not merely ones that were deployed.

### Ethereum (1)

| Token     | Feed address                                                                                                            |
| --------- | ----------------------------------------------------------------------------------------------------------------------- |
| ETHx      | [`0x21BAd3b9B08D256639099f6932d93684e1a3C759`](https://etherscan.io/address/0x21BAd3b9B08D256639099f6932d93684e1a3C759) |
| LBTC      | [`0x91FF9E29C489350a977570adF94f67361f0cE52F`](https://etherscan.io/address/0x91FF9E29C489350a977570adF94f67361f0cE52F) |
| LDO       | [`0x1c876c765e7E0A60Fe6C637B6fef93415e6a60AF`](https://etherscan.io/address/0x1c876c765e7E0A60Fe6C637B6fef93415e6a60AF) |
| NXM       | [`0x8D5B6d7aD70a71663a1d7A0552E904b1DDBF1dC3`](https://etherscan.io/address/0x8D5B6d7aD70a71663a1d7A0552E904b1DDBF1dC3) |
| RWIV      | [`0x668738cdCA8ab586d5BBA7D0a41441b84B42C4B4`](https://etherscan.io/address/0x668738cdCA8ab586d5BBA7D0a41441b84B42C4B4) |
| ankrETH   | [`0x410004373F97fc53145BCD3D72D94BF0BEC69148`](https://etherscan.io/address/0x410004373F97fc53145BCD3D72D94BF0BEC69148) |
| eETH      | [`0xc81C4777a2A6CA69AE42558cB22e821d5114FdAF`](https://etherscan.io/address/0xc81C4777a2A6CA69AE42558cB22e821d5114FdAF) |
| ezETH     | [`0x692b06246bA895D2b7e82c635Cec6F2C3B32302b`](https://etherscan.io/address/0x692b06246bA895D2b7e82c635Cec6F2C3B32302b) |
| lsETH     | [`0x746749660345B1EcB570Af44111eb91a5A8e9d45`](https://etherscan.io/address/0x746749660345B1EcB570Af44111eb91a5A8e9d45) |
| osETH     | [`0x6470d36F9daC1e52993FA7690f7561773ca79D6C`](https://etherscan.io/address/0x6470d36F9daC1e52993FA7690f7561773ca79D6C) |
| rETH      | [`0x28998b0BE5Dc06525FB0d5585Dc7eD7cEa448DF7`](https://etherscan.io/address/0x28998b0BE5Dc06525FB0d5585Dc7eD7cEa448DF7) |
| rsETH     | [`0x39d4D331BbEd4c3Eca4c80f7F674e3268b93b6c3`](https://etherscan.io/address/0x39d4D331BbEd4c3Eca4c80f7F674e3268b93b6c3) |
| sGHO      | [`0x6c258D6B0e7b934cA96e8eA0CF5543FEF045B8c2`](https://etherscan.io/address/0x6c258D6B0e7b934cA96e8eA0CF5543FEF045B8c2) |
| sUSDS     | [`0x1581d5Cecb71fEca102C61572924CC33859714DA`](https://etherscan.io/address/0x1581d5Cecb71fEca102C61572924CC33859714DA) |
| sUSDe     | [`0x901cBc08fAc7296200cF69F27a904D1d218c8D90`](https://etherscan.io/address/0x901cBc08fAc7296200cF69F27a904D1d218c8D90) |
| stETH     | [`0x1F707cA1Ffd99ae3215f223f6a41571e0d3a4F47`](https://etherscan.io/address/0x1F707cA1Ffd99ae3215f223f6a41571e0d3a4F47) |
| syrupUSDC | [`0xB88B09725A2d8c3D05f77d0D4225b5DAE673Dfeb`](https://etherscan.io/address/0xB88B09725A2d8c3D05f77d0D4225b5DAE673Dfeb) |
| syrupUSDG | [`0x99b00DC4a17eE195a691B9c2281Ea5928e923e41`](https://etherscan.io/address/0x99b00DC4a17eE195a691B9c2281Ea5928e923e41) |
| syrupUSDT | [`0xfA25d9C240bA0d43b71672c1E04De882E324cFA9`](https://etherscan.io/address/0xfA25d9C240bA0d43b71672c1E04De882E324cFA9) |
| wARS      | [`0xbAcb08665c68b5798D6C6138cA02471e7c704931`](https://etherscan.io/address/0xbAcb08665c68b5798D6C6138cA02471e7c704931) |
| wNXM      | [`0x9e032A4536Bd09cF253ec1097d1c4D9540d4273A`](https://etherscan.io/address/0x9e032A4536Bd09cF253ec1097d1c4D9540d4273A) |
| weETH     | [`0x9E435B56E0e210342Db30d30cafeb5855F73c5A4`](https://etherscan.io/address/0x9E435B56E0e210342Db30d30cafeb5855F73c5A4) |
| wstETH    | [`0x1Da18b841caB327C850854dA964D08F1F0ef7EA6`](https://etherscan.io/address/0x1Da18b841caB327C850854dA964D08F1F0ef7EA6) |
| sDAI      | [`0x84eDa44d3ab2982692804b153D4bE88A0AfD533C`](https://etherscan.io/address/0x84eDa44d3ab2982692804b153D4bE88A0AfD533C) |
| spETH     | [`0x93E408dF0A0BEb203e24163bE513e25ca6EF983B`](https://etherscan.io/address/0x93E408dF0A0BEb203e24163bE513e25ca6EF983B) |
| spUSDC    | [`0x571cB9069eb2ad434d8A5bB4853602eb6D2a6d43`](https://etherscan.io/address/0x571cB9069eb2ad434d8A5bB4853602eb6D2a6d43) |
| spUSDT    | [`0xA41107cb75012A399358482cC850630D9dD8672A`](https://etherscan.io/address/0xA41107cb75012A399358482cC850630D9dD8672A) |

### Gnosis (100)

| Token  | Feed address                                                                                                             |
| ------ | ------------------------------------------------------------------------------------------------------------------------ |
| osGNO  | [`0xd0e852a932490290B5bAACe8DafF04D9ec19c164`](https://gnosisscan.io/address/0xd0e852a932490290B5bAACe8DafF04D9ec19c164) |
| sDAI   | [`0x513F159f9a2a65fec8AC61d16203450fDC7cA893`](https://gnosisscan.io/address/0x513F159f9a2a65fec8AC61d16203450fDC7cA893) |
| wstETH | [`0x2A0Fa71f5923f720c4a622cD4Da0b7837C466612`](https://gnosisscan.io/address/0x2A0Fa71f5923f720c4a622cD4Da0b7837C466612) |

### Base (8453)

| Token  | Feed address                                                                                                            |
| ------ | ----------------------------------------------------------------------------------------------------------------------- |
| cbETH  | [`0x0E415360A30dFd38aC50EB6F32C27b2A3a181DE1`](https://basescan.org/address/0x0E415360A30dFd38aC50EB6F32C27b2A3a181DE1) |
| weETH  | [`0x02C628215643cBa80C96C620cB935eD808AABDDC`](https://basescan.org/address/0x02C628215643cBa80C96C620cB935eD808AABDDC) |
| wstETH | [`0xD4583b75B09f40E6a39F508afD502249E4bfEb48`](https://basescan.org/address/0xD4583b75B09f40E6a39F508afD502249E4bfEb48) |

### Arbitrum (42161)

| Token     | Feed address                                                                                                           |
| --------- | ---------------------------------------------------------------------------------------------------------------------- |
| ezETH     | [`0xc8FDD6D339864eEd91Ea94f9018439Ff5c98f5EC`](https://arbiscan.io/address/0xc8FDD6D339864eEd91Ea94f9018439Ff5c98f5EC) |
| rsETH     | [`0x500b6920248eAFbB2a11Eaa79193A0C5Cd219c5E`](https://arbiscan.io/address/0x500b6920248eAFbB2a11Eaa79193A0C5Cd219c5E) |
| syrupUSDC | [`0x925194Ba011f5907943BC1A1CbF7bCaee806aeb5`](https://arbiscan.io/address/0x925194Ba011f5907943BC1A1CbF7bCaee806aeb5) |
| weETH     | [`0xE641c8cD8e3f7C31C0312b30cB20620bc80749Ef`](https://arbiscan.io/address/0xE641c8cD8e3f7C31C0312b30cB20620bc80749Ef) |
| wstETH    | [`0x24ACa7b73dda9f4f6Fbd366f0537b70C491FEe30`](https://arbiscan.io/address/0x24ACa7b73dda9f4f6Fbd366f0537b70C491FEe30) |

### Arc (5042)

| Token | Feed address                                                                                                                |
| ----- | --------------------------------------------------------------------------------------------------------------------------- |
| wARS  | [`0xC67360B2Cf8d23849ad2b7F3E79AB813D64A94C5`](https://arc.etherscan.io/address/0xC67360B2Cf8d23849ad2b7F3E79AB813D64A94C5) |


# Code examples

Read NAV, positions, and prices from the NAV Calculator with viem, ethers.js, and Python.

Practical examples for reading NAV, prices, and position data from KPK funds via the NAV Calculator.

**Source:** `INAVCalculator.sol`

{% hint style="info" %}
Addresses below are placeholders. Use the canonical **proxy** address for your chain from [Deployment addresses](/funds/infrastructure/architecture/onchain-accounting/deployment-addresses), and call the proxy; never the implementation.
{% endhint %}

{% hint style="info" %}
USD values are returned with **8 decimals** (`usdDecimals()`). Pass `address(0)` as the quote asset for USD.
{% endhint %}

## 1. Check chain health

Before trusting a NAV reading, verify the chain is healthy, no stale prices, sequencer up.

```solidity
function healthCheck()
    external view returns (bool sequencerDown, address[] memory stalePriceAssets, address[] memory irregularPriceAssets);
```

{% hint style="info" %}
`healthCheck` returns three values, not four: it deliberately carries **no `monitorsUnhealthyPriceAssets`**, because widening its return was not affordable against the implementation's EIP-170 headroom. So an empty `irregularPriceAssets` here does not establish that the divergence check ran. Read that array off a `getAccountNav` / `getAccountNavForAdapters` result; see [Stale prices & sequencer](/funds/infrastructure/architecture/onchain-accounting/concepts/stale-prices-and-sequencer#when-no-monitor-answered-monitorsunhealthypriceassets).
{% endhint %}

{% tabs %}
{% tab title="viem" %}

```typescript
import { createPublicClient, http, parseAbi } from "viem";
import { mainnet } from "viem/chains";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses

const ABI = parseAbi([
  "function healthCheck() view returns (bool sequencerDown, address[] stalePriceAssets, address[] irregularPriceAssets)",
]);

const client = createPublicClient({ chain: mainnet, transport: http() });

async function checkHealth() {
  const [sequencerDown, stalePriceAssets, irregularPriceAssets] = await client.readContract({
    address: NAV_CALCULATOR, abi: ABI, functionName: "healthCheck",
  });
  if (sequencerDown || stalePriceAssets.length > 0) {
    console.warn("Chain unhealthy:", { sequencerDown, stalePriceAssets });
    return false;
  }
  if (irregularPriceAssets.length > 0) {
    console.warn("Price divergence (soft alert):", { irregularPriceAssets }); // still usable — investigate
  }
  return true;
}
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses

const ABI = [
  "function healthCheck() view returns (bool sequencerDown, address[] stalePriceAssets, address[] irregularPriceAssets)",
];

async function checkHealth() {
  const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
  const calc = new ethers.Contract(NAV_CALCULATOR, ABI, provider);

  const [sequencerDown, stalePriceAssets, irregularPriceAssets] = await calc.healthCheck();
  if (sequencerDown || stalePriceAssets.length > 0) {
    console.warn("Chain unhealthy:", { sequencerDown, stalePriceAssets });
    return false;
  }
  if (irregularPriceAssets.length > 0) {
    console.warn("Price divergence (soft alert):", { irregularPriceAssets }); // still usable — investigate
  }
  return true;
}
```

{% endtab %}

{% tab title="Python" %}

```python
from web3 import Web3

NAV_CALCULATOR = "0x..."  # proxy — see Deployment addresses
ABI = [{
    "name": "healthCheck", "type": "function", "stateMutability": "view",
    "inputs": [],
    "outputs": [
        {"name": "sequencerDown", "type": "bool"},
        {"name": "stalePriceAssets", "type": "address[]"},
        {"name": "irregularPriceAssets", "type": "address[]"},
    ],
}]

w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
calc = w3.eth.contract(address=NAV_CALCULATOR, abi=ABI)
sequencer_down, stale_assets, irregular_assets = calc.functions.healthCheck().call()

if sequencer_down or stale_assets:
    print(f"Unhealthy: sequencerDown={sequencer_down}, stale={stale_assets}")
elif irregular_assets:
    print(f"Price divergence (soft alert, still usable): {irregular_assets}")
```

{% endtab %}
{% endtabs %}

## 2. Read NAV: single chain

Pass the account (portfolio Safe) and the quote asset (`address(0)` for USD).

{% tabs %}
{% tab title="viem" %}

```typescript
import { createPublicClient, http, parseAbi, zeroAddress } from "viem";
import { mainnet } from "viem/chains";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const ACCOUNT        = "0x..."; // portfolio Safe

const ABI = parseAbi([
  `function getAccountNav(address account, address quoteAsset) view returns (
     (int256 value, (address asset, string symbol, uint8 decimals) quoteAsset,
      uint64 timestamp, (address asset, string symbol, uint8 decimals)[] stalePriceAssets,
      bool sequencerDown, bool quoteAssetStale,
      (address asset, string symbol, uint8 decimals)[] irregularPriceAssets, bool quoteAssetIrregular,
      (address asset, string symbol, uint8 decimals)[] monitorsUnhealthyPriceAssets) nav
   )`,
]);

const client = createPublicClient({ chain: mainnet, transport: http() });

async function readNav() {
  const nav = await client.readContract({
    address: NAV_CALCULATOR, abi: ABI, functionName: "getAccountNav",
    args: [ACCOUNT, zeroAddress], // zeroAddress = USD
  });

  if (nav.sequencerDown) throw new Error("L2 sequencer is down");
  if (nav.quoteAssetStale) throw new Error("Quote-asset feed stale (value fell back to USD)");
  if (nav.stalePriceAssets.length > 0) {
    throw new Error(`Stale prices for: ${nav.stalePriceAssets.map((a) => a.symbol).join(", ")}`);
  }

  // Soft signals. Read them together: an asset in the second list was NOT checked,
  // so its absence from the first list carries no information.
  if (nav.irregularPriceAssets.length > 0) {
    console.warn("Diverged:", nav.irregularPriceAssets.map((a) => a.symbol).join(", "));
  }
  if (nav.monitorsUnhealthyPriceAssets.length > 0) {
    console.warn("Unchecked (no monitor answered):",
      nav.monitorsUnhealthyPriceAssets.map((a) => a.symbol).join(", "));
  }

  console.log(`Chain NAV (USD): ${Number(nav.value) / 1e8}`);
}

readNav();
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const ACCOUNT        = "0x..."; // portfolio Safe
const USD            = ethers.ZeroAddress;

const ABI = [
  `function getAccountNav(address account, address quoteAsset) view returns (
     tuple(int256 value, tuple(address asset, string symbol, uint8 decimals) quoteAsset,
           uint64 timestamp, tuple(address asset, string symbol, uint8 decimals)[] stalePriceAssets,
           bool sequencerDown, bool quoteAssetStale,
           tuple(address asset, string symbol, uint8 decimals)[] irregularPriceAssets, bool quoteAssetIrregular,
           tuple(address asset, string symbol, uint8 decimals)[] monitorsUnhealthyPriceAssets) nav
   )`,
];

async function readNav() {
  const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
  const navCalc  = new ethers.Contract(NAV_CALCULATOR, ABI, provider);
  const nav = await navCalc.getAccountNav(ACCOUNT, USD);

  if (nav.sequencerDown) throw new Error("L2 sequencer is down");
  if (nav.quoteAssetStale) throw new Error("Quote-asset feed stale");
  if (nav.stalePriceAssets.length > 0) {
    throw new Error(`Stale: ${nav.stalePriceAssets.map((a: any) => a.symbol).join(", ")}`);
  }

  // Soft signals — an asset in the second list was not checked at all.
  if (nav.irregularPriceAssets.length > 0) {
    console.warn("Diverged:", nav.irregularPriceAssets.map((a: any) => a.symbol).join(", "));
  }
  if (nav.monitorsUnhealthyPriceAssets.length > 0) {
    console.warn("Unchecked (no monitor answered):",
      nav.monitorsUnhealthyPriceAssets.map((a: any) => a.symbol).join(", "));
  }

  console.log(`Chain NAV (USD): ${Number(nav.value) / 1e8}`);
}

readNav();
```

{% endtab %}

{% tab title="Python" %}

```python
from web3 import Web3

NAV_CALCULATOR = "0x..."  # proxy — see Deployment addresses
ACCOUNT        = "0x..."  # portfolio Safe
USD            = "0x0000000000000000000000000000000000000000"  # address(0) = USD

ASSET = [{"name": "asset", "type": "address"}, {"name": "symbol", "type": "string"}, {"name": "decimals", "type": "uint8"}]
NAV_ABI = [{
    "name": "getAccountNav", "type": "function", "stateMutability": "view",
    "inputs": [{"name": "account", "type": "address"}, {"name": "quoteAsset", "type": "address"}],
    "outputs": [{"name": "nav", "type": "tuple", "components": [
        {"name": "value",            "type": "int256"},
        {"name": "quoteAsset",       "type": "tuple", "components": ASSET},
        {"name": "timestamp",        "type": "uint64"},
        {"name": "stalePriceAssets", "type": "tuple[]", "components": ASSET},
        {"name": "sequencerDown",    "type": "bool"},
        {"name": "quoteAssetStale",  "type": "bool"},
        {"name": "irregularPriceAssets", "type": "tuple[]", "components": ASSET},
        {"name": "quoteAssetIrregular",  "type": "bool"},
        {"name": "monitorsUnhealthyPriceAssets", "type": "tuple[]", "components": ASSET},
    ]}],
}]

w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
calc = w3.eth.contract(address=NAV_CALCULATOR, abi=NAV_ABI)
nav = calc.functions.getAccountNav(ACCOUNT, USD).call()

if nav[4]:           # sequencerDown
    raise RuntimeError("L2 sequencer is down")
if nav[5]:           # quoteAssetStale
    raise RuntimeError("Quote-asset feed stale")
if nav[3]:           # stalePriceAssets
    raise RuntimeError(f"Stale prices for: {[a[1] for a in nav[3]]}")
if nav[6]:           # irregularPriceAssets — soft alert, value still usable
    print(f"Price divergence (investigate): {[a[1] for a in nav[6]]}")
if nav[8]:           # monitorsUnhealthyPriceAssets — the divergence check did NOT run for these
    print(f"Unchecked, no monitor answered: {[a[1] for a in nav[8]]}")

print(f"Chain NAV (USD): {nav[0] / 1e8}")
```

{% endtab %}
{% endtabs %}

### Paging a read that no longer fits

`getAccountNav` scans **every** registered adapter in one `eth_call`. If that ever exceeds the node's gas cap you get no NAV at all, so `getAccountNavForAdapters` lets you ask the same question over a subset and reassemble.

Reassembly is where this goes wrong, so the example below is the reassembly, not the call. Three rules:

* **Only `value` adds up.** `stalePriceAssets` / `irregularPriceAssets` / `monitorsUnhealthyPriceAssets` cover only the assets *that slice* touched, and `sequencerDown` / `quoteAssetStale` / `quoteAssetIrregular` are reported per slice. Union the arrays and OR the booleans across **every** slice, checking them on one slice yields a NAV that looks healthy while silently dropping the others' warnings. Dropping the third array under-reports monitor failure, which is the falsely-reassuring direction.
* **Slice by adapter set, never by index range.** The adapter list is swap-and-pop, so a removal between calls moves the tail adapter down into a slot you already read, and it is skipped.
* **Verify with `adaptersCovered`.** Concatenate it across slices and compare against `getAllAdapters()`. A missing address means the total is short by that adapter's positions; undetectable from `value` alone, because a short NAV is a perfectly plausible number.

Pass the **same** `quoteAsset` to every slice.

{% hint style="warning" %}
`PartialNAV` places `monitorsUnhealthyPriceAssets` **after** `adaptersCovered`, whereas `NAV` ends with it. The two structs mirror each other only across their first eight fields, so a decoder written against `NAV` cannot be pointed at a `PartialNAV` past that prefix. The ordering below is `PartialNAV`'s.
{% endhint %}

{% tabs %}
{% tab title="viem" %}

```typescript
import { createPublicClient, http, parseAbi, zeroAddress } from "viem";
import { mainnet } from "viem/chains";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const ACCOUNT        = "0x..."; // portfolio Safe
const ASSET_T        = "(address asset, string symbol, uint8 decimals)";

const ABI = parseAbi([
  `function getAllAdapters() view returns (address[])`,
  `function getAccountNavForAdapters(address account, address quoteAsset, address[] adapters) view returns (
     (int256 value, ${ASSET_T} quoteAsset, uint64 timestamp, ${ASSET_T}[] stalePriceAssets,
      bool sequencerDown, bool quoteAssetStale, ${ASSET_T}[] irregularPriceAssets,
      bool quoteAssetIrregular, address[] adaptersCovered,
      ${ASSET_T}[] monitorsUnhealthyPriceAssets) partialNav
   )`,
]);

const client = createPublicClient({ chain: mainnet, transport: http() });

async function readNavPaged(sliceSize = 5) {
  // Read the adapter set ONCE, then slice that snapshot — never re-read per page.
  const all = await client.readContract({
    address: NAV_CALCULATOR, abi: ABI, functionName: "getAllAdapters",
  });

  const slices: `0x${string}`[][] = [];
  for (let i = 0; i < all.length; i += sliceSize) slices.push([...all.slice(i, i + sliceSize)]);

  const parts = await Promise.all(slices.map((adapters) =>
    client.readContract({
      address: NAV_CALCULATOR, abi: ABI, functionName: "getAccountNavForAdapters",
      args: [ACCOUNT, zeroAddress, adapters], // zeroAddress = USD
    })
  ));

  // value SUMS; every health signal is unioned / ORed.
  const total    = parts.reduce((acc, p) => acc + p.value, 0n);
  const stale    = new Map(parts.flatMap((p) => p.stalePriceAssets).map((a) => [a.asset, a]));
  const irregular= new Map(parts.flatMap((p) => p.irregularPriceAssets).map((a) => [a.asset, a]));
  const unchecked= new Map(parts.flatMap((p) => p.monitorsUnhealthyPriceAssets).map((a) => [a.asset, a]));
  const covered  = parts.flatMap((p) => [...p.adaptersCovered]);

  // Completeness check — a short NAV is a plausible number, so prove it isn't one.
  const missing = all.filter((a) => !covered.includes(a));
  if (missing.length) throw new Error(`NAV incomplete — adapters never read: ${missing.join(", ")}`);

  if (parts.some((p) => p.sequencerDown))   throw new Error("L2 sequencer is down");
  if (parts.some((p) => p.quoteAssetStale)) throw new Error("Quote-asset feed stale");
  if (stale.size) throw new Error(`Stale prices for: ${[...stale.values()].map((a) => a.symbol).join(", ")}`);
  if (irregular.size) console.warn(`Price divergence: ${[...irregular.values()].map((a) => a.symbol).join(", ")}`);
  if (unchecked.size) console.warn(`Unchecked, no monitor answered: ${[...unchecked.values()].map((a) => a.symbol).join(", ")}`);

  console.log(`Chain NAV (USD): ${Number(total) / 1e8}`);
}

readNavPaged();
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const ACCOUNT        = "0x..."; // portfolio Safe
const USD            = ethers.ZeroAddress;
const ASSET_T        = "tuple(address asset, string symbol, uint8 decimals)";

const ABI = [
  `function getAllAdapters() view returns (address[])`,
  `function getAccountNavForAdapters(address account, address quoteAsset, address[] adapters) view returns (
     tuple(int256 value, ${ASSET_T} quoteAsset, uint64 timestamp, ${ASSET_T}[] stalePriceAssets,
           bool sequencerDown, bool quoteAssetStale, ${ASSET_T}[] irregularPriceAssets,
           bool quoteAssetIrregular, address[] adaptersCovered,
           ${ASSET_T}[] monitorsUnhealthyPriceAssets) partialNav
   )`,
];

async function readNavPaged(sliceSize = 5) {
  const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
  const navCalc  = new ethers.Contract(NAV_CALCULATOR, ABI, provider);

  const all: string[] = [...(await navCalc.getAllAdapters())];
  const slices: string[][] = [];
  for (let i = 0; i < all.length; i += sliceSize) slices.push(all.slice(i, i + sliceSize));

  const parts = await Promise.all(
    slices.map((adapters) => navCalc.getAccountNavForAdapters(ACCOUNT, USD, adapters))
  );

  const total   = parts.reduce((acc: bigint, p: any) => acc + p.value, 0n);
  const stale   = new Map(parts.flatMap((p: any) => p.stalePriceAssets).map((a: any) => [a.asset, a]));
  const irregular = new Map(parts.flatMap((p: any) => p.irregularPriceAssets).map((a: any) => [a.asset, a]));
  const unchecked = new Map(parts.flatMap((p: any) => p.monitorsUnhealthyPriceAssets).map((a: any) => [a.asset, a]));
  const covered = parts.flatMap((p: any) => [...p.adaptersCovered]);

  const missing = all.filter((a) => !covered.includes(a));
  if (missing.length) throw new Error(`NAV incomplete — adapters never read: ${missing.join(", ")}`);

  if (parts.some((p: any) => p.sequencerDown))   throw new Error("L2 sequencer is down");
  if (parts.some((p: any) => p.quoteAssetStale)) throw new Error("Quote-asset feed stale");
  if (stale.size) throw new Error(`Stale: ${[...stale.values()].map((a: any) => a.symbol).join(", ")}`);
  if (irregular.size) console.warn(`Price divergence: ${[...irregular.values()].map((a: any) => a.symbol).join(", ")}`);
  if (unchecked.size) console.warn(`Unchecked, no monitor answered: ${[...unchecked.values()].map((a: any) => a.symbol).join(", ")}`);

  console.log(`Chain NAV (USD): ${Number(total) / 1e8}`);
}

readNavPaged();
```

{% endtab %}

{% tab title="Python" %}

```python
from web3 import Web3

NAV_CALCULATOR = "0x..."  # proxy — see Deployment addresses
ACCOUNT        = "0x..."  # portfolio Safe
USD            = "0x0000000000000000000000000000000000000000"
SLICE_SIZE     = 5

ASSET = [{"name": "asset", "type": "address"}, {"name": "symbol", "type": "string"}, {"name": "decimals", "type": "uint8"}]
ABI = [
    {"name": "getAllAdapters", "type": "function", "stateMutability": "view",
     "inputs": [], "outputs": [{"name": "adapters", "type": "address[]"}]},
    {"name": "getAccountNavForAdapters", "type": "function", "stateMutability": "view",
     "inputs": [{"name": "account", "type": "address"}, {"name": "quoteAsset", "type": "address"},
                {"name": "adapters", "type": "address[]"}],
     "outputs": [{"name": "partialNav", "type": "tuple", "components": [
         {"name": "value",                "type": "int256"},
         {"name": "quoteAsset",           "type": "tuple", "components": ASSET},
         {"name": "timestamp",            "type": "uint64"},
         {"name": "stalePriceAssets",     "type": "tuple[]", "components": ASSET},
         {"name": "sequencerDown",        "type": "bool"},
         {"name": "quoteAssetStale",      "type": "bool"},
         {"name": "irregularPriceAssets", "type": "tuple[]", "components": ASSET},
         {"name": "quoteAssetIrregular",  "type": "bool"},
         {"name": "adaptersCovered",      "type": "address[]"},
         # PartialNAV appends this AFTER adaptersCovered — unlike NAV, where it is last.
         {"name": "monitorsUnhealthyPriceAssets", "type": "tuple[]", "components": ASSET},
     ]}]},
]

w3   = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
calc = w3.eth.contract(address=NAV_CALCULATOR, abi=ABI)

# Snapshot the adapter set once, then slice it — never re-read between pages.
all_adapters = calc.functions.getAllAdapters().call()
slices = [all_adapters[i:i + SLICE_SIZE] for i in range(0, len(all_adapters), SLICE_SIZE)]
parts  = [calc.functions.getAccountNavForAdapters(ACCOUNT, USD, s).call() for s in slices]

total      = sum(p[0] for p in parts)                        # only value adds up
stale      = {a[0]: a for p in parts for a in p[3]}          # union, deduped by address
irregular  = {a[0]: a for p in parts for a in p[6]}
covered    = [a for p in parts for a in p[8]]                # adaptersCovered
unchecked  = {a[0]: a for p in parts for a in p[9]}          # monitorsUnhealthyPriceAssets

missing = [a for a in all_adapters if a not in covered]
if missing:
    raise RuntimeError(f"NAV incomplete — adapters never read: {missing}")

if any(p[4] for p in parts):                                 # sequencerDown, ORed
    raise RuntimeError("L2 sequencer is down")
if any(p[5] for p in parts):                                 # quoteAssetStale, ORed
    raise RuntimeError("Quote-asset feed stale")
if stale:
    raise RuntimeError(f"Stale prices for: {[a[1] for a in stale.values()]}")
if irregular:
    print(f"Price divergence (investigate): {[a[1] for a in irregular.values()]}")
if unchecked:
    print(f"Unchecked, no monitor answered: {[a[1] for a in unchecked.values()]}")

print(f"Chain NAV (USD): {total / 1e8}")
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
A slice is rejected rather than returning a quietly wrong `value`: `EmptyAdapterSlice()`, `AdapterNotRegistered(address)` for anything absent from `getAllAdapters()`, and `DuplicateAdapterInSlice(address)` for a repeat. A **meta-adapter is atomic**; naming one covers all its governed instances, and there is no paging within a single adapter, so pagination cannot rescue one that is oversized on its own.
{% endhint %}

## 3. Read global NAV: all chains

Funds deployed on multiple chains: read each chain and sum the per-chain `value` once every chain is healthy.

{% tabs %}
{% tab title="viem" %}

```typescript
import { createPublicClient, http, parseAbi, zeroAddress } from "viem";
import { mainnet, arbitrum, base } from "viem/chains";

const NAV_CALCULATOR = "0x..."; // same proxy address on every chain
const ACCOUNT        = "0x...";

const ABI = parseAbi([
  `function getAccountNav(address account, address quoteAsset) view returns (
     (int256 value, (address asset, string symbol, uint8 decimals) quoteAsset,
      uint64 timestamp, (address asset, string symbol, uint8 decimals)[] stalePriceAssets,
      bool sequencerDown, bool quoteAssetStale,
      (address asset, string symbol, uint8 decimals)[] irregularPriceAssets, bool quoteAssetIrregular,
      (address asset, string symbol, uint8 decimals)[] monitorsUnhealthyPriceAssets))`,
]);

const CHAINS = [
  { chain: mainnet,  rpc: "https://eth.llamarpc.com" },
  { chain: arbitrum, rpc: "https://arb1.arbitrum.io/rpc" },
  { chain: base,     rpc: "https://mainnet.base.org" },
];

async function readGlobalNav() {
  const results = await Promise.all(
    CHAINS.map(({ chain, rpc }) =>
      createPublicClient({ chain, transport: http(rpc) }).readContract({
        address: NAV_CALCULATOR, abi: ABI, functionName: "getAccountNav",
        args: [ACCOUNT, zeroAddress],
      })
    )
  );

  results.forEach((nav, i) => {
    if (nav.sequencerDown || nav.quoteAssetStale || nav.stalePriceAssets.length > 0) {
      throw new Error(`Chain ${CHAINS[i].chain.name} is unhealthy`);
    }
  });

  const globalNav = results.reduce((sum, nav) => sum + nav.value, 0n);
  console.log(`Global NAV (USD): ${Number(globalNav) / 1e8}`);
  return globalNav;
}

readGlobalNav();
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const NAV_CALCULATOR = "0x..."; // same proxy address on every chain
const ACCOUNT        = "0x...";
const USD            = ethers.ZeroAddress;

const ABI = [
  `function getAccountNav(address account, address quoteAsset) view returns (
     tuple(int256 value, tuple(address asset, string symbol, uint8 decimals) quoteAsset,
           uint64 timestamp, tuple(address asset, string symbol, uint8 decimals)[] stalePriceAssets,
           bool sequencerDown, bool quoteAssetStale,
           tuple(address asset, string symbol, uint8 decimals)[] irregularPriceAssets, bool quoteAssetIrregular,
           tuple(address asset, string symbol, uint8 decimals)[] monitorsUnhealthyPriceAssets))`,
];

const RPCS = {
  mainnet:  "https://eth.llamarpc.com",
  arbitrum: "https://arb1.arbitrum.io/rpc",
  base:     "https://mainnet.base.org",
};

async function readGlobalNav() {
  const entries = Object.entries(RPCS);
  const results = await Promise.all(
    entries.map(([, rpc]) => {
      const provider = new ethers.JsonRpcProvider(rpc);
      return new ethers.Contract(NAV_CALCULATOR, ABI, provider).getAccountNav(ACCOUNT, USD);
    })
  );

  results.forEach((nav, i) => {
    if (nav.sequencerDown || nav.quoteAssetStale || nav.stalePriceAssets.length > 0) {
      throw new Error(`Chain ${entries[i][0]} is unhealthy`);
    }
  });

  const globalNav = results.reduce((sum, nav) => sum + nav.value, 0n);
  console.log(`Global NAV (USD): ${Number(globalNav) / 1e8}`);
  return globalNav;
}

readGlobalNav();
```

{% endtab %}

{% tab title="Python" %}

```python
import asyncio
from web3 import AsyncWeb3

NAV_CALCULATOR = "0x..."  # same proxy address on every chain
ACCOUNT        = "0x..."
USD            = "0x0000000000000000000000000000000000000000"

ASSET = [{"name": "asset", "type": "address"}, {"name": "symbol", "type": "string"}, {"name": "decimals", "type": "uint8"}]
ABI = [{"name": "getAccountNav", "type": "function", "stateMutability": "view",
        "inputs": [{"name": "account", "type": "address"}, {"name": "quoteAsset", "type": "address"}],
        "outputs": [{"name": "", "type": "tuple", "components": [
            {"name": "value", "type": "int256"},
            {"name": "quoteAsset", "type": "tuple", "components": ASSET},
            {"name": "timestamp", "type": "uint64"},
            {"name": "stalePriceAssets", "type": "tuple[]", "components": ASSET},
            {"name": "sequencerDown", "type": "bool"},
            {"name": "quoteAssetStale", "type": "bool"},
            {"name": "irregularPriceAssets", "type": "tuple[]", "components": ASSET},
            {"name": "quoteAssetIrregular", "type": "bool"},
            {"name": "monitorsUnhealthyPriceAssets", "type": "tuple[]", "components": ASSET},
        ]}]}]

RPCS = {
    "mainnet":  "https://eth.llamarpc.com",
    "arbitrum": "https://arb1.arbitrum.io/rpc",
    "base":     "https://mainnet.base.org",
}

async def read_chain(chain, rpc):
    w3 = AsyncWeb3(AsyncWeb3.AsyncHTTPProvider(rpc))
    nav = await w3.eth.contract(address=NAV_CALCULATOR, abi=ABI).functions.getAccountNav(ACCOUNT, USD).call()
    return chain, nav

async def read_global_nav():
    results = await asyncio.gather(*[read_chain(c, r) for c, r in RPCS.items()])
    for chain, nav in results:
        if nav[4] or nav[5] or nav[3]:
            raise RuntimeError(f"{chain} is unhealthy")
    total = sum(nav[0] for _, nav in results)
    print(f"Global NAV (USD): {total / 1e8}")
    return total

asyncio.run(read_global_nav())
```

{% endtab %}
{% endtabs %}

## 4. Enumerate positions with labels

`getAccountPositions` returns the typed positions. For human-readable labels in one call, use the NAV Calculator's verbose read `getAccountPositionsVerbose`, which adds `protocolName` and a `labels` array (the breadcrumb each adapter exposes through `IPositionDescribable`). There is no separate lens contract, call the NAV Calculator proxy directly.

{% tabs %}
{% tab title="viem (verbose)" %}

```typescript
import { createPublicClient, http, parseAbi, zeroAddress } from "viem";
import { mainnet } from "viem/chains";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const ACCOUNT  = "0x...";

const ABI = parseAbi([
  `function getAccountPositionsVerbose(address account, address quoteAsset) view returns (
     (
       (
         (address asset, string symbol, uint8 decimals) balanceAsset,
         address balanceAdapter, uint256 amount, bool isDebt,
         (address asset, string symbol, uint8 decimals) quoteAsset,
         int256 value, int256 price, uint8 priceDecimals, bool stale, bool quoteAssetStale,
         bytes32 protocolBrand, bytes32 protocolId, bytes32 protocolSubId, bytes positionId, uint8 positionKind, bool isLocked, bytes32 positionInstanceId, bool irregular, bool quoteAssetIrregular
       ) position,
       string protocolName,
       string[] labels
     )[]
   )`,
]);

const client = createPublicClient({ chain: mainnet, transport: http() });

async function getPortfolio() {
  const rows = await client.readContract({
    address: NAV_CALCULATOR, abi: ABI, functionName: "getAccountPositionsVerbose",
    args: [ACCOUNT, zeroAddress],
  });

  for (const { position: p, protocolName, labels } of rows) {
    const usd = Number(p.value) / 1e8;
    const detail = [protocolName, ...labels].filter(Boolean).join(" — ");
    const label = detail || p.balanceAsset.symbol;
    console.log(`${label}: $${usd.toFixed(2)}${p.stale ? " ⚠ stale" : ""}`);
  }
}

getPortfolio();
```

{% endtab %}

{% tab title="ethers.js (verbose)" %}

```typescript
import { ethers } from "ethers";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const ACCOUNT  = "0x...";
const USD      = ethers.ZeroAddress;

const ABI = [
  `function getAccountPositionsVerbose(address account, address quoteAsset) view returns (
     tuple(
       tuple(
         tuple(address asset, string symbol, uint8 decimals) balanceAsset,
         address balanceAdapter, uint256 amount, bool isDebt,
         tuple(address asset, string symbol, uint8 decimals) quoteAsset,
         int256 value, int256 price, uint8 priceDecimals, bool stale, bool quoteAssetStale,
         bytes32 protocolBrand, bytes32 protocolId, bytes32 protocolSubId, bytes positionId, uint8 positionKind, bool isLocked, bytes32 positionInstanceId, bool irregular, bool quoteAssetIrregular
       ) position,
       string protocolName,
       string[] labels
     )[]
   )`,
];

async function getPortfolio() {
  const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
  const calc = new ethers.Contract(NAV_CALCULATOR, ABI, provider);
  const rows = await calc.getAccountPositionsVerbose(ACCOUNT, USD);

  for (const { position: p, protocolName, labels } of rows) {
    const usd = Number(p.value) / 1e8;
    const detail = [protocolName, ...labels].filter(Boolean).join(" — ");
    const label = detail || p.balanceAsset.symbol;
    console.log(`${label}: $${usd.toFixed(2)}${p.stale ? " ⚠ stale" : ""}`);
  }
}

getPortfolio();
```

{% endtab %}

{% tab title="Python (typed)" %}

```python
from web3 import Web3

NAV_CALCULATOR = "0x..."  # proxy — see Deployment addresses
ACCOUNT        = "0x..."
USD            = "0x0000000000000000000000000000000000000000"

ASSET = [{"name": "asset", "type": "address"}, {"name": "symbol", "type": "string"}, {"name": "decimals", "type": "uint8"}]
POS = [
    {"name": "balanceAsset",   "type": "tuple", "components": ASSET},
    {"name": "balanceAdapter", "type": "address"},
    {"name": "amount",         "type": "uint256"},
    {"name": "isDebt",         "type": "bool"},
    {"name": "quoteAsset",     "type": "tuple", "components": ASSET},
    {"name": "value",          "type": "int256"},
    {"name": "price",          "type": "int256"},
    {"name": "priceDecimals",  "type": "uint8"},
    {"name": "stale",          "type": "bool"},
    {"name": "quoteAssetStale","type": "bool"},
    {"name": "protocolBrand",      "type": "bytes32"},
    {"name": "protocolId",         "type": "bytes32"},
    {"name": "protocolSubId",      "type": "bytes32"},
    {"name": "positionId",         "type": "bytes"},
    {"name": "positionKind",       "type": "uint8"},
    {"name": "isLocked",           "type": "bool"},
    {"name": "positionInstanceId", "type": "bytes32"},
    {"name": "irregular",          "type": "bool"},
    {"name": "quoteAssetIrregular","type": "bool"},
]
ABI = [{"name": "getAccountPositions", "type": "function", "stateMutability": "view",
        "inputs": [{"name": "account", "type": "address"}, {"name": "quoteAsset", "type": "address"}],
        "outputs": [{"name": "", "type": "tuple[]", "components": POS}]}]

KIND = ["Wallet", "Supplied", "Borrowed", "Collateral", "Staking", "Rewards", "Fees"]

w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
positions = w3.eth.contract(address=NAV_CALCULATOR, abi=ABI).functions.getAccountPositions(ACCOUNT, USD).call()

for p in positions:
    usd = p[5] / 1e8                       # value
    stale = " ⚠ stale" if p[8] else ""
    print(f"{p[0][1]} [{KIND[p[14]]}]: ${usd:,.2f}{stale}")   # positionKind
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
Position **identity** is the typed key `protocolSubId` + `positionId` + `balanceAsset.asset` + `positionKind` (per chain). The broader `protocolBrand` / `protocolId` are grouping layers; `positionInstanceId` is an ephemeral per-item handle and `isLocked` is a mutable per-leg attribute; both excluded from the key (so claimable/locked and per-item legs sum). See [Position identity](/funds/infrastructure/architecture/onchain-accounting/concepts/position-identity) for decode schemas and the reconciliation key. The `protocolName` string and `labels[]` array from the verbose reads are display-only.
{% endhint %}

## 5. Read the latest price for an asset

Each asset is priced by its primary feed. Read it with `getPriceData(asset)`, which returns the full `PriceFeedData` snapshot, `price`, `decimals`, `stale`, plus the primary feed's type, heartbeat, `updatedAt`, `healthyFeedCount`, and the divergence signal (`irregular`, `divergenceBps`, `monitorFeedCount`).

For a **Custom** (composite) feed, `updatedAt` and `chainlinkHeartbeat` report the [governing leg](/funds/infrastructure/architecture/onchain-accounting/price-feeds#governing-leg-freshness), the underlying Chainlink leg closest to its own staleness deadline, rather than the registered config heartbeat. Treat that pair as advisory and gate on `stale` / `sequencerDown`.

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

{% tabs %}
{% tab title="viem" %}

```typescript
import { createPublicClient, http, parseAbi } from "viem";
import { mainnet } from "viem/chains";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";

const ABI = parseAbi([
  `function getPriceData(address token) view returns (
     (address priceFeed, uint8 priceType, int256 price, uint8 decimals,
      uint256 chainlinkHeartbeat, uint256 updatedAt, bool stale, bool sequencerDown,
      uint256 healthyFeedCount, bool irregular, uint256 divergenceBps, uint256 monitorFeedCount)
   )`,
]);

const client = createPublicClient({ chain: mainnet, transport: http() });

const d = await client.readContract({
  address: NAV_CALCULATOR, abi: ABI, functionName: "getPriceData", args: [WETH],
});

if (d.stale) console.warn("Price is stale");
console.log(`WETH price: $${(Number(d.price) / 10 ** d.decimals).toFixed(2)}`);
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";

const ABI = [
  `function getPriceData(address token) view returns (
     tuple(address priceFeed, uint8 priceType, int256 price, uint8 decimals,
           uint256 chainlinkHeartbeat, uint256 updatedAt, bool stale, bool sequencerDown,
           uint256 healthyFeedCount, bool irregular, uint256 divergenceBps, uint256 monitorFeedCount)
   )`,
];

async function readPrice() {
  const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
  const calc = new ethers.Contract(NAV_CALCULATOR, ABI, provider);

  const d = await calc.getPriceData(WETH);
  if (d.stale) console.warn("Price is stale");
  console.log(`WETH price: $${(Number(d.price) / 10 ** Number(d.decimals)).toFixed(2)}`);
}

readPrice();
```

{% endtab %}

{% tab title="Python" %}

```python
from web3 import Web3

NAV_CALCULATOR = "0x..."  # proxy — see Deployment addresses
WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"

PRICE_FEED_DATA = [
    {"name": "priceFeed",          "type": "address"},
    {"name": "priceType",          "type": "uint8"},
    {"name": "price",              "type": "int256"},
    {"name": "decimals",           "type": "uint8"},
    {"name": "chainlinkHeartbeat", "type": "uint256"},
    {"name": "updatedAt",          "type": "uint256"},
    {"name": "stale",              "type": "bool"},
    {"name": "sequencerDown",      "type": "bool"},
    {"name": "healthyFeedCount",   "type": "uint256"},
    {"name": "irregular",          "type": "bool"},
    {"name": "divergenceBps",      "type": "uint256"},
    {"name": "monitorFeedCount",   "type": "uint256"},
]
ABI = [{"name": "getPriceData", "type": "function", "stateMutability": "view",
        "inputs": [{"name": "token", "type": "address"}],
        "outputs": [{"name": "", "type": "tuple", "components": PRICE_FEED_DATA}]}]

w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
d = w3.eth.contract(address=NAV_CALCULATOR, abi=ABI).functions.getPriceData(WETH).call()
price, decimals, stale = d[2], d[3], d[6]
if stale:
    print("Warning: stale")
print(f"WETH: ${price / 10**decimals:,.2f}")
```

{% endtab %}
{% endtabs %}

## 6. Value a basket of (asset, amount) pairs

`calculateValue` prices an arbitrary set of `(asset, amount)` pairs in one call; handy for valuing assets you already know offchain (e.g. the inputs of a pending subscription) without enumerating positions. Returns the total in the quote currency (USD = 8 decimals) and a `quoteAssetStale` flag.

```solidity
function calculateValue(address[] assets, uint256[] amounts, address quoteAsset)
    external view returns (int256 value, bool quoteAssetStale);
```

{% tabs %}
{% tab title="viem" %}

```typescript
import { createPublicClient, http, parseAbi, zeroAddress } from "viem";
import { mainnet } from "viem/chains";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";

const ABI = parseAbi([
  "function calculateValue(address[] assets, uint256[] amounts, address quoteAsset) view returns (int256 value, bool quoteAssetStale)",
]);

const client = createPublicClient({ chain: mainnet, transport: http() });

const [value, quoteAssetStale] = await client.readContract({
  address: NAV_CALCULATOR, abi: ABI, functionName: "calculateValue",
  args: [[WETH, USDC], [10n ** 18n, 5_000n * 10n ** 6n], zeroAddress], // 1 WETH + 5,000 USDC, in USD
});

if (quoteAssetStale) console.warn("Quote-asset feed stale (value fell back to USD)");
console.log(`Basket value (USD): ${Number(value) / 1e8}`);
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";

const ABI = [
  "function calculateValue(address[] assets, uint256[] amounts, address quoteAsset) view returns (int256 value, bool quoteAssetStale)",
];

const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
const calc = new ethers.Contract(NAV_CALCULATOR, ABI, provider);

const [value, quoteAssetStale] = await calc.calculateValue(
  [WETH, USDC], [10n ** 18n, 5_000n * 10n ** 6n], ethers.ZeroAddress,
);
if (quoteAssetStale) console.warn("Quote-asset feed stale");
console.log(`Basket value (USD): ${Number(value) / 1e8}`);
```

{% endtab %}

{% tab title="Python" %}

```python
from web3 import Web3

NAV_CALCULATOR = "0x..."  # proxy — see Deployment addresses
WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
USD  = "0x0000000000000000000000000000000000000000"

ABI = [{"name": "calculateValue", "type": "function", "stateMutability": "view",
        "inputs": [{"name": "assets", "type": "address[]"}, {"name": "amounts", "type": "uint256[]"},
                   {"name": "quoteAsset", "type": "address"}],
        "outputs": [{"name": "value", "type": "int256"}, {"name": "quoteAssetStale", "type": "bool"}]}]

w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
value, quote_stale = w3.eth.contract(address=NAV_CALCULATOR, abi=ABI).functions.calculateValue(
    [WETH, USDC], [10**18, 5_000 * 10**6], USD).call()

if quote_stale:
    print("Warning: quote-asset feed stale")
print(f"Basket value (USD): {value / 1e8}")
```

{% endtab %}
{% endtabs %}

## 7. Decode position identity & group by protocol

Each position carries the 3-level protocol taxonomy plus `positionId` + `positionKind` + `isLocked` (see [Position identity](/funds/infrastructure/architecture/onchain-accounting/concepts/position-identity)). The identity-key level is **`protocolSubId`** (`keccak256` of the product slug); `positionId` is `abi.encode(...)` whose decode schema is fixed by that slug. This example maps each position back to its product, decodes the coordinate, totals value per product, and builds the stable reconciliation key.

{% tabs %}
{% tab title="viem" %}

```typescript
import { createPublicClient, http, parseAbi, zeroAddress, keccak256, toHex, decodeAbiParameters, encodeAbiParameters } from "viem";
import { mainnet } from "viem/chains";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const ACCOUNT = "0x...";

const ABI = parseAbi([
  `function getAccountPositions(address account, address quoteAsset) view returns (
     ((address asset, string symbol, uint8 decimals) balanceAsset, address balanceAdapter,
      uint256 amount, bool isDebt, (address asset, string symbol, uint8 decimals) quoteAsset,
      int256 value, int256 price, uint8 priceDecimals, bool stale, bool quoteAssetStale,
      bytes32 protocolBrand, bytes32 protocolId, bytes32 protocolSubId, bytes positionId, uint8 positionKind, bool isLocked, bytes32 positionInstanceId)[]
   )`,
]);

// protocolSubId = keccak256(product slug) — the identity-key level. Build the reverse lookup for the slugs you care about.
const SLUGS = ["wallet", "aave-v3-lending", "spark-lend", "morpho-vaults", "morpho-markets",
  "compound-v3-comets", "balancer-v2-pools", "balancer-v3-pools", "curve-pools", "convex-curve-lp-staking",
  "uniswap-v3", "stakewise-v3-vaults", "nexus-mutual-staking-pools", "stakewise-v3-exit",
  "lido-staked-eth", "etherfi-staked-eth", "sky-savings", "spark-savings"];   // four of the 17 wallet-yield slugs
const SLUG_BY_ID = new Map(SLUGS.map((s) => [keccak256(toHex(s)), s]));

// positionId decode schema, keyed by protocolSubId (see Position identity for the full table)
function decodeId(slug: string, positionId: `0x${string}`) {
  if (positionId === "0x") return null;                                   // defensive: empty id
  if (["morpho-markets", "balancer-v2-pools"].includes(slug)) return decodeAbiParameters([{ type: "bytes32" }], positionId)[0];
  return decodeAbiParameters([{ type: "address" }], positionId)[0];      // default: a single address (pool / vault / queue anchor / token)
}

const client = createPublicClient({ chain: mainnet, transport: http() });
const chainId = BigInt(client.chain.id);
const positions = await client.readContract({
  address: NAV_CALCULATOR, abi: ABI, functionName: "getAccountPositions", args: [ACCOUNT, zeroAddress],
});

const byProtocol = new Map<string, bigint>();
for (const p of positions) {
  const slug = SLUG_BY_ID.get(p.protocolSubId) ?? p.protocolSubId;      // reconcile on protocolSubId (not protocolId)
  const coord = decodeId(slug, p.positionId);
  byProtocol.set(slug, (byProtocol.get(slug) ?? 0n) + p.value);
  // stable key across reads & adapter redeploys (excludes protocolBrand/protocolId, balanceAdapter, positionInstanceId, isLocked, amount, pricing)
  const key = keccak256(encodeAbiParameters(
    [{ type: "uint256" }, { type: "bytes32" }, { type: "bytes" }, { type: "address" }, { type: "uint8" }],
    [chainId, p.protocolSubId, p.positionId, p.balanceAsset.asset, p.positionKind],
  ));
  console.log(`${slug} ${coord ?? ""} — $${(Number(p.value) / 1e8).toFixed(2)}  key=${key.slice(0, 10)}…`);
}
for (const [slug, total] of byProtocol) console.log(`Σ ${slug}: $${(Number(total) / 1e8).toFixed(2)}`);
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const ACCOUNT = "0x...";

const ABI = [
  `function getAccountPositions(address account, address quoteAsset) view returns (
     tuple(tuple(address asset, string symbol, uint8 decimals) balanceAsset, address balanceAdapter,
      uint256 amount, bool isDebt, tuple(address asset, string symbol, uint8 decimals) quoteAsset,
      int256 value, int256 price, uint8 priceDecimals, bool stale, bool quoteAssetStale,
      bytes32 protocolBrand, bytes32 protocolId, bytes32 protocolSubId, bytes positionId, uint8 positionKind, bool isLocked, bytes32 positionInstanceId)[]
   )`,
];

const SLUGS = ["wallet", "aave-v3-lending", "spark-lend", "morpho-vaults", "morpho-markets",
  "compound-v3-comets", "balancer-v2-pools", "balancer-v3-pools", "curve-pools", "convex-curve-lp-staking",
  "uniswap-v3", "stakewise-v3-vaults", "nexus-mutual-staking-pools", "stakewise-v3-exit",
  "lido-staked-eth", "etherfi-staked-eth", "sky-savings", "spark-savings"];   // four of the 17 wallet-yield slugs
const SLUG_BY_ID = new Map(SLUGS.map((s) => [ethers.id(s), s]));        // ethers.id = keccak256(utf8)
const abi = ethers.AbiCoder.defaultAbiCoder();

function decodeId(slug: string, positionId: string) {
  if (positionId === "0x") return null;                                   // defensive: empty id
  if (["morpho-markets", "balancer-v2-pools"].includes(slug)) return abi.decode(["bytes32"], positionId)[0];
  return abi.decode(["address"], positionId)[0];                          // default: a single address (pool / vault / queue anchor / token)
}

const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
const chainId = (await provider.getNetwork()).chainId;
const calc = new ethers.Contract(NAV_CALCULATOR, ABI, provider);
const positions = await calc.getAccountPositions(ACCOUNT, ethers.ZeroAddress);

const byProtocol = new Map<string, bigint>();
for (const p of positions) {
  const slug = SLUG_BY_ID.get(p.protocolSubId) ?? p.protocolSubId;      // reconcile on protocolSubId (not protocolId)
  const coord = decodeId(slug, p.positionId);
  byProtocol.set(slug, (byProtocol.get(slug) ?? 0n) + p.value);
  const key = ethers.keccak256(abi.encode(
    ["uint256", "bytes32", "bytes", "address", "uint8"],
    [chainId, p.protocolSubId, p.positionId, p.balanceAsset.asset, p.positionKind],
  ));
  console.log(`${slug} ${coord ?? ""} — $${(Number(p.value) / 1e8).toFixed(2)}  key=${key.slice(0, 10)}…`);
}
for (const [slug, total] of byProtocol) console.log(`Σ ${slug}: $${(Number(total) / 1e8).toFixed(2)}`);
```

{% endtab %}

{% tab title="Python" %}

```python
from web3 import Web3
from eth_abi import decode, encode

NAV_CALCULATOR = "0x..."  # proxy — see Deployment addresses
ACCOUNT        = "0x..."
USD            = "0x0000000000000000000000000000000000000000"

ASSET = [{"name": "asset", "type": "address"}, {"name": "symbol", "type": "string"}, {"name": "decimals", "type": "uint8"}]
POS = [
    {"name": "balanceAsset", "type": "tuple", "components": ASSET}, {"name": "balanceAdapter", "type": "address"},
    {"name": "amount", "type": "uint256"}, {"name": "isDebt", "type": "bool"},
    {"name": "quoteAsset", "type": "tuple", "components": ASSET}, {"name": "value", "type": "int256"},
    {"name": "price", "type": "int256"}, {"name": "priceDecimals", "type": "uint8"},
    {"name": "stale", "type": "bool"}, {"name": "quoteAssetStale", "type": "bool"},
    {"name": "protocolBrand", "type": "bytes32"}, {"name": "protocolId", "type": "bytes32"},
    {"name": "protocolSubId", "type": "bytes32"}, {"name": "positionId", "type": "bytes"},
    {"name": "positionKind", "type": "uint8"},
    {"name": "isLocked", "type": "bool"}, {"name": "positionInstanceId", "type": "bytes32"},
]
ABI = [{"name": "getAccountPositions", "type": "function", "stateMutability": "view",
        "inputs": [{"name": "account", "type": "address"}, {"name": "quoteAsset", "type": "address"}],
        "outputs": [{"name": "", "type": "tuple[]", "components": POS}]}]

SLUGS = ["wallet", "aave-v3-lending", "spark-lend", "morpho-vaults", "morpho-markets",
         "compound-v3-comets", "balancer-v2-pools", "balancer-v3-pools", "curve-pools", "convex-curve-lp-staking",
         "uniswap-v3", "stakewise-v3-vaults", "nexus-mutual-staking-pools", "stakewise-v3-exit",
         "lido-staked-eth", "etherfi-staked-eth", "sky-savings", "spark-savings"]   # four of the 17 wallet-yield slugs
SLUG_BY_ID = {bytes(Web3.keccak(text=s)): s for s in SLUGS}

def decode_id(slug, pid):                          # pid is bytes (the bytes positionId)
    if not pid:
        return None                                 # defensive: empty id
    if slug in ("morpho-markets", "balancer-v2-pools"):
        return "0x" + decode(["bytes32"], pid)[0].hex()
    return decode(["address"], pid)[0]              # default: a single address (pool / vault / queue anchor / token)

w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
chain_id = w3.eth.chain_id
positions = w3.eth.contract(address=NAV_CALCULATOR, abi=ABI).functions.getAccountPositions(ACCOUNT, USD).call()

by_protocol = {}
for p in positions:
    slug = SLUG_BY_ID.get(bytes(p[12]), "0x" + p[12].hex())     # protocolSubId (the identity-key level)
    coord = decode_id(slug, p[13])                              # positionId
    by_protocol[slug] = by_protocol.get(slug, 0) + p[5]         # value
    key = Web3.keccak(encode(
        ["uint256", "bytes32", "bytes", "address", "uint8"],
        [chain_id, p[12], p[13], p[0][0], p[14]],               # chainId, protocolSubId, positionId, asset, positionKind
    ))
    print(f"{slug} {coord if coord is not None else ''} — ${p[5] / 1e8:,.2f}  key={key.hex()[:10]}…")

for slug, total in by_protocol.items():
    print(f"Σ {slug}: ${total / 1e8:,.2f}")
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
The slug list above is illustrative; the full `protocolSubId` catalogue and every `positionId` decode schema are in [Position identity](/funds/infrastructure/architecture/onchain-accounting/concepts/position-identity). The reconciliation key deliberately excludes `protocolBrand`/`protocolId` (grouping only), `positionInstanceId` (ephemeral), `isLocked` (mutable per-leg), `balanceAdapter`, `amount`, and pricing, so it stays stable across reads and adapter redeploys.
{% endhint %}

## 8. Single-asset exposure

`hasPositions` is a cheap pre-check before enumerating; `getAssetsWithPositions` lists the assets an account holds something in; `getAccountPositionsForAsset` returns the positions for one asset across every adapter that reports it (e.g. all WETH exposure, wallet, supplied, staked).

```solidity
function hasPositions(address account) external view returns (bool);
function getAssetsWithPositions(address account) external view returns (address[] memory);
function getAccountPositionsForAsset(address account, address asset, address quoteAsset)
    external view returns (Position[] memory);
```

{% tabs %}
{% tab title="viem" %}

```typescript
import { createPublicClient, http, parseAbi, zeroAddress } from "viem";
import { mainnet } from "viem/chains";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const ACCOUNT = "0x...";
const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";

const ABI = parseAbi([
  "function hasPositions(address account) view returns (bool)",
  `function getAccountPositionsForAsset(address account, address asset, address quoteAsset) view returns (
     ((address asset, string symbol, uint8 decimals) balanceAsset, address balanceAdapter,
      uint256 amount, bool isDebt, (address asset, string symbol, uint8 decimals) quoteAsset,
      int256 value, int256 price, uint8 priceDecimals, bool stale, bool quoteAssetStale,
      bytes32 protocolBrand, bytes32 protocolId, bytes32 protocolSubId, bytes positionId, uint8 positionKind, bool isLocked, bytes32 positionInstanceId)[]
   )`,
]);

const client = createPublicClient({ chain: mainnet, transport: http() });

if (!(await client.readContract({ address: NAV_CALCULATOR, abi: ABI, functionName: "hasPositions", args: [ACCOUNT] }))) {
  console.log("No positions");
} else {
  const rows = await client.readContract({
    address: NAV_CALCULATOR, abi: ABI, functionName: "getAccountPositionsForAsset",
    args: [ACCOUNT, WETH, zeroAddress],
  });
  const total = rows.reduce((s, p) => s + p.value, 0n);
  console.log(`WETH exposure across ${rows.length} positions: $${Number(total) / 1e8}`);
}
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses
const ACCOUNT = "0x...";
const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";

const ABI = [
  "function hasPositions(address account) view returns (bool)",
  `function getAccountPositionsForAsset(address account, address asset, address quoteAsset) view returns (
     tuple(tuple(address asset, string symbol, uint8 decimals) balanceAsset, address balanceAdapter,
      uint256 amount, bool isDebt, tuple(address asset, string symbol, uint8 decimals) quoteAsset,
      int256 value, int256 price, uint8 priceDecimals, bool stale, bool quoteAssetStale,
      bytes32 protocolBrand, bytes32 protocolId, bytes32 protocolSubId, bytes positionId, uint8 positionKind, bool isLocked, bytes32 positionInstanceId)[]
   )`,
];

const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
const calc = new ethers.Contract(NAV_CALCULATOR, ABI, provider);

if (!(await calc.hasPositions(ACCOUNT))) {
  console.log("No positions");
} else {
  const rows = await calc.getAccountPositionsForAsset(ACCOUNT, WETH, ethers.ZeroAddress);
  const total = rows.reduce((s: bigint, p: any) => s + p.value, 0n);
  console.log(`WETH exposure across ${rows.length} positions: $${Number(total) / 1e8}`);
}
```

{% endtab %}

{% tab title="Python" %}

```python
from web3 import Web3

NAV_CALCULATOR = "0x..."  # proxy — see Deployment addresses
ACCOUNT        = "0x..."
WETH           = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
USD            = "0x0000000000000000000000000000000000000000"

ASSET = [{"name": "asset", "type": "address"}, {"name": "symbol", "type": "string"}, {"name": "decimals", "type": "uint8"}]
POS = [
    {"name": "balanceAsset", "type": "tuple", "components": ASSET}, {"name": "balanceAdapter", "type": "address"},
    {"name": "amount", "type": "uint256"}, {"name": "isDebt", "type": "bool"},
    {"name": "quoteAsset", "type": "tuple", "components": ASSET}, {"name": "value", "type": "int256"},
    {"name": "price", "type": "int256"}, {"name": "priceDecimals", "type": "uint8"},
    {"name": "stale", "type": "bool"}, {"name": "quoteAssetStale", "type": "bool"},
    {"name": "protocolBrand", "type": "bytes32"}, {"name": "protocolId", "type": "bytes32"},
    {"name": "protocolSubId", "type": "bytes32"}, {"name": "positionId", "type": "bytes"},
    {"name": "positionKind", "type": "uint8"},
    {"name": "isLocked", "type": "bool"}, {"name": "positionInstanceId", "type": "bytes32"},
]
ABI = [
    {"name": "hasPositions", "type": "function", "stateMutability": "view",
     "inputs": [{"name": "account", "type": "address"}], "outputs": [{"type": "bool"}]},
    {"name": "getAccountPositionsForAsset", "type": "function", "stateMutability": "view",
     "inputs": [{"name": "account", "type": "address"}, {"name": "asset", "type": "address"}, {"name": "quoteAsset", "type": "address"}],
     "outputs": [{"name": "", "type": "tuple[]", "components": POS}]},
]

w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
calc = w3.eth.contract(address=NAV_CALCULATOR, abi=ABI)

if not calc.functions.hasPositions(ACCOUNT).call():
    print("No positions")
else:
    rows = calc.functions.getAccountPositionsForAsset(ACCOUNT, WETH, USD).call()
    total = sum(p[5] for p in rows)            # value
    print(f"WETH exposure across {len(rows)} positions: ${total / 1e8:,.2f}")
```

{% endtab %}
{% endtabs %}

## 9. List the assets a fund tracks

`getRegisteredAssets` returns every asset registered on the chain's NAV Calculator (with symbol and decimals); `isAssetRegistered` checks a single one. Useful to discover coverage before pricing or to validate an integration.

```solidity
function getRegisteredAssets() external view returns (Asset[] memory); // Asset = (address, string symbol, uint8 decimals)
function isAssetRegistered(address asset) external view returns (bool);
```

{% tabs %}
{% tab title="viem" %}

```typescript
import { createPublicClient, http, parseAbi } from "viem";
import { mainnet } from "viem/chains";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses

const ABI = parseAbi([
  "function getRegisteredAssets() view returns ((address asset, string symbol, uint8 decimals)[])",
  "function isAssetRegistered(address asset) view returns (bool)",
]);

const client = createPublicClient({ chain: mainnet, transport: http() });

const assets = await client.readContract({ address: NAV_CALCULATOR, abi: ABI, functionName: "getRegisteredAssets" });
console.log(`${assets.length} registered assets:`);
for (const a of assets) console.log(`  ${a.symbol} (${a.decimals} dp) — ${a.asset}`);
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const NAV_CALCULATOR = "0x..."; // proxy — see Deployment addresses

const ABI = [
  "function getRegisteredAssets() view returns (tuple(address asset, string symbol, uint8 decimals)[])",
  "function isAssetRegistered(address asset) view returns (bool)",
];

const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
const calc = new ethers.Contract(NAV_CALCULATOR, ABI, provider);

const assets = await calc.getRegisteredAssets();
console.log(`${assets.length} registered assets:`);
for (const a of assets) console.log(`  ${a.symbol} (${a.decimals} dp) — ${a.asset}`);
```

{% endtab %}

{% tab title="Python" %}

```python
from web3 import Web3

NAV_CALCULATOR = "0x..."  # proxy — see Deployment addresses

ASSET = [{"name": "asset", "type": "address"}, {"name": "symbol", "type": "string"}, {"name": "decimals", "type": "uint8"}]
ABI = [
    {"name": "getRegisteredAssets", "type": "function", "stateMutability": "view",
     "inputs": [], "outputs": [{"name": "", "type": "tuple[]", "components": ASSET}]},
    {"name": "isAssetRegistered", "type": "function", "stateMutability": "view",
     "inputs": [{"name": "asset", "type": "address"}], "outputs": [{"type": "bool"}]},
]

w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
assets = w3.eth.contract(address=NAV_CALCULATOR, abi=ABI).functions.getRegisteredAssets().call()
print(f"{len(assets)} registered assets:")
for a in assets:
    print(f"  {a[1]} ({a[2]} dp) — {a[0]}")
```

{% endtab %}
{% endtabs %}


# Subscription request

Approve the asset and create a subscription request on the shares contract.

Subscribing is a two-step flow, just like a standard deposit:

1. **Approve** the shares contract to spend your subscription asset, a standard ERC-20 `approve` call.
2. **Create the request** with `requestSubscription` on the shares contract:

```solidity
function requestSubscription(
    uint256 assetsIn,         // amount of the asset to deposit
    uint256 minSharesOut,     // slippage protection (see Calculate share price)
    address subscriptionAsset,// must be approved for deposits
    address receiver          // who receives the minted shares
) external returns (uint256 requestId);
```

{% hint style="warning" %}
`minSharesOut` is **slippage protection**. The actual shares are computed by the operator from the share price at settlement time; that result must be `≥ minSharesOut` or the request fails the slippage check. Derive it from the fund's current share price, see [Calculate share price](/funds/kpk-fund-integration/calculate-share-price), then convert with `assetsToShares` and apply a tolerance. The 0.5% used below is only an example; set it to how much slippage you are willing to accept.
{% endhint %}

## Example

Subscribing **1,000 USDC** (6 decimals) into a fund. We read the fund's current share price (see [Calculate share price](/funds/kpk-fund-integration/calculate-share-price)), convert it to expected shares with `assetsToShares`, and apply a 0.5% example tolerance to get `minSharesOut`.

{% tabs %}
{% tab title="viem" %}

```typescript
import { createWalletClient, createPublicClient, http, parseAbi, parseUnits } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";

const SHARES = "0x...";  // fund shares contract — see the fund's Policies and addresses
const USDC   = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";

const sharesAbi = parseAbi([
  "function assetsToShares(uint256 assetAmount, uint256 sharesPrice, address subscriptionAsset) view returns (uint256)",
  "function requestSubscription(uint256 assetsIn, uint256 minSharesOut, address subscriptionAsset, address receiver) returns (uint256)",
]);
const erc20Abi = parseAbi(["function approve(address spender, uint256 amount) returns (bool)"]);

const account = privateKeyToAccount("0x...");
const pub = createPublicClient({ chain: mainnet, transport: http() });
const wallet = createWalletClient({ account, chain: mainnet, transport: http() });

const assetsIn = parseUnits("1000", 6); // 1,000 USDC

// 1. Current share price in 8-decimal USD — compute it onchain per Calculate share price.
const sharePrice = await getSharePrice(pub); // bigint, 8 decimals — see Calculate share price

// 2. Convert to expected shares at that price, then apply your tolerance.
const expectedShares = await pub.readContract({
  address: SHARES, abi: sharesAbi, functionName: "assetsToShares",
  args: [assetsIn, sharePrice, USDC],
});
const minSharesOut = (expectedShares * 9950n) / 10000n; // 0.5% is only an example — set your own tolerance

// 3. Approve, then request.
await wallet.writeContract({ address: USDC, abi: erc20Abi, functionName: "approve", args: [SHARES, assetsIn] });
const hash = await wallet.writeContract({
  address: SHARES, abi: sharesAbi, functionName: "requestSubscription",
  args: [assetsIn, minSharesOut, USDC, account.address],
});
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const SHARES = "0x...";  // fund shares contract — see the fund's Policies and addresses
const USDC   = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";

const sharesAbi = [
  "function assetsToShares(uint256 assetAmount, uint256 sharesPrice, address subscriptionAsset) view returns (uint256)",
  "function requestSubscription(uint256 assetsIn, uint256 minSharesOut, address subscriptionAsset, address receiver) returns (uint256)",
];
const erc20Abi = ["function approve(address spender, uint256 amount) returns (bool)"];

const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
const signer = new ethers.Wallet("0x...", provider);
const shares = new ethers.Contract(SHARES, sharesAbi, signer);
const usdc = new ethers.Contract(USDC, erc20Abi, signer);

const assetsIn = ethers.parseUnits("1000", 6); // 1,000 USDC

const sharePrice = await getSharePrice(provider); // 8-dec USD — see Calculate share price
const expectedShares = await shares.assetsToShares(assetsIn, sharePrice, USDC);
const minSharesOut = (expectedShares * 9950n) / 10000n; // 0.5% is only an example — set your own tolerance

await (await usdc.approve(SHARES, assetsIn)).wait();
const tx = await shares.requestSubscription(assetsIn, minSharesOut, USDC, await signer.getAddress());
await tx.wait();
```

{% endtab %}

{% tab title="Python" %}

```python
from web3 import Web3

SHARES = "0x..."  # fund shares contract — see the fund's Policies and addresses
USDC   = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"

shares_abi = [
    {"name": "assetsToShares", "type": "function", "stateMutability": "view",
     "inputs": [{"name": "assetAmount", "type": "uint256"}, {"name": "sharesPrice", "type": "uint256"},
                {"name": "subscriptionAsset", "type": "address"}],
     "outputs": [{"type": "uint256"}]},
    {"name": "requestSubscription", "type": "function", "stateMutability": "nonpayable",
     "inputs": [{"name": "assetsIn", "type": "uint256"}, {"name": "minSharesOut", "type": "uint256"},
                {"name": "subscriptionAsset", "type": "address"}, {"name": "receiver", "type": "address"}],
     "outputs": [{"type": "uint256"}]},
]
erc20_abi = [{"name": "approve", "type": "function", "stateMutability": "nonpayable",
              "inputs": [{"name": "spender", "type": "address"}, {"name": "amount", "type": "uint256"}],
              "outputs": [{"type": "bool"}]}]

w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
acct = w3.eth.account.from_key("0x...")
shares = w3.eth.contract(address=SHARES, abi=shares_abi)
usdc = w3.eth.contract(address=USDC, abi=erc20_abi)

assets_in = 1000 * 10**6  # 1,000 USDC

share_price = get_share_price(w3)  # 8-decimal USD — see Calculate share price
expected = shares.functions.assetsToShares(assets_in, share_price, USDC).call()
min_shares_out = expected * 9950 // 10000  # 0.5% is only an example — set your own tolerance

# Build, sign, and send approve + requestSubscription (nonce/gas handling omitted for brevity).
approve = usdc.functions.approve(SHARES, assets_in).build_transaction({"from": acct.address})
# ... sign & send approve ...
req = shares.functions.requestSubscription(
    assets_in, min_shares_out, USDC, acct.address
).build_transaction({"from": acct.address})
# ... sign & send req ...
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}

## Events

Creating a subscription request emits a `SubscriptionRequest` event containing the `requestId`, `cancelableFrom`, and `expiryAt`. Track it as described in [Monitor a request](/funds/kpk-fund-integration/monitor-a-request).
{% endhint %}

<figure><picture><source srcset="/files/fMRNxtuXyn7U776YiNmf" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2Fpf8Dr49WWEofgFOikcq8%2Fkpk_funds_subs_request.png?alt=media&amp;token=9ba294ae-8332-45c7-9589-7e1e856cd036" alt=""></picture><figcaption><p>Subscription flow example, with USDC as subscription token.</p></figcaption></figure>


# Redemption request

Create a redemption request on the shares contract to exit a fund.

Redeeming is a single step; no approval is needed, because the shares contract pulls the shares you already hold into escrow:

```solidity
function requestRedemption(
    uint256 sharesIn,       // amount of shares to redeem
    uint256 minAssetsOut,   // slippage protection, applied AFTER the redemption fee
    address redemptionAsset,// must be approved for redemptions (canRedeem = true)
    address receiver        // who receives the assets
) external returns (uint256 requestId);
```

{% hint style="warning" %}
`minAssetsOut` is **slippage protection** measured **after** the redemption fee. The operator's settlement must yield at least `minAssetsOut` assets or the request fails. Derive it from the fund's current share price, see [Calculate share price](/funds/kpk-fund-integration/calculate-share-price), then deduct the redemption fee (`redemptionFeeRate`), convert the remainder with `sharesToAssets`, and apply a tolerance. The 0.5% used below is only an example; set it to how much slippage you are willing to accept.
{% endhint %}

## Example

Redeeming **500 shares** (18 decimals) for USDC. We read the fund's current share price (see [Calculate share price](/funds/kpk-fund-integration/calculate-share-price)), deduct the redemption fee, convert the remainder with `sharesToAssets`, and apply a 0.5% example tolerance to get `minAssetsOut`.

{% tabs %}
{% tab title="viem" %}

```typescript
import { createWalletClient, createPublicClient, http, parseAbi, parseUnits } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";

const SHARES = "0x...";  // fund shares contract
const USDC   = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";

const sharesAbi = parseAbi([
  "function sharesToAssets(uint256 shares, uint256 sharesPrice, address redemptionAsset) view returns (uint256)",
  "function redemptionFeeRate() view returns (uint256)",
  "function requestRedemption(uint256 sharesIn, uint256 minAssetsOut, address redemptionAsset, address receiver) returns (uint256)",
]);

const account = privateKeyToAccount("0x...");
const pub = createPublicClient({ chain: mainnet, transport: http() });
const wallet = createWalletClient({ account, chain: mainnet, transport: http() });

const sharesIn = parseUnits("500", 18); // 500 shares

// 1. Current share price in 8-decimal USD — compute it onchain per Calculate share price.
const sharePrice = await getSharePrice(pub); // bigint, 8 decimals — see Calculate share price

// 2. Deduct the redemption fee, convert the remainder, then apply your tolerance.
const feeRate = await pub.readContract({ address: SHARES, abi: sharesAbi, functionName: "redemptionFeeRate" });
const netShares = sharesIn - (sharesIn * feeRate) / 10000n;
const expectedAssets = await pub.readContract({
  address: SHARES, abi: sharesAbi, functionName: "sharesToAssets",
  args: [netShares, sharePrice, USDC],
});
const minAssetsOut = (expectedAssets * 9950n) / 10000n; // 0.5% is only an example — set your own tolerance

const hash = await wallet.writeContract({
  address: SHARES, abi: sharesAbi, functionName: "requestRedemption",
  args: [sharesIn, minAssetsOut, USDC, account.address],
});
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const SHARES = "0x...";  // fund shares contract
const USDC   = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";

const sharesAbi = [
  "function sharesToAssets(uint256 shares, uint256 sharesPrice, address redemptionAsset) view returns (uint256)",
  "function redemptionFeeRate() view returns (uint256)",
  "function requestRedemption(uint256 sharesIn, uint256 minAssetsOut, address redemptionAsset, address receiver) returns (uint256)",
];

const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
const signer = new ethers.Wallet("0x...", provider);
const shares = new ethers.Contract(SHARES, sharesAbi, signer);

const sharesIn = ethers.parseUnits("500", 18);

const sharePrice = await getSharePrice(provider); // 8-dec USD — see Calculate share price
const feeRate = await shares.redemptionFeeRate();
const netShares = sharesIn - (sharesIn * feeRate) / 10000n;
const expectedAssets = await shares.sharesToAssets(netShares, sharePrice, USDC);
const minAssetsOut = (expectedAssets * 9950n) / 10000n; // 0.5% is only an example — set your own tolerance

const tx = await shares.requestRedemption(sharesIn, minAssetsOut, USDC, await signer.getAddress());
await tx.wait();
```

{% endtab %}

{% tab title="Python" %}

```python
from web3 import Web3

SHARES = "0x..."  # fund shares contract
USDC   = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"

shares_abi = [
    {"name": "sharesToAssets", "type": "function", "stateMutability": "view",
     "inputs": [{"name": "shares", "type": "uint256"}, {"name": "sharesPrice", "type": "uint256"},
                {"name": "redemptionAsset", "type": "address"}],
     "outputs": [{"type": "uint256"}]},
    {"name": "redemptionFeeRate", "type": "function", "stateMutability": "view",
     "inputs": [], "outputs": [{"type": "uint256"}]},
    {"name": "requestRedemption", "type": "function", "stateMutability": "nonpayable",
     "inputs": [{"name": "sharesIn", "type": "uint256"}, {"name": "minAssetsOut", "type": "uint256"},
                {"name": "redemptionAsset", "type": "address"}, {"name": "receiver", "type": "address"}],
     "outputs": [{"type": "uint256"}]},
]

w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
acct = w3.eth.account.from_key("0x...")
shares = w3.eth.contract(address=SHARES, abi=shares_abi)

shares_in = 500 * 10**18  # 500 shares

share_price = get_share_price(w3)  # 8-decimal USD — see Calculate share price
fee_rate = shares.functions.redemptionFeeRate().call()
net_shares = shares_in - (shares_in * fee_rate) // 10000
expected_assets = shares.functions.sharesToAssets(net_shares, share_price, USDC).call()
min_assets_out = expected_assets * 9950 // 10000  # 0.5% is only an example — set your own tolerance

req = shares.functions.requestRedemption(
    shares_in, min_assets_out, USDC, acct.address
).build_transaction({"from": acct.address})
# ... sign & send req ...
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}

## Events

Creating a redemption request emits a `RedemptionRequest` event containing the `requestId`, `cancelableFrom`, and `expiryAt`. Track it as described in [Monitor a request](/funds/kpk-fund-integration/monitor-a-request).
{% endhint %}

<figure><picture><source srcset="/files/QZEgPpXU4vmjED4jf0xO" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2FB7Nh2cj2ZpJjw9T1Lmbu%2Fkpk_funds_redemption_request.png?alt=media&amp;token=6754993f-4675-4cac-94f0-f945000ebc4e" alt=""></picture><figcaption><p>Redemption flow example</p></figcaption></figure>


# Monitor a request

Track a request's lifecycle through events or a synchronous getRequest call.

Once a subscription or redemption request is in place, its whole lifecycle can be monitored either by reading the events emitted by the shares contract, or by synchronously querying it.

<figure><picture><source srcset="/files/jACl5BEQ9cxtdUuGLIbk" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2FACe50o0vuJFod4dNBhKo%2Fkpk_funds_monitor_request.png?alt=media&amp;token=a528d64e-84e0-4abf-96c1-d94ea4e13d74" alt=""></picture><figcaption></figcaption></figure>

## Asynchronous: events

The key events for tracking a request's lifecycle:

#### Subscription

<table><thead><tr><th width="256.34765625">Event</th><th>Description</th></tr></thead><tbody><tr><td><strong>SubscriptionRequest</strong></td><td>Subscription request created</td></tr><tr><td><strong>SubscriptionApproval</strong></td><td>Subscription request approved</td></tr><tr><td><strong>SubscriptionCancellation</strong></td><td>Subscription request cancelled by the user or receiver</td></tr><tr><td><strong>SubscriptionDenial</strong></td><td>Subscription request denied by a fund operator</td></tr><tr><td><strong>SubscriptionRequestExpired</strong></td><td>Subscription request expired without action</td></tr></tbody></table>

#### Redemption

<table><thead><tr><th width="256.49609375">Event</th><th>Description</th></tr></thead><tbody><tr><td><strong>RedemptionRequest</strong></td><td>Redemption request created</td></tr><tr><td><strong>RedemptionApproval</strong></td><td>Redemption request approved</td></tr><tr><td><strong>RedemptionCancellation</strong></td><td>Redemption request cancelled by the user or receiver</td></tr><tr><td><strong>RedemptionDenial</strong></td><td>Redemption request denied by a fund operator</td></tr><tr><td><strong>RedemptionRequestExpired</strong></td><td>Redemption request expired without action</td></tr></tbody></table>

The `requestId` returned when you create a request is the key to filter on. For example, to capture the id from your own subscription:

{% tabs %}
{% tab title="viem" %}

```typescript
import { createPublicClient, http, parseAbiItem } from "viem";
import { mainnet } from "viem/chains";

const SHARES = "0x...";
const client = createPublicClient({ chain: mainnet, transport: http() });

const logs = await client.getLogs({
  address: SHARES,
  event: parseAbiItem(
    "event SubscriptionRequest(address indexed investor, uint256 requestId, address indexed receiver, address indexed subscriptionAsset, uint256 assetsAmount, uint256 sharesAmount, uint64 timestamp, uint64 cancelableFrom, uint64 expiryAt)"
  ),
  args: { investor: "0xYourAddress" },
  fromBlock: "earliest",
});
for (const log of logs) console.log("requestId:", log.args.requestId);
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const SHARES = "0x...";
const abi = [
  "event SubscriptionRequest(address indexed investor, uint256 requestId, address indexed receiver, address indexed subscriptionAsset, uint256 assetsAmount, uint256 sharesAmount, uint64 timestamp, uint64 cancelableFrom, uint64 expiryAt)",
];

const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
const shares = new ethers.Contract(SHARES, abi, provider);

const filter = shares.filters.SubscriptionRequest("0xYourAddress");
const events = await shares.queryFilter(filter);
for (const e of events) console.log("requestId:", e.args.requestId.toString());
```

{% endtab %}

{% tab title="Python" %}

```python
from web3 import Web3

SHARES = "0x..."
abi = [{
    "name": "SubscriptionRequest", "type": "event", "anonymous": False,
    "inputs": [
        {"name": "investor", "type": "address", "indexed": True},
        {"name": "requestId", "type": "uint256", "indexed": False},
        {"name": "receiver", "type": "address", "indexed": True},
        {"name": "subscriptionAsset", "type": "address", "indexed": True},
        {"name": "assetsAmount", "type": "uint256", "indexed": False},
        {"name": "sharesAmount", "type": "uint256", "indexed": False},
        {"name": "timestamp", "type": "uint64", "indexed": False},
        {"name": "cancelableFrom", "type": "uint64", "indexed": False},
        {"name": "expiryAt", "type": "uint64", "indexed": False},
    ],
}]

w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
shares = w3.eth.contract(address=SHARES, abi=abi)
logs = shares.events.SubscriptionRequest().get_logs(
    from_block=0, argument_filters={"investor": "0xYourAddress"})
for log in logs:
    print("requestId:", log["args"]["requestId"])
```

{% endtab %}
{% endtabs %}

## Synchronous: `getRequest`

To query a request's current state directly, pass the `requestId`:

```solidity
function getRequest(uint256 id) external view returns (UserRequest memory);

struct UserRequest {
    RequestType   requestType;   // 0 = SUBSCRIPTION, 1 = REDEMPTION
    RequestStatus requestStatus; // 0 = PENDING, 1 = PROCESSED, 2 = REJECTED, 3 = CANCELLED
    address asset;
    uint256 assetAmount;         // subscription: deposited assets; redemption: minAssetsOut
    uint256 sharesAmount;        // subscription: minSharesOut; redemption: shares redeemed
    address investor;
    address receiver;
    uint64  timestamp;
    uint64  expiryAt;
}
```

{% tabs %}
{% tab title="viem" %}

```typescript
import { createPublicClient, http, parseAbi } from "viem";
import { mainnet } from "viem/chains";

const SHARES = "0x...";
const abi = parseAbi([
  "function getRequest(uint256 id) view returns ((uint8 requestType, uint8 requestStatus, address asset, uint256 assetAmount, uint256 sharesAmount, address investor, address receiver, uint64 timestamp, uint64 expiryAt))",
]);

const client = createPublicClient({ chain: mainnet, transport: http() });
const req = await client.readContract({ address: SHARES, abi, functionName: "getRequest", args: [42n] });
// req.requestStatus === 0 => still PENDING
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const SHARES = "0x...";
const abi = [
  "function getRequest(uint256 id) view returns (tuple(uint8 requestType, uint8 requestStatus, address asset, uint256 assetAmount, uint256 sharesAmount, address investor, address receiver, uint64 timestamp, uint64 expiryAt))",
];

const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
const shares = new ethers.Contract(SHARES, abi, provider);
const req = await shares.getRequest(42n);
console.log("status:", req.requestStatus); // 0 = PENDING
```

{% endtab %}

{% tab title="Python" %}

```python
from web3 import Web3

SHARES = "0x..."
abi = [{
    "name": "getRequest", "type": "function", "stateMutability": "view",
    "inputs": [{"name": "id", "type": "uint256"}],
    "outputs": [{"type": "tuple", "components": [
        {"name": "requestType", "type": "uint8"},
        {"name": "requestStatus", "type": "uint8"},
        {"name": "asset", "type": "address"},
        {"name": "assetAmount", "type": "uint256"},
        {"name": "sharesAmount", "type": "uint256"},
        {"name": "investor", "type": "address"},
        {"name": "receiver", "type": "address"},
        {"name": "timestamp", "type": "uint64"},
        {"name": "expiryAt", "type": "uint64"},
    ]}],
}]

w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
shares = w3.eth.contract(address=SHARES, abi=abi)
req = shares.functions.getRequest(42).call()
print("status:", req[1])  # 0 = PENDING
```

{% endtab %}
{% endtabs %}


# Cancel a request

Recover tokens from a stale, still-pending request after its TTL has elapsed.

A subscription or redemption request can be **cancelled** by its investor or receiver, provided it is still **pending** (not yet approved or denied) **and** its cancellation TTL has elapsed. This lets users recover tokens from stale requests.

```solidity
function cancelSubscription(uint256 id) external; // returns escrowed assets to the investor
function cancelRedemption(uint256 id) external;   // returns escrowed shares to the investor
```

The TTL after which a request becomes cancellable is the `cancelableFrom` timestamp in the request's creation event (`timestamp + subscriptionRequestTtl` / `+ redemptionRequestTtl`). Calling before then reverts with `RequestNotPastTtl`; calling on a non-pending request reverts with `RequestNotPending`.

{% tabs %}
{% tab title="viem" %}

```typescript
import { createWalletClient, http, parseAbi } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";

const SHARES = "0x...";
const abi = parseAbi([
  "function cancelSubscription(uint256 id)",
  "function cancelRedemption(uint256 id)",
]);

const account = privateKeyToAccount("0x...");
const wallet = createWalletClient({ account, chain: mainnet, transport: http() });

await wallet.writeContract({ address: SHARES, abi, functionName: "cancelSubscription", args: [42n] });
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const SHARES = "0x...";
const abi = ["function cancelSubscription(uint256 id)", "function cancelRedemption(uint256 id)"];

const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
const signer = new ethers.Wallet("0x...", provider);
const shares = new ethers.Contract(SHARES, abi, signer);

await (await shares.cancelSubscription(42n)).wait();
```

{% endtab %}

{% tab title="Python" %}

```python
from web3 import Web3

SHARES = "0x..."
abi = [
    {"name": "cancelSubscription", "type": "function", "stateMutability": "nonpayable",
     "inputs": [{"name": "id", "type": "uint256"}], "outputs": []},
    {"name": "cancelRedemption", "type": "function", "stateMutability": "nonpayable",
     "inputs": [{"name": "id", "type": "uint256"}], "outputs": []},
]

w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
acct = w3.eth.account.from_key("0x...")
shares = w3.eth.contract(address=SHARES, abi=abi)

tx = shares.functions.cancelSubscription(42).build_transaction({"from": acct.address})
# ... sign & send tx ...
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}

## Events

Cancelling emits a `SubscriptionCancellation` or `RedemptionCancellation` event with the request id and the canceller's address.
{% endhint %}

<figure><picture><source srcset="/files/KAA7kVGGFD9VNtHiNTEX" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2F7N2SpSnyUfJ3tQKxXmCV%2Fkpk_funds_cancel_request.png?alt=media&amp;token=41dbe794-0638-4f8b-9fd8-1be38e9a8515" alt=""></picture><figcaption></figcaption></figure>


# Calculate share price

Read a fund's current share price onchain, from each chain's NAV Calculator and the shares contract's total supply.

A fund's **share price** must be computed **onchain**; this is the authoritative source, and the only one that reflects the fund's value right now. NAV is produced onchain by each chain's NAV Calculator; the circulating share supply lives in the shares contract. Read both live and divide.

{% hint style="info" %}

#### Definition

A fund's **share price** is the sum of NAVs across all chains where it holds portfolio Safes, divided by the total circulating share supply (adjusting for decimals).
{% endhint %}

## Read NAV and total supply

Read NAV from each chain's **NAV Calculator** and divide by the shares contract's `totalSupply()`. The NAV Calculator returns USD with 8 decimals (`usdDecimals()`); pass `address(0)` as the quote asset for USD.

```solidity
function getAccountNav(address account, address quoteAsset) external view returns (NAV memory nav);
function totalSupply() external view returns (uint256);
```

{% tabs %}
{% tab title="viem" %}

```typescript
import { createPublicClient, http, parseAbi, zeroAddress } from "viem";
import { mainnet } from "viem/chains";

const NAV_CALCULATOR = "0x..."; // per-chain NAV Calculator proxy
const PORTFOLIO_SAFE = "0x..."; // the fund's Portfolio Safe on this chain
const SHARES         = "0x..."; // shares contract (the chain it is deployed on)

const ASSET_T = "(address asset, string symbol, uint8 decimals)";
const navAbi = parseAbi([
  `function getAccountNav(address account, address quoteAsset) view returns (
     (int256 value, ${ASSET_T} quoteAsset, uint64 timestamp, ${ASSET_T}[] stalePriceAssets,
      bool sequencerDown, bool quoteAssetStale, ${ASSET_T}[] irregularPriceAssets,
      bool quoteAssetIrregular, ${ASSET_T}[] monitorsUnhealthyPriceAssets))`,
]);
const sharesAbi = parseAbi(["function totalSupply() view returns (uint256)"]);

const client = createPublicClient({ chain: mainnet, transport: http() });

const nav = await client.readContract({
  address: NAV_CALCULATOR, abi: navAbi, functionName: "getAccountNav",
  args: [PORTFOLIO_SAFE, zeroAddress],
});
const supply = await client.readContract({ address: SHARES, abi: sharesAbi, functionName: "totalSupply" });

// price (8-dec USD per 1e18 shares) = nav.value * 1e18 / supply
const sharePrice = (nav.value * 10n ** 18n) / supply;
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const NAV_CALCULATOR = "0x...";
const PORTFOLIO_SAFE = "0x...";
const SHARES         = "0x...";

const ASSET_T = "tuple(address asset, string symbol, uint8 decimals)";
const navAbi = [
  `function getAccountNav(address account, address quoteAsset) view returns (
     tuple(int256 value, ${ASSET_T} quoteAsset, uint64 timestamp, ${ASSET_T}[] stalePriceAssets,
           bool sequencerDown, bool quoteAssetStale, ${ASSET_T}[] irregularPriceAssets,
           bool quoteAssetIrregular, ${ASSET_T}[] monitorsUnhealthyPriceAssets))`,
];
const sharesAbi = ["function totalSupply() view returns (uint256)"];

const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
const calc = new ethers.Contract(NAV_CALCULATOR, navAbi, provider);
const shares = new ethers.Contract(SHARES, sharesAbi, provider);

const nav = await calc.getAccountNav(PORTFOLIO_SAFE, ethers.ZeroAddress);
const supply = await shares.totalSupply();
const sharePrice = (nav.value * 10n ** 18n) / supply;
```

{% endtab %}

{% tab title="Python" %}

```python
from web3 import Web3

NAV_CALCULATOR = "0x..."
PORTFOLIO_SAFE = "0x..."
SHARES         = "0x..."

ASSET = [{"name": "asset", "type": "address"}, {"name": "symbol", "type": "string"}, {"name": "decimals", "type": "uint8"}]
nav_abi = [{
    "name": "getAccountNav", "type": "function", "stateMutability": "view",
    "inputs": [{"name": "account", "type": "address"}, {"name": "quoteAsset", "type": "address"}],
    "outputs": [{"type": "tuple", "components": [
        {"name": "value", "type": "int256"},
        {"name": "quoteAsset", "type": "tuple", "components": ASSET},
        {"name": "timestamp", "type": "uint64"},
        {"name": "stalePriceAssets", "type": "tuple[]", "components": ASSET},
        {"name": "sequencerDown", "type": "bool"},
        {"name": "quoteAssetStale", "type": "bool"},
        {"name": "irregularPriceAssets", "type": "tuple[]", "components": ASSET},
        {"name": "quoteAssetIrregular", "type": "bool"},
        {"name": "monitorsUnhealthyPriceAssets", "type": "tuple[]", "components": ASSET},
    ]}],
}]
shares_abi = [{"name": "totalSupply", "type": "function", "stateMutability": "view",
               "inputs": [], "outputs": [{"type": "uint256"}]}]

w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
calc = w3.eth.contract(address=NAV_CALCULATOR, abi=nav_abi)
shares = w3.eth.contract(address=SHARES, abi=shares_abi)

nav = calc.functions.getAccountNav(PORTFOLIO_SAFE, "0x0000000000000000000000000000000000000000").call()
supply = shares.functions.totalSupply().call()
share_price = nav[0] * 10**18 // supply
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}

## Only use a complete reading

A NAV reading must not be used for subscriptions/redemptions if it is incomplete: check that `stalePriceAssets` is empty and that `sequencerDown` and `quoteAssetStale` are both `false` on **every** chain. For multichain funds, sum each chain's `value` before dividing by the global `totalSupply`. See the NAV Calculator reference for the full reliability model.

The two soft signals do not block a reading, but read them **together**: `irregularPriceAssets` flags a primary that disagreed with its monitors, while `monitorsUnhealthyPriceAssets` flags assets where no monitor answered, so the disagreement check never ran. An asset in the second list is unverified, not verified-clean.
{% endhint %}

{% content-ref url="/pages/Uwrpt7y1t7nhi7zJBgVd" %}
[NAV Calculator](/funds/infrastructure/architecture/onchain-accounting/contracts/nav-calculator)
{% endcontent-ref %}


# List approved assets

Discover which assets a fund accepts for subscriptions and redemptions.

Different funds accept different assets, and some support several. Query the shares contract to discover what a fund accepts:

```solidity
function getApprovedAssets() external view returns (address[] memory);
function getApprovedAsset(address asset) external view returns (ApprovedAsset memory);
function isApprovedAsset(address asset) external view returns (bool);
function assetDecimals(address asset) external view returns (uint8);

struct ApprovedAsset {
    address asset;
    string  symbol;
    uint8   decimals;
    bool    isFeeModuleAsset;
    bool    canDeposit;   // accepted for subscriptions
    bool    canRedeem;    // accepted for redemptions
}
```

`isApprovedAsset` returns `true` if the asset is approved for deposits **or** redemptions; check the `canDeposit` / `canRedeem` flags from `getApprovedAsset` to know which.

{% tabs %}
{% tab title="viem" %}

```typescript
import { createPublicClient, http, parseAbi } from "viem";
import { mainnet } from "viem/chains";

const SHARES = "0x...";
const abi = parseAbi([
  "function getApprovedAssets() view returns (address[])",
  "function getApprovedAsset(address asset) view returns ((address asset, string symbol, uint8 decimals, bool isFeeModuleAsset, bool canDeposit, bool canRedeem))",
  "function isApprovedAsset(address asset) view returns (bool)",
]);

const client = createPublicClient({ chain: mainnet, transport: http() });

const assets = await client.readContract({ address: SHARES, abi, functionName: "getApprovedAssets" });
// e.g. ["0xA0b8...eB48", "0xdAC1...1ec7"]

for (const a of assets) {
  const info = await client.readContract({ address: SHARES, abi, functionName: "getApprovedAsset", args: [a] });
  console.log(info.symbol, { canDeposit: info.canDeposit, canRedeem: info.canRedeem });
  // "USDC" { canDeposit: true, canRedeem: true }
}
```

{% endtab %}

{% tab title="ethers.js" %}

```typescript
import { ethers } from "ethers";

const SHARES = "0x...";
const abi = [
  "function getApprovedAssets() view returns (address[])",
  "function getApprovedAsset(address asset) view returns (tuple(address asset, string symbol, uint8 decimals, bool isFeeModuleAsset, bool canDeposit, bool canRedeem))",
  "function isApprovedAsset(address asset) view returns (bool)",
];

const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
const shares = new ethers.Contract(SHARES, abi, provider);

const assets = await shares.getApprovedAssets();
for (const a of assets) {
  const info = await shares.getApprovedAsset(a);
  console.log(info.symbol, { canDeposit: info.canDeposit, canRedeem: info.canRedeem });
}
```

{% endtab %}

{% tab title="Python" %}

```python
from web3 import Web3

SHARES = "0x..."
abi = [
    {"name": "getApprovedAssets", "type": "function", "stateMutability": "view",
     "inputs": [], "outputs": [{"type": "address[]"}]},
    {"name": "getApprovedAsset", "type": "function", "stateMutability": "view",
     "inputs": [{"name": "asset", "type": "address"}],
     "outputs": [{"type": "tuple", "components": [
        {"name": "asset", "type": "address"}, {"name": "symbol", "type": "string"},
        {"name": "decimals", "type": "uint8"}, {"name": "isFeeModuleAsset", "type": "bool"},
        {"name": "canDeposit", "type": "bool"}, {"name": "canRedeem", "type": "bool"}]}]},
    {"name": "isApprovedAsset", "type": "function", "stateMutability": "view",
     "inputs": [{"name": "asset", "type": "address"}], "outputs": [{"type": "bool"}]},
]

w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
shares = w3.eth.contract(address=SHARES, abi=abi)

for a in shares.functions.getApprovedAssets().call():
    info = shares.functions.getApprovedAsset(a).call()
    print(info[1], {"canDeposit": info[4], "canRedeem": info[5]})  # symbol, flags
```

{% endtab %}
{% endtabs %}

<figure><picture><source srcset="/files/fgttZo98gjl3WwwWhJaB" media="(prefers-color-scheme: dark)"><img src="https://1699303810-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUsQM4bzcmOPSgkWddkeN%2Fuploads%2FWg48Ug8XXW9sty0gPUny%2Fkpk_approved_assets.png?alt=media&amp;token=a3abd6c0-5f95-4f4e-b832-d4a16dd504bd" alt=""></picture><figcaption></figcaption></figure>


# Audits

All KPK's onchain and offchain infrastructure has undergone thorough review and testing, prior to deployment. Independent audits of key smart contracts have also been conducted. The audit reports detail the findings and confirm that any identified issues have been resolved.

<table><thead><tr><th width="119.77734375">Date</th><th width="279.7109375">Scope</th><th>Auditor</th><th>Report</th></tr></thead><tbody><tr><td>Dec 2025</td><td>Onchain investment vehicles</td><td><a href="https://cantina.xyz/welcome">Cantina</a></td><td><a href="https://drive.google.com/file/d/1w75RwyvGxLnQWmgeLaN52kCFpw4TJFP4/view?usp=sharing">Link</a></td></tr><tr><td>Dec 2025</td><td>Onchain investment vehicles</td><td><a href="https://teamomega.eth.limo/">Team Omega</a></td><td><a href="https://github.com/karpatkey/onchain-investment-vehicles/blob/main/audit-reports/team-omega-kpk-oivs-oct-2025.pdf">Link</a></td></tr></tbody></table>


# Changelog

Dated log of onchain configuration changes to KPK's Morpho, Gearbox, and Symbiotic vaults and to KPK Funds, covering markets, caps, oracles, fees, timelocks, and roles, with transaction links.

<button type="button" class="button primary" data-action="search" data-icon="magnifying-glass">Search…</button>

{% updates format="full" %}
{% update date="2026-10-20" tags="ethereum,morpho" %}

## USDC Yield

**Change:** Hard shutdown of the ftUSD/USDC market; absolute and relative caps set to 0

**Rationale:** Full migration of the vault's Flying Tulip exposure to the ssftUSD/USDC market.

**Tx:** [Shutdown tx](https://etherscan.io/tx/0x31c7443312f1d2abddcbeacc0f5734ef66514263e586896fad1a733020489e06)
{% endupdate %}

{% update date="2026-10-19" tags="ethereum,morpho" %}

## USDC Yield

**Change:** Hard shutdown of the srRoyUSDC/USDC market; absolute and relative caps set to 0

**Rationale:** Royco is sunsetting the srRoyUSDC vault.

**Tx:** [Shutdown tx](https://etherscan.io/tx/0x955a3db466f87cb6cac3088d51734d8a5ddf00dd605f76053c6896c5e9c0ab3f)
{% endupdate %}

{% update date="2026-10-18" tags="ethereum,funds" %}

## BTC Carry

**Change:** Added Aave v3 Core, Spark and Morpho Markets to the Manager role as venues for BTC collateral and stablecoin borrowing, alongside Aave v4, whose access is limited to named reserves on the Bluechip and Maple SyrupUSDG spokes. The role can also hold Maple syrup positions and deposit into KPK USDC Prime and USDT Prime. The health-factor, exit and settlement roles on the Sub Roles Modifier gained Aave v3 Core supply and withdraw for WBTC, and borrow and repay for USDC.

**Rationale:** Lets the Fund borrow where the carry is best at its size, with Aave v4 kept as an approved venue. The Fund moved its WBTC collateral and USDC borrow to Aave v3 Core on 19 October 2026.

**Tx:** [Manager role 1 of 2](https://etherscan.io/tx/0x01d4bd1abc309af0ebdd2601f67a162f6927267957cc5ae1ee6bfd2cc468b2cf), [Manager role 2 of 2](https://etherscan.io/tx/0xcb8450669eaaa5d41d47ae5229b00bb4719e76f87c18752aaed99268369a57cb), [Agent roles](https://etherscan.io/tx/0xc050b4c49b6964232e12d52d58b0ad8e1ae74eb4cf7d5dc6c08ceecd54c26887)
{% endupdate %}

{% update date="2026-10-18" tags="ethereum,funds" %}

## USD Alpha

**Change:** Added the YO Ecosystem vault (`deposit`, `withdraw`, `redeem`, `forceDeallocate`) and the USDC RWA Liquidity vault (`deposit`, `withdraw`, `redeem`) to the Manager role, with receiver, owner and `onBehalf` pinned to the Portfolio Safe, and allowed USDC approvals to both. CowSwap orders signed by the role must now carry a zero fee.

**Rationale:** Lets the Fund deploy USDC into two more KPK-curated vaults.

**Tx:** [Manager role](https://etherscan.io/tx/0xcb8450669eaaa5d41d47ae5229b00bb4719e76f87c18752aaed99268369a57cb)
{% endupdate %}

{% update date="2026-10-15" tags="ethereum,base,arbitrum,morpho" %}

## Multiple vaults

**Change:** Narrowed the monitoring agent's permissions on all thirteen Morpho v2 vaults on Ethereum, Base and Arbitrum: its `submit` call now accepts only `setForceDeallocatePenalty` on the vault's own adapter, at most 2%. Revoked the rebalancing and exit agents' remaining permissions on the legacy v1 vaults of USDC Prime, USDT Prime, ETH Prime, USDC Yield, ETH Yield, EURC Yield and USDC Yield (Arbitrum).

**Rationale:** Keep each agent's permissions to the calls it makes; the monitoring agent only ever sets the force-deallocate penalty and cannot appoint a new Allocator.

**Tx:** [USDC Prime](https://etherscan.io/tx/0xd93d600b511abf6dfcda3ab13b7018ded5100bab7b232f89bbb09878df0bc16b), [USDC Yield](https://etherscan.io/tx/0x8cf7399c72828318218762eb416a02e699144c6d4056408a0da28c0f84353ad9), [USDT Prime](https://etherscan.io/tx/0x4309fd27929191146c923baef6f7910e4dc93939d941ed11f6080c5f07a1cb37), [ETH Prime](https://etherscan.io/tx/0xe0acd22ca9cf9a841963813b936e3c7c7d5d58844c771ff5d92e20d9a75eb424), [ETH Yield](https://etherscan.io/tx/0xb294ac1b95c458e8963f2957cc93a1ee6cf0a2638c2f9cb6cbd034200afe7027), [EURC Yield](https://etherscan.io/tx/0xbaccf361e6aebb67b10b0d93bddaf84f3792309356a37c2592f7311350772593), [USDC Prime Core](https://etherscan.io/tx/0x7d3ccc8653c0eb70dd98440161fa3f82fcfecd42e2dd39c7f01716839b6aa3d3), [USDC Yield RWA](https://etherscan.io/tx/0x6b827d596a98489ee1169b95f49f339b20592721bc556e0622d4ebae5064ddad), [wARS Yield](https://etherscan.io/tx/0xdb6d5be612e02813c358d44695e1df9aa066ccda544ce9af2f0c6bb7efb2f042), [YO Ecosystem](https://etherscan.io/tx/0xa334c32a25b0a7832e6f796b0107eccdbabb6323c2036089aa4c01653d33bb73), [USDC Yield (Base)](https://basescan.org/tx/0xe494d188295069ccca4bc18cd5e4b0ce28fe818bbe4c894f0e02980d250486a3), [USDC Yield (Arbitrum)](https://arbiscan.io/tx/0x446eb0e55be2469dd4827130fdde7942e1550835740157385704292b302f9a7c), [EURe Yield (Arbitrum)](https://arbiscan.io/tx/0x5b6d40675fa2f2e1a45e6ac32d25b2647302d1b14572f7defdc87bcabdbc7784)
{% endupdate %}

{% update date="2026-10-14" tags="robinhood,funds" %}

## Stocks-USD, SPY and ETH-USD

**Change:** Moved `DEFAULT_ADMIN_ROLE` on each Fund's shares contract to its own 3-day TimelockController, with the Security Council Safe as proposer and canceller; the Security Council no longer holds the role directly. Timelocks: Stocks-USD [`0x35494534943457c9bCA309f0075BEEC8432E7226`](https://robin.etherscan.io/address/0x35494534943457c9bCA309f0075BEEC8432E7226), SPY [`0xdDcd8aa130E4D2c294650FC090AbDE1F47E06C19`](https://robin.etherscan.io/address/0xdDcd8aa130E4D2c294650FC090AbDE1F47E06C19), ETH-USD [`0xE3Be505EE3897F2212BA904c2Aa2B61A84207Dd4`](https://robin.etherscan.io/address/0xE3Be505EE3897F2212BA904c2Aa2B61A84207Dd4). The Exec Roles Modifiers stay with the Governance Safe.

**Rationale:** Changes to a Fund's fees, request timeouts, operator roles or shares contract are now scheduled onchain and take effect only after 3 days.

**Tx:** [Shares admin to timelocks](https://robin.etherscan.io/tx/0xcaf805862f176154e4bc4bce71cc74eae3bab14264fb0a5a1f6d1f351f03a81f)
{% endupdate %}

{% update date="2026-10-13" tags="ethereum,morpho" %}

## USDC Yield RWA

**Change:** Moved the liquidity adapter from the AA\_FalconXUSDC/USDC market to wFalconX/USDC.

**Rationale:** wFalconX/USDC holds more free liquidity, so more of the vault can be withdrawn straight away without `forceDeallocate`.

**Tx:** [setLiquidityAdapterAndData](https://etherscan.io/tx/0x777ca4bf1c0a965b60193b739f383c92f28d1205a0b8a89fbb1584c14addac45)
{% endupdate %}

{% update date="2026-10-13" tags="ethereum,symbiotic" %}

## USDC RWA Liquidity

**Change:** Set the auto-allocate queue to USDC Prime, USDC Yield and USDC Yield RWA.

**Rationale:** Replaces the Aave v3 adapter, removed on 17 August, with USDC Yield RWA in the queue.

**Tx:** [setAutoAllocateAdapters](https://etherscan.io/tx/0x778a9bffd2bd362dcda570eaf06c0ada72d858c9915e17cdff9dc4f60919027e)
{% endupdate %}

{% update date="2026-10-13" tags="ethereum,morpho" %}

## YO Ecosystem

**Change:** Set timelocks of 3 days on adding adapters and raising caps, and 7 days on removing adapters, abdicating and increasing timelocks. Abdicated the sendShares, receiveShares and receiveAssets gate setters, raised the Curator Safe threshold to 2/5 and transferred vault ownership to the Security Council Safe `0x8b884f80B3B839F52b6cE168f133e7a5D1f0A537`.

**Rationale:** Completes the launch configuration: parameter changes are now timelocked, and control no longer sits on a deployer EOA.

**Tx:** [Timelocks](https://etherscan.io/tx/0x444d4d58459ea53e49a4487f649fd2aa18e03bb46805dff809c2ee7dff6953c7), [Abdicate](https://etherscan.io/tx/0x9aa2311ada3e699d9a58adb0073626c60a41b8538adbc0672ddebbc393d5ef79), [Set owner](https://etherscan.io/tx/0xca76f92801c652a4cc1c887bf33bfe1fe8494f17439b7ee9de17877d46f648af), [Safe threshold](https://etherscan.io/tx/0xf29e4fa7dfbda9979c95651c98ad6c0bc1b099bd02663247b845bfa49424e660)
{% endupdate %}

{% update date="2026-10-09" tags="ethereum,gearbox" %}

## frxUSD pool

**Change:** Paused the original mGLOBAL Credit Manager, `0x24cCb70bCd6B5962F99eF84FC201a9D2143B9DDd`. The mGLOBAL Credit Manager redeployed on 30 September is not affected.

**Rationale:** Precautionary, after KPK's monitoring flagged that mGLOBAL's instant-redemption liquidity was depleted.

**Tx:** [Pause](https://etherscan.io/tx/0x1db6f17505ecdfc65c4d957d1d352f3a0bf2e94d2dfb5f120c57804fe5f10181)
{% endupdate %}

{% update date="2026-10-09" tags="ethereum,funds" %}

## ETH Alpha and USD Alpha

**Change:** Moved both Funds to new Exec Roles Modifiers on Roles v2.1.1, with permissions unchanged. Ownership of each Exec Roles Modifier and `DEFAULT_ADMIN_ROLE` on each shares contract now sit with 3-day timelocks controlled by the Security Council Safe, which no longer holds either role directly.

**Rationale:** Roles v2.1.1 fixes defects in the v2.1.0 permission checker. The timelocks mean changes to what a Fund can do with its assets, its fees, or its shares contract are scheduled onchain and only take effect after 3 days.

**Tx:** [ETH Alpha Roles switch](https://etherscan.io/tx/0x772e3ecc920338c2b0bac4a2b90c950c2c94a9edbd940d683a79c7e3c63b43ff), [USD Alpha Roles switch](https://etherscan.io/tx/0xe99f8c580c758108c7b8d5523951833ac450553b662b395505d5560c215ee8fd), [Shares admin to timelocks](https://etherscan.io/tx/0xaf501d9cb54801d59da2f520ba646ac9bf2543eac97335de1116c6606b17e687), [Roles ownership to timelocks](https://etherscan.io/tx/0x9108818d454751f56545f36c08b3b6db239e72c376862ee4476125312f19a90f)
{% endupdate %}

{% update date="2026-10-09" tags="ethereum,morpho" %}

## USDC Yield

**Change:** Onboarded ssftUSD/USDC (Tier C, 30% cap) and PT-siUSD-7JAN2027/USDC (Tier C, 30% cap). Lowered ftUSD/USDC from 30% to 15% and siUSD/USDC from 60% to 30%.

**Rationale:** Adds staked and fixed-rate exposure to issuers already in the vault, while capping total Flying Tulip exposure at 45% and InfiniFi at 60%.

**Tx:** [setCaps](https://etherscan.io/tx/0x46759173506d962fdc3e3e7baa6d9e84fbf875f9186128bab0e0b1c889541744)
{% endupdate %}

{% update date="2026-10-09" tags="funds" %}

## NAV Calculator

**Change:** Moved NAV Calculator administration on all 25 chains to the TimelockController 0xAB92EA75D07792915eB1f29259354866B880dFC2 and the Security Council Safe, and removed the deployment account. The Security Council Safe is now the sole admin of the Merkl Rewards adapter, and the Safe's threshold on X Layer was raised to 5-of-8, matching every other chain.

**Rationale:** Puts upgrades and configuration of the NAV Calculator under Security Council governance on all 25 chains, with a TimelockController (delay currently zero) alongside the Safe's direct roles.

**Tx:** [Ethereum role removal](https://etherscan.io/tx/0x8e56b5d12840fb7c299d158042c7d7c496ca866c149be041bdd638db1d89092f), [Merkl admin](https://etherscan.io/tx/0x1ab5b95ef2154c376599e233dd938867c1428ce0eaa7a2b1ba282cdb19774a92), [X Layer threshold](https://www.oklink.com/x-layer/evm/tx/0x13702798714a89e5542f6af6496b31043d41c3c40fa21f1e4c05614edae56f0d); the same change on every other chain
{% endupdate %}

{% update date="2026-10-07" tags="ethereum,morpho" %}

## USDC Yield

**Change:** Onboarded the USD3/USDC market (Tier D, 30% cap)

**Rationale:** Adds a 3Jane market already assessed and monitored for KPK USDC Yield RWA, diversifying the vault's allocation.

**Tx:** [setCaps](https://etherscan.io/tx/0xd5b422ea877ece696cc8a72c57624f6e779a13e4b651b640f16691c20f691705)
{% endupdate %}

{% update date="2026-10-06" tags="ethereum,morpho" %}

## YO Ecosystem

**Change:** Deployed the YO Ecosystem vault on Morpho v2, `0x355A1c6A3f1946d4DbD7cE932492925AB9F261a7`, with a Markets V1 Adapter, and enabled the yoUSD/USDC (86% LLTV, Tier C, 90% cap) and yoUSD Edge/USDC (77% LLTV, Tier D, 65% cap) markets. Launch parameters are a 5% performance fee, a 0% management fee and a 0.01% forceDeallocate penalty, with the adapter-registry setter abdicated. The curator role sits with the Curator Safe `0xA2AE32ab472BBc1ceF8d5A6F0712c652E414FF62`, whose Roles Modifier was enabled on 7 October.

**Rationale:** A dedicated USDC vault for the YO ecosystem, lending against YO Protocol's vault tokens.

**Tx:** [Deploy vault](https://etherscan.io/tx/0x6adb38b8ad19a0dd319f3e374d722ac6f11471dc7d5ecf905d811791c94868bf), [Add adapter](https://etherscan.io/tx/0xba0b33e4cf0d0ca038cb54c7af15d863c156d4a8f1ad3f40942aa4656cb7e394), [Fees and roles](https://etherscan.io/tx/0x0eeef23adc54454d66ff42db846a2d21d49d0ff6bd2b08a7cf9a6bc0b5daa388), [Enable Roles Modifier](https://etherscan.io/tx/0x3f3e112ceab64ec6aaf5571e797e52884fd5262eaa181d67cf95bde6733d54e3)
{% endupdate %}

{% update date="2026-10-06" tags="ethereum,arbitrum,morpho" %}

## EURC Yield, USDC Yield (Arbitrum)

**Change:** Sunset the KPK EURC Yield (Ethereum) and KPK USDC Yield (Arbitrum) vaults. A soft shutdown was executed on every market in both vaults: risk appetite set to 0 and allocations drained into the vault's idle balance. Both vaults are removed from the docs.

**Rationale:** Low TVL and utilisation. Funds sit idle in each vault, so depositors can withdraw at any time.

**Tx:** EURC Yield: [wstETH](https://etherscan.io/tx/0x3b418a75e004c0f5253b30bc6241be8f716686b00c86b55296493543f053a9ce), [WBTC](https://etherscan.io/tx/0x41f236ba6019e967d578cd68266f085b6c52f59ea5df0c92a4c7b85f32ca3e5c), [WETH](https://etherscan.io/tx/0xa7bcb94b9418f323f1959ff15d7051754130e5ee364af1e424e8c20d0e5acf2f). USDC Yield (Arbitrum): [wstETH](https://arbiscan.io/tx/0xa0caf2497103d493b05de43583b3dfd99f08d7a8284658a9c84e79f195212d79), [WBTC](https://arbiscan.io/tx/0x5d25d153e535d13e60f6d4c571a67f54394887848fab251c6c6a113f6da8532a), [WETH](https://arbiscan.io/tx/0x5f06f2448d97a3ab132eea7c109687b4f0de2b4e4c89ba656593b6cabbf7105f), [weETH](https://arbiscan.io/tx/0x868719b54bb1c5f915f45e5f21dcae9a1b73f78280975706308d6ce1cf26b6fb)
{% endupdate %}

{% update date="2026-10-06" tags="funds,robinhood" %}

## NAV Calculator (Robinhood Chain)

**Change:** Upgraded the NAV Calculator on Robinhood Chain to the same implementation as the other chains. Its roles are held by the Robinhood Chain Governance Safe (2-of-5), with no timelock; the deployment account was removed on 8 October 2026.

**Rationale:** Keeps Robinhood Chain on the same NAV logic as the rest of the fleet.

**Tx:** [Upgrade](https://robin.etherscan.io/tx/0x2089ff8621b0fa436d52201b1dd3079bb899cc6d1a1a4be1f70398323d4c0c6a), [deployment account removal](https://robin.etherscan.io/tx/0xf97a6878199cd79c1f0da2b96ab51b15f12c62c86d4be73d4fabcf9e3c1b846d)
{% endupdate %}

{% update date="2026-10-06" tags="funds,robinhood" %}

## Fund factory (salt v4)

**Change:** Deployed the salt v4 Fund factory on Robinhood Chain at the same addresses as on the other 19 chains, owned by the Security Council Safe. Robinhood Chain is not in the CCIP orchestrator's chain registry, so Funds there deploy locally.

**Rationale:** Lets new Funds launch on Robinhood Chain with the same contracts and governance options as on every other supported chain.

**Tx:** [Robinhood Chain deployment](https://robin.etherscan.io/tx/0xea49f5b3265c93db39ac141885aa52d7375b78dd2ad817eb8166b67b2934ed76)
{% endupdate %}

{% update date="2026-09-30" tags="arbitrum,morpho" %}

## EURe Yield

**Change:** Set timelocks of 3 days on adding adapters and raising caps, and 7 days on removing adapters, abdicating and increasing timelocks. Raised the Curator Safe threshold to 2/5 and transferred vault ownership to the Security Council Safe `0x8b884f80B3B839F52b6cE168f133e7a5D1f0A537`.

**Rationale:** Completes the launch configuration: parameter changes are now timelocked, and control no longer sits on a deployer EOA.

**Tx:** [Timelocks](https://arbiscan.io/tx/0xdfc0c88ba67c5948c60f4aed169650ca7c7b2beabec6ac8cee929094757e6bf2), [Set owner](https://arbiscan.io/tx/0x11207b5b5ce707657f0b56de905c4526ab22429a82981b2f0dc6c4029cdeb4fe), [Safe threshold](https://arbiscan.io/tx/0xaa7ea56887ce65638e45d85a1b235209eca934275b39c12396315a8124f732d6)
{% endupdate %}

{% update date="2026-09-30" tags="ethereum,symbiotic" %}

## USDC RWA Liquidity

**Change:** Whitelisted HYB (NYLIM US High Yield Bond Fund, issuer: Centrifuge) on the Liquid Lane adapter, with a 65,000 USDC acquisition limit and a 0.10% minimum discount.

**Rationale:** Risk Framework assessment closed at tier D. The limit is sized to HYB's small fund NAV (\~$640k).

**Tx:** [addTokenToRedeem + setLimit + setMinDiscount](https://etherscan.io/tx/0xdc104fd7efe37cf5da2da550f01e93d433dcccfccbac11fb55f29f3176e82982)
{% endupdate %}

{% update date="2026-09-30" tags="ethereum,gearbox" %}

## frxUSD pool

**Change:** IRM update (U2 80% → 85%, base rate 4% → 3%); redeployed the mGLOBAL Credit Manager with a 10% interest fee (from 20%) and 2 max enabled tokens; set quota rates of 0.58% for mF-ONE and 0.55% for mGLOBAL.

**Rationale:** Brings borrower cost in line with market-clearing rates (5.5% mF-ONE, 5.0% mGLOBAL, down from 6.0%) ahead of expected mGLOBAL borrow demand. Quota rates price the two collaterals separately.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreidngeu2iyzwizygb4g63fr5onlmak5knxati7szu5adv2tendvnrq)
{% endupdate %}

{% update date="2026-09-30" tags="funds" %}

## NAV Calculator

**Change:** Upgraded the NAV Calculator on all 25 chains, on 29 and 30 September 2026, to implementation 0xA7243DCDF7e4D6f8FA20A9F022186C0d0778C2b6; the proxy address is unchanged. The meta-adapters were redeployed and re-registered at new addresses on the chains that carry them; plain adapters and `ERC20Default` are unchanged (see [Deployment addresses](https://docs.kpk.io/funds/infrastructure/onchain-accounting/deployment-addresses)). Price-feed heartbeats can now be updated in place, a token or market coordinate can no longer be claimed by two adapters at once, and 17 wallet-yield adapters on Ethereum, Arbitrum, Base, Gnosis, and Optimism report yield-bearing tokens held in a wallet as their protocol position.

**Rationale:** Prevents a coordinate from being reported by two adapters, and reports wallet-held yield tokens by the protocol behind them rather than as plain balances.

**Tx:** [Ethereum upgrade](https://etherscan.io/tx/0x55c51f1d4ea5f9941523d9ffd9717c13a9748976eaaedfb10a91dff02a83a0a1); the same upgrade on every other chain
{% endupdate %}

{% update date="2026-09-29" tags="funds" %}

## Fund factory (salt v4)

**Change:** Deployed the salt v4 Fund factory on 19 chains at the same addresses, owned by the Security Council Safe. Factory: 0x73Bb12a05669748f3c9cbE8764271c69182f49E5. New Funds can launch with exec and shares timelocks (12 hours to 30 days). Existing Funds are unchanged.

**Rationale:** Lets new Funds put their governance behind a timelock from launch: changes to what a Fund can do with its assets, its fees, or its shares contract are scheduled onchain and only take effect after the delay. A Fund's shares can also live on more than one chain.

**Tx:** [Ethereum deployment](https://etherscan.io/tx/0x4b91121f3a3923ee18ee6478c4f1e1fe6763e9a2b23f48bccec014af8519f240), other chains in the [deployment records](https://github.com/karpatkey/onchain-investment-vehicles/blob/main/script/deployed-infra.json)
{% endupdate %}

{% update date="2026-09-22" tags="arbitrum,morpho" %}

## EURe Yield

**Change:** Deployed the KPK EURe Yield vault on Arbitrum, `0x7bfdE3A7bd346Da92521D384563F89BB45dE1f60`, with a Markets V1 Adapter, and enabled wstETH/EURe, WBTC/EURe and WETH/EURe (86% LLTV) and USDC/EURe (91.5% LLTV), each Tier A with a 90% cap. Launch parameters are a 0% performance fee, a 0% management fee, 14-day fee timelocks, a 7-day timelock on the sendAssets gate and a 0.01% forceDeallocate penalty, with the adapter-registry, sendShares, receiveShares and receiveAssets setters abdicated. On 23 September the curator role moved to the Curator Safe `0xb3E0fe1ac9AcaBC64132171CD88A91312Da71C25` and its Roles Modifier was enabled.

**Rationale:** KPK's first EURe vault, deployed on Arbitrum.

**Tx:** [Deploy vault](https://arbiscan.io/tx/0xcae58d08967233e81b38f111ccd6a9a87f9f727e7e3aaf1e75a26a9b106c078a), [Add adapter](https://arbiscan.io/tx/0xc0c39a317faf80f06b4e87dab44c826bce48293c3ef846d81fb9a2713c36bd2c), [Fee and gate timelocks](https://arbiscan.io/tx/0xeee36f64d17d4b841bba9f8c3a0e06cb08f292b140489abe0cd2a51d66642fdd), [Set curator](https://arbiscan.io/tx/0x241603367bd064b7db98c28e02e57e728a8505dfa4c966b3814829df9c82def3), [Enable Roles Modifier](https://arbiscan.io/tx/0xe17100e42ef888a78421229c395bf73cadcddd42041f009c8aeae6130ac366ee)
{% endupdate %}

{% update date="2026-09-22" tags="ethereum,gearbox" %}

## frxUSD pool

**Change:** Onboard mGLOBAL as collateral via a dedicated Credit Manager

**Rationale:** Second RWA collateral for the pool (issuer: Midas). $1mln quota limit, 80% utilization limit, 82% LT, Fundamental oracle.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreibaakkttpeicmd22ik5q7cdze5e6cx47altwn6xvxhcqxlxg4l7ti)
{% endupdate %}

{% update date="2026-09-21" tags="ethereum,arbitrum,base,morpho,symbiotic" %}

## All KPK-curated vaults

**Change:** Executed the vault ownership transfer to the new Security Council Safe. Owner and Sentinel roles on every KPK-curated Morpho v2 vault and the Symbiotic vault and delegator moved from `0x354C92aF243d53A24feb3dFF20372Af7b7c47478` to `0x8b884f80B3B839F52b6cE168f133e7a5D1f0A537`, across Ethereum, Arbitrum and Base.

**Rationale:** Completes the migration announced on 11 Sep, when the Security Council signer was swapped on all Curator Safes. Vault strategy, risk parameters, caps and automation are unaffected, and no depositor or integrator action is required. The deprecated Morpho v1 vaults are out of scope and stay with the old Safe.

**Tx:** [Ethereum, old SC](https://app.safe.global/transactions/tx?safe=eth:0x354C92aF243d53A24feb3dFF20372Af7b7c47478\&id=multisig_0x354C92aF243d53A24feb3dFF20372Af7b7c47478_0x339fa889f06a435f18ee677dee06866af2133b18a8ef10bbbaa616cf3d4d3d65), [Arbitrum, old SC](https://app.safe.global/transactions/tx?safe=arb1:0x354C92aF243d53A24feb3dFF20372Af7b7c47478\&id=multisig_0x354C92aF243d53A24feb3dFF20372Af7b7c47478_0x3236470c0605b65004dd0666e93c7768c4c2b32a8fb7fc44ba963c60ee943bd7), [Base, old SC](https://app.safe.global/transactions/tx?safe=base:0x354C92aF243d53A24feb3dFF20372Af7b7c47478\&id=multisig_0x354C92aF243d53A24feb3dFF20372Af7b7c47478_0x17473a5242e4dbc23c06f6cb47664cdd0366f3785f0b8c81810047ac54e9dc6e), [Ethereum, new SC](https://app.safe.global/transactions/tx?safe=eth:0x8b884f80B3B839F52b6cE168f133e7a5D1f0A537\&id=multisig_0x8b884f80B3B839F52b6cE168f133e7a5D1f0A537_0xb4071fe7d066a2dba0ef3c8c97004f635d48d08d704fb7dd11ab122411356985)
{% endupdate %}

{% update date="2026-09-21" tags="ethereum,symbiotic" %}

## USDC RWA Liquidity

**Change:** Set acquisition limits on the Liquid Lane adapter for deJAAA (150,000 USDC) and deJTRSY (200,000 USDC). Minimum discounts unchanged at 0.05%.

**Rationale:** Issuer whitelisting cleared, opening the redemption path for both wrappers. Limits match the underlying JAAA and JTRSY.

**Tx:** [setLimit](https://etherscan.io/tx/0xc820f49dd29d57b059a67922eb7c7096e317fa6777a1d3893be3708585c3f0e3)
{% endupdate %}

{% update date="2026-09-17" tags="arc,morpho" %}

## wARS Yield

**Change:** Enabled the Roles Modifier `0xCC96396ae50e17B6DD64fa5e93D930cf62fE5deE` on the Arc Curator Safe `0x89f810ec64e472bDDA2740dD85D56C051A895eFF`.

**Rationale:** Sets up the Permissions Layer for KPK's automation agents on the Arc vault.

**Tx:** [Enable Roles Modifier](https://explorer.arc.io/tx/0x269dab5db307b71f86e1397c3801db912f037e07fb9777609e5c65c142c0496a)
{% endupdate %}

{% update date="2026-09-17" tags="ethereum,symbiotic" %}

## USDC RWA Liquidity

**Change:** Raised the mF-ONE acquisition limit on the Liquid Lane adapter from 50,000 to 200,000 USDC and lowered its minimum discount from 2.50% to 0.80%.

**Rationale:** Sized to meet incoming mF-ONE redemption requests.

**Tx:** [setLimit + setMinDiscount](https://etherscan.io/tx/0x196cf444df1bbb36bfe404d6e0f84844c12973f963bce32d4d1d4c96221c5199)
{% endupdate %}

{% update date="2026-09-16" tags="ethereum,gearbox" %}

## Emergency Admin

**Change:** Enabled a Roles Modifier, `0xD9649A14d27680C35CB4E58e0e062e35CA472377`, on the Emergency Admin Safe.

**Rationale:** Lets KPK's automation agents execute scoped emergency actions, such as pausing a Credit Manager, through the Permissions Layer.

**Tx:** [Enable Roles Modifier](https://etherscan.io/tx/0x151c81d94630acb826617ae810945b9a44e630707386bff6d1cf9fa49c77cd40)
{% endupdate %}

{% update date="2026-09-15" tags="arc,morpho" %}

## wARS Yield

**Change:** Set the remaining timelocks on the new Arc vault, covering cap increases and adapter changes, then transferred the curator role to the Arc Curator Safe (2/5) and vault ownership to the Security Council Safe (5/8).

**Rationale:** Completes the launch configuration. The timelock set now matches the Ethereum wARS Yield vault, and control no longer sits on a deployer EOA. Arc is the first chain where the vault owner is the canonical Security Council Safe `0x8b884f80B3B839F52b6cE168f133e7a5D1f0A537`.

**Tx:** [Cap and adapter timelocks](https://explorer.arc.io/tx/0x137b9eaaf28ac97eb1b9b4d18a7f3b5edd7e1b68c5d7343ea10c6cb23c4cb7c6), [Set curator](https://explorer.arc.io/tx/0xdb46b2cf16ad7503c0e3f3de688ca5749c879f7fd536f855f95a61c42450592f), [Set owner](https://explorer.arc.io/tx/0x59f434446794178334437b6c040b8b1bce8b6191f18bf7c62c750108e91030fe)
{% endupdate %}

{% update date="2026-09-15" tags="ethereum,symbiotic" %}

## USDC RWA Liquidity

**Change:** Whitelisted AA\_FalconXUSDC (Pareto AA tranche) on the Liquid Lane adapter, with acquisition limit and minimum discount at 0.

**Rationale:** Whitelisting ahead of a Risk Framework assessment. At a zero limit it cannot be acquired.

**Tx:** [addTokenToRedeem](https://etherscan.io/tx/0x0593791a9255bc927b6c4bba8dc15081f936f358338471a2f738b348f2b56f13)
{% endupdate %}

{% update date="2026-09-14" tags="base,morpho" %}

## USDC Yield

**Change:** Set 14-day timelocks on management and performance fee changes and a 7-day timelock on the sendAssets gate.

**Rationale:** Brings the Base vault's fee and gate timelocks in line with KPK's other Morpho v2 vaults.

**Tx:** [Timelocks](https://basescan.org/tx/0xac73817339322aeb869cf45fcf7e2f320f5cdf259463259542f6ca254ef03684)
{% endupdate %}

{% update date="2026-09-14" tags="ethereum,gearbox" %}

## frxUSD pool

**Change:** Launch frxUSD Earn Pool; onboard mF-ONE as the only collateral accepted at launch

**Rationale:** New Gearbox Earn Pool market for frxUSD to be lent against RWA collateral. Configured with mF-ONE for launch (issuer: Midas). $5mln quota limit, 80% utilization limit, 84% LT, Fundamental oracle.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreicl6tlwqbdeulbezkjum2nrzdyrt3yqd3uewyvdw5crpur2trhhaq)
{% endupdate %}

{% update date="2026-09-14" tags="arc,morpho" %}

## wARS Yield

**Change:** Deployed the KPK wARS Yield vault on Arc, `0xF0943081f646f21cF5BFBa8e0F670C4248ab0ba5`, with a Morpho Market V1 Adapter. Launch parameters are a 0% performance fee, a 0% management fee, 14-day fee timelocks and a 7-day timelock on the sendAssets gate.

**Rationale:** A second wARS vault, deployed for Arc's public mainnet launch and KPK's first vault on that chain. No markets are enabled at launch, because pricing a wARS market on Arc requires an ARS price feed that was not yet published on the chain.

**Tx:** [Deploy vault](https://explorer.arc.io/tx/0x515c1fe67d62f2607f39c591b03b695bee65975df6318c910d78a2428af73278), [Fee and gate timelocks](https://explorer.arc.io/tx/0xbb1c75e91b31eca1c40ad214f7991f62b64a5619d970c275a53b6b22be42265e)
{% endupdate %}

{% update date="2026-09-11" tags="ethereum,symbiotic" %}

## USDC RWA Liquidity

**Change:** Removed the Euler adapter for the HINC/USDC market (eUSDC-137), `0xE75894cB7FfB89E0e8A621D44930C350Ef5D5753`.

**Rationale:** The adapter was registered on 26 August without a cap and never allocated to.

**Tx:** [removeAdapter](https://etherscan.io/tx/0x5c37db2ead7222d5f3746fe8e2db71603e2c3dbf4d4a0517a0ee7577652eb9a0)
{% endupdate %}

{% update date="2026-09-11" tags="ethereum,base,arbitrum,morpho,gearbox,symbiotic" %}

## All KPK-curated vaults

**Change:** Replaced the Security Council signer on all Curator Safes across Ethereum, Arbitrum and Base. Old `0x354C92aF243d53A24feb3dFF20372Af7b7c47478`, new `0x8b884f80B3B839F52b6cE168f133e7a5D1f0A537`. Each change was a single `swapOwner`. Safe thresholds are unchanged.

**Rationale:** Planned migration to a canonical Security Council Safe at one address across all chains, announced before execution on [X](https://x.com/kpk_io/status/2098344498040357198). Vault strategy, risk parameters, caps and automation are unaffected, and no depositor or integrator action is required. Vault `owner()` and Sentinel transfers follow as a separate step.

**Tx:** [USDC Prime](https://etherscan.io/tx/0x78b70d699124515cbaabe43f94daa011b06a0c65c0ea3ed4c973203f781cef30), [USDC Prime Core](https://etherscan.io/tx/0xc0419a2b47eba852130cec3668ac4a41e3b95bcfcd32fd49b0f8b3eb9c5b3db4), [USDT Prime](https://etherscan.io/tx/0x02ab5ada6e57f70e926f433d5d388f033837b8219593bd3ae0b1da5b455870b7), [ETH Prime](https://etherscan.io/tx/0x30d15e7653c162018416277bb911f866a99bbb73a064293797bf92b136146013), [USDC Yield](https://etherscan.io/tx/0xfc57e2616145821a40f599a14ab00b73d4993a0b00c3ed86efaa918f8a606a56), [ETH Yield](https://etherscan.io/tx/0x223865daa5e176244668a67476df6ce1fa44bb84c1e458081b642562d099cafa), [EURC Yield](https://etherscan.io/tx/0x75478907eb4e77515580093c4cca683a716006b75d08d3e7bd35e80ce6851a7d), [EURe Yield](https://etherscan.io/tx/0xe81b5bc3def2992d3b114acf53d9f94afd90ab73c7d64219146c3265508b6f9d), [USDC Yield RWA](https://etherscan.io/tx/0x53d414d04f6ddc50ac0f35cd18eab7dddc5035fda078972f642fb9fa0df1c187), [wARS Yield](https://etherscan.io/tx/0xe6042dae6129cf1a4de3f79690a14462f53ad1086afa25c98625f2373d9a9e54), [USDC Yield (Arbitrum)](https://arbiscan.io/tx/0x48ddf7c0eb4d4a4a2497b2c63bd598aef3a7e43a08b54407cb23ee920bec7ab1), [USDC Yield (Base)](https://basescan.org/tx/0x929439d363ecf0fb1b387d2f3da846561f40d48a6d36acdbe70914c75b4fdb09), [Main Curation (Gearbox)](https://etherscan.io/tx/0xff09c2b3bc09a6dbbd5007c76699f8757a9273a8e93138fb3138465e70afe556), [Symbiotic Liquid Lane](https://etherscan.io/tx/0x7c26c404c7b52c863e237e4d9717711830186976f9ff5577f936c3246fbeb789), [Upshift lsETH](https://etherscan.io/tx/0x2fc4d688369aa13daa3ef5a57bd03bbb657dc1f7efdd786a0988b693e802992d)
{% endupdate %}

{% update date="2026-09-08" tags="ethereum,symbiotic" %}

## USDC RWA Liquidity

**Change:** Set per-asset acquisition limits and minimum discounts on the Liquid Lane adapter: JTRSY 200,000 USDC at 0.05%, JAAA 150,000 USDC at 0.05%, mGLOBAL 50,000 USDC at 3.00%, mF-ONE 50,000 USDC at 2.50%. deJAAA and deJTRSY stay at a zero limit.

**Rationale:** First enablement of the RWA leg. Discounts clear 10% APY over normal settlement and preserve capital at the worst case; limits follow the risk tier. The wrappers await issuer whitelisting.

**Tx:** [setLimit + setMinDiscount](https://app.safe.global/transactions/tx?safe=eth:0x73af5fcdF830035401e00c322D657982b4a71288\&id=multisig_0x73af5fcdF830035401e00c322D657982b4a71288_0xe07b678ad0fcbef533ed554dcad43ca5076c565b58a36706f7a36b737f5ab74f)
{% endupdate %}

{% update date="2026-09-03" tags="ethereum,morpho" %}

## USDC Prime

**Change:** Hard shutdown of the weETH/USDC market; sweep stranded remnant

**Rationale:** Low borrow demand; market shut down, and remaining funds swept.

**Tx:** [Shutdown tx](https://etherscan.io/tx/0x3f7a618ac40b57f637f0a69758a2c7a889d5101266e87df17fab036375fea146)
{% endupdate %}

{% update date="2026-09-02" tags="ethereum,symbiotic" %}

## USDC RWA Liquidity

**Change:** Reordered the adapter queue to USDC Prime, USDC Yield, USDC Yield RWA, by swapping USDC Prime and USDC Yield RWA.

**Rationale:** Puts the most liquid leg first, so smaller Liquid Lane fills draw on USDC Prime before the other two.

**Tx:** [swapAdapters](https://etherscan.io/tx/0xf87d1d39ac33090afb13c26fcd1025d08dd0fb126f2c622a2d91badea0fd766c)
{% endupdate %}

{% update date="2026-09-01" tags="ethereum,symbiotic" %}

## USDC RWA Liquidity

**Change:** First RWA acquisition: bought 89,736.14 JTRSY for 100,019.36 USDC, redeemed with Centrifuge on 2 Sep for 100,029.49 USDC. Ahead of the fill, JTRSY's acquisition limit was set to 101,000 USDC and its minimum discount to 0.01%.

**Rationale:** First end-to-end run of the RWA leg, from fill through redemption account to issuer settlement.

**Tx:** [fill](https://etherscan.io/tx/0x576728e560826f4181de69bdf91a81326444a91579d180c4b3f467536c9b0d2f), [redeem](https://etherscan.io/tx/0x884fc099142ac3cc61c484a8ee598241e4fad632a09fb61f2e4fff3659aa0447), [JTRSY limit](https://etherscan.io/tx/0x7e6f99db923b0f29d039608f1d8421a3efb50377fe80cd6657ca88444c773996), [minimum discount](https://etherscan.io/tx/0x33fc631c195dad2d9acebe57cd229e82797726bd6f887f719fded0f172bf0b15)
{% endupdate %}

{% update date="2026-08-31" tags="base,morpho" %}

## USDC Yield

**Change:** Deployed Roles Modifier and granted rebalance, monitoring, and shutdown agent roles on Curator Safe

**Rationale:** Enables 24/7 automation (rebalancing, monitoring, and exit agents) on the newly deployed Base vault.

**Tx:** [Add Roles Modifier](https://app.safe.global/transactions/tx?safe=base:0x58C83Ce55164537F9C73d5B65C426691bC26Ffa0\&id=multisig_0x58C83Ce55164537F9C73d5B65C426691bC26Ffa0_0xd6c86e4c2bd5a10de24812777f075a2ba1a3075dd4625d24823699abb5a5e6aa), [Add agent roles](https://app.safe.global/transactions/tx?safe=base:0x58C83Ce55164537F9C73d5B65C426691bC26Ffa0\&id=multisig_0x58C83Ce55164537F9C73d5B65C426691bC26Ffa0_0x45859b94155d979f44a9e1569c73e44558a2cbd7676c9ca0d00296814aaa1fb9)
{% endupdate %}

{% update date="2026-08-31" tags="ethereum,symbiotic" %}

## USDC RWA Liquidity

**Change:** Liquid Lane adapter's relative cap increased from 5% to 20% through an intermediate 10% step

**Rationale:** Gives the RWA leg headroom to carry outstanding redemption claims as acquisitions come online, ahead of any assets actually being enabled.

**Tx:** [setLimits 5->10%](https://etherscan.io/tx/0x9d2aabe3a9ce6109babd73b045e0927460e2693e857b94e5c98fd217f1c23740), [setLimits 10->20%](https://etherscan.io/tx/0x8c1deddaee04d63ff871970da9fcdd58175308cd71317cd78ea6f6c434b13f7d)
{% endupdate %}

{% update date="2026-08-27" tags="base,morpho" %}

## USDC Yield

**Change:** Deployed KPK USDC Yield vault on Morpho v2 on Base; enabled savUSD/USDC, cbBTC/USDC, mGLO/USDC markets; set timelocks; transferred ownership to Security Council Safe

**Rationale:** Inaugural KPK-curated Morpho vault on Base.

**Tx:** [Vault](https://app.morpho.org/base/vault/0x392B3CCf36C8adB8094F45573346B457414cD752/kpk-usdc-yield)
{% endupdate %}

{% update date="2026-08-26" tags="ethereum,symbiotic" %}

## USDC RWA Liquidity

**Change:** Added fourth adapter routing to KPK USDC Yield RWA (0xE075f215684CE141c426301991535817B8e6C373), reordered deposit queue, reset caps to USDC Prime 10%, USDC Yield 40%, USDC Yield RWA 50%. Also registered an Euler adapter for the HINC/USDC market (eUSDC-137, `0xE75894cB7FfB89E0e8A621D44930C350Ef5D5753`) without a cap, removed on 11 September

**Rationale:** Adds a third Morpho lending leg ahead of Liquid Lane in the queue, redistributing the overflow that previously went entirely to USDC Yield.

**Tx:** [Tx](https://etherscan.io/tx/0x41d9f1b6ae2d5beda648e28ef73722c17ff9b585c8b22a0a2e9d88dbd635cc29), [Add Euler adapter](https://etherscan.io/tx/0x38291411fd265e5853afca6fec2e39e4469c5a8374a0dbeb9eefb6d07b6118d4)
{% endupdate %}

{% update date="2026-08-24" tags="ethereum,morpho" %}

## wARS Yield

**Change:** Onboarded cbBTC/wARS (Tier A, 90% cap), wstETH/wARS (Tier A, 90% cap) and sUSDS/wARS (Tier B, 60% cap), all at 62.5% LLTV.

**Rationale:** Adds three collateral markets for wARS borrowers.

**Tx:** [Enable markets](https://etherscan.io/tx/0xc248d62068a60368c1e3395e4b2ca44401955232fce60a03aa57e70314e20dd7)
{% endupdate %}

{% update date="2026-08-24" tags="base,morpho" %}

## USDC Yield

**Change:** Deployed savUSD/USDC market (Avant, 91.5% LLTV)

**Rationale:** Market deployed for the new Base vault.

**Tx:** [Create market](https://basescan.org/tx/0x71fa0e760ef555c46dbe4825bf5d77d117b7d97c5d1d5d73b502e19677147153), [Market](https://app.morpho.org/base/variable/0x4217fed63c853537fb818f4ba4416bc708c3d7534913532feb34c3511ce2aba6/savusd-usdc)
{% endupdate %}

{% update date="2026-08-17" tags="ethereum,symbiotic" %}

## USDC RWA Liquidity

**Change:** Removed the Aave v3 adapter.

**Rationale:** Its cap had been at 0% since 11 August, so the leg was no longer in use.

**Tx:** [removeAdapter](https://etherscan.io/tx/0x0b5bbc3b4c56e6c1f6509fe1f789c3ddb5ee1677defb14c61ce43f58d0a7525b)
{% endupdate %}

{% update date="2026-08-17" tags="ethereum,morpho" %}

## USDC Yield RWA

**Change:** Granted rebalancing, monitoring, and shutdown agent EOAs their scoped roles on Curator Safe's Roles Modifier

**Rationale:** Activates KPK's automated curation stack (rebalance, exit, monitoring) for the vault.

**Tx:** [Add roles](https://app.safe.global/transactions/tx?safe=eth:0xFfbCF26270A90FCdAAF56AcF6e235730B04546c4\&id=multisig_0xFfbCF26270A90FCdAAF56AcF6e235730B04546c4_0xfa0bbce85f05c455005b7669e106905d72819488b1a600ecc6b5f38de8a698d0)
{% endupdate %}

{% update date="2026-08-13" tags="ethereum,symbiotic" %}

## USDC RWA Liquidity

**Change:** Raised the Liquid Lane adapter's relative cap from 0% to 5%, and set initial acquisition limits of 1,000 USDC on deJAAA (0.1% minimum discount) and deJTRSY (0.05% minimum discount).

**Rationale:** A low starting cap and small limits for the RWA leg, ahead of the first acquisitions.

**Tx:** [setLimits](https://etherscan.io/tx/0x0959946857c73e71f7ac3cf350607657dc5d0a07823815a399959bf73e1c2429), [Acquisition limits](https://etherscan.io/tx/0x10ea6bbe2a8088cdcb4b22e43143ee9bbe26551a4c2f71de497e67b90a07e2d9), [Raise limits](https://etherscan.io/tx/0x1347504118fe923c8d7cd17df406a5b117d9c2f7d553487e87af7ee640ae18ff)
{% endupdate %}

{% update date="2026-08-11" tags="ethereum,symbiotic" %}

## USDC RWA Liquidity

**Change:** Lowered the Aave v3 adapter's relative cap from 0.01% to 0%.

**Rationale:** Brings the onchain cap in line with the 0% intended for the Aave leg.

**Tx:** [setLimits](https://etherscan.io/tx/0x7b1764e17134ce9d5833f750dfa18c55896ede04f0f5465ef5f8cbaa59c6e88e)
{% endupdate %}

{% update date="2026-08-10" tags="ethereum,morpho" %}

## USDC Yield

**Change:** Offboarded sNUSD/USDC market; re-onboarded siUSD/USDC market (Tier C, 60% cap)

**Rationale:** Precautionary shutdown following a decline in sNUSD/USDC market liquidity; siUSD/USDC re-onboarded at its prior risk profile to backfill capacity.

**Tx:** [sNUSD shutdown](https://etherscan.io/tx/0x30159e9ee4dfc24070e44cda857c4b4f5ebd86922334cdca41f3d494f2d7f631), [siUSD caps](https://app.safe.global/transactions/tx?safe=eth:0x7E43Df1c1c5A2245858b60D4655fDA83704e4171\&id=multisig_0x7E43Df1c1c5A2245858b60D4655fDA83704e4171_0x662cbaa6fa588e85ba8e6dc6537b95eb9015e252ae3300c56fdf71de45f54c23)
{% endupdate %}

{% update date="2026-08-06" tags="ethereum,morpho" %}

## USDC Yield RWA

**Change:** Deployed and enabled Roles Modifier on Curator Safe

**Rationale:** Standard Permissions Layer rollout ahead of agent onboarding.

**Tx:** [Enable Roles Modifier](https://app.safe.global/transactions/tx?safe=eth:0xFfbCF26270A90FCdAAF56AcF6e235730B04546c4\&id=multisig_0xFfbCF26270A90FCdAAF56AcF6e235730B04546c4_0xd09da3a9cf0327caedec35b383df71199cbfe6e8c8923bada85930359eeaeeea)
{% endupdate %}

{% update date="2026-08-04" tags="ethereum,morpho" %}

## USDC Prime

**Change:** Onboarded the LsETH/USDC market (Tier B, 10% cap, 86% LLTV).

**Rationale:** Adds Liquid Collective LsETH as a Tier B collateral to USDC Prime.

**Tx:** [Enable market](https://etherscan.io/tx/0x62e2257bdfc1ccf730fe1ab46d588e2ed41e7f62267937533859d2eef57b973c)
{% endupdate %}

{% update date="2026-08-04" tags="ethereum,morpho" %}

## ETH Yield

**Change:** Offboarded Origin ARM markets (stETH and eETH); set allocation caps to 0

**Rationale:** Origin is deprecating the ARM vaults, alongside declining borrow demand on both markets.

**Tx:** [ARM stETH setCaps](https://etherscan.io/tx/0xa3b5e38d53ec6d2d0e499a8aa59b262cb3b580ca437ff4f46d697e83efc91781), [ARM eETH setCaps](https://etherscan.io/tx/0x342ef27675fa2c7260b59223d8f2f934c1c7ac0314ad44d9a0ad9fecbd9c9f3f)
{% endupdate %}

{% update date="2026-08-04" tags="ethereum,symbiotic" %}

## USDC RWA Liquidity

**Change:** Revoked deploying EOA's DEFAULT\_ADMIN\_ROLE and transferred vault ownership to the Security Council

**Rationale:** Removes the last single-EOA authority; both had to move.

**Tx:** [transferOwnership](https://etherscan.io/tx/0x51a3a1953c532b516baa06acbd88a0af11681d45d8bbc2ca84e0d503ac2682db), [revokeRole (vault)](https://etherscan.io/tx/0x196d621671c7015868e55edfd7ee4441b3a1d35880139f534c2ef332eda03c98), [revokeRole (delegator)](https://etherscan.io/tx/0xe9bb435b43adb565ac221c22f21ebf4edb4f67dea3e99104fb3a56f9e775b26f)
{% endupdate %}

{% update date="2026-07-31" tags="ethereum,morpho" %}

## wARS Yield

**Change:** Enabled Roles Modifier on Curator Safe and granted rebalancing, monitoring, shutdown agent EOAs their scoped permissions

**Rationale:** Activates KPK's automated curation stack (rebalance, exit, monitoring) for the vault, following the standard Permissions Layer rollout.

**Tx:** [Enable Roles Modifier](https://app.safe.global/transactions/tx?safe=eth:0xEaCFA855d172F1B40D9b04245960b2c12266C5f5\&id=multisig_0xEaCFA855d172F1B40D9b04245960b2c12266C5f5_0xd9b28abd88306217dc80274b3491ed082815e87fd6328bba3a3edf203e3828b0)
{% endupdate %}

{% update date="2026-07-30" tags="ethereum,symbiotic" %}

## USDC RWA Liquidity

**Change:** Enabled the Roles Modifier `0x7349EC4650f6DE0748342efb77Bab80d5558028F` on the Curator Safe.

**Rationale:** Sets up the Permissions Layer for KPK's rebalancing agent.

**Tx:** [Enable Roles Modifier](https://etherscan.io/tx/0xc86660de25b78005644814e3cc0771bbb37a0b1d88c311c3d415b9e40b82b9fd)
{% endupdate %}

{% update date="2026-07-30" tags="ethereum,symbiotic" %}

## USDC RWA Liquidity

**Change:** Whitelisted seven tokenised RWAs (JAAA, JTRSY, deJAAA, deJTRSY, mF-ONE, mGLOBAL, HYBOND) with acquisition limits and minimum discounts at 0

**Rationale:** Whitelisting ahead of enablement, so limits can be set per asset as each Risk Framework assessment closes. At 0 limits, no RWA can be acquired.

**Tx:** [Tx](https://etherscan.io/tx/0x1f15c266a89fcd4bd7d2147a61a9f6c1683f8efc7f79f6f6ecf192f214b8a135)
{% endupdate %}

{% update date="2026-07-29" tags="ethereum,morpho" %}

## wARS Yield

**Change:** Set function-specific timelocks: 3 days for add adapter/increase caps; 7 days for increase timelock/remove adapter

**Rationale:** Aligns the vault with KPK's standard timelock schedule.

**Tx:** [Safe batch](https://app.safe.global/transactions/tx?safe=eth:0xEaCFA855d172F1B40D9b04245960b2c12266C5f5\&id=multisig_0xEaCFA855d172F1B40D9b04245960b2c12266C5f5_0x06364ecdb6f29e89d9900d59578272fb8f1dbf2ab3f49783ea0a35c841c22120)
{% endupdate %}

{% update date="2026-07-29" tags="ethereum,symbiotic" %}

## USDC RWA Liquidity

**Change:** Set adapter relative caps (USDC Yield 90%, USDC Prime 10%, Aave v3 0.01%, Liquid Lane 0%) and deposit queue order

**Rationale:** Caps plus queue order set the allocation: the most liquid venue fills first, overflow goes to the higher-yielding vault, and caps are maxima rather than targets.

**Tx:** [setLimits](https://etherscan.io/tx/0x548f1b0f5ee2500d9bb6ad1d925a50aa1bfe1a45e96bbbed513b622ec79c8932), [set deposit queue](https://etherscan.io/tx/0xcdd10968691b15617c1211276264a61c71cf4099f6def75454a4dad7a36f38e8), [first auto-allocate](https://etherscan.io/tx/0x16ebc38707839a0602663973b88dbb36e18cdcc548c13b584ebf17c52639b84e)
{% endupdate %}

{% update date="2026-07-28" tags="ethereum,morpho" %}

## wARS Yield

**Change:** Set maxRate to 31,709,791,983 (100% APR cap); forceDeallocate penalty to 0.01%; raised Curator Safe threshold 1/5 to 2/5; initiated Owner transfer to Security Council Safe

**Rationale:** Aligns the vault with KPK's standard configuration.

**Tx:** n/a (no on-chain transaction linked on source page)
{% endupdate %}

{% update date="2026-07-28" tags="ethereum,morpho" %}

## wARS Yield

**Change:** Enabled WBTC/wARS market (Tier C, 62.5% LLTV)

**Rationale:** Completes the four planned collateral markets once the WBTC/wARS oracle composition was finalised.

**Tx:** [Market](https://app.morpho.org/ethereum/variable/0x7730ef5352412a37d441ab960f5c8f33ea8d1c9f01c34fc2340f65fa1c59ce69/wbtc-wars)
{% endupdate %}

{% update date="2026-07-28" tags="ethereum,morpho" %}

## wARS Yield

**Change:** Enabled WETH/wARS, USDC/wARS, USDT/wARS markets (Tier C)

**Rationale:** First markets live for the vault, deployed ahead of the WBTC/wARS oracle.

**Tx:** [WETH/wARS](https://app.morpho.org/ethereum/variable/0x9666c9b61cc58e3187c6e344697ed31fe558dc01809740417beee07dc77d96b3/weth-wars), [USDC/wARS](https://app.morpho.org/ethereum/variable/0x7bb3c45550c02e7e00ddbf2f847717dc4485df4bbae3b992178a74f0d45c4e87/usdc-wars), [USDT/wARS](https://app.morpho.org/ethereum/variable/0x8e6271e428ee5b41c98a3765919e359f16b0c6261c85d84f9b9b8b5242ab9c9e/usdt-wars)
{% endupdate %}

{% update date="2026-07-28" tags="ethereum,morpho" %}

## wARS Yield

**Change:** Deployed KPK wARS Yield vault on Morpho v2, seeded dead deposit, deployed Curator Safe (initially 1/5)

**Rationale:** Inaugural KPK-curated wFIAT lending vault, using wARS (Ripio's Wrapped Argentine Peso) as the loan asset.

**Tx:** [Vault contract](https://etherscan.io/address/0xb5ce3CA2C774b72955C25875022FdD91f7a7B938)
{% endupdate %}

{% update date="2026-07-28" tags="ethereum,morpho" %}

## USDC Yield RWA

**Change:** Transferred ownership to Security Council Safe; granted Allocator role to KPK Deployer 1

**Rationale:** Aligns the vault with KPK's standard role configuration.

**Tx:** [setOwner](https://app.safe.global/transactions/tx?safe=eth:0xFfbCF26270A90FCdAAF56AcF6e235730B04546c4\&id=multisig_0xFfbCF26270A90FCdAAF56AcF6e235730B04546c4_0x16cbb4d4cf24955b050336915f21a9b45e8a10a6ea73fef44592d78c81a97e98)
{% endupdate %}

{% update date="2026-07-27" tags="ethereum,morpho" %}

## USDC Yield RWA

**Change:** Deployed KPK USDC Yield RWA vault on Morpho v2 with Markets V1 Adapter; enabled wFalconX/USDC, AA\_FalconXUSDC/USDC, USD3/USDC, PT-USD3-17DEC2026/USDC, mF-ONE/USDC markets; set timelocks, 100% APR maxRate, 0.01% forceDeallocate penalty

**Rationale:** Inaugural KPK vault lending against tokenised real-world-asset collateral.

**Tx:** [Vault contract](https://etherscan.io/address/0x7a72bcD2c3F7F7e4D6679170a0625bAB15D7DDa1)
{% endupdate %}

{% update date="2026-07-27" tags="ethereum,symbiotic" %}

## USDC RWA Liquidity

**Change:** Assigned Security Council as admin; Curator Safe (0x73af5fcdF830035401e00c322D657982b4a71288, 2-of-5) for operational parameters; scoped symbiotic\_rebalancer\_agent

**Rationale:** Splits authority by response speed: adapter additions, roles, and fees sit with the Security Council; operational parameters sit with the 2/5 Curator Safe.

**Tx:** [Live roles view](https://curator.symbiotic.fi)
{% endupdate %}

{% update date="2026-07-27" tags="ethereum,symbiotic" %}

## USDC RWA Liquidity

**Change:** Deployed vault (0x8BcD746976885b5832bAD07B4921E3f2dD1D3703) with four adapters and fees set at 10% performance, 0% management

**Rationale:** First KPK-curated Symbiotic vault, providing standby liquidity for tokenised RWA holders exiting before issuer settlement. Onchain name and symbol are immutable; the product name is carried in Symbiotic's vault metadata.

**Tx:** [Tx](https://etherscan.io/tx/0x9476481a04a41a7fe8f87c77659a1d70990a8ebd807aaf5411b0cccfc51119e1)
{% endupdate %}

{% update date="2026-07-17" tags="ethereum,morpho" %}

## Morpho V2 vaults

**Change:** Set forceDeallocate penalty to 0.01% across all V2 vaults

**Rationale:** Raised the default penalty on every V2 vault in response to a forceDeallocate griefing pattern.

**Tx:** [Safe tx](https://app.safe.global/transactions/tx?safe=eth:0xf8182E5827c06a47A985eC565A3bcd56437a97bE\&id=multisig_0xf8182E5827c06a47A985eC565A3bcd56437a97bE_0xcb40d529540c2875e06962864902e069b128df70417b83fd809be3349552aace)
{% endupdate %}

{% update date="2026-07-16" tags="ethereum,morpho" %}

## USDC Yield

**Change:** Removed stcUSD/USDC market; set caps to 0 and deallocated

**Rationale:** Market liquidity dwindled; vault exposure had already been reduced to dust as a precaution.

**Tx:** [setCaps + deallocate](https://etherscan.io/tx/0xfd31cc2ff369c78e4d7dfc16a2a2adcd1b76386afbae212dea83ec31124e09dd)
{% endupdate %}

{% update date="2026-07-08" tags="ethereum,morpho" %}

## ETH Prime

**Change:** Removed WBTC/WETH market; set allocation cap to 0

**Rationale:** Borrow demand shrank below what we consider a usable level.

**Tx:** [setCaps](https://etherscan.io/tx/0x7c3f84d4cc89e0cfb3dc7ee5e6995af274383385f6dbe95e16de2ddeb81a75dc)
{% endupdate %}

{% update date="2026-07-08" tags="ethereum,morpho" %}

## ETH Yield

**Change:** Removed ETH+/WETH market; set allocation cap to 0

**Rationale:** Borrow demand shrank below what we consider a usable level.

**Tx:** [setCaps](https://etherscan.io/tx/0x848b3fec9f27b1184ffa25ae3064483296cba3d6595320c83fa8aaa2f695547d)
{% endupdate %}

{% update date="2026-07-03" tags="ethereum,morpho" %}

## USDC Yield

**Change:** Onboarded ftUSD/USDC market

**Rationale:** Add Flying Tulip ftUSD as a new collateral market.

**Tx:** [setCaps](https://app.safe.global/transactions/tx?safe=eth:0x7E43Df1c1c5A2245858b60D4655fDA83704e4171\&id=multisig_0x7E43Df1c1c5A2245858b60D4655fDA83704e4171_0x176baba9085890e889432ee1316d31cdf8455ae86f0859c800eff04f8ac2001d)
{% endupdate %}

{% update date="2026-07-02" tags="ethereum,morpho" %}

## USDC Yield

**Change:** Removed DUSD/USDC market; set allocation cap to 0

**Rationale:** Limited borrow demand in the market did not justify continued allocation.

**Tx:** [setCaps](https://etherscan.io/tx/0x131db224febb7b25ce50f1fde8074cd82bed2ba91eddf6f1cce65b92eb8aa597)
{% endupdate %}

{% update date="2026-06-23" tags="ethereum,morpho" %}

## USDC Prime, USDC Prime Core, USDC Yield, EURC Yield, ETH Yield

**Change:** Reset forceDeallocatePenalty to 0%

**Rationale:** Restored the force deallocate fee to 0%.

**Tx:**
{% endupdate %}

{% update date="2026-06-22" tags="ethereum,morpho" %}

## USDC Yield

**Change:** Onboarded the weETH/USDC market

**Rationale:** Add ether.fi weETH as an overflow collateral market to absorb large incoming deposits. Queued through 3-day timelock.

**Tx:** [weETH collateral cap](https://etherscan.io/tx/0xbb693d29b9ea956f00cc0078ec96ff01a4d29446350bd87f4508625428011bc4), [weETH/USDC market cap](https://etherscan.io/tx/0x809215894a3702ed5d5346ca4af6dfcbe0cd73c46808023f6d89925c7ab1a987)
{% endupdate %}

{% update date="2026-06-21" tags="ethereum,morpho" %}

## Multiple vaults

**Change:** Set forceDeallocatePenalty to 0.5% (50 bps)

**Rationale:** Raised the deallocate fee precautionarily following a griefing attack.

**Tx:**
{% endupdate %}

{% update date="2026-06-15" tags="ethereum,gearbox" %}

## WETH and wstETH pools

**Change:** Remove an address from the Emergency Liquidator set

**Rationale:** Routine access-control hygiene; rotated out an Emergency Liquidator key that is no longer in use.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreiaqaivo73mxiknycp6ci2v4tuvfgqzpd4mhwemfluvgmlxqsa25tq)
{% endupdate %}

{% update date="2026-06-15" tags="ethereum,gearbox" %}

## wstETH pool

**Change:** Onboard savETH (Avant) as collateral; IRM update

**Rationale:** Added savETH with a 3,000 wstETH quota limit and 91.5% LT via a dedicated Credit Manager. Pool IRM adjusted to a higher baseline yield.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreidazqwpin6i452fynoon3bpietkxmqdinpt2sa6q4htsupconn2lu)
{% endupdate %}

{% update date="2026-06-04" tags="ethereum,morpho" %}

## USDC Yield

**Change:** Offboarded PT-sNUSD-4JUN2026

**Rationale:** PT-sNUSD-4JUN2026 reached maturity; permanently offboarded post-expiry.

**Tx:** [Shutdown](https://etherscan.io/tx/0x41a6c1a6104644587bbe39f61f6e2df6ce27b407704258bb64b3d105d8f1c83b)
{% endupdate %}

{% update date="2026-06-04" tags="ethereum,morpho" %}

## USDC Yield

**Change:** Offboarded the siUSD/USDC market

**Rationale:** Precautionary shutdown following a decline in siUSD/USDC market liquidity.

**Tx:** [Shutdown](https://etherscan.io/tx/0x34c05c01c94987f90c87ae7adf4fd571f33d65e1c139b874b9abad3c4228493f)
{% endupdate %}

{% update date="2026-05-28" tags="ethereum,morpho" %}

## USDC Prime

**Change:** Onboarded the weETH/USDC market

**Rationale:** New market with healthy liquidity and borrow demand. Adds ether.fi weETH as a Tier B collateral yield source to USDC Prime.

**Tx:** [submitCap](https://app.safe.global/transactions/tx?safe=eth:0xf8182E5827c06a47A985eC565A3bcd56437a97bE\&id=multisig_0xf8182E5827c06a47A985eC565A3bcd56437a97bE_0x6113f73ca41b5d1cf431cf03c483207a712b4a34cc3a26fbe337dea22746bd82)
{% endupdate %}

{% update date="2026-05-28" tags="ethereum,arbitrum,morpho" %}

## ETH Prime, USDC Yield, USDC Yield

**Change:** Offboarded underused markets across three vaults

**Rationale:** Continuing derisking of underused markets, with various reasons per market.

**Tx:**
{% endupdate %}

{% update date="2026-05-27" tags="ethereum,gearbox" %}

## WETH pool

**Change:** rETH oracle swap (Main to Reserve) and IRM update

**Rationale:** Setting Fundamental pricing as the main feed to avoid unwanted liquidations, and increasing the max ETH borrow rate.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreicdpz3pjzrwfji3dbdodpvwqnsnd2kwuld3cwlchzvq3s6may67za)
{% endupdate %}

{% update date="2026-05-22" tags="ethereum,morpho" %}

## USDC Yield

**Change:** Offboarded PT-savUSD-14MAY2026/USDC; expanded spot savUSD cap (60% to 85%)

**Rationale:** PT-savUSD expired, permanently offboarded. With PT-savUSD gone, expanding the spot savUSD cap to capture Avant's underlying USD yield.

**Tx:** [decreaseCaps](https://etherscan.io/tx/0x070aa36f45ebb42bc57f8fcaa43b68ec9b7a9c0698736682b1ebf61504bfc6b7)
{% endupdate %}

{% update date="2026-05-18" tags="ethereum,gearbox" %}

## Emergency Admin

**Change:** Added a new signer to the Emergency Admin Safe

**Rationale:** New signer onboarded with threshold 2/5.

**Tx:** [Tx](https://app.safe.global/transactions/tx?safe=eth:0x4Ad2419dc6DE75C3f57d7b6Aa200D494c74c1443\&id=multisig_0x4Ad2419dc6DE75C3f57d7b6Aa200D494c74c1443_0x659386458b07b77f9ae4aab3beecd20a6cf57eb36e1368d2b4cf2fa5885c800d)
{% endupdate %}

{% update date="2026-05-14" tags="ethereum,morpho" %}

## ETH Prime, USDC Yield

**Change:** Onboarded rETH/WETH (ETH Prime) and DUSD/USDC (USDC Yield)

**Rationale:** Cover borrow demand from existing counterparties. Queued through 3-day timelock.

**Tx:** [ETH Prime setCaps](https://app.safe.global/transactions/tx?safe=eth:0xC266b1181A80E84EDC2c6596718E88E8115c1eaa\&id=multisig_0xC266b1181A80E84EDC2c6596718E88E8115c1eaa_0x89d0774402522d5425801411fc8976c9e94b9e35b537ac7a514e22d63e203a2b), [USDC Yield setCaps](https://app.safe.global/transactions/tx?safe=eth:0x7E43Df1c1c5A2245858b60D4655fDA83704e4171\&id=multisig_0x7E43Df1c1c5A2245858b60D4655fDA83704e4171_0x702423e0c10ff8a7c6d6adbd1743abdb525a86572538380a8f12a63bc1cf0995)
{% endupdate %}

{% update date="2026-05-13" tags="ethereum,morpho" %}

## Morpho V2 vaults

**Change:** Onboarded new operator 0xAF150C6D108D0C2c96BbFc3CeF9B4848B5d99440 as Safe owner and allocator; granted MORPHO\_SHUTDOWN\_AGENT permission

**Rationale:** Add a new operator across all curator Safes (thresholds unchanged) with allocator role on each V2 vault.

**Tx:**
{% endupdate %}

{% update date="2026-05-12" tags="ethereum,arbitrum,morpho" %}

## Morpho V2 vaults

**Change:** Rebranded vaults: prepended KPK to name and symbol

**Rationale:** Align on-chain vault identities with the KPK brand.

**Tx:** [Ethereum tx](https://app.safe.global/transactions/tx?safe=eth:0x354C92aF243d53A24feb3dFF20372Af7b7c47478\&id=multisig_0x354C92aF243d53A24feb3dFF20372Af7b7c47478_0x2773d8acecfa0c9c894463050368cd9662155d7774b7d07ac94d8dc2c8814f0a), [Arbitrum tx](https://app.safe.global/transactions/tx?safe=arb1:0x354C92aF243d53A24feb3dFF20372Af7b7c47478\&id=multisig_0x354C92aF243d53A24feb3dFF20372Af7b7c47478_0xc5cd68f1d23a4588f4b12ae0e310ae0889ac16ff865b1edfea3bc8035816fbe1)
{% endupdate %}

{% update date="2026-05-11" tags="ethereum,morpho" %}

## USDC Prime

**Change:** Onboarded WETH/USDC market

**Rationale:** Add WETH as a collateral option, an additional yield source with deep liquidity.

**Tx:** [submitCap](https://app.safe.global/transactions/tx?safe=eth:0xf8182E5827c06a47A985eC565A3bcd56437a97bE\&id=multisig_0xf8182E5827c06a47A985eC565A3bcd56437a97bE_0x4ea58e301e78da4c59de296b17fa2a6ae2fe9e11478406c0ded68dc5a90b7cd6)
{% endupdate %}

{% update date="2026-05-09" tags="ethereum,gearbox" %}

## wstETH pool

**Change:** Re-enable three Beefy LP strategies; onboard tETH

**Rationale:** Reopening LST/LRT looping plays following Aave WETH liquidity restoration, with quotas below pre-Kelp-exploit levels.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreicjpyp4ge35qgw33nkxxpe7momxskcm4rf3jryw5nanskksvyxdoq)
{% endupdate %}

{% update date="2026-05-09" tags="ethereum,gearbox" %}

## WETH pool

**Change:** Broad single-asset and LP quota cuts (\~80-85%); offboard DETH and Beefy tETH/wstETH

**Rationale:** Concentrate vanilla assets in the WETH market and push LST/LRT leverage to the wstETH market.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreicjpyp4ge35qgw33nkxxpe7momxskcm4rf3jryw5nanskksvyxdoq)
{% endupdate %}

{% update date="2026-05-09" tags="ethereum,gearbox" %}

## WETH pool

**Change:** Set ezETH quota to 0 (full offboard)

**Rationale:** Removed due to low utilisation; minor borrow activity remains in risk-tier snapshot.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreicjpyp4ge35qgw33nkxxpe7momxskcm4rf3jryw5nanskksvyxdoq)
{% endupdate %}

{% update date="2026-05-08" tags="ethereum,gearbox" %}

## WETH and wstETH pools

**Change:** Switch ETH+ main price feed to the Fundamental oracle

**Rationale:** Deprecated RedStone ETH+ market feed; replaced with the Fundamental oracle on both markets.

**Tx:** [WETH tx](https://etherscan.io/tx/0x677a394ef4b4a8bb271c3adeebff5dc3998210ec97d34ff7c7bda69c7b57c894), [wstETH tx](https://etherscan.io/tx/0x26539c998c05bbd84795cc5f2568ce8051beaca385da6ac5c906b5a1a3a065a9)
{% endupdate %}

{% update date="2026-05-08" tags="ethereum,arbitrum,morpho" %}

## USDC Prime, USDC Yield, USDC Yield

**Change:** Offboarded underused markets across three vaults

**Rationale:** Remove markets below 1% of vault interest over 90 days.

**Tx:**
{% endupdate %}

{% update date="2026-05-06" tags="ethereum,morpho" %}

## USDC Yield V2, ETH Yield V2

**Change:** Raised absolute and relative caps on Markets V1 Adapter route for selected V1 markets

**Rationale:** Raise V1 Adapter caps to allow V2 vaults to absorb V1 vault allocations during migration.

**Tx:** [USDC Yield V2](https://etherscan.io/tx/0x381dc1e6df6294e3b8f51f774acf1e2ddb41a3ff9626a3edfa81b2ecec727b5e), [ETH Yield V2](https://etherscan.io/tx/0xbc8013570baddbd3d3c161610af54b543c8fa09dd36f0b0989743d1a50bbd7d2)
{% endupdate %}

{% update date="2026-05-04" tags="ethereum,morpho" %}

## USDC Prime, USDT Prime, ETH Prime, USDC Yield, EURC Yield, ETH Yield, USDC Yield

**Change:** Switched curated allocation path on V2 vaults from Vault V1 Adapter to Markets V1 Adapter; removed legacy Vault V1 Adapter

**Rationale:** V2 vaults now allocate directly into V1 markets via the Markets V1 Adapter; legacy MetaMorpho vault layer removed from the curated path.

**Tx:**
{% endupdate %}

{% update date="2026-04-28" tags="ethereum,morpho" %}

## USDC Prime Core

**Change:** Launched new USDC Prime Core vault on Morpho V2 and completed operational setup

**Rationale:** New USDC vault with a tighter risk profile than Prime.

**Tx:** [Curator Safe](https://app.safe.global/transactions/history?safe=eth:0x7a2CE012Fe37dB488C24e31f5e38816cAd82b45E)
{% endupdate %}

{% update date="2026-04-28" tags="ethereum,morpho" %}

## All V2 vaults

**Change:** Granted MORPHO\_REBALANCER\_AGENT and MORPHO\_SHUTDOWN\_AGENT roles, allocate and deallocate permissions

**Rationale:** Activate V2 allocation bots. Rebalancer gets routing only; shutdown agent additionally gets unilateral cap-lowering to drain at-risk markets.

**Tx:**
{% endupdate %}

{% update date="2026-04-22" tags="ethereum,morpho" %}

## Multiple vaults

**Change:** Re-enabled markets unaffected by rsETH/Aave contagion; offboarded smaller wstETH and cbBTC markets

**Rationale:** Lift precautionary shutdowns for markets unaffected by Aave/rsETH contagion.

**Tx:**
{% endupdate %}

{% update date="2026-04-21" tags="ethereum,gearbox" %}

## WETH pool

**Change:** Flatten IRM on ETH pool

**Rationale:** Reducing strain on borrowers following the rsETH exploit market contagion.

**Tx:** [Tx](https://etherscan.io/tx/0xddd0454e89e6ac581e953e3b7e0d0ac4ac6d82130c7dd04e46d689508d6539d8)
{% endupdate %}

{% update date="2026-04-20" tags="ethereum,morpho" %}

## ETH Prime

**Change:** Exited rsETH and savETH exposure; re-enabled deposits

**Rationale:** Fully offboard rsETH following the Kelp DAO exploit, remove savETH to eliminate indirect Aave exposure, and re-enable deposits on the vault.

**Tx:**
{% endupdate %}

{% update date="2026-04-18" tags="ethereum,gearbox" %}

## WETH and wstETH pools

**Change:** Emergency responses to the Kelp DAO rsETH exploit

**Rationale:** Removed collaterals with potential direct or indirect exposure to affected protocols.

**Tx:**
{% endupdate %}

{% update date="2026-04-18" tags="ethereum,morpho" %}

## USDC Prime

**Change:** Precautionary response to Kelp DAO exploit: paused USDC Prime deposits (re-enabled same day); triggered tETH exit and re-queued rsETH exit

**Rationale:** Precautionary measure in response to the Kelp DAO rsETH exploit.

**Tx:** n/a (no on-chain transaction linked on source page)
{% endupdate %}

{% update date="2026-04-13" tags="ethereum,morpho" %}

## ETH Yield

**Change:** Removed the lsETH market

**Rationale:** Market liquidity didn't improve.

**Tx:** [submitCap](https://app.safe.global/transactions/tx?safe=eth:0xE5aEC7D0E795456F90ceBefBa56470F0E5dfC075\&id=module_0xE5aEC7D0E795456F90ceBefBa56470F0E5dfC075_i38ef9238a6415168c8d9d601480910c14f3b80ec9069e0a4b7e7bcb549419d5f0,1,0)
{% endupdate %}

{% update date="2026-04-13" tags="ethereum,morpho" %}

## Morpho V2 vaults

**Change:** Deployed Market V1 adapter to all V2 vaults

**Rationale:** Prepare V2 migration: adapters enable direct V1 market supply through V2 vaults.

**Tx:**
{% endupdate %}

{% update date="2026-04-13" tags="ethereum,morpho" %}

## Morpho V2 vaults

**Change:** Set forceDeallocate timelock to 0 days

**Rationale:** Preparation for 0% deallocation penalty and V1 Markets Adapter migration. Replicates V1 withdrawal queue behaviour.

**Tx:**
{% endupdate %}

{% update date="2026-04-10" tags="ethereum,morpho" %}

## ETH Yield

**Change:** Added tETH market

**Rationale:** KPK-deployed tETH market, an additional yield source.

**Tx:** [submitCap](https://app.safe.global/transactions/tx?safe=eth:0xE5aEC7D0E795456F90ceBefBa56470F0E5dfC075\&id=multisig_0xE5aEC7D0E795456F90ceBefBa56470F0E5dfC075_0xef0498f3f1ce8489f5658cdb43dd8a62d2a7ba7d25d1b778d105635a8b627bb6)
{% endupdate %}

{% update date="2026-04-07" tags="ethereum,morpho" %}

## USDC Prime

**Change:** Removed WBTC market

**Rationale:** Liquidity migrated, market unused.

**Tx:** [Shutdown](https://etherscan.io/tx/0xa03b4e7da8262f440a3261a5f44ed88041f7ccc5d0a7647177df715f58416a7d)
{% endupdate %}

{% update date="2026-03-30" tags="ethereum,morpho" %}

## USDC Prime

**Change:** Added newly deployed rETH market (86% LTV)

**Rationale:** KPK deployed the rETH/USDC market.

**Tx:** [submitCap](https://app.safe.global/transactions/tx?safe=eth:0xf8182E5827c06a47A985eC565A3bcd56437a97bE\&id=multisig_0xf8182E5827c06a47A985eC565A3bcd56437a97bE_0x00ee6604a81e0eb12b1e58fdc27cbc67a09e6fa7ccf342e39f179c872bc04bb0)
{% endupdate %}

{% update date="2026-03-30" tags="ethereum,morpho" %}

## USDC Yield

**Change:** Rolled over Pendle PTs: added PT-USDG-28MAY2026; removed expired PT-srUSDe and PT-jrUSDe

**Rationale:** Remove expired PTs; add PT-USDG-28MAY2026.

**Tx:**
{% endupdate %}

{% update date="2026-03-26" tags="ethereum,morpho" %}

## USDC Yield

**Change:** Removed deprecated Pendle PT

**Rationale:** PT-siUSD-26MAR2026 removal.

**Tx:**
{% endupdate %}

{% update date="2026-03-23" tags="ethereum,morpho" %}

## USDC Yield

**Change:** Reallocated RLP dust, updated withdraw queue, and re-enabled the supply

**Rationale:** Supply queue management ahead of RLP market withdrawal.

**Tx:**
{% endupdate %}

{% update date="2026-03-22" tags="arbitrum,morpho" %}

## USDC Yield

**Change:** Removed the syrupUSDC market

**Rationale:** Avoid potential second-order exposure to Resolv.

**Tx:** [Shutdown](https://arbiscan.io/tx/0xd4edfff189bad4b54ccec2af7eacdc5b725b91dabe3e071bbba44535a6320de1)
{% endupdate %}

{% update date="2026-03-22" tags="ethereum,arbitrum,morpho" %}

## USDC Yield

**Change:** Set allocation cap to zero on RLP markets following USR exploit

**Rationale:** RLP/USDC capped at zero.

**Tx:** [Ethereum setCap](https://etherscan.io/tx/0x6422ac5ea6bc3aefdfaa59f6fbe3379024d6fc8f411190576a48e27c2323e9d0), [Arbitrum setCap](https://arbiscan.io/tx/0xc1c766907a4db71e2eec20934ee160b315e24b3c984e26f3d377d279f0d03158)
{% endupdate %}

{% update date="2026-03-19" tags="ethereum,morpho" %}

## USDC Yield & Prime

**Change:** Onboarded new PT; migrated syrupUSDC from USDC Yield to Prime

**Rationale:** Add PT-sNUSD-4JUN2026 to USDC Yield; migrate syrupUSDC from USDC Yield to USDC Prime.

**Tx:**
{% endupdate %}

{% update date="2026-03-18" tags="ethereum,morpho" %}

## ETH Yield

**Change:** Onboarded Origin ARM markets (stETH and eETH)

**Rationale:** ARM-WETH-stETH and ARM-WETH-eETH.

**Tx:**
{% endupdate %}

{% update date="2026-03-18" tags="ethereum,morpho" %}

## USDT Prime

**Change:** Onboarded new markets

**Rationale:** syrupUSDT and weETH.

**Tx:**
{% endupdate %}

{% update date="2026-03-18" tags="arbitrum,morpho" %}

## USDC Yield

**Change:** Rolled over PT-thBILL to new maturity

**Rationale:** PT-thBILL-18JUN2026.

**Tx:**
{% endupdate %}

{% update date="2026-03-09" tags="ethereum,morpho" %}

## USDC Yield

**Change:** Onboarded 7 new markets

**Rationale:** Add new markets to improve yield.

**Tx:** [submitCap](https://etherscan.io/tx/0x7cc4d0881f662f2f4e02c9fdf130f1da2cfe4d2e1504268a93233317ccce7d3c), [acceptCap](https://etherscan.io/tx/0x660119353e3e379e34b9d4571d8487a91d90a567436249164d9a3477a92251cf)
{% endupdate %}

{% update date="2026-03-04" tags="ethereum,morpho" %}

## USDC Yield

**Change:** Offboarded 4 underperforming markets

**Rationale:** PT-stcUSD-23JUL2026, PT-RLP-9APR2026, liUSD-1w, wbravUSDC.

**Tx:**
{% endupdate %}

{% update date="2026-03-03" tags="ethereum,gearbox" %}

## WETH pools

**Change:** Switch rETH main price feed to RedStone

**Rationale:** Addressing the upcoming deprecation of Rocket Pool's CL feed.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreiejwjm76i46fkjn6awsvm5uy6lkli3mafjbltjfdwe67eiw4bljwm)
{% endupdate %}

{% update date="2026-03-02" tags="ethereum,morpho" %}

## USDC Yield

**Change:** Onboarded srRoyUSDC market

**Rationale:** Add a new market to improve yield.

**Tx:** [submitCap](https://app.safe.global/transactions/tx?safe=eth:0x7E43Df1c1c5A2245858b60D4655fDA83704e4171\&id=multisig_0x7E43Df1c1c5A2245858b60D4655fDA83704e4171_0x5d47e2185d2c870952eacc7f38e438f0842672ef750826f2b8e2b4f66b14f806), [acceptCap](https://etherscan.io/tx/0xfb79a769e80a5cb4f1c3f8d86bb0497d007e51a4038f0efdcde3e0e39ff6b6bf)
{% endupdate %}

{% update date="2026-03-01" tags="ethereum,morpho" %}

## All vaults

**Change:** Upgraded the Roles module to the new unwrapping contract

**Rationale:** Improve operational security.

**Tx:**
{% endupdate %}

{% update date="2026-02-25" tags="ethereum,gearbox" %}

## WETH and wstETH markets

**Change:** Onboard tETH-wstETH LP (Aura) Beefy strategy as collateral

**Rationale:** Enable new looping opportunities, with 90% LT and 1,500 quota.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreiauyix2vu3fsagydnsuwokhsvs6gfivxrzfiu6blpkyvhgeaz6nx4)
{% endupdate %}

{% update date="2026-02-20" tags="ethereum,arbitrum,morpho" %}

## USDC Yield & USDC Yield

**Change:** Removed matured Pendle markets

**Rationale:** PT-iUSD-19FEB2026 (Ethereum) and PT-thBILL-27FEB2026 (Arbitrum) matured; offboard to reduce risk.

**Tx:** =
{% endupdate %}

{% update date="2026-02-19" tags="ethereum,morpho" %}

## ETH Yield

**Change:** Removed savETH market

**Rationale:** Deprecate the old savETH market, after migrating liquidity to the newer market with a better oracle design.

**Tx:** [submitCap/reallocate](https://etherscan.io/tx/0x643c6c11ed4a5fdca34c3dba96b89972c01a68fe814158f4128066166ae9134f)
{% endupdate %}

{% update date="2026-02-15" tags="arbitrum,morpho" %}

## USDC Yield

**Change:** Re-enabled the weETH market

**Rationale:** Re-enabled market now that liquidity has improved.

**Tx:** [submitCap](https://arbiscan.io/tx/0xe3e082270a996c3917a86bbe41c0905ce1d0aa71f65bacc6a4b2113a31c07eec), [acceptCap](https://arbiscan.io/tx/0x86cc591efd81166c6d713bfc7cb01702cc3c8d9a62f2148a2a980c2864087898)
{% endupdate %}

{% update date="2026-02-15" tags="ethereum,morpho" %}

## USDC Yield

**Change:** Onboarded the YFI market

**Rationale:** Add a new market to improve yield.

**Tx:** [submitCap](https://app.safe.global/transactions/tx?safe=eth:0x7E43Df1c1c5A2245858b60D4655fDA83704e4171\&id=multisig_0x7E43Df1c1c5A2245858b60D4655fDA83704e4171_0x232694b1c823c0b54b5801df0077493ea956dd4740360bd46d311c7e6fc5cea9), [acceptCap](https://etherscan.io/tx/0x894cfcac40bf075182f799af3ce75d3503ce90a6f2269a1981b5312edd34f646)
{% endupdate %}

{% update date="2026-02-10" tags="ethereum,morpho" %}

## ETH Prime

**Change:** Removed savETH market

**Rationale:** Deprecate the old savETH market, after migrating liquidity to the newer market with a better oracle design.

**Tx:** [submitCap/reallocate](https://etherscan.io/tx/0x79981b9fc9a003414f27ab36d8adf22f95449adc5c8a692fb9ad2933598d9789), [updateWithdrawQueue](https://app.safe.global/transactions/tx?safe=eth:0xC266b1181A80E84EDC2c6596718E88E8115c1eaa\&id=multisig_0xC266b1181A80E84EDC2c6596718E88E8115c1eaa_0xdb573666cb614da8216f16f6602664361edfae09b991933289af6ee17cc065e7)
{% endupdate %}

{% update date="2026-02-06" tags="ethereum,gearbox" %}

## WETH and wstETH pools

**Change:** Onboard PT-DETH23APR2026, native rsETH integration and kpwWETH (1,000 WETH quota, 93% liquidation threshold in Tier I and Tier II)

**Rationale:** Included longer expiry PT for DETH and enabled native rsETH minting/redeeming.

**Tx:** [Tx 1](https://safe.gearbox.finance/txs/?cid=bafkreibgiudwx7ypb32sqhnaycybtiab5e6czvzq7gynhkgx33goxrufii), [Tx 2](https://safe.gearbox.finance/txs/?cid=bafkreigviv6tk74zrnwduccciorxhzglvr33onzoghyh3fayp4kk3pishe)
{% endupdate %}

{% update date="2026-02-02" tags="ethereum,morpho" %}

## ETH Prime

**Change:** Removed wstETH market

**Rationale:** Removal of the unused market due to low liquidity.

**Tx:** n/a (no on-chain transaction linked on source page)
{% endupdate %}

{% update date="2026-01-30" tags="arbitrum,morpho" %}

## USDC Yield

**Change:** Removed matured PT-syrupUSDC-29JAN2026 market

**Rationale:** Offboard matured Pendle PT.

**Tx:** [submitCap](https://app.safe.global/transactions/tx?safe=arb1:0xE8bED28828f4DD93FB98232F8e85c8880D1f7e1d\&id=multisig_0xE8bED28828f4DD93FB98232F8e85c8880D1f7e1d_0x1c852ec7f671ae184373a571e18eaa022d6138ab66cb0416889a185d2b49e629)
{% endupdate %}

{% update date="2026-01-29" tags="ethereum,gearbox" %}

## WETH and wstETH pools

**Change:** Re-enabling DETH

**Rationale:** Re-enabled DETH following Makina's re-activation of machines (v1.1).

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreiedksjb6e6w6iezsheviv6rd2derdqhq3jm3tabkctop5qdzr5vj4)
{% endupdate %}

{% update date="2026-01-26" tags="ethereum,morpho" %}

## ETH Prime

**Change:** Enabled savETH market

**Rationale:** Additional yield source: market deployed by KPK that has a fundamental exchange rate oracle.

**Tx:** [submitCap](https://app.safe.global/transactions/tx?safe=eth:0xC266b1181A80E84EDC2c6596718E88E8115c1eaa\&id=multisig_0xC266b1181A80E84EDC2c6596718E88E8115c1eaa_0x3bade5143cdd74f9e66acbb416476c96074e745ab9b7c1dba441ad5313f749ab), [acceptCap](https://etherscan.io/tx/0x93664340bbdda131a113f791da911028dbb7ad2ecedf309a3aef2272c4bc8beb)
{% endupdate %}

{% update date="2026-01-25" tags="ethereum,gearbox" %}

## WETH pool

**Change:** Re-enabling rsETH

**Rationale:** Re-enabled rsETH as collateral after precautionary forbiddal.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreiggvvhylgkd46tzyiabcwx77f6l5renq3hxjhbopxsaj7hebenr3m)
{% endupdate %}

{% update date="2026-01-21" tags="ethereum,gearbox" %}

## WETH and wstETH pools

**Change:** Calling updateRates on Tumbler

**Rationale:** Final step in osETH-aWETH LP onboarding (applying interest rate).

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreig4z23oly2hi4mqp3ypxztdllxto3uud3ospfx263rtkvh3tsbnv4)
{% endupdate %}

{% update date="2026-01-20" tags="ethereum,gearbox" %}

## WETH and wstETH pools

**Change:** Forbid DETH and PT-DETH tokens and forbid borrowing in the CM

**Rationale:** Precautionary measure responding to the DUSD Curve exploit to reduce risk.

**Tx:** [Tx](https://app.safe.global/transactions/tx?id=multisig_0x4Ad2419dc6DE75C3f57d7b6Aa200D494c74c1443_0xad762a8f619c99675a062d59b495d615a5b6d87d8ec961a6ff04fb4a0231b473\&safe=eth:0x4Ad2419dc6DE75C3f57d7b6Aa200D494c74c1443)
{% endupdate %}

{% update date="2026-01-20" tags="ethereum,morpho" %}

## ETH Prime

**Change:** Re-enabled the rsETH market

**Rationale:** Re-add the market to improve yield.

**Tx:** [submitCap](https://app.safe.global/transactions/tx?safe=eth:0xC266b1181A80E84EDC2c6596718E88E8115c1eaa\&id=multisig_0xC266b1181A80E84EDC2c6596718E88E8115c1eaa_0xc8c06429249021df99fe3a97082fa4c15ed778725743b12aa73ebc0d30e29dd3)
{% endupdate %}

{% update date="2026-01-20" tags="ethereum,morpho" %}

## USDC Prime

**Change:** Onboarded OETH market

**Rationale:** Add a new market to improve yield.

**Tx:** [submitCap](https://app.safe.global/transactions/tx?safe=eth:0xf8182E5827c06a47A985eC565A3bcd56437a97bE\&id=multisig_0xf8182E5827c06a47A985eC565A3bcd56437a97bE_0x14456096987b04c36c18d1fd163fd6ad92a5d19955530768799b5ffe08072dd9), [acceptCap](https://etherscan.io/tx/0xebfd62ad83032e999ffad466915c7ca9589f4f8e2375954f2e414c0d5d100ede)
{% endupdate %}

{% update date="2026-01-20" tags="ethereum,morpho" %}

## ETH Prime

**Change:** Offboarded wstETH; temporarily offboarded rsETH market

**Rationale:** Avoid low liquidity and precautionary measures due to risk alert, respectively.

**Tx:**
{% endupdate %}

{% update date="2026-01-16" tags="ethereum,gearbox" %}

## WETH & wstETH pools

**Change:** Onboard osETH-aWETH LP (Aura) Beefy strategy; reduce ezETH quota; update routes

**Rationale:** Enable new looping opportunities; reduce risk; improve swap execution; balance lending and borrowing rates.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreig3iaio3qg4wnu7hmu6tfwmazdbihotngv5i5qs5v3yqldugnccyu)
{% endupdate %}

{% update date="2026-01-15" tags="ethereum,gearbox" %}

## WETH pool

**Change:** Update Balancer v3 Gateway to v3.11

**Rationale:** Improve swap execution.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreiaayztdgps4ug6636frs5oisj3q7ukifmzddbv2dxwspb4bneix6q)
{% endupdate %}

{% update date="2026-01-15" tags="ethereum,gearbox" %}

## WETH & wstETH pools

**Change:** Set quota and LT to 0 for RE7LRT and pzETH; add ERC4626 rETH Beefy adapter

**Rationale:** Reduce risk by offboarding unused assets; improve swap execution and prepare for new asset onboarding.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreiajmvfu7wpaooomlwekyces32op7nhoysrdwvybndxmopnv4tl4ve)
{% endupdate %}

{% update date="2026-01-13" tags="ethereum,gearbox" %}

## WETH & wstETH pools

**Change:** Onboarded rETH-aWETH LP (Aura) Beefy strategy and set rstETH quota to 0

**Rationale:** Enable new looping opportunities and offboard unused assets.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreigb7feotmafj4ahto7iqyz4j3ryzcjx5qtmewi3lllura46tej6vu)
{% endupdate %}

{% update date="2026-01-13" tags="ethereum,morpho" %}

## ETH Prime

**Change:** Added WBTC market; removed ezETH market

**Rationale:** Adjust market exposure as liquidity conditions change.

**Tx:** [acceptCap (WBTC)](https://app.safe.global/transactions/tx?safe=eth:0xC266b1181A80E84EDC2c6596718E88E8115c1eaa\&id=multisig_0xC266b1181A80E84EDC2c6596718E88E8115c1eaa_0x5838360f36ebe5d3d69a6274beecb58d14771f6dfd8a344f962e264c5ed469cc), [submitCap (ezETH)](https://app.safe.global/transactions/tx?safe=eth:0xC266b1181A80E84EDC2c6596718E88E8115c1eaa\&id=multisig_0xC266b1181A80E84EDC2c6596718E88E8115c1eaa_0x039c1605381233f9dfcd56810fd17937079a74ccfcd1738901777d24d9de6c14)
{% endupdate %}

{% update date="2026-01-09" tags="ethereum,gearbox" %}

## WETH and wstETH pools

**Change:** New Balancer route for rETH

**Rationale:** Includes the Aave boosted rETH/WETH pool to improve rETH swaps.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreiaqqlezzsecms66awgg7gb2dqm5o4qjf6bxtore35h3jhuh45my3e)
{% endupdate %}

{% update date="2026-01-08" tags="ethereum,gearbox" %}

## WETH and wstETH pools

**Change:** New Balancer route for wstETH

**Rationale:** Includes Fluid boosted wstETH/WETH pool to route wstETH swaps.

**Tx:** [Tx 1](https://safe.gearbox.finance/txs/?cid=bafkreib2vptbp7w6npbxjyaeafeueo3juzv3qfyvrxnvrhd6xnu345ow3m), [Tx 2](https://safe.gearbox.finance/txs/?cid=bafkreiax35oibr7nhtded3b7a76n3f7pso4rvcb5ywk6edcuambawrlnqi)
{% endupdate %}

{% update date="2026-01-07" tags="ethereum,gearbox" %}

## wstETH pool

**Change:** Unpause Credit Managers Tier I and Tier II

**Rationale:** Unpause of CMs after successful rstETH offboarding.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreicc67lobhpg7w7kbbyk4mvkqxjo53n3odbiamjao2znqgpigmp2iy)
{% endupdate %}

{% update date="2026-01-02" tags="arbitrum,morpho" %}

## USDC Yield

**Change:** Added RLP, thBILL, PT-thBILL markets

**Rationale:** Onboard new yield sources.

**Tx:** [RLP](https://arbiscan.io/tx/0xdd78b561f8f0761738454748baad1c259bbdfe1c1ac1d1121b55073a661e19de), [thBILL](https://arbiscan.io/tx/0x7fcb687044bd72ea09dbf17e23cb59eacb79687c4a4418eb352e6fc0320b7e41), [PT-thBILL acceptCap](https://arbiscan.io/tx/0x5f45bc3e41165403a2bd0bc9d021b4b24a4bab7afef81c8b0be1c24e5adaf1b5)
{% endupdate %}

{% update date="2025-12-30" tags="ethereum,gearbox" %}

## WETH pool

**Change:** Update price feed for B-rETH-STABLE to zero

**Rationale:** Improved monitoring and rETH swaps.

**Tx:** [Tx](https://safe.gearbox.finance/emergency/tx/?chainId=1\&mc=0x1B265B97EB169fB6668E3258007C3b0242C7bDBE\&action=ORACLE%3A%3AsetPriceFeed\&params=%7B%22pool%22%3A%220x9396DCbf78fc526bb003665337C5E73b699571EF%22%2C%22priceFeed%22%3A%220xfe1ec406d777b0c4e2bf5e12d0f645721b2b86be%22%2C%22token%22%3A%220x1e19cf2d73a72ef1332c882f20534b6519be0276%22%7D\&nonce=140)
{% endupdate %}

{% update date="2025-12-20" tags="ethereum,gearbox" %}

## wstETH pool

**Change:** Pause CMs containing rstETH

**Rationale:** Planned asset offboarding.

**Tx:** [Tx](https://app.safe.global/transactions/tx?safe=eth:0x4Ad2419dc6DE75C3f57d7b6Aa200D494c74c1443\&id=multisig_0x4Ad2419dc6DE75C3f57d7b6Aa200D494c74c1443_0xa20b2e7bd59d39aee7cfd4432016c3561945610e6e6500729d10ce59a2ff6dd5)
{% endupdate %}

{% update date="2025-12-20" tags="ethereum,gearbox" %}

## WETH and wstETH pools

**Change:** Added Gearbox DAO addresses as Emergency Liquidators

**Rationale:** Improved liquidation process for planned rstETH offboarding.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreiay4zidyhtzxhsaavhd3yonjeamcdh32wng64e6y4wutnlhnzkauy)
{% endupdate %}

{% update date="2025-12-20" tags="ethereum,gearbox" %}

## WETH and wstETH pools

**Change:** Ramp rstETH LT to zero over a 48-hour period; lower quota for other Mellow assets

**Rationale:** Second step in offboarding rstETH. Lower general risk exposure.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreigsea55ng7k7n72qrfkcyk7vnd5bi5iums36t75bmksq2gltdlhie)
{% endupdate %}

{% update date="2025-12-19" tags="ethereum,gearbox" %}

## WETH and wstETH pools

**Change:** Set rstETH quota to zero

**Rationale:** First step in offboarding rstETH, due to a planned contract update.

**Tx:** [Tx](https://app.safe.global/transactions/tx?safe=eth:0x4Ad2419dc6DE75C3f57d7b6Aa200D494c74c1443\&id=multisig_0x4Ad2419dc6DE75C3f57d7b6Aa200D494c74c1443_0x064bbc77a0c15842f731b6083b50701ea1ac87a655fc54f700a225b371ad80b1)
{% endupdate %}

{% update date="2025-12-12" tags="ethereum,gearbox" %}

## WETH and wstETH pools

**Change:** Onboard DETH, PT-DETH-29JAN2026, and wmooCurveETH+-WETH; improve Balancer v3 routing

**Rationale:** Enable new looping opportunities in both markets and improve wstETH trade execution.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafybeihywlrvt54yjois5ks7o44u3aoisptlbkkbhdk5qj7zyfpsoiznc4)
{% endupdate %}

{% update date="2025-12-12" tags="ethereum,gearbox" %}

## wstETH pool

**Change:** Enable ETH+ as collateral with similar parameters as in the ETH market

**Rationale:** Enable new looping opportunities.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafybeihywlrvt54yjois5ks7o44u3aoisptlbkkbhdk5qj7zyfpsoiznc4)
{% endupdate %}

{% update date="2025-12-04" tags="ethereum,gearbox" %}

## WETH pool

**Change:** Update LT for stETH and wstETH in pzETH and RE7LRT CM

**Rationale:** Improve configuration for smoother native Mellow redemptions.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreia5i6mdwhd2ifhayxrdidkpt5s3sc6ovtwr4e6ayyspudeg7apng4)
{% endupdate %}

{% update date="2025-12-03" tags="ethereum,gearbox" %}

## Emergency Admin

**Change:** Replaced a signer on the Emergency Admin Safe

**Rationale:** Signer rotation; threshold unchanged.

**Tx:** [Tx](https://etherscan.io/tx/0x973e24c0952fcec74102fdf9cc40f5cf89a1dc0f4abf71ffc95507b75a81e7e6)
{% endupdate %}

{% update date="2025-11-12" tags="ethereum,gearbox" %}

## WETH pool

**Change:** ezETH oracle update

**Rationale:** Chainlink deprecated ezETH oracle; moved to RedStone market price feed.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreicef73mjswnpzziovfo2ek6z6z2cqgah6joq3ns77c27mkbp5zrqu)
{% endupdate %}

{% update date="2025-11-07" tags="ethereum,gearbox" %}

## WETH pool

**Change:** New interest rate model (IRM) and updated reserve oracles for ezETH, weETH, and rsETH

**Rationale:** IRM adjustment to reduce max ETH borrow rate; switch to fundamental oracles that query the underlying exchange rates directly.

**Tx:** [Tx](https://safe.gearbox.finance/txs/?cid=bafkreias7estqs345ubpynwjuj73ntpnbs7iyt5zptll7us3o2lkah6b7i)
{% endupdate %}

{% update date="2025-11-03" tags="ethereum,gearbox" %}

## WETH pool

**Change:** Disabled BALANCER\_VAULT adaptor in Tier 1 and Tier 2

**Rationale:** Preventive measure in response to confirmed Balancer v2 exploit.

**Tx:** [Tx](https://app.safe.global/transactions/tx?safe=eth:0x4Ad2419dc6DE75C3f57d7b6Aa200D494c74c1443\&id=multisig_0x4Ad2419dc6DE75C3f57d7b6Aa200D494c74c1443_0xad2ffacd563a7c96f03ca8db85dd4c30d32cc7bfae12283d04e5d105076ae7c1)
{% endupdate %}

{% update date="2025-10-31" tags="ethereum,gearbox" %}

## WETH and wstETH pools

**Change:** Add Loss Policy

**Rationale:** Added Loss Policy using Fundamental Oracles for both WETH and wstETH markets.

**Tx:** [Tx](https://etherscan.io/tx/0x927edc91e5a6b35bd145d49d615ba06500c3361adf35541583267a44993f679e)
{% endupdate %}

{% update date="2025-10-24" tags="ethereum,gearbox" %}

## WETH and wstETH pools

**Change:** Granted the PAUSABLE\_ADMIN role in the Market Configurator to a KPK automation EOA, `0x664AcdD11F7d2f2e253D7DAae7fF63CD618117f6`.

**Rationale:** Lets KPK's automation trigger emergency pauses on the Pools and Credit Managers.

**Tx:** [Tx](https://etherscan.io/tx/0x4898737fc36f1bf5f0325b60ec1bb2728e4f4a5b8d30ed0961e490e5d6b11d08)
{% endupdate %}

{% update date="2025-10-23" tags="ethereum,gearbox" %}

## WETH pools

**Change:** Update wstETH reserve oracle

**Rationale:** Re-enabling the reserve oracle for wstETH after the prior day's change.

**Tx:** [Tx](https://permissionless-safe.gearbox.foundation/txs/?cid=bafkreievqmsvehyudx77gizd6devaaohu7h5o3zrhccbvi7arqh6fzwkua)
{% endupdate %}

{% update date="2025-10-22" tags="ethereum,gearbox" %}

## WETH and wstETH pools

**Change:** Update main oracle for wstETH

**Rationale:** Avoid potential liquidations on rstETH/wstETH and wstETH/ETH positions.

**Tx:** [Tx 1](https://permissionless-safe.gearbox.foundation/emergency/tx/?chainId=1\&mc=0x1B265B97EB169fB6668E3258007C3b0242C7bDBE), [Tx 2](https://permissionless-safe.gearbox.foundation/emergency/tx/?chainId=1\&mc=0x1B265B97EB169fB6668E3258007C3b0242C7bDBE)
{% endupdate %}

{% update date="2025-10-21" tags="ethereum,gearbox" %}

## WETH and wstETH pools

**Change:** Configure MELLOW\_CLAIMER adapters in the newest CMs

**Rationale:** Enable native unstaking of Mellow assets.

**Tx:** [Tx](https://permissionless-safe.gearbox.foundation/txs/?cid=bafkreidt7qqgj6aweehxnetvpwmyrynvspryabhv3kjcxp5bzkle5c3jrq)
{% endupdate %}

{% update date="2025-10-17" tags="ethereum,gearbox" %}

## WETH and wstETH pools

**Change:** Create new Credit Managers and set Debt Limit to 0 on existing ones

**Rationale:** Updated Interest Rate fee for all new borrowers.

**Tx:** [Tx](https://permissionless-safe.gearbox.foundation/txs/?cid=bafybeibzc3hedi65hg42j567df3konql6wc2x6ieamuxvrl5j7fc5yrqo4)
{% endupdate %}

{% update date="2025-10-09" tags="ethereum,gearbox" %}

## WETH and wstETH pools

**Change:** Add PAUSABLE\_ADMIN role in Market Configurator

**Rationale:** Improved emergency pause role for active Pools and Credit Managers.

**Tx:** [Tx](https://permissionless-safe.gearbox.foundation/txs/?cid=bafkreihshp2nps3epi7cze7j5jqhe7x5ol7gfutbj7tcfut63e2xvbbd2a)
{% endupdate %}
{% endupdates %}


