Restrict a delegation
Caveat enforcers are used to apply specific rules and conditions to a delegation, ensuring that delegated executions are only performed under predefined circumstances.
A delegation has a property caveats
, which is an array of CaveatStruct
s.
A CaveatStruct
is specified as follows:
export type CaveatStruct = {
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 CaveatBuilder
interface offers an intuitive way to define the caveats
property of a delegation.
Use the CaveatBuilder
to easily ensure that your delegations grant only the necessary authority.
Create the CaveatBuilder
To use the CaveatBuilder
, 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
You can 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();
Be aware that 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.
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 = createRootDelegation(
delegate,
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.
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 AllowedCalldataEnforcer
terms, but this is tedious and error-prone.
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.
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.
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.
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.
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.
Parameters
- A contract address as a hex string
- A factory address as a hex string
- Bytecode as a hex string
Example
caveatBuilder.addCaveat("deployed",
"0xc11F3a8E5C7D16b75c9E2F60d26f5321C6Af5E92",
"0x4fa079Afaa26A601FF458bC826FB498621f5E2e1",
"0x..." // The deploy bytecode for the contract at 0xc11F3a8E5C7D16b75c9E2F60d26f5321C6Af5E92
);
erc20TransferAmount
Limits the transfer of ERC-20 tokens.
Parameters
- An ERC-20 contract address as a hex string
- Amount as a
bigint
Example
caveatBuilder.addCaveat("erc20TransferAmount",
"0xc11F3a8E5C7D16b75c9E2F60d26f5321C6Af5E92",
1_000_000n
);
erc20BalanceGte
Ensures the delegator's ERC-20 balance has increased by at least a specified amount after the execution has been performed, regardless of what the execution is.
Parameters
- An ERC-20 contract address as a hex string
- Amount as a
bigint
Example
caveatBuilder.addCaveat("erc20BalanceGte",
"0xc11F3a8E5C7D16b75c9E2F60d26f5321C6Af5E92",
1_000_000n
);
id
Specifies an ID for multiple delegations. Once one of them is redeemed, the other delegations with the same ID are revoked.
Parameters
- An ID as a number
Example
caveatBuilder.addCaveat("id",
123456
);
limitedCalls
Limits the number of times the delegate can perform executions on the delegator's behalf.
Parameters
- A count as a number
Example
caveatBuilder.addCaveat("limitedCalls",
1
);
nativeBalanceGte
Ensures that the recipient's native token balance has increased by at least the specified amount.
Parameters
- The recipient's address as a hex string
- The amount by which the balance must have increased as a
bigint
Example
caveatBuilder.addCaveat("nativeBalanceGte",
"0x3fF528De37cd95b67845C1c55303e7685c72F319",
1_000_000n
);
nativeTokenPayment
Enforces payment in native token (for example, ETH) for the right to use the delegation.
A permissions context allowing payment must be provided as the args
when
redeeming the delegation.
Parameters
- The recipient's address as a hex string
- The amount that must be paid as a
bigint
Example
caveatBuilder.addCaveat("nativeTokenPayment",
"0x3fF528De37cd95b67845C1c55303e7685c72F319",
1_000_000n
);
nativeTokenTransferAmount
Enforces an allowance of native currency (for example, ETH).
Parameters
- The allowance as a
bigint
Example
caveatBuilder.addCaveat("nativeTokenTransferAmount",
1_000_000n
);
nonce
Adds a nonce to a delegation, and revokes previous delegations by incrementing the current nonce by calling incrementNonce(address _delegationManager)
.
Parameters
- A nonce as a hex string
Example
caveatBuilder.addCaveat("nonce",
"0x1"
);
redeemer
Limits the addresses that can redeem the delegation. This caveat enforcer is designed for restricting smart contracts or EOAs lacking delegation support, and can be placed anywhere in the delegation chain to restrict the redeemer.
Delegator accounts with delegation functionalities can bypass these restrictions by delegating to other addresses. For example, Alice makes Bob the redeemer. This condition is enforced, but if Bob is a delegator he can create a separate delegation to Carol that allows her to redeem Alice's delegation through Bob.
Parameters
- An array of addresses as hex strings
Example
caveatBuilder.addCaveat("redeemer",
[
"0xb4aE654Aca577781Ca1c5DE8FbE60c2F423f37da",
"0x6be97c23596ECed7170fdFb28e8dA1Ca5cdc54C5"
]
);
timestamp
Specifies a range of timestamps through which the delegation will be valid.
Parameters
- After threshold timestamp as a number
- Before threshold timestamp as a number
You can specify 0
to indicate that there is no limitation on a threshold.
Example
caveatBuilder.addCaveat("timestamp",
499165200,
1445412480
);
valueLte
Limits the value of native tokens that the delegate can spend.
Parameters
- A value as a
bigint
Example
caveatBuilder.addCaveat("valueLte",
1_000_000_000_000_000_000n // 1 ETH in wei
);