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
- 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 { 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:
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,
},
});