Manage Vault V2 Roles
This tutorial guides you through setting up and managing roles for a Morpho Vault V2. A clear understanding and proper assignment of roles are critical for the vault's security, governance, and operational success.
The Role Setup Workflow
Setting up roles for a new Vault V2 follows a specific sequence. The Owner
first assigns the Curator
and Sentinels
(actions which are immediate). Then, the Curator
proposes strategic changes, such as appointing an Allocator
.
Crucially, most of the Curator
's actions are timelocked. The process for these actions depends on whether a timelock duration is set.
Managing Roles via Scripts (Recommended)
Using scripts is the most reliable way to manage vault roles, as it minimizes manual error. A reference deployment repository is available to simplify this process: https://github.com/morpho-org/vault-v2-deployment
Below are conceptual examples of how to perform these actions using Foundry scripts.
Scenario 1: Initial Vault Setup (Zero Timelock)
When a Vault V2 is first deployed, all timelocks are set to zero. This is intentional, allowing for a rapid, one-time initial configuration. In this scenario, submit
and execute
can be done in immediate succession.
Example Script: Appointing an Allocator at Setup
This single script submits the proposal and executes it right away, as the waiting period is zero.
// This is a simplified example for initial setup.
import {IVaultV2} from "vault-v2/src/interfaces/IVaultV2.sol";
import {Script, console} from "forge-std/Script.sol";
contract InitialRoleSetup is Script {
function run() external {
address vaultAddress = vm.envAddress("VAULT_V2_ADDRESS");
address newAllocator = vm.envAddress("NEW_ALLOCATOR_ADDRESS");
uint256 curatorPrivateKey = vm.envUint("CURATOR_PRIVATE_KEY");
IVaultV2 vault = IVaultV2(vaultAddress);
vm.startBroadcast(curatorPrivateKey);
// Step 1: Submit the proposal as Curator
bytes memory data = abi.encodeCall(IVaultV2.setIsAllocator, (newAllocator, true));
vault.submit(data);
// Step 2: Execute immediately.
// Since the default timelock is 0, this can be executed right after submission.
// In a real script, this would be a separate transaction in the same broadcast.
vault.setIsAllocator(newAllocator, true);
vm.stopBroadcast();
console.log("Allocator role assigned successfully during initial setup.");
}
}
Scenario 2: Production Management (Non-Zero Timelock)
Once a vault is live and "hardened" with non-zero timelocks, role management becomes a true multi-step process that requires waiting for a real-world time delay. You cannot use vm.warp
on a live network.
The process must be split into two separate actions: one to submit, and another to execute after the delay.
Step 1: Submitting the Proposal
This script is run by the Curator
to propose appointing a new Allocator
.
script/SubmitAllocator.s.sol
import {IVaultV2} from "vault-v2/src/interfaces/IVaultV2.sol";
import {Script, console} from "forge-std/Script.sol";
contract SubmitAllocator is Script {
function run() external {
address vaultAddress = vm.envAddress("VAULT_V2_ADDRESS");
address newAllocator = vm.envAddress("NEW_ALLOCATOR_ADDRESS");
uint256 curatorPrivateKey = vm.envUint("CURATOR_PRIVATE_KEY");
IVaultV2 vault = IVaultV2(vaultAddress);
console.log("Submitting proposal to appoint", newAllocator, "as an Allocator...");
vm.startBroadcast(curatorPrivateKey);
bytes memory data = abi.encodeCall(IVaultV2.setIsAllocator, (newAllocator, true));
vault.submit(data);
vm.stopBroadcast();
uint256 timelock = vault.timelock(IVaultV2.setIsAllocator.selector);
console.log("Proposal submitted. You must wait at least", timelock, "seconds before executing.");
}
}
After running this script, the Curator
must wait for the timelock period to pass.
Step 2: Executing the Proposal
After the waiting period, anyone can call the setIsAllocator
function to finalize the proposal. This script does not require the Curator
's key.
script/ExecuteAllocator.s.sol
import {IVaultV2} from "vault-v2/src/interfaces/IVaultV2.sol";
import {Script, console} from "forge-std/Script.sol";
contract ExecuteAllocator is Script {
function run() external {
address vaultAddress = vm.envAddress("VAULT_V2_ADDRESS");
address newAllocator = vm.envAddress("NEW_ALLOCATOR_ADDRESS");
// Note: This can be run by any account, so a generic broadcaster key is fine.
uint256 broadcasterPrivateKey = vm.envUint("BROADCASTER_PRIVATE_KEY");
IVaultV2 vault = IVaultV2(vaultAddress);
console.log("Executing proposal to appoint", newAllocator, "as an Allocator...");
vm.startBroadcast(broadcasterPrivateKey);
vault.setIsAllocator(newAllocator, true);
vm.stopBroadcast();
console.log("Allocator role assigned successfully.");
}
}
Managing Roles via Etherscan
For manual management, the process mirrors the scripted approach.
Owner Actions (Immediate)
As the Owner
, connect your wallet to the Vault V2 contract on Etherscan and use the Write Contract
tab to:
- Execute
setCurator(address newCurator)
to appoint the Curator. - Execute
setIsSentinel(address account, bool newIsSentinel)
to add or remove Sentinels.
Curator Actions (Timelocked)
As the Curator
, connect your wallet to:
- Submit the Proposal:
- Use a tool like Foundry's
cast calldata
to ABI-encode your intended function call (e.g.,setIsAllocator(0x..., true)
). - Call the
submit(bytes)
function with the encoded data.
- Use a tool like Foundry's
- Wait for the Timelock: Wait for the required duration.
- Execute the Proposal: After the timelock, anyone can connect their wallet and call the original function (e.g.,
setIsAllocator(0x..., true)
) to finalize the change.