For the complete documentation index, see llms.txt. This page is also available as Markdown.

Balance adapters

How positions are located — the adapter interface, the default wallet adapter, and the singleton adapters.

A balance adapter is a contract that tells the NAV Calculator how much of an underlying asset an account holds in a given protocol — including positions that are not visible as a simple ERC-20 balanceOf.

Source: IBalanceAdapter.sol

Diagram key: Input/source is light blue, Transform is violet, Staleness gate is amber, Output is green, Off-chain is dashed grey.
Diagram key — the same colours are used across every adapter and price-feed diagram.

Why balance adapters are needed

Many DeFi positions are not directly represented by a wallet balance:

  • A Morpho supply position is tracked as shares inside the Morpho protocol.

  • An Aave deposit is represented by an aToken; a borrow by a debt token.

  • A staked Curve/Balancer LP sits in a gauge, with rewards accruing separately.

Each adapter abstracts these differences and returns balance-only PositionBalance entries denominated in the underlying asset, tagged with a machine-readable identity. The NAV Calculator prices them uniformly.


The adapter interface

Every adapter implements IBalanceAdapter (and advertises it via ERC-165):

getAdapterPositions is the path the NAV Calculator uses; it returns PositionBalance entries with the typed identity — the 3-level protocol taxonomy (protocolBrand / protocolId / protocolSubId), positionId, positionKind, isLocked, plus the ephemeral positionInstanceId (see Position identity). Labels are not on the position; adapters implement IPositionDescribable (protocolName() + positionLabels(positionId)) so the verbose reads can build the breadcrumb lazily.

Example — querying the Aave V3 meta-adapter for an account that supplies USDC and borrows WETH (illustrative values, abbreviated addresses):


Plain adapters vs. meta-adapters

Adapters come in two flavours:

A plain adapter covers a single, fixed scope — one protocol singleton, one queue, one reward distributor. It needs no external configuration once deployed.

Registered with:

Examples: Cap, AaveV3SafetyModule, SparkLend, the withdrawal-queue adapters.

A meta-adapter covers many instances of one protocol from a single immutable contract — e.g. one MorphoMarketsMeta handles every Morpho market the fund touches. Which instances it queries is a governed, on-chain instance set (bytes32 coordinates — usually a vault/pool/market address widened to bytes32, or a raw bytes32 market id).

Registered and adjusted with:

Adding a new market/vault/pool is then just an addMetaInstances call — no new contract deploy. A malformed coordinate is isolated per-instance by the adapter's fan-out and cannot zero out the rest of the batch.

Examples: AaveV3InstancesMeta, MorphoVaultsMeta, BalancerV3PoolsMeta, ConvexRewardsMeta. See Meta balance adapters for the full model and catalogue.

A single meta-adapter contract serves all instances of its protocol, configured through its instance set, rather than one deployed adapter per market or pool.

Meta balance adapters

Assisted adapters

A third family — assisted adapters — handles positions that cannot be discovered purely on-chain (behind a non-enumerable NFT or an opaque exit-queue ticket). The position owner caches only the position coordinates itself, on-chain and permissionlessly (msg.sender-keyed, no roles) — typically via a delegatecall assistant, a thin wrapper you use in place of the protocol's own deposit/withdraw call that performs the same action and records the resulting position on the adapter. The amounts are still valued live on every read, so a faulty feeder can only under-report, never inflate NAV. Full model and catalogue:

Assisted balance adapters

The default adapter

ERC20DefaultAdapter is deployed automatically inside NAVCalculator.initialize() and reports plain wallet balances for every registered ERC-20 plus the native token (ERC-7528 sentinel 0xEeee…EeeE). It cannot be removed. An asset's wallet reporting can be muted/unmuted without unregistering it:


Reading positions

getAccountPositions returns one priced Position per balance entry across all registered adapters, making a full portfolio breakdown straightforward. The same underlying asset can be reported by several adapters at once (held in the wallet, supplied to Morpho, staked in a gauge) — each is a distinct position with its own identity.

For human-readable labels, use the NAV Calculator's verbose reads (getAccountPositionsVerbose / getAccountNavVerbose); see NAV Calculator.


How an adapter is built

Most balance adapters are small, immutable, view-only contracts: no admin functions and no governed config stored on them. They only read chain state and return balance-only PositionBalance entries — the NAV Calculator does all the pricing. The sanctioned exception is the assisted adapters, which hold a permissionless, msg.sender-keyed coordinate cache (no roles) fed by the position owner.

Required of every adapter — implement IBalanceAdapter and advertise it via ERC-165 (supportsInterface(IBalanceAdapter)true). When building each PositionBalance:

  • amount is always positive; direction is carried by isDebt (true subtracts from NAV).

  • balanceAdapter is address(this) (provenance).

  • set the three protocol ids (protocolBrand / protocolId / protocolSubId, keccak256 slug constants from ProtocolIds.sol), encode positionId per the protocolSubId schema, tag positionKind, set isLocked (false unless the leg is a non-withdrawable withdrawal-queue / cooldown portion), and set positionInstanceId to the per-item handle (NFT id, withdrawal-request id, exit ticket) or bytes32(0) — see Position identity.

  • only registered assets contribute to NAV; returning an unregistered asset is harmless for a credit leg (it's filtered out). It is not harmless for a debt leg — see the invariant below. Adapters usually take the NAV Calculator address in their constructor to resolve asset metadata.

Plain vs. meta:

  • Plain — pin the single scope (one vault / pool / market / token, or hardcoded constants) in the constructor and implement the IBalanceAdapter reads directly.

  • Meta — extend MetaBalanceAdapter; pin only protocol-level constants in the constructor (e.g. the Morpho singleton, a position manager) — never the instances. Implement _instancePositions(account, instance, assetFilter) and _instanceAssets(instance), and optionally override _instancePositionId (default abi.encode(_toAddress(instance)); use abi.encode(instance) for raw-bytes32 coordinates like Morpho market ids). The base supplies ERC-165, per-instance revert isolation (a starved instance raises rather than being absorbed — see above), and reads the configured instance set from the NAV Calculator.

Labels — implement IPositionDescribable (protocolName() + positionLabels(bytes positionId)) so the verbose reads can build the breadcrumb. Both are called lazily on the verbose path only (fail-open, gas-capped staticcalls) — labels are not stored on the PositionBalance. positionLabels derives its segments purely from the static positionId; the ephemeral per-item id rides in positionInstanceId, never in labels.

protocolName() names the protocol, never the product. It is not a constructor argument — adapters derive it from their own protocolId against a canonical table of display names, so same protocolId ⇒ same protocolName holds by construction and cannot drift between two adapters of one protocol. Adapters that are different products of the same protocol (Aave V3 lending / Safety Module / Umbrella; Morpho vaults / markets; Balancer pools / gauges) therefore all return the same name; what distinguishes the product is positionLabels() and the typed protocolSubId.

This matters because consumers group configured adapters by display name. The typed protocolBrand / protocolId / protocolSubId would group them correctly, but they only ride on PositionBalance structs from an account read — a consumer enumerating registered adapters never sees them. The display name is the only protocol identity at that layer, so an adapter returning its product name splits itself into a phantom sibling protocol.

Shape enumeration (optional) — an adapter whose position shapes are bounded and derivable on-chain may also implement IPositionEnumerable (positionIds()), so an account-less configuration view can list and label what the NAV is configured to account for. Like IPositionDescribable it is ERC-165 advertised, read through a fail-open staticcall, and never called on the NAV path.

Adding & removing adapters

Registration is governed by the Safe (MANAGER role) on the NAV Calculator — see Admin / Manager API. Because the contracts are immutable, "changing" an adapter means deploying a new one and swapping it in the registry.

Plain adapter
Meta-adapter

Add

addBalanceAdapters([adapter])

addMetaBalanceAdapter(adapter, instances) (register + seed); then addMetaInstances to grow coverage

Remove

removeBalanceAdapters([adapter]) — the ERC20Default adapter cannot be removed

removeMetaInstances to drop instances; removeBalanceAdapters([adapter]) to remove the adapter entirely

The full authoring → test → deploy recipe lives in the contracts repo: docs/howto/add-balance-adapter.md and src/balances/BALANCE_ADAPTERS.md.


Adapter catalogue

Default

Adapter
Protocol
Positions

ERC20Default

Wallet

Wallet balance per registered asset

Plain (singleton) adapters

Adapter
Protocol
Positions

SparkLend

Spark

Supply · Collateral · Borrow

Cap

Cap

cUSD (Supplied) + stcUSD (Staking), in USDC

Aave Safety Module

Aave

Staked (locked + claimable) · Rewards

Aave Umbrella

Aave

Staked (locked + claimable) · Rewards

Lido Withdrawal Queue

Lido

One ETH claim per open request (isLocked per request)

Stader Withdrawal Queue

Stader

One ETHx→ETH claim per open request

Kelp Withdrawal Queue

Kelp

One exit claim per open request

Ethena Cooldown

Ethena

sUSDe→USDe cooldown (single leg, isLocked toggles)

Assisted adapters

Permissionless, owner-fed adapters (msg.sender-keyed, no roles) for positions not discoverable on-chain — ether.fi Withdrawal Queue, StakeWise V3 Exit Queue, Nexus Mutual Staking. The full model and catalogue are on a dedicated page:

Assisted balance adapters

Meta-adapters

One immutable contract per protocol, each serving many instances via a governed instance set. The full model and catalogue are on a dedicated page:

Meta balance adapters

Last updated