Create a custom caveat enforcer
Caveat enforcers are used to apply specific rules and conditions to a delegation, ensuring that delegated executions are only performed under predefined circumstances.
The MetaMask Delegation Toolkit provides some out-of-the-box caveat enforcer contracts that cover common use cases. For more granular or custom control, you can create custom caveat enforcers from scratch.
Prerequisites
- Install and set up the MetaMask Delegation Toolkit.
- Configure the toolkit.
Steps
1. Create the caveat enforcer
Create a contract that extends the
ICaveatEnforcer.sol
interface.
The following is a basic example of the caveat enforcer interface:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import { CaveatEnforcer } from "@delegator/src/enforcers/CaveatEnforcer.sol";
import { ModeCode } from "../utils/Types.sol";
contract RandomEnforcer is CaveatEnforcer {
// Enforces the conditions that should hold before a transaction is performed.
// This function MUST revert if the conditions are not met.
function beforeHook(
bytes calldata _terms, // Terms to enforce set by the delegator.
bytes calldata _args, // Optional input parameter set by the redeemer at time of invocation.
ModeCode _mode, // Mode for single or batch execution
bytes calldata _executionCalldata, // Encoded execution parameters
bytes32 _delegationHash, // Hash of the delegation.
address _delegator, // Address of the delegator.
address _redeemer // Address redeeming the delegation.
)
external;
}
As another example,
LimitedCallsEnforcer.sol
is an out-of-the-box caveat enforcer that allows the delegator to restrict the
number of times a delegation is redeemed:
pragma solidity 0.8.23;
import { CaveatEnforcer } from "./CaveatEnforcer.sol";
import { ModeCode } from "../utils/Types.sol";
/**
* @title Limited Calls Enforcer Contract
* @dev This contract extends the CaveatEnforcer contract. It provides functionality to enforce a
* limit on the number of times a delegate may perform transactions on behalf of the delegator.
*/
contract LimitedCallsEnforcer is CaveatEnforcer {
// State
mapping(address delegationManager
=> mapping(bytes32 delegationHash => uint256 count)) public callCounts;
// Events
event IncreasedCount(
address indexed sender,
address indexed redeemer,
bytes32 indexed delegationHash,
uint256 limit,
uint256 callCount
);
// Public methods
/**
* @notice Allows the delegator to specify a maximum number of times the recipient may perform
* transactions on their behalf.
* @param _terms - The maximum number of times the delegate may perform transactions on their behalf.
* @param _delegationHash - The hash of the delegation being operated on.
*/
function beforeHook(
bytes calldata _terms,
bytes calldata,
ModeCode,
bytes calldata,
bytes32 _delegationHash,
address,
address _redeemer
)
public
override
{
uint256 limit_ = getTermsInfo(_terms);
uint256 callCounts_ = ++callCounts[msg.sender][_delegationHash];
require(callCounts_ <= limit_, "LimitedCallsEnforcer:limit-exceeded");
emit IncreasedCount(msg.sender, _redeemer, _delegationHash, limit_, callCounts_);
}
/**
* @notice Decodes the terms used in this CaveatEnforcer.
* @param _terms encoded data that is used during the execution hooks.
* @return limit_ The maximum number of times the delegate may perform transactions on the
* delegator's behalf.
*/
function getTermsInfo(bytes calldata _terms) public pure returns (uint256 limit_) {
require(_terms.length == 32, "LimitedCallsEnforcer:invalid-terms-length");
limit_ = uint256(bytes32(_terms));
}
}
2. Deploy the caveat enforcer
Deploy your custom caveat enforcer to obtain its contract address.
As an out-of-the-box caveat enforcer, LimitedCallsEnforcer.sol
is already deployed to Sepolia.
3. Apply the caveat enforcer
To apply your caveat enforcer to a delegation, create the caveat object with the necessary
information, and pass it to the caveats
array when
creating a delegation.
In the following example, LimitedCallsEnforcer.sol
is applied to a new delegation, configured to
only allow the delegate to redeem the delegation twice.
Since the example doesn't apply additional caveat enforcers, the delegate has full freedom to do
anything that the delegator can do – but only twice.
import {
createCaveatBuilder,
createRootDelegation
} from "@codefi/delegator-core-viem";
// ...
const caveatBuilder = createCaveatBuilder(environment);
const limit = 2; // Limit to 2 calls
const caveats = caveatBuilder
.addCaveat("limitedCalls", limit);
const delegation = createRootDelegation(
delegate,
delegator,
caveats
);