MetaMask Delegation Toolkit LLM introduction
The following text is a condensed introduction to the MetaMask Delegation Toolkit, for use in an LLM's limited context. You can copy and paste it into an LLM-based chatbot such as ChatGPT to provide context about the toolkit. You can also ask the Consensys documentation chatbot questions about the toolkit.
Copy the following text by selecting the copy icon in the upper right corner of the text block:
This is a software toolkit for developing applications for the Ethereum Virtual Machine (EVM) in a
novel way, where users of the application have embedded "delegator" accounts, which are smart
contract accounts adhering to ERC-4337. Users can sign "delegations" to other addresses, either
granting them unconditional permissions, or creating "offers," by writing enforcement terms around
the permission's usage.
By putting the responsibility of permissions management onto the individual users' accounts, you can
reduce the complexity of the business logic in your smart contracts, and you can express any custom
authorization-related code as `CaveatEnforcer` contracts. Core business logic may still deal with
ownership and transfer rights, but you can use the toolkit to express the logic around delegations,
offers, and caveats.
Additionally, any holder of a delegation message from one of these accounts can redelegate that
permission or offer, even with additional terms, creating an invitation link experience that can be
used to invite new users to the system to enjoy some permissions without the need for them to have a
pre-existing crypto account.
A delegator account can be initialized "counterfactually" (without any gas, because its address is
deterministic from the signatory's address and the salt), and can even grant delegations to other
accounts before it's been put on-chain. The account redeeming a delegation is the one that ends up
needing to put the delegating account on-chain (and any intermediate delegating accounts, as these
delegations can be chained).
Let's look at how you can initialize a delegator account using the MetaMask Delegation Toolkit.
First, install Viem and the toolkit's modules into your project:
```bash
yarn add viem @codefi/delegator-core-viem
```
Then, import the modules and initialize a burner account (you can back it up later):
```typescript
import { http, createPublicClient } from "viem";
import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
import { lineaSepolia as chain } from "viem/chains";
import {
Implementation,
toMetaMaskSmartAccount,
type MetaMaskSmartAccount
} from "@codefi/delegator-core-viem";
const transport = http();
const publicClient = createPublicClient({ transport, chain });
const privateKey = generatePrivateKey();
const owner = privateKeyToAccount(privateKey);
const deploySalt = "0x";
// Although a value of type `PublicClient` is passed here, the required type for the `client` argument is `Client`.
const account: MetaMaskSmartAccount<Implementation.Hybrid> = await toMetaMaskSmartAccount({
client: publicClient,
implementation: Implementation.Hybrid,
deployParams: [owner.address, [], [], []],
deploySalt,
signatory: { account: owner },
});
```
## Send a user operation
The following example demonstrates how to send a user operation using the toolkit:
```typescript
import { createPublicClient, http, parseEther, privateKeyToAccount, generatePrivateKey } from "viem";
import { createBundlerClient, createPaymaster } from "viem/account-abstraction";
import { toMetaMaskSmartAccount, Implementation } from "@codefi/delegator-core-viem";
const publicClient = createPublicClient({
transport: http(),
chain
});
const bundler = createBundlerClient({
transport: http("BUNDLER_RPC_URL"),
chain
});
const privateKey = generatePrivateKey();
const owner = privateKeyToAccount(privateKey);
const deploySalt = "0x";
const delegatorSmartAccount = await toMetaMaskSmartAccount({
client: publicClient,
implementation: Implementation.Hybrid,
deployParams: [signatory.address, [], [], []],
deploySalt,
signatory: { account: owner },
});
// This mechanism will depend on the specific bundler being used. This example uses the blockchain's
// gas fee, fetched using the PublicClient, which may not be the same as the gas fees required by the
// bundler.
const { maxFeePerGas, maxPriorityFeePerGas } = await publicClient.estimateFeesPerGas();
const userOperationHash = await bundler.sendUserOperation({
account: delegatorSmartAccount,
calls: [
{
to: "0x1234567890123456789012345678901234567890", // the address to which the call will be made
value: parseEther("1") // the value that will be sent to the address
}
],
maxFeePerGas,
maxPriorityFeePerGas
});
const receipt = await bundler.waitForUserOperationReceipt({
hash: userOperationHash
});
console.log(receipt.receipt.transactionHash);
```
## Create a delegation
A delegation is an instance of `DelegationStruct`, where `delegator` is the account granting
permission to the `delegate` account. Here's a simple example of creating a delegation without any
caveats:
```typescript
import {
createRootDelegation,
type DelegationStruct
} from "@codefi/delegator-core-viem";
const delegatorAddress = "0x1234567890123456789012345678901234567890"; // The address of the delegator account
const delegateAddress = "0x5678901234567890123456789012345678901234"; // The address of the delegate account
const delegation: DelegationStruct = createRootDelegation(
delegateAddress,
delegatorAddress,
[]
);
```
This creates a basic delegation where the `delegateAddress` is granted full permissions to act on
behalf of the `delegatorAddress`. However, it's important to note that this delegation has no
restrictions or caveats applied to it.
## Apply caveats to a delegation
While the previous example demonstrates a simple delegation, granting unrestricted access is
generally not recommended. It's crucial to apply caveats to limit the scope of the delegated
permissions.
Caveat enforcers are solidity contracts that extend the `ICaveatEnforcer.sol` interface, and are
passed to the delegation struct to enable fine-grained control over delegated permissions.
Here's an example of how to apply a caveat to a delegation:
```typescript
import { createPublicClient, http, parseEther, privateKeyToAccount, generatePrivateKey, createCaveatBuilder } from "viem";
import { createBundlerClient, createPaymaster } from "viem/account-abstraction";
import { toMetaMaskSmartAccount, Implementation, type DelegationStruct } from "@codefi/delegator-core-viem";
const privateKey = generatePrivateKey();
const owner = privateKeyToAccount(privateKey);
const deploySalt = "0x";
const delegatorSmartAccount = await toMetaMaskSmartAccount({
client: publicClient,
implementation: Implementation.Hybrid,
deployParams: [owner.address, [], [], []],
deploySalt,
signatory: { account: owner },
});
const delegatorAddress = delegatorSmartAccount.address; // the address granting the delegation
const delegateAddress = "0x5678901234567890123456789012345678901234"; // the address of the recipient of the delegation
const environment = delegatorSmartAccount.environment;
const caveatBuilder = createCaveatBuilder(environment);
// Set up payment terms
const limit = 2; // Limit to 2 calls
caveatBuilder.addCaveat("limitedCalls", limit);
const delegation: DelegationStruct = createRootDelegation(
delegateAddress,
delegatorAddress,
caveatBuilder
);
```
This example applies the `LimitedCallsEnforcer` caveat, which restricts the delegate to only two
calls using this delegation. This demonstrates how caveats can be used to add specific restrictions
to a delegation.
You can apply multiple caveats to a single delegation by adding more caveat objects to the array,
allowing for highly customizable access control.
## Caveats included in the toolkit
The MetaMask Delegation Toolkit includes several built-in caveat enforcers that can be used to restrict delegations in various ways:
### `allowedCalldata`
Restricts execution to specific calldata. Best used for validating static types rather than dynamic types.
#### Parameters
1. `startIndex`: Index in calldata byte array (including 4-byte method selector)
2. `expectedCalldata`: Expected calldata as hex string
#### Example
```typescript
caveatBuilder.addCaveat("allowedCalldata",
4,
encodeAbiParameters([
{ type: "string" },
{ type: "uint256" }
], [
"Hello Gator",
12345n
])
);
```
### `allowedMethods`
Restricts which methods the delegate can call.
#### Parameters
1. Array of methods as 4-byte hex strings, ABI signatures, or `ABIFunction` objects
#### Example
```typescript
caveatBuilder.addCaveat("allowedMethods", [
"0xa9059cbb", // As 4-byte selector
"transfer(address,uint256)", // As ABI signature
{ // As ABIFunction
name: 'transfer',
type: 'function',
inputs: [
{ name: 'recipient', type: 'address' },
{ name: 'amount', type: 'uint256' }
],
outputs: [],
stateMutability: 'nonpayable',
}
]);
```
### `allowedTargets`
Restricts which addresses the delegate can call.
#### Parameters
1. Array of addresses as hex strings
#### Example
```typescript
caveatBuilder.addCaveat("allowedTargets", [
"0xc11F3a8E5C7D16b75c9E2F60d26f5321C6Af5E92",
"0xB2880E3862f1024cAC05E66095148C0a9251718b"
]);
```
### `argsEqualityCheck`
Validates that redemption args match specified terms.
#### Parameters
1. Expected args as hex string
#### Example
```typescript
caveatBuilder.addCaveat("argsEqualityCheck",
"0xf2bef872456302645b7c0bb59dcd96ffe6d4a844f311ebf95e7cf439c9393de2"
);
```
### `blockNumber`
Sets valid block range for delegation. Use 0n for no limit.
#### Parameters
1. After block number as `bigint`
2. Before block number as `bigint`
#### Example
```typescript
caveatBuilder.addCaveat("blocknumber",
19426587n,
0n
);
```
### `deployed`
Ensures contract deployment status.
#### Parameters
1. Contract address
2. Factory address
3. Contract bytecode
#### Example
```typescript
caveatBuilder.addCaveat("deployed",
"0xc11F3a8E5C7D16b75c9E2F60d26f5321C6Af5E92",
"0x4fa079Afaa26A601FF458bC826FB498621f5E2e1",
"0x..." // Contract bytecode
);
```
### `erc20TransferAmount`
Limits ERC-20 token transfers.
#### Parameters
1. Token contract address
2. Maximum amount as `bigint`
#### Example
```typescript
caveatBuilder.addCaveat("erc20TransferAmount",
"0xc11F3a8E5C7D16b75c9E2F60d26f5321C6Af5E92",
1_000_000n
);
```
### `erc20BalanceGte`
Ensures minimum ERC-20 balance increase after execution.
#### Parameters
1. Token contract address
2. Minimum increase as `bigint`
#### Example
```typescript
caveatBuilder.addCaveat("erc20BalanceGte",
"0xc11F3a8E5C7D16b75c9E2F60d26f5321C6Af5E92",
1_000_000n
);
```
### `id`
Groups delegations - when one is redeemed, others in group are revoked.
#### Parameters
1. Group ID as number
#### Example
```typescript
caveatBuilder.addCaveat("id", 123456);
```
### `limitedCalls`
Restricts number of executions allowed.
#### Parameters
1. Maximum calls as number
#### Example
```typescript
caveatBuilder.addCaveat("limitedCalls", 1);
```
### `nativeBalanceGte`
Ensures minimum native token balance increase.
#### Parameters
1. Recipient address
2. Minimum increase as `bigint`
˜
#### Example
```typescript
caveatBuilder.addCaveat("nativeBalanceGte",
"0x3fF528De37cd95b67845C1c55303e7685c72F319",
1_000_000n
);
```
### `nativeTokenPayment`
Requires native token payment to use delegation.
#### Parameters
1. Payment recipient address
2. Required payment as `bigint`
#### Example
```typescript
caveatBuilder.addCaveat("nativeTokenPayment",
"0x3fF528De37cd95b67845C1c55303e7685c72F319",
1_000_000n
);
```
### `nativeTokenTransferAmount`
Sets native token transfer limit.
#### Parameters
1. Maximum amount as `bigint`
#### Example
```typescript
caveatBuilder.addCaveat("nativeTokenTransferAmount",
1_000_000n
);
```
### `nonce`
Adds nonce to delegation, revokes previous delegations on increment.
#### Parameters
1. Nonce as hex string
#### Example
```typescript
caveatBuilder.addCaveat("nonce", "0x1");
```
### `redeemer`
Restricts which addresses can redeem the delegation. Note: Delegator accounts can bypass by re-delegations.
#### Parameters
1. Array of allowed redeemer addresses
#### Example
```typescript
caveatBuilder.addCaveat("redeemer", [
"0xb4aE654Aca577781Ca1c5DE8FbE60c2F423f37da",
"0x6be97c23596ECed7170fdFb28e8dA1Ca5cdc54C5"
]);
```
### `timestamp`
Sets valid time range for delegation. Use 0 for no limit.
#### Parameters
1. After timestamp in seconds
2. Before timestamp in seconds
#### Example
```typescript
caveatBuilder.addCaveat("timestamp",
499165200,
1445412480
);
```
### `valueLte`
Limits native token value parameter.
#### Parameters
1. Maximum value as `bigint`
#### Example
```typescript
caveatBuilder.addCaveat("valueLte",
1_000_000_000_000_000_000n // 1 ETH
);
```
## Create a custom caveat enforcer
While the MetaMask Delegation Toolkit provides several out-of-the-box caveat enforcers, you can
create custom ones for more specific needs:
1. Create a contract that extends the `ICaveatEnforcer.sol` interface.
2. Implement the required functions, such as `beforeHook` and `afterHook`.
Here's a basic example of a custom caveat enforcer:
```solidity
// 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 _args,
ModeCode _mode,
bytes calldata _executionCalldata,
bytes32 _delegationHash,
address _delegator,
address _redeemer
)
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_);
}
```
This example shows a `LimitedCallsEnforcer` that restricts the number of times a delegate can
perform transactions on behalf of the delegator.
## Concepts
### Delegation
Delegation allows a smart contract account (the delegator) to grant permission to another account to perform
specific executions on their behalf. Delegations can include caveats to apply rules and restrictions.
A delegation conforms to the following structure:
```typescript
export type DelegationStruct = {
delegate: Hex; // The account that receives permissions to perform executions on behalf of another account.
delegator: Hex; // The account that assigns the permission to another account.
authority: Hex; // The authority under which the delegation is made. The default is ROOT_AUTHORITY.
caveats: CaveatStruct[]; // An array of caveat enforcers.
salt: bigint; // A unique value to ensure the uniqueness of the delegation.
signature: Hex; // The cryptographic signature that verifies the delegation.
};
```
### Caveat Enforcers
Caveat enforcers apply specific conditions or restrictions to a delegation. The MetaMask Delegation
Toolkit provides several out-of-the-box caveat enforcers, including:
- `AllowedCalldataEnforcer.sol`
- `AllowedMethodsEnforcer.sol`
- `AllowedTargetsEnforcer.sol`
- `BlockNumberEnforcer.sol`
- `DeployedEnforcer.sol`
- `ERC20TransferAmountEnforcer.sol`
- `ERC20BalanceGteEnforcer.sol`
- `NonceEnforcer.sol`
- `LimitedCallsEnforcer.sol`
- `IdEnforcer.sol`
- `TimestampEnforcer.sol`
- `ValueLteEnforcer.sol`
- `ArgsEqualityCheckEnforcer.sol`
- `NativeTokenPaymentEnforcer.sol`
- `NativeBalanceGteEnforcer.sol`
- `NativeTokenTransferAmountEnforcer.sol`
- `RedeemerEnforcer.sol`
Each of these enforcers provides specific functionality to limit and control delegated executions.
## Paid delegations
Sometimes you may want to require a user to pay for a delegation. You can do this by using the
`NativeTokenPaymentEnforcer` to accept that EVM chain's native token as payment for the delegation.
```typescript
// Start by initializing two clients, one for the delegator and one for the delegate.
const createCounterfactualSmartAccount = async () => {
const privateKey = generatePrivateKey();
const owner = privateKeyToAccount(privateKey);
const publicClient = createPublicClient({
transport: http(),
chain
});
const account: MetaMaskSmartAccount<Implementation.Hybrid> = await toMetaMaskSmartAccount({
client: publicClient,
implementation: Implementation.Hybrid,
deployParams: [owner.address, [], [], []],
deploySalt: "0x",
signatory: { account: owner },
});
return account;
};
const delegatorSmartAccount = createCounterfactualSmartAccount();
const delegateSmartAccount = createCounterfactualSmartAccount();
const caveatBuilder = createCaveatBuilder(delegatorSmartAccount.environment);
// Set up payment terms
const paymentAmount = parseEther("0.1");
const recipient = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef"; // the recipient of 0.1 ether
caveatBuilder
.addCaveat("nativeTokenPayment", recipient, paymentAmount);
// Create delegation with payment caveat
const delegation: DelegationStruct = createRootDelegation(
delegateClient.account.address,
delegatorClient.account.address,
caveatBuilder
);
// Sign delegation
const signedDelegation = {
...delegation,
signature: await delegatorClient.signDelegation({ delegation })
};
// Create execution
const executionTarget = "0x1234567890abcdef1234567890abcdef12345678"
const executionValue = 0n;
const executionCallData = abi.encodeWithSelector(SomeContract.someMethod.selector);
const execution = createExecution(executionTarget, executionValue, executionCallData);
// Redeem delegation
const delegationsArray = [[signedDelegation]];
const executionsArray = [[execution]];
const redeemDelegationsCalldata = DelegationFramework.encode.redeemDelegations(
delegationsArray,
[SINGLE_DEFAULT_MODE],
executionsArray
);
const userOperationHash = await bundler.sendUserOperation({
account: delegatorSmartAccount,
calls: [
{
to: delegatorSmartAccount.address,
data: redeemDelegationsCalldata
}
],
maxFeePerGas,
maxPriorityFeePerGas
});
```
This example demonstrates creating a delegation with a `NativeTokenPaymentEnforcer` caveat, signing it, and
using it to perform an execution. The caveat ensures that Bob pays Alice 0.1 ether when using the
delegation.
You can create your own conditional caveat enforcer using the `afterHook` function when defining a
custom `CaveatEnforcer` contract. For example, to make sure that the delegator owns a specific NFT
before allowing the delegation to be used (enabling any execution that results in owning that NFT), you
could do something like this:
```solidity
import { CaveatEnforcer } from "@delegator/src/enforcers/CaveatEnforcer.sol";
import { ModeCode } from "../utils/Types.sol";
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
contract NFTOwnershipEnforcer is CaveatEnforcer {
struct OwnershipTerms {
address nftContract;
uint256 tokenId;
}
error NotNFTOwner(address account, address nftContract, uint256 tokenId);
function beforeHook(
bytes calldata _terms,
bytes calldata,
ModeCode,
bytes calldata,
bytes32 _delegationHash,
address,
address _redeemer
)
public
override
{}
function afterHook(
bytes calldata _terms,
bytes calldata _args,
ModeCode _mode,
bytes calldata _executionCalldata,
bytes32 _delegationHash,
address _delegator,
address _redeemer
) public pure override {
OwnershipTerms memory terms = abi.decode(_terms, (OwnershipTerms));
IERC721 nft = IERC721(terms.nftContract);
if (nft.ownerOf(terms.tokenId) != _redeemer) {
revert NotNFTOwner(_redeemer, terms.nftContract, terms.tokenId);
}
}
}
```
This `NFTOwnershipEnforcer` checks if the redeemer owns a specific NFT _after_ allowing the
delegation to be used, but the execution will be reverted if the `afterHook` the owner of the NFT is not the redeemer. It's a
simple state lookup that doesn't modify any state.
To use this enforcer, create a caveat like this:
```typescript
// Create a custom caveat builder
const nftOwnershipBuilder = (
environment: DeleGatorEnvironment,
nft: Address,
tokenId: bigint
): CaveatStruct => {
const terms = encodePacked(["address", "uint256"], [nft, tokenId]);
return {
enforcer: "0xAbCdEf1234567890AbCdEf1234567890AbCdEf12", // Address of the NFTOwnershipEnforcer contract
terms,
args: "0x"
};
};
const caveatBuilder = delegatorClient.createCaveatBuilder()
.extend("nftOwnership", nftOwnershipBuilder)
const nftContract = "0xAbCdEf1234567890AbCdEf1234567890AbCdEf12"; // Random address of the NFT contract
const tokenId = 123n; // ID of the required NFT
const caveats = caveatBuilder
.addCaveat("nftOwnership", nftContract, tokenId);
const delegation: DelegationStruct = createRootDelegation(
delegateAddress,
delegatorAddress,
caveats
);
```
This example demonstrates a minimal state lookup without modifying any state, which is simpler than
the previous payment example.