Skip to main content
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.

Build docs developers (and LLMs) love