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:
This keeps every meta-adapter immutable while making the instance set a governed, onchain registry.
How it works
IMetaBalanceAdapterextendsIBalanceAdapterand is keyed by abytes32instance coordinate. Its inheritedgetAdapterPositions(account, assetFilter)reads the adapter's own NAV-configured set viaNAV_CALCULATOR.getMetaInstances(address(this))and fans out over it, so the NAV Calculator's read path is unchanged (no meta-specific branch).MetaBalanceAdapteris 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 raisesInstanceGasExhausted(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
getRegisteredAssetand revertsNAVCalculatorTooOld(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 tobytes32(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 fromProtocolIds.sol. Display labels are produced lazily, the adapter'spositionLabels(positionId)derives the breadcrumb (e.g. from the vault's ERC-20name()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:
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:
addMetaInstancesrevertsDuplicateMetaInstanceif this adapter already holds a coordinate or it is repeated in the batch (a duplicate would double-count NAV), andMetaInstanceClaimedByAnotherAdapter(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.
addMetaBalanceAdapteralways starts the adapter from an empty set, andremoveBalanceAdaptersclears 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.
Meta-adapter catalogue
Polygon PoS
ValidatorShare (addr)
Delegated principal (Staking) · Unclaimed rewards (Rewards)
Last updated