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 actions 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 { Action } from "@delegator/src/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.
Action calldata _action, // The action to be taken.
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 { Action } from "../utils/Types.sol";

// This enforcer 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 {

mapping(address delegationManager => mapping(bytes32 delegationHash => uint256 count)) public callCounts;

event IncreasedCount(address indexed sender, bytes32 indexed delegationHash, uint256 limit, uint256 callCount);

function beforeHook(
bytes calldata _terms, // Maximum number of times the delegate can redeem the delegation.
bytes calldata,
Action calldata,
bytes32 _delegationHash, // Hash of the delegation being operated on.
address,
address
)
public
override
{
uint256 limit_ = getTermsInfo(_terms);
uint256 callCounts_ = ++callCounts[msg.sender][_delegationHash];
require(callCounts_ <= limit_, "LimitedCallsEnforcer:limit-exceeded");
emit IncreasedCount(msg.sender, _delegationHash, limit_, callCounts_);
}

function getTermsInfo(bytes calldata _terms) public pure returns (uint256 limit_) {
require(_terms.length == 32, "LimitedCallsEnforcer:invalid-terms-length");
limit_ = uint256(bytes32(_terms[:32]));
}
}

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.

const enforcerAddress = "0x4CE496Aed14427DB90328c97895998d1a0837f75"; // LimitedCallsEnforcer.sol Sepolia address
// Encode the terms parameter.
const terms = encodeAbiParameters([{ type: "uint256" }], [2]);
const args = "0x"; // Or any required arguments.

const caveat = {
enforcer: enforcerAddress,
terms: terms,
args: args,
};
const delegation = await (connector as any).createDelegation({
delegate,
options: {
caveats: [caveat],
salt: 0x1,
},
});