Skip to main content
Morpho Vault V2 implements a role-based access control system with four distinct roles, each with specific permissions and responsibilities.

Role hierarchy

The vault defines four roles with different levels of access:
  1. Owner - Sets curator and sentinels, controls vault metadata
  2. Curator - Configures vault parameters (timelocked)
  3. Allocator(s) - Manages asset allocation within caps
  4. Sentinel(s) - Emergency derisking capabilities
Roles are not “two-step”, meaning anyone can give a role to anyone, but the recipient must actively exercise it.

Owner role

The owner has control over high-level vault governance:

Permissions

address public owner;
Can perform:
  • Set the curator
  • Set sentinels (add/remove)
  • Set vault name
  • Set vault symbol
  • Transfer ownership
Cannot do:
  • Actions that directly hurt depositors
  • Configure vault parameters (curator’s role)
  • Manage allocations (allocator’s role)

Owner functions

function setOwner(address newOwner) external {
    require(msg.sender == owner, ErrorsLib.Unauthorized());
    owner = newOwner;
    emit EventsLib.SetOwner(newOwner);
}
Transfers ownership to a new address.
function setCurator(address newCurator) external {
    require(msg.sender == owner, ErrorsLib.Unauthorized());
    curator = newCurator;
    emit EventsLib.SetCurator(newCurator);
}
Assigns the curator role to an address. Only one curator can exist at a time.
function setIsSentinel(address account, bool newIsSentinel) external {
    require(msg.sender == owner, ErrorsLib.Unauthorized());
    isSentinel[account] = newIsSentinel;
    emit EventsLib.SetIsSentinel(account, newIsSentinel);
}
Grants or revokes sentinel role. Multiple sentinels can exist.
function setName(string memory newName) external {
    require(msg.sender == owner, ErrorsLib.Unauthorized());
    name = newName;
    emit EventsLib.SetName(newName);
}
Updates the vault’s ERC-20 token name.
function setSymbol(string memory newSymbol) external {
    require(msg.sender == owner, ErrorsLib.Unauthorized());
    symbol = newSymbol;
    emit EventsLib.SetSymbol(newSymbol);
}
Updates the vault’s ERC-20 token symbol.

Curator role

The curator configures the vault’s parameters and manages risk controls:

Permissions

address public curator;
Can configure:
  • Allocators
  • Gates (receive/send shares/assets)
  • Adapter registry
  • Adapters (add/remove)
  • Timelocks
  • Fees and fee recipients
  • Caps (absolute and relative)
  • Force deallocate penalties
All curator actions are timelockable except decreasing absolute and relative caps.

Curator functions

Allocator management

function setIsAllocator(address account, bool newIsAllocator) external {
    timelocked();
    isAllocator[account] = newIsAllocator;
    emit EventsLib.SetIsAllocator(account, newIsAllocator);
}
If setIsAllocator is timelocked, removing an allocator will take time.

Gate management

function setReceiveSharesGate(address newReceiveSharesGate) external {
    timelocked();
    receiveSharesGate = newReceiveSharesGate;
    emit EventsLib.SetReceiveSharesGate(newReceiveSharesGate);
}

function setSendSharesGate(address newSendSharesGate) external {
    timelocked();
    sendSharesGate = newSendSharesGate;
    emit EventsLib.SetSendSharesGate(newSendSharesGate);
}

function setReceiveAssetsGate(address newReceiveAssetsGate) external {
    timelocked();
    receiveAssetsGate = newReceiveAssetsGate;
    emit EventsLib.SetReceiveAssetsGate(newReceiveAssetsGate);
}

function setSendAssetsGate(address newSendAssetsGate) external {
    timelocked();
    sendAssetsGate = newSendAssetsGate;
    emit EventsLib.SetSendAssetsGate(newSendAssetsGate);
}

Fee management

function setPerformanceFee(uint256 newPerformanceFee) external {
    timelocked();
    require(newPerformanceFee <= MAX_PERFORMANCE_FEE, ErrorsLib.FeeTooHigh()); // Capped at 50%
    require(performanceFeeRecipient != address(0) || newPerformanceFee == 0, ErrorsLib.FeeInvariantBroken());
    accrueInterest();
    performanceFee = uint96(newPerformanceFee);
    emit EventsLib.SetPerformanceFee(newPerformanceFee);
}

function setManagementFee(uint256 newManagementFee) external {
    timelocked();
    require(newManagementFee <= MAX_MANAGEMENT_FEE, ErrorsLib.FeeTooHigh()); // Capped at 5%/year
    require(managementFeeRecipient != address(0) || newManagementFee == 0, ErrorsLib.FeeInvariantBroken());
    accrueInterest();
    managementFee = uint96(newManagementFee);
    emit EventsLib.SetManagementFee(newManagementFee);
}
Fee constraints:
  • Performance fee: Capped at 50% (cut on interest)
  • Management fee: Capped at 5%/year (cut on principal)
  • Invariant: fee != 0 => recipient != address(0)

Allocator role

Allocators manage the vault’s position across underlying markets:

Permissions

mapping(address account => bool) public isAllocator;
Can perform:
  • Allocate assets to enabled adapters (within caps)
  • Deallocate assets from adapters
  • Set liquidity adapter and data
  • Set max rate
Responsibilities:
  • Vault’s performance
  • Liquidity management
  • Ensuring users can withdraw at any time
Allocators can move funds between markets without going through timelocks. They can also set the liquidity adapter, which can prevent deposits and/or withdrawals (but cannot prevent in-kind redemptions via forceDeallocate).

Allocator functions

function allocate(address adapter, bytes memory data, uint256 assets) external {
    require(isAllocator[msg.sender], ErrorsLib.Unauthorized());
    allocateInternal(adapter, data, assets);
}
Allocates assets to an adapter. Caps are checked for all returned IDs.
function deallocate(address adapter, bytes memory data, uint256 assets) external {
    require(isAllocator[msg.sender] || isSentinel[msg.sender], ErrorsLib.Unauthorized());
    deallocateInternal(adapter, data, assets);
}
Deallocates assets from an adapter. Also available to sentinels.
function setLiquidityAdapterAndData(address newLiquidityAdapter, bytes memory newLiquidityData) external {
    require(isAllocator[msg.sender], ErrorsLib.Unauthorized());
    liquidityAdapter = newLiquidityAdapter;
    liquidityData = newLiquidityData;
    emit EventsLib.SetLiquidityAdapterAndData(msg.sender, newLiquidityAdapter, newLiquidityData);
}
Sets the liquidity adapter for deposits/withdrawals. Whether the adapter is valid is checked during allocate/deallocate.
function setMaxRate(uint256 newMaxRate) external {
    require(isAllocator[msg.sender], ErrorsLib.Unauthorized());
    require(newMaxRate <= MAX_MAX_RATE, ErrorsLib.MaxRateTooHigh());
    accrueInterest();
    maxRate = uint64(newMaxRate);
    emit EventsLib.SetMaxRate(newMaxRate);
}
Sets the maximum rate at which the share price can increase.

Sentinel role

Sentinels provide emergency derisking capabilities:

Permissions

mapping(address account => bool) public isSentinel;
Can perform:
  • Revoke pending timelock actions
  • Deallocate funds to idle
  • Decrease caps (absolute and relative)
Purpose:
  • Quickly derisk a vault in emergency situations
  • Provide additional security layer

Sentinel functions

function revoke(bytes calldata data) external {
    require(msg.sender == curator || isSentinel[msg.sender], ErrorsLib.Unauthorized());
    require(executableAt[data] != 0, ErrorsLib.DataNotTimelocked());
    executableAt[data] = 0;
    bytes4 selector = bytes4(data);
    emit EventsLib.Revoke(msg.sender, selector, data);
}
Cancels a pending timelocked action. Also available to curator.
function deallocate(address adapter, bytes memory data, uint256 assets) external {
    require(isAllocator[msg.sender] || isSentinel[msg.sender], ErrorsLib.Unauthorized());
    deallocateInternal(adapter, data, assets);
}
Deallocates assets from adapters back to idle. Shared with allocators.
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);
}
Decreases absolute cap. Not timelocked. Also available to curator.
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);
}
Decreases relative cap. Not timelocked. Also available to curator.

Role design philosophy

1

Owner governance

Owner cannot directly hurt depositors but controls who has operational roles
2

Curator safety

Curator cannot directly hurt depositors without going through timelocks
3

Allocator flexibility

Allocators can move funds efficiently within curator-set boundaries (caps)
4

Sentinel protection

Sentinels provide emergency response without going through timelocks
Multiple allocators and sentinels can exist, but only one owner and one curator at a time.

Build docs developers (and LLMs) love