Create a custom caveat enforcer
This tutorial walks you through creating a custom caveat enforcer and applying it to a delegation.
The MetaMask Delegation Toolkit includes out-of-the-box caveat enforcers that define rules and restrictions for common use cases. For more specific control or other use cases, you can create custom caveat enforcers. In this tutorial, you'll create and apply a caveat enforcer that only allows a delegation to be redeemed after a specific timestamp.
Prerequisites
- Install Node.js v18 or later.
- Install Yarn, npm, or another package manager.
- Install Foundry and Forge.
- Get an Infura API key from the MetaMask Developer dashboard.
- Have a MetaMask account with some Sepolia ETH to deploy your contract.
note
You can use the MetaMask faucet to get Sepolia ETH.
Steps
1. Install the toolkit
Install the MetaMask Delegation Toolkit in your project:
- npm
- Yarn
- pnpm
- Bun
npm install @metamask/delegation-toolkit
yarn add @metamask/delegation-toolkit
pnpm add @metamask/delegation-toolkit
bun add @metamask/delegation-toolkit
2. Create the caveat enforcer
At the root of your project, create a contracts
directory.
Inside that directory, create a new contract named AfterTimestampEnforcer.sol
.
Add the following code to AfterTimestampEnforcer.sol
. This contract implements a caveat enforcer that
extends the ICaveatEnforcer.sol
interface and ensures that a delegation can only be redeemed after
a specific timestamp.
This contract overrides the beforeHook
function, which is responsible for enforcing
conditions before a delegation's execution during the redemption process. In this example, it verifies that
the current block timestamp is later than the defined allowed timestamp.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import { CaveatEnforcer } from "@delegator/src/enforcers/CaveatEnforcer.sol";
import { ModeCode } from "/delegation-toolkit/utils/Types.sol";
contract AfterTimestampEnforcer is CaveatEnforcer {
/**
* @notice The delegation may only be redeemed after the specified timestamp - validAfter in seconds.
* @param _terms The validAfter, timestamp in seconds after which the delegation may be redeemed.
* @param _delegationHash - The hash of the delegation being operated on.
*/
function beforeHook(
bytes calldata _terms,
bytes calldata,
ModeCode,
bytes calldata,
bytes32 _delegationHash,
address,
address _redeemer
)
public
override
{
// Enforces the conditions that should hold before a transaction is performed.
// This function MUST revert if the conditions are not met.
// Get the current timestamp
uint256 timestamp = block.timestamp;
// Convert the encoded `terms` into a uint256 timestamp.
// Casting to bytes32 ensures the data is exactly 32 bytes, matching
// the size of a uint256.
uint256 validAfter = uint256(bytes32(_terms));
require(timestamp > validAfter, "AfterTimestampEnforcer:cannot-redeem-too-early");
}
}
3. Deploy the caveat enforcer
Deploy your custom caveat enforcer using Forge to obtain its contract address.
Replace <YOUR-API-KEY>
with your Infura API key, and <YOUR-PRIVATE-KEY>
with the private key of your MetaMask account:
forge create src/AfterTimestampEnforcer.sol:AfterTimestampEnforcer \
--rpc-url https://sepolia.infura.io/v3/<YOUR-API-KEY> \
--private-key <YOUR-PRIVATE-KEY> \
--broadcast
The Forge CLI will display the address of the deployed caveat enforcer.
4. Apply the caveat enforcer
Specify the address of the deployed AfterTimestampEnforcer.sol
contract, add it to the caveat builder, and create a delegation.
The following code snippet uses the custom caveat enforcer to create a delegation granting a 0.01 ETH allowance that becomes spendable one hour after it is created:
- delegation.ts
- config.ts
import { createDelegation, ROOT_AUTHORITY } from '@metamask/delegation-toolkit'
import { createCaveatBuilder } from '@metamask/delegation-toolkit/utils'
import { toHex, parseEther } from 'viem'
import { delegatorSmartAccount } from './config.ts'
const environment = delegatorSmartAccount.environment
// Replace this with the address of the deployed AfterTimestampEnforcer.sol contract.
const afterTimestampEnforcer = '0x22Ae4c4919C3aB4B5FC309713Bf707569B74876F'
const caveatBuilder = createCaveatBuilder(environment)
// Since block.timestamp is in seconds, convert milliseconds to seconds.
const currentTime = Math.floor(Date.now() / 1000)
// Add an hour to the currentTime
const validTimestamp = currentTime + 3600
const caveats = caveatBuilder.addCaveat('nativeTokenTransferAmount', parseEther('0.01')).addCaveat({
enforcer: afterTimestampEnforcer,
terms: toHex(validTimestamp),
})
const delegation: Delegation = {
delegate: "DELEGATE_ADDRESS",
delegator: delegatorSmartAccount.address,
authority: ROOT_AUTHORITY,
caveats: caveats.build(),
salt: '0x',
};
import { createPublicClient, http } from 'viem'
import { sepolia as chain } from 'viem/chains'
import { Implementation, toMetaMaskSmartAccount } from '@metamask/delegation-toolkit'
const publicClient = createPublicClient({
chain,
transport: http(),
})
const privateKey = generatePrivateKey()
const account = privateKeyToAccount(privateKey)
export const delegatorSmartAccount = await toMetaMaskSmartAccount({
client: publicClient,
implementation: Implementation.Hybrid,
deployParams: [account.address, [], [], []],
deploySalt: '0x',
signer: { account },
})
You've successfully created, deployed, and applied a custom caveat enforcer!
For production use cases, you might need to add additional caveats to restrict the delegation further. Learn more about caveat enforcers.