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

Meta balance adapters

One immutable adapter per protocol, serving many instances via a governed onchain instance set.

A meta-adapter is a single immutable contract that reports positions for many instances of one protocol, every Morpho vault, every Aave market, every Balancer pool, instead of one deployed adapter per instance. It is the model used for all high-cardinality protocols.

Source: MetaBalanceAdapter.sol

Why

The original model deployed one adapter contract per pool / market / vault: covering N Morpho vaults meant N immutable contracts, each a CREATE deploy plus a governance registration. For protocols with many fungible instances this scaled badly; every new pool was a deploy, an audit-surface delta, and a governance action.

The framework's core rule is that adapters are immutable, no Ownable, no admin functions. So the instance set can't live (mutably) inside the adapter. Instead:

The adapter stays a pure function of (account, instance) over any instance of its protocol. The governed "which instances do we actually query" set lives on the NAVCalculator, which is already upgradeable and MANAGER-gated, and is passed to the adapter at query time.

This keeps every meta-adapter immutable while making the instance set a governed, onchain registry.

How it works

  • IMetaBalanceAdapter extends IBalanceAdapter and is keyed by a bytes32 instance coordinate. Its inherited getAdapterPositions(account, assetFilter) reads the adapter's own NAV-configured set via NAV_CALCULATOR.getMetaInstances(address(this)) and fans out over it, so the NAV Calculator's read path is unchanged (no meta-specific branch).

  • MetaBalanceAdapter is the abstract base. It provides ERC-165, the registered-asset helper, and per-instance fan-out with per-instance revert isolation; a single bad/unknown instance coordinate is caught and skipped, and cannot zero out the rest of the batch. An instance that instead runs out of gas is not absorbed: the fan-out raises InstanceGasExhausted(adapter, instance, stipend, consumed), because recording a starved instance as an empty leg list would drop its debt leg and over-report NAV behind a complete-looking answer. See a starved read is not an empty read. Concrete adapters implement two hooks (_instancePositions, _instanceAssets) and optionally _instancePositionId.

  • Deploy-time compatibility check. The base constructor probes the NAVCalculator it is given for getRegisteredAsset and reverts NAVCalculatorTooOld(navCalculator) if it cannot serve it, meta-adapters rely on that O(1) lookup, and pointing one at an older implementation would otherwise fail later, at read time. The guard binds the next adapter deployed, not those already live.

  • Instance coordinate (bytes32). For most protocols this is an address widened to bytes32 (a vault, pool, gauge, or data-provider address). For protocols whose native identifier is already 32 bytes it is used raw, Morpho market ids and Balancer V2 pool ids.

  • Identity is derived onchain from the coordinate: positionId = abi.encode(vault) and the 3-level taxonomy (protocolBrand / protocolId / protocolSubId) set from ProtocolIds.sol. Display labels are produced lazily, the adapter's positionLabels(positionId) derives the breadcrumb (e.g. from the vault's ERC-20 name() or the pool's coin symbols) on the verbose read path only. Nothing instance-specific is stored in config; see Position identity.

Registration

Meta-adapters are registered and curated on the NAV Calculator by the holders of the MANAGER role: the NAV timelock and the Security Council Safe (the Governance Safe on Robinhood Chain), plus AssetKindRegistry, which holds it so that it can register, classify, and label an asset in one transaction. The adapter contract is immutable; the instance set is the governed, mutable part, with its own add/remove lifecycle:

Action
Call

Register the adapter + seed instances (atomic)

addMetaBalanceAdapter(adapter, instances)

Add instances (extend coverage)

addMetaInstances(adapter, instances)

Remove instances (drop coverage)

removeMetaInstances(adapter, instances)

Remove the adapter entirely (clears its set)

removeBalanceAdapters([adapter])

Inspect the configured set

getMetaInstances(adapter) (view)

Each coordinate has at most one owning adapter:

  • addMetaInstances reverts DuplicateMetaInstance if this adapter already holds a coordinate or it is repeated in the batch (a duplicate would double-count NAV), and MetaInstanceClaimedByAnotherAdapter(instance, claimedBy) if a different adapter holds it.

  • Adding a coordinate that is a registered token also removes that token from the default adapter's inclusion list, in the same transaction.

  • addMetaBalanceAdapter always starts the adapter from an empty set, and removeBalanceAdapters clears the whole set.

  • Removing a coordinate does not put the token back on the default adapter's list; it is reported by nobody until re-included with includeAssetInDefaultAdapter.

removeMetaInstances reverts MetaInstanceNotFound if a coordinate isn't present. addMetaInstances and removeMetaInstances both revert NotMetaAdapter if the target isn't a meta-adapter, and on an empty list. All are all-or-nothing. Full detail: NAV Calculator Admin / Manager API.

Example, the configured instance set of the Morpho Vaults meta-adapter (illustrative; each bytes32 is a vault address widened to 32 bytes):

For raw-bytes32 adapters (Morpho Markets, Balancer V2 Pools) the coordinates are the native ids themselves, not widened addresses.

Adding a new pool/vault/market is therefore a single addMetaInstances transaction; not a contract deploy.

Plain (single-scope) adapters use addBalanceAdapters instead and have no instance set. See Balance adapters. A separate assisted-adapter family (ether.fi exit, StakeWise exit, Nexus Mutual) caches position coordinates the owner feeds permissionlessly onchain (msg.sender-keyed, no roles), distinct from the meta model here.

Meta-adapter catalogue

Adapter
Protocol
Instance key
Positions

Aave V3

Data provider (addr)

Supply · Collateral · Borrow

Morpho

bytes32 market id (raw)

Collateral · Supply · Borrow

Morpho

Vault (addr)

Vault shares → assets

Gearbox V3

Farming pool (addr)

Pool shares → assets

Gearbox V3

Credit Account (addr)

Borrowed (debt) · Collateral

Euler

Vault (addr)

Supply/Collateral · Borrow

Fluid

fToken (addr)

Supply → assets

Fluid

Vault (addr)

Collateral · Borrow

Compound V3

Comet (addr)

Supply · Borrow · Collateral · Rewards

StakeWise

Vault (addr)

Staking · osToken debt

Balancer V3

Pool (addr)

Pro-rata pool tokens

Balancer V3

Gauge (addr)

Staked pool tokens · Rewards

Balancer V2

bytes32 pool id (raw)

Pro-rata pool tokens

Balancer V2

Gauge (addr)

Staked pool tokens · Rewards

Curve

Pool (addr)

Pro-rata pool coins

Curve

Gauge (addr)

Staked coins · CRV + extra rewards

Convex

Reward pool (addr)

Staked coins · CRV/CVX + extras

Uniswap V2

Pair (addr)

Pro-rata pair tokens

Uniswap V3

Pool (addr)

In-range liquidity (Supplied) · Fees, per LP NFT

PancakeSwap V2

Pair (addr)

Pro-rata pair tokens

PancakeSwap V3

Pool (addr)

In-range liquidity (Supplied) · Fees, per LP NFT

17 protocols

Token (addr)

Wallet-held yield token (Staking · Supplied)

Symbiotic

Vault (addr)

Vault shares redeemed to the underlying (Supplied)

Polygon PoS

ValidatorShare (addr)

Delegated principal (Staking) · Unclaimed rewards (Rewards)

Polygon Staking is an Ethereum mainnet adapter. Polygon PoS staking settles on L1 via the StakeManager, which mints one ValidatorShare per validator, so the instance coordinate is an L1 contract and nothing about it runs on the Polygon chain.

The Uniswap V3 / PancakeSwap V3 meta-adapters discover an account's LP NFTs onchain (the NonfungiblePositionManager is ERC721Enumerable) and value them per configured pool. They link the GPL-2.0-or-later Uniswap V3 math libraries, so those two contracts are GPL-2.0-or-later rather than the repo's default BUSL-1.1.

Last updated