Vaults allocate assets to underlying markets via separate contracts called adapters. Adapters hold positions on behalf of the vault and report the current value of investments.
Adapter interface
All adapters must implement the IAdapter interface:
interface IAdapter {
/// @dev Returns the market ids and the change in assets on this market
function allocate(bytes memory data, uint256 assets, bytes4 selector, address sender)
external
returns (bytes32[] memory ids, int256 change);
/// @dev Returns the market ids and the change in assets on this market
function deallocate(bytes memory data, uint256 assets, bytes4 selector, address sender)
external
returns (bytes32[] memory ids, int256 change);
/// @dev Returns the current value of the investments (in underlying asset)
function realAssets() external view returns (uint256 assets);
}
How adapters work
Allocation process
When allocating assets to a market:
Transfer assets
The vault transfers assets to the adapter
Allocate to market
The adapter calls allocate() to enter the underlying market
Return IDs and change
The adapter returns risk IDs and allocation change
Check caps
The vault validates that caps are not exceeded for returned IDs
function allocateInternal(address adapter, bytes memory data, uint256 assets) internal {
require(isAdapter[adapter], ErrorsLib.NotAdapter());
accrueInterest();
SafeERC20Lib.safeTransfer(asset, adapter, assets);
(bytes32[] memory ids, int256 change) = IAdapter(adapter).allocate(data, assets, msg.sig, msg.sender);
for (uint256 i; i < ids.length; i++) {
Caps storage _caps = caps[ids[i]];
_caps.allocation = (int256(_caps.allocation) + change).toUint256();
require(_caps.absoluteCap > 0, ErrorsLib.ZeroAbsoluteCap());
require(_caps.allocation <= _caps.absoluteCap, ErrorsLib.AbsoluteCapExceeded());
require(
_caps.relativeCap == WAD || _caps.allocation <= firstTotalAssets.mulDivDown(_caps.relativeCap, WAD),
ErrorsLib.RelativeCapExceeded()
);
}
emit EventsLib.Allocate(msg.sender, adapter, assets, ids, change);
}
Deallocation process
When deallocating from a market:
Deallocate from market
The adapter calls deallocate() to exit the underlying market
Update allocations
The vault updates allocation tracking for returned IDs
Transfer assets back
The adapter transfers assets back to the vault
function deallocateInternal(address adapter, bytes memory data, uint256 assets)
internal
returns (bytes32[] memory)
{
require(isAdapter[adapter], ErrorsLib.NotAdapter());
(bytes32[] memory ids, int256 change) = IAdapter(adapter).deallocate(data, assets, msg.sig, msg.sender);
for (uint256 i; i < ids.length; i++) {
Caps storage _caps = caps[ids[i]];
require(_caps.allocation > 0, ErrorsLib.ZeroAllocation());
_caps.allocation = (int256(_caps.allocation) + change).toUint256();
}
SafeERC20Lib.safeTransferFrom(asset, adapter, address(this), assets);
emit EventsLib.Deallocate(msg.sender, adapter, assets, ids, change);
return ids;
}
Adapter specification
Adapters must follow these requirements:
Security requirements:
- Only the vault can call
allocate/deallocate
- Must not re-enter (directly or indirectly) the vault
- Must enter/exit markets only in
allocate/deallocate calls
Functional requirements:
- Return correct IDs on
allocate/deallocate (IDs must not repeat)
- After
deallocate, vault must have approval to transfer at least assets from the adapter
- Make
deallocate possible for in-kind redemptions
- After updates, sum of changes must equal current estimated position
Rounding and fees:
- Adapters may lose small amounts due to rounding errors or entry/exit fees
- Such losses should stay negligible compared to gas costs
- Curators should not interact with markets that create significant entry/exit losses
Adapter registry
An adapter registry constrains which adapters a vault can add:
interface IAdapterRegistry {
function isInRegistry(address account) external view returns (bool);
}
Registry behavior
- If
adapterRegistry is address(0), the vault can have any adapters
- When set, the registry retroactively checks already added adapters
- Useful when abdicated to ensure vault forever uses authorized adapters
- Registry should be “add only” to maintain invariant that all vault adapters are in registry
Setting adapter registry
function setAdapterRegistry(address newAdapterRegistry) external {
timelocked();
if (newAdapterRegistry != address(0)) {
for (uint256 i = 0; i < adapters.length; i++) {
require(
IAdapterRegistry(newAdapterRegistry).isInRegistry(adapters[i]),
ErrorsLib.NotInAdapterRegistry()
);
}
}
adapterRegistry = newAdapterRegistry;
emit EventsLib.SetAdapterRegistry(newAdapterRegistry);
}
Available adapters
Morpho Vault V2 supports the following adapters:
Morpho Market V1 Adapter V2
Purpose: Allocates to Morpho Blue markets (also known as Morpho Market V1)
Key features:
- Only works with markets using the adaptive curve IRM
- Tracks multiple market positions via
marketIds array
- Has its own timelock system for curator functions
- Supports burning shares for loss realization
IDs returned:
function ids(MarketParams memory marketParams) public view returns (bytes32[] memory) {
bytes32[] memory ids_ = new bytes32[](3);
ids_[0] = adapterId; // keccak256(abi.encode("this", address(this)))
ids_[1] = keccak256(abi.encode("collateralToken", marketParams.collateralToken));
ids_[2] = keccak256(abi.encode("this/marketParams", address(this), marketParams));
return ids_;
}
This adapter must be used with Morpho markets protected against inflation attacks with an initial supply.
Morpho Vault V1 Adapter
Purpose: Allocates to Morpho Vaults V1 (MetaMorpho V1.0 and V1.1)
Key features:
- Simpler design than Market V1 adapter
- Returns single ID (adapter ID)
- Uses ERC-4626
deposit/withdraw interface
- Cannot skim MetaMorpho vault shares (only other tokens)
IDs returned:
function ids() public view returns (bytes32[] memory) {
bytes32[] memory ids_ = new bytes32[](1);
ids_[0] = adapterId; // keccak256(abi.encode("this", address(this)))
return ids_;
}
Morpho Vaults V1.1 do not realize bad debt, so Morpho Vaults V2 supplying in them will not realize corresponding bad debt.
Adding and removing adapters
Adding an adapter
function addAdapter(address account) external {
timelocked();
require(
adapterRegistry == address(0) || IAdapterRegistry(adapterRegistry).isInRegistry(account),
ErrorsLib.NotInAdapterRegistry()
);
if (!isAdapter[account]) {
adapters.push(account);
isAdapter[account] = true;
}
emit EventsLib.AddAdapter(account);
}
Removing an adapter
function removeAdapter(address account) external {
timelocked();
if (isAdapter[account]) {
for (uint256 i = 0; i < adapters.length; i++) {
if (adapters[i] == account) {
adapters[i] = adapters[adapters.length - 1];
adapters.pop();
break;
}
}
isAdapter[account] = false;
}
emit EventsLib.RemoveAdapter(account);
}
Adapters should be removed only if they have no assets. To ensure no allocator can allocate during removal, set an exclusive ID cap to zero.
ID-based risk tracking
Adapters return IDs that represent common risk factors:
- Adapter ID: Unique to the adapter instance
- Collateral token ID: Shared across markets with same collateral
- Market-specific ID: Unique to a specific market configuration
IDs can be reused across multiple markets to cap total exposure to a common risk factor (e.g., a specific collateral type, oracle, or protocol).