Restrict a delegation
Use caveat enforcers to apply specific rules and restrictions to a delegation, ensuring that delegated executions are only performed under predefined circumstances.
A delegation has a caveats
property, which is an array of Caveat
objects.
Each caveat is specified as follows:
export type Caveat = {
enforcer: Hex; // The address of the caveat enforcer contract.
terms: Hex; // Data passed to the caveat enforcer, describing how the redemption should be validated.
args: Hex; // Data that may be specified by the redeemer when redeeming the delegation (only used in limited cases).
};
The MetaMask Delegation Toolkit provides a CaveatBuilder
interface, which offers an intuitive way to define the caveats
array.
Use the CaveatBuilder
to easily ensure that your delegations grant only the necessary authority.
Create the caveat builder
To create the caveat builder, call the createCaveatBuilder()
function, passing an instance of DeleGatorEnvironment
.
The environment can be accessed from the MetaMaskSmartAccount
, as in this example:
const environment = delegatorSmartAccount.environment;
const caveatBuilder = createCaveatBuilder(environment);
By default, the CaveatBuilder
does not allow empty caveats. To allow the CaveatBuilder
to build an empty caveats array, provide the following configuration:
const caveatBuilder = createCaveatBuilder(environment, { allowEmptyCaveats: true });
Add caveats to the builder
Add caveats to the builder using the addCaveat
method, specifying the caveat type and its parameters. You can chain multiple calls to addCaveat
as in the following example:
const caveats = caveatBuilder
// allowedTargets accepts an array of addresses.
// This caveat restricts the caller to only use the delegation to interact with the specified address.
.addCaveat("allowedTargets", ["0xc11F3a8E5C7D16b75c9E2F60d26f5321C6Af5E92"])
// allowedMethods accepts an array of methods.
// This caveat restricts the caller to only use the delegation to invoke the specified methods.
.addCaveat("allowedMethods", [
"approve(address,uint256)",
"transfer(address,uint256)"
])
// limitedCalls accepts a number.
// This caveat restricts the caller to only use the delegation one time.
.addCaveat("limitedCalls", 1)
.build();
Important considerations when using caveat enforcers
- Delegations without caveats are entirely permissive. It is crucial to add appropriate caveats to restrict the delegated authority sufficiently. Failing to do so could result in unintended access or actions.
- Caveat enforcers safeguard the execution process but do not guarantee a final state post-redemption. Always combine caveat enforcers thoughtfully to create comprehensive protection.
- When using multiple caveat enforcers that modify external contract states, the order matters.
For example, if you include both
NativeBalanceGteEnforcer
to ensure a balance has increased andNativeTokenPaymentEnforcer
to deduct from that balance, executingNativeTokenPaymentEnforcer
first might causeNativeBalanceGteEnforcer
to fail validation. Consider the sequence of enforcers carefully when creating delegations with interdependent caveats.
For convenience, you can also pass the CaveatBuilder
directly to the various helper methods for creating a delegation. For example:
const caveats = caveatBuilder
// allowedTargets accepts an array of addresses.
.addCaveat("allowedTargets", ["0xc11F3a8E5C7D16b75c9E2F60d26f5321C6Af5E92"])
// allowedMethods accepts an array of methods.
.addCaveat("allowedMethods", [
"approve(address,uint256)",
"transfer(address,uint256)"
])
// limitedCalls accepts a number.
.addCaveat("limitedCalls", 1);
const delegation = createDelegation({
to: delegate,
from: delegator,
caveats
});
Caveat types
The CaveatBuilder
supports various caveat types, each serving a specific purpose.
These caveat types correspond to the out-of-the-box caveat enforcers
that the MetaMask Delegation Toolkit provides.
For more granular or custom control, you can also create custom caveat enforcers and add them to the caveat builder.
allowedCalldata
Limits the calldata that is executed.
You can use this caveat to enforce function parameters.
We strongly recommend using this caveat to validate static types and not dynamic types.
You can validate dynamic types through a series of allowedCalldata
terms, but this is tedious and error-prone.
Caveat enforcer contract: AllowedCalldataEnforcer.sol
Parameters
- Index in the calldata byte array (including the 4-byte method selector) where the expected calldata starts
- Expected calldata as a hex string
Example
caveatBuilder.addCaveat("allowedCalldata",
4,
encodeAbiParameters([
{ type: "string" },
{ type: "uint256" }
], [
"Hello Gator",
12345n
])
);
This example uses Viem's encodeAbiParameters
utility to encode the parameters as ABI-encoded hex strings.
allowedMethods
Limits what methods the delegate can call.
Caveat enforcer contract: AllowedMethodsEnforcer.sol
Parameters
- An array of methods as 4-byte hex strings, ABI function signatures, or
ABIFunction
objects
Example
caveatBuilder.addCaveat("allowedMethods", [
"0xa9059cbb",
"transfer(address,uint256)",
{
name: 'transfer',
type: 'function',
inputs: [
{ name: 'recipient', type: 'address' },
{ name: 'amount', type: 'uint256' }
],
outputs: [],
stateMutability: 'nonpayable',
}
]);
This example adds the transfer
function to the allowed methods in three different ways - as the 4-byte function selector, the ABI function signature, and the ABIFunction
object.
allowedTargets
Limits what addresses the delegate can call.
Caveat enforcer contract: AllowedTargetsEnforcer.sol
Parameters
- An array of addresses as hex strings
Example
caveatBuilder.addCaveat("allowedTargets", [
"0xc11F3a8E5C7D16b75c9E2F60d26f5321C6Af5E92",
"0xB2880E3862f1024cAC05E66095148C0a9251718b"
]);
argsEqualityCheck
Ensures that the args
provided when redeeming the delegation are equal to the terms specified on the caveat.
Caveat enforcer contract: ArgsEqualityCheckEnforcer.sol
Parameters
- The expected
args
as a hex string
Example
caveatBuilder.addCaveat("argsEqualityCheck",
"0xf2bef872456302645b7c0bb59dcd96ffe6d4a844f311ebf95e7cf439c9393de2"
);
blockNumber
Specifies a range of blocks through which the delegation will be valid.
Caveat enforcer contract: BlockNumberEnforcer.sol
Parameters
- After threshold block number as a
bigint
- Before threshold block number as a
bigint
You can specify 0n
to indicate that there is no limitation on a threshold.
Example
caveatBuilder.addCaveat("blocknumber",
19426587n,
0n
);
deployed
Ensures a contract is deployed, and if not, deploys the contract.
Caveat enforcer contract: DeployedEnforcer.sol
Parameters
- A contract address as a hex string
- The salt to use with the contract, as a hex string
- The bytecode of the contract as a hex string
Example
caveatBuilder.addCaveat("deployed",
"0xc11F3a8E5C7D16b75c9E2F60d26f5321C6Af5E92",
"0x0e3e8e2381fde0e8515ed47ec9caec8ba2bc12603bc2b36133fa3e3fa4d88587",
"0x..." // The deploy bytecode for the contract at 0xc11F3a8E5C7D16b75c9E2F60d26f5321C6Af5E92
);
erc1155BalanceGte
Ensures the ERC-1155 balance of a specified address has increased by at least a specified amount after the execution has been performed, regardless of what the execution is.
Caveat enforcer contract: ERC1155BalanceGteEnforcer.sol
Parameters
- An ERC-1155 contract address as a hex string
- The recipient's address as a hex string
- The ID of the ERC-1155 token as a bigint
- The amount by which the balance must have increased as a bigint
Example
caveatBuilder.addCaveat("erc1155BalanceGte",
"0xc11F3a8E5C7D16b75c9E2F60d26f5321C6Af5E92",
"0x3fF528De37cd95b67845C1c55303e7685c72F319",
1n,
1_000_000n
);
erc20BalanceGte
Ensures the delegator's ERC-20 balance increases by at least the specified amount after execution, regardless of the execution.
Caveat enforcer contract: ERC20BalanceGteEnforcer.sol
Parameters
- An ERC-20 contract address as a hex string
- Amount as a
bigint
Example
caveatBuilder.addCaveat("erc20BalanceGte",
"0xc11F3a8E5C7D16b75c9E2F60d26f5321C6Af5E92",
1_000_000n
);
erc20PeriodTransfer
Ensures that ERC-20 token transfers remain within a predefined limit during a specified time window. At the start of each new period, the allowed transfer amount resets. Any unused transfer allowance from the previous period does not carry over and is forfeited.
Caveat enforcer contract: ERC20PeriodTransferEnforcer.sol
Parameters
- The address of the ERC-20 token contract.
- The maximum amount of tokens that can be transferred per period, in wei.
- The duration of each period in seconds.
- The timestamp when the first period begins.
Example
caveatBuilder.addCaveat("erc20PeriodTransfer",
"0xb4aE654Aca577781Ca1c5DE8FbE60c2F423f37da", // Address of the ERC-20 token
1000000000000000000n, // 1 ERC-20 token - 18 decimals, in wei
86400, // 1 day in seconds
1743763600, // April 4th, 2025, at 00:00:00 UTC
);
erc20Streaming
Enforces a linear streaming transfer limit for ERC-20 tokens. Block token access until the specified start timestamp. At the start timestamp, immediately release the specified initial amount. Afterward, accrue tokens linearly at the specified rate, up to the specified maximum.
Caveat enforcer contract: ERC20StreamingEnforcer.sol
Parameters
- An ERC-20 contract address as a hex string
- Initial amount available at start time as a
bigint
- Maximum total amount that can be unlocked as a
bigint
- Rate at which tokens accrue per second as a
bigint
- Start timestamp as a number
Example
caveatBuilder.addCaveat("erc20Streaming",
"0xc11F3a8E5C7D16b75c9E2F60d26f5321C6Af5E92",
1_000_000n,
10_000_000n,
100n,
1703980800
);
erc20TransferAmount
Limits the transfer of ERC-20 tokens.
Caveat enforcer contract: ERC20TransferAmountEnforcer.sol
Parameters
- An ERC-20 contract address as a hex string
- Amount as a
bigint
Example
caveatBuilder.addCaveat("erc20TransferAmount",
"0xc11F3a8E5C7D16b75c9E2F60d26f5321C6Af5E92",
1_000_000n
);
erc721BalanceGte
Ensures the ERC-721 balance of the specified recipient address increases by at least the specified amount after execution, regardless of execution type.
Caveat enforcer contract: ERC721BalanceGteEnforcer.sol
Parameters
- An ERC-721 contract address as a hex string
- The recipient's address as a hex string
- The amount by which the balance must have increased as a
bigint
Example
caveatBuilder.addCaveat("erc721BalanceGte",
"0xc11F3a8E5C7D16b75c9E2F60d26f5321C6Af5E92",
"0x3fF528De37cd95b67845C1c55303e7685c72F319",
1_000_000n
);
erc721Transfer
Restricts the execution to only allow ERC-721 token transfers, specifically the transferFrom(from, to, tokenId)
function, for a specified token ID and contract.
Caveat enforcer contract: ERC721TransferEnforcer.sol
Parameters
- The permitted ERC-721 contract address as a hex string
- The permitted ID of the ERC-721 token as a
bigint
Example
caveatBuilder.addCaveat("erc721Transfer",
"0x3fF528De37cd95b67845C1c55303e7685c72F319",
1n
);
exactCalldata
Verifies that the transaction calldata matches the expected calldata. For batch transactions,
see exactCalldataBatch
.
Caveat enforcer contract: ExactCalldataEnforcer.sol
Parameters
- A hex value for calldata.
Example
caveatBuilder.addCaveat("exactCalldata",
"0x1234567890abcdef" // Calldata to be matched
);
exactCalldataBatch
Verifies that the provided batch execution calldata matches the expected calldata for each individual execution in the batch.
Caveat enforcer contract: ExactCalldataBatchEnforcer.sol