> For the complete documentation index, see [llms.txt](https://docs.kpk.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kpk.io/funds/infrastructure/onchain-accounting/balance-adapters.md).

# Balance 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`](https://github.com/karpatkey/onchain-accounting/blob/main/src/balances/IBalanceAdapter.sol)

<figure><img src="/files/gQagfplUSMnAUF2AdUqq" alt="Diagram key: Input/source is light blue, Transform is violet, Staleness gate is amber, Output is green, Off-chain is dashed grey."><figcaption><p>Diagram key — the same colours are used across every adapter and price-feed diagram.</p></figcaption></figure>

***

## 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):

```solidity
interface IBalanceAdapter is IERC165 {
    /// Balance-only positions for an account (assetFilter == address(0) for all assets).
    function getAdapterPositions(address account, address assetFilter)
        external view returns (PositionBalance[] memory positions);

    function getAdapterBalances(address account)
        external view returns (address[] memory assets, uint256[] memory amounts, bool[] memory isDebt);

    function getAdapterBalanceForAsset(address account, address asset)
        external view returns (uint256 amount, bool isDebt);

    /// Assets this adapter reports on (may be empty if determined dynamically).
    function underlyingAssetsSupported() external view returns (address[] memory);
}
```

`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](/funds/infrastructure/onchain-accounting/concepts/position-identity.md)). Labels are **not** on the position; adapters implement [`IPositionDescribable`](/funds/infrastructure/onchain-accounting/concepts/position-identity.md#human-readable-labels) (`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):

```
getAdapterPositions(0x1F98…E984, address(0))   // address(0) = all assets
→ [
    PositionBalance { balanceAsset:{0xA0b8…eB48,"USDC",6}, balanceAdapter:0x5035…a550,
      amount:12000000000 /* 12,000 USDC */, isDebt:false,
      protocolBrand:keccak256("aave"), protocolId:keccak256("aave-v3"), protocolSubId:keccak256("aave-v3-lending"),
      positionId:abi.encode(pool), positionKind:3 /* Collateral */, isLocked:false, positionInstanceId:bytes32(0) },
    PositionBalance { balanceAsset:{0xC02a…6Cc2,"WETH",18}, balanceAdapter:0x5035…a550,
      amount:1500000000000000000 /* 1.5 WETH */, isDebt:true,
      protocolBrand:keccak256("aave"), protocolId:keccak256("aave-v3"), protocolSubId:keccak256("aave-v3-lending"),
      positionId:abi.encode(pool), positionKind:2 /* Borrowed */, isLocked:false, positionInstanceId:bytes32(0) }
  ]

getAdapterBalances(0x1F98…E984)
→ ( [0xA0b8…eB48, 0xC02a…6Cc2],          // assets
    [12000000000, 1500000000000000000], // amounts
    [false, true] )                      // isDebt

getAdapterBalanceForAsset(0x1F98…E984, 0xC02a…6Cc2)   → (1500000000000000000, true)   // 1.5 WETH debt

underlyingAssetsSupported()   → [0xA0b8…eB48, 0xC02a…6Cc2, …]   // assets the configured instances report on
```

***

## Plain adapters vs. meta-adapters

Adapters come in two flavours:

{% tabs %}
{% tab title="Plain adapter" %}
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:

```solidity
function addBalanceAdapters(address[] calldata adapters) external;     // MANAGER
function removeBalanceAdapters(address[] calldata adapters) external;
```

Examples: `Cap`, `AaveV3SafetyModule`, `SparkLend`, the withdrawal-queue adapters.
{% endtab %}

{% tab title="Meta-adapter" %}
A **meta-adapter** (ADR 0010) 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:

```solidity
function addMetaBalanceAdapter(address adapter, bytes32[] calldata instances) external;  // register + seed
function addMetaInstances(address adapter, bytes32[] calldata instances) external;       // add coordinates
function removeMetaInstances(address adapter, bytes32[] calldata instances) external;    // remove coordinates
```

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](/funds/infrastructure/onchain-accounting/meta-balance-adapters.md) for the full model and catalogue.
{% endtab %}
{% endtabs %}

{% hint style="info" %}
This replaces the earlier "one deployed adapter per market/pool" model. A single meta-adapter contract now serves all instances of its protocol, configured through its instance set.
{% endhint %}

{% content-ref url="/pages/WVS5FrCAb7oPDpSaxXEw" %}
[Meta Balance Adapters](/funds/infrastructure/onchain-accounting/meta-balance-adapters.md)
{% endcontent-ref %}

***

## 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:

{% content-ref url="/pages/5jla9uxBHtbur2nGloBe" %}
[Assisted Balance Adapters](/funds/infrastructure/onchain-accounting/assisted-balance-adapters.md)
{% endcontent-ref %}

***

## 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:

```solidity
function excludeAssetFromDefaultAdapter(address asset) external;
function includeAssetInDefaultAdapter(address asset) external;
```

***

## 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.

```solidity
function getAccountPositions(address account, address quoteAsset)
    external view returns (Position[] memory);
```

For human-readable labels, use the NAV Calculator's verbose reads (`getAccountPositionsVerbose` / `getAccountNavVerbose`); see [NAV Calculator](/funds/infrastructure/onchain-accounting/contracts/nav-calculator.md#read-functions).

***

## 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](/funds/infrastructure/onchain-accounting/assisted-balance-adapters.md), 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](/funds/infrastructure/onchain-accounting/concepts/position-identity.md).
* only **registered** assets contribute to NAV; returning an unregistered asset is harmless (it's filtered out). 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, 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.

### Adding & removing adapters

Registration is governed by the Safe (`MANAGER` role) on the NAV Calculator — see [Admin / Manager API](/funds/infrastructure/onchain-accounting/contracts/nav-calculator.md#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`](https://github.com/karpatkey/onchain-accounting/blob/main/docs/howto/add-balance-adapter.md) and [`src/balances/BALANCE_ADAPTERS.md`](https://github.com/karpatkey/onchain-accounting/blob/main/src/balances/BALANCE_ADAPTERS.md).

***

## Adapter catalogue

### Default

| Adapter                                                                                    | Protocol | Positions                           |
| ------------------------------------------------------------------------------------------ | -------- | ----------------------------------- |
| [ERC20Default](/funds/infrastructure/onchain-accounting/balance-adapters/erc20-default.md) | Wallet   | Wallet balance per registered asset |

### Plain (singleton) adapters

| Adapter                                                                                                         | Protocol | Positions                                               |
| --------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------- |
| [SparkLend](/funds/infrastructure/onchain-accounting/balance-adapters/sparklend.md)                             | Spark    | Supply · Collateral · Borrow                            |
| [Cap](/funds/infrastructure/onchain-accounting/balance-adapters/cap-protocol.md)                                | Cap      | cUSD (Supplied) + stcUSD (Staking), in USDC             |
| [Aave Safety Module](/funds/infrastructure/onchain-accounting/balance-adapters/aave-safety-module.md)           | Aave     | Staked (locked + claimable) · Rewards                   |
| [Aave Umbrella](/funds/infrastructure/onchain-accounting/balance-adapters/aave-umbrella.md)                     | Aave     | Staked (locked + claimable) · Rewards                   |
| [Lido Withdrawal Queue](/funds/infrastructure/onchain-accounting/balance-adapters/lido-withdrawal-queue.md)     | Lido     | One ETH claim per open request (`isLocked` per request) |
| [Stader Withdrawal Queue](/funds/infrastructure/onchain-accounting/balance-adapters/stader-withdrawal-queue.md) | Stader   | One ETHx→ETH claim per open request                     |
| [Kelp Withdrawal Queue](/funds/infrastructure/onchain-accounting/balance-adapters/kelp-withdrawal-queue.md)     | Kelp     | One exit claim per open request                         |
| [Ethena Cooldown](/funds/infrastructure/onchain-accounting/balance-adapters/ethena-cooldown.md)                 | 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:

{% content-ref url="/pages/5jla9uxBHtbur2nGloBe" %}
[Assisted Balance Adapters](/funds/infrastructure/onchain-accounting/assisted-balance-adapters.md)
{% endcontent-ref %}

### 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:

{% content-ref url="/pages/WVS5FrCAb7oPDpSaxXEw" %}
[Meta Balance Adapters](/funds/infrastructure/onchain-accounting/meta-balance-adapters.md)
{% endcontent-ref %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.kpk.io/funds/infrastructure/onchain-accounting/balance-adapters.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
