Morpho Vault V2 uses an ID-based caps system to constrain fund allocation. IDs represent abstract identifiers for common risk factors (collateral, oracle, protocol, etc.), and allocations are limited by both absolute and relative caps.
Cap structure
Each ID has three components:
struct Caps {
uint256 allocation; // Current allocation to this ID
uint128 absoluteCap; // Maximum allocation in asset units
uint128 relativeCap; // Maximum allocation as % of total assets (WAD units)
}
Storage
mapping ( bytes32 id => Caps) internal caps;
IDs are generated by hashing descriptive data:
bytes32 id = keccak256 (idData);
ID-based allocation
IDs represent common risk factors across multiple positions:
Example ID types
Adapter-level ID:
bytes32 adapterId = keccak256 ( abi . encode ( "this" , address ( this )));
Collateral token ID:
bytes32 collateralId = keccak256 ( abi . encode ( "collateralToken" , collateralTokenAddress));
Market-specific ID:
bytes32 marketId = keccak256 ( abi . encode ( "this/marketParams" , address ( this ), marketParams));
IDs can be reused across multiple markets to cap total exposure to a shared risk factor. For example, multiple markets using the same collateral token would share a collateral ID.
Absolute caps
Absolute caps limit allocation in asset units (e.g., USDC, WETH).
Setting absolute caps
function increaseAbsoluteCap ( bytes memory idData , uint256 newAbsoluteCap ) external {
timelocked ();
bytes32 id = keccak256 (idData);
require (newAbsoluteCap >= caps[id].absoluteCap, ErrorsLib. AbsoluteCapNotIncreasing ());
caps[id].absoluteCap = newAbsoluteCap. toUint128 ();
emit EventsLib. IncreaseAbsoluteCap (id, idData, newAbsoluteCap);
}
Called by curator. Requires timelock.
function decreaseAbsoluteCap ( bytes memory idData , uint256 newAbsoluteCap ) external {
bytes32 id = keccak256 (idData);
require ( msg.sender == curator || isSentinel[ msg.sender ], ErrorsLib. Unauthorized ());
require (newAbsoluteCap <= caps[id].absoluteCap, ErrorsLib. AbsoluteCapNotDecreasing ());
caps[id].absoluteCap = uint128 (newAbsoluteCap);
emit EventsLib. DecreaseAbsoluteCap ( msg.sender , id, idData, newAbsoluteCap);
}
Called by curator or sentinel. No timelock required.
Absolute cap enforcement
Absolute caps are checked during allocation:
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 ());
// ...
}
Allocating is prevented if an ID’s absolute cap is zero. Deallocating is prevented if the ID’s allocation is zero. This prevents interactions with unknown markets.
Relative caps
Relative caps limit allocation as a percentage of total vault assets.
Relative cap units
Relative caps use WAD (18 decimal) units:
WAD = 1e18 = 100%
0.5e18 = 50%
0.1e18 = 10%
Setting relative caps
function increaseRelativeCap ( bytes memory idData , uint256 newRelativeCap ) external {
timelocked ();
bytes32 id = keccak256 (idData);
require (newRelativeCap <= WAD, ErrorsLib. RelativeCapAboveOne ());
require (newRelativeCap >= caps[id].relativeCap, ErrorsLib. RelativeCapNotIncreasing ());
caps[id].relativeCap = uint128 (newRelativeCap);
emit EventsLib. IncreaseRelativeCap (id, idData, newRelativeCap);
}
Called by curator. Requires timelock. Maximum value is WAD (100%).
function decreaseRelativeCap ( bytes memory idData , uint256 newRelativeCap ) external {
bytes32 id = keccak256 (idData);
require ( msg.sender == curator || isSentinel[ msg.sender ], ErrorsLib. Unauthorized ());
require (newRelativeCap <= caps[id].relativeCap, ErrorsLib. RelativeCapNotDecreasing ());
caps[id].relativeCap = uint128 (newRelativeCap);
emit EventsLib. DecreaseRelativeCap ( msg.sender , id, idData, newRelativeCap);
}
Called by curator or sentinel. No timelock required.
Relative cap enforcement
Relative caps are checked during allocation:
require (
_caps.relativeCap == WAD || _caps.allocation <= firstTotalAssets. mulDivDown (_caps.relativeCap, WAD),
ErrorsLib. RelativeCapExceeded ()
);
Relative caps are relative to firstTotalAssets (total assets after first interest accrual in transaction), not realAssets. This prevents bypassing caps with flashloans.
Soft cap behavior
Relative caps are “soft” - they can be exceeded:
Relative caps are only checked on allocation (where allocations can increase), not on withdrawal. They can be exceeded due to:
Withdrawals from the vault (reducing total assets)
Interest accrual in adapters
Donations to adapters (if not prevented)
Allocation tracking
Allocations are updated during allocate/deallocate operations:
On allocation
function allocateInternal ( address adapter , bytes memory data , uint256 assets ) internal {
// ...
( 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 ();
// ... cap checks
}
emit EventsLib. Allocate ( msg.sender , adapter, assets, ids, change);
}
On deallocation
function deallocateInternal ( address adapter , bytes memory data , uint256 assets )
internal
returns ( bytes32 [] memory )
{
( 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 ();
}
// ...
}
Allocations are not always up to date because interest and losses are only accounted when (de)allocating in corresponding markets. To track allocations using events, use only the Allocate and Deallocate events.
Cap strategies
Conservative approach
Set both absolute and relative caps:
// Cap at 1M USDC AND 20% of vault
increaseAbsoluteCap (idData, 1_000_000e6 ); // 1M USDC
increaseRelativeCap (idData, 0 . 2e18 ); // 20%
Absolute-only approach
Set only absolute cap:
// Cap at 5M USDC regardless of vault size
increaseAbsoluteCap (idData, 5_000_000e6 );
increaseRelativeCap (idData, WAD); // 100% = no relative constraint
Percentage-based approach
Set only relative cap:
// Cap at 30% of vault, no fixed limit
increaseAbsoluteCap (idData, type ( uint128 ).max); // Effectively unlimited
increaseRelativeCap (idData, 0 . 3e18 ); // 30%
Cap manipulation prevention
Flashloan protection
The vault uses firstTotalAssets to prevent relative cap manipulation:
uint256 public transient firstTotalAssets;
This variable:
Tracks total assets after first interest accrual in transaction
Prevents bypassing relative caps with flashloan deposits
Can generate false positives for large deposits through liquidity adapter
Allocator capital requirement
Relative caps can still be manipulated by allocators with short-term deposits, but this requires capital and generates costs.
Reading cap data
// Get absolute cap
function absoluteCap ( bytes32 id ) external view returns ( uint256 ) {
return caps[id].absoluteCap;
}
// Get relative cap
function relativeCap ( bytes32 id ) external view returns ( uint256 ) {
return caps[id].relativeCap;
}
// Get current allocation
function allocation ( bytes32 id ) external view returns ( uint256 ) {
return caps[id].allocation;
}
Disabling markets
To prevent interactions with a market, set its absolute cap to zero:
decreaseAbsoluteCap (marketIdData, 0 );
For markets sharing IDs with other markets, it’s impossible to “disable” one without affecting the others. Each market should have at least one exclusive ID if individual control is needed.