Skip to main content

Delegation Toolkit documentation

Embed MetaMask Smart Accounts into your dapp, enabling new user experiences.

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

Steps

1. Create the caveat enforcer

At the root of your project, create a contracts directory. In that directory, create a new contract named AfterTimestampEnforcer.sol.

Add the following code to AfterTimestampEnforcer.sol, which creates a caveat enforcer that extends the ICaveatEnforcer.sol interface and only allows a delegation to be redeemed after a specific timestamp:

AfterTimestampEnforcer.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

import { CaveatEnforcer } from "@delegator/src/enforcers/CaveatEnforcer.sol";
import { ModeCode } from "../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;

uint256 validAfter = uint256(bytes32(_terms));

require(timestamp > validAfter, "AfterTimestampEnforcer:cannot-redeem-too-early");
}
}

2. 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.

3. Apply the caveat enforcer

Specify the address of the deployed AfterTimestampEnforcer.sol contract, add it to the caveat builder, and create a delegation. Learn more about applying caveats to a delegation.

The following code snippet uses the custom caveat enforcer to create a delegation granting a 1,000,000 wei allowance that becomes spendable one hour after it is created:

import {
createCaveatBuilder,
createDelegation,
} from "@metamask/delegation-toolkit";
import { toHex } 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);

const tenAM = 10 * 60 * 60; // 10:00 AM as seconds since midnight.

const caveats = caveatBuilder
.addCaveat("nativeTokenTransferAmount", 1_000_000)
.addCaveat({
enforcer: afterTimestampEnforcer,
terms: toHex(tenAm)
});

const delegation = createDelegation({
to: delegate,
from: delegatorSmartAccount.address,
caveats
});

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.