Skip to main content

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

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:

LimitedCallsEnforcer.sol
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
);