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:
Owner - Sets curator and sentinels, controls vault metadata
Curator - Configures vault parameters (timelocked)
Allocator(s) - Manages asset allocation within caps
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
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
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.
setLiquidityAdapterAndData
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
Owner governance
Owner cannot directly hurt depositors but controls who has operational roles
Curator safety
Curator cannot directly hurt depositors without going through timelocks
Allocator flexibility
Allocators can move funds efficiently within curator-set boundaries (caps)
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.