Enable Relaying Using EIP-7702

To batch transactions and use gas‑sponsored UserOperations with Zircuit you must first delegate your account to the Zircuit Relayer Account via an EIP‑7702 SetCode transaction. The guide below shows how to do this securely with Foundry’s cast CLI—without ever pasting your private key into the browser.

Why Delegation Is Required

EIP‑7702 lets you temporarily replace your externally‑owned account’s code for a single transaction batch—perfect for ERC‑4337 wallets. Until mainstream wallets such as MetaMask or Rabby add native support for sending a SetCode transaction to arbitrary addresses, you have to perform the delegation yourself.

Security Principles

  • Never paste your private key into any website form or browser console.

  • Export the key only in a hardened, preferably offline shell session.

  • Once delegation is confirmed, remove the key from your environment.

Prerequisites

Foundry must be installed as described in the official installation guide here.

1

Export your private key in a local terminal

export PRIVATE_KEY=0xYOUR_PRIVATE_KEY
2

Send the SetCode transaction to delegate to ZircuitRelayerAccount

cast send $(cast az) --auth <ZIRCUIT_RELAYER_ADDRESS> --private-key $PRIVATE_KEY --rpc-url https://mainnet.zircuit.com
3

Verify the delegation on https://explorer.zircuit.com, verifying your address has been successfully delegated

Next Steps

Contract Details

Zircuit Relayer Account address:

Garfield Testnet: 0xD4f99Ef25e5aAB3A0575D9C2dB2E4f09f18442D8

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.28;

import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import "@openzeppelin/contracts/interfaces/IERC1271.sol";
import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../core/Helpers.sol";
import "../core/BaseAccount.sol";

/**
 * ZircuitRelayerAccount.sol
 * A minimal account to be used with EIP‑7702 (for batching) and ERC‑4337 (for gas sponsoring)
 */
contract ZircuitRelayerAccount is BaseAccount, IERC165, IERC1271, ERC1155Holder, ERC721Holder {
    address public immutable ZIRCUIT_TOKEN_ADDRESS;
    address public immutable ZIRCUIT_PAYMASTER_ADDRESS;
    IEntryPoint public immutable ENTRY_POINT;

    constructor(address _zircuitTokenAddress, IEntryPoint _entryPoint, address _zircuitPaymasterAddress) {
        ZIRCUIT_TOKEN_ADDRESS = _zircuitTokenAddress;
        ENTRY_POINT = _entryPoint;
        ZIRCUIT_PAYMASTER_ADDRESS = _zircuitPaymasterAddress;
    }

    // address of entryPoint v0.8
    function entryPoint() public view override returns (IEntryPoint) {
        return ENTRY_POINT;
    }

    /**
     * Make this account callable through ERC‑4337 EntryPoint.
     * The UserOperation should be signed by this account's private key.
     */
    function _validateSignature(PackedUserOperation calldata userOp, bytes32 userOpHash)
        internal
        virtual
        override
        returns (uint256 validationData)
    {
        if (!_checkSignature(userOpHash, userOp.signature)) {
            return SIG_VALIDATION_FAILED;
        }

        bytes calldata paymasterAndData = userOp.paymasterAndData;
        // Check if paymasterAndData exists and has at least the 52‑byte prefix
        if (paymasterAndData.length >= 52) {
            // Extract paymaster address from prefix (needed for approve)
            address paymasterAddress = address(bytes20(paymasterAndData[0:20]));

            if (paymasterAddress == ZIRCUIT_PAYMASTER_ADDRESS) {
                // Bytes 52‑83: Packed data (mode + validUntil + validAfter)
                // Bytes 84‑115: Amount (32 bytes)

                // Extract packed data (bytes 52‑83)
                bytes32 packedData;
                assembly {
                    packedData := calldataload(add(paymasterAndData.offset, 52))
                }

                // Extract mode from packed data (first byte)
                uint8 mode = uint8(uint256(packedData) >> 248);

                require(mode <= 1, "Unsupported paymaster mode");

                // If mode is 1, approve the paymaster for the specified amount
                if (mode == 1) {
                    require(paymasterAddress != address(0), "Invalid paymaster address"); // Sanity check
                    uint256 amount;
                    assembly {
                        amount := calldataload(add(paymasterAndData.offset, 84))
                    }

                    IERC20(ZIRCUIT_TOKEN_ADDRESS).approve(paymasterAddress, amount);
                }
            }
        }

        return SIG_VALIDATION_SUCCESS;
    }

    function isValidSignature(bytes32 hash, bytes memory signature) public view returns (bytes4 magicValue) {
        return _checkSignature(hash, signature) ? this.isValidSignature.selector : bytes4(0xffffffff);
    }

    function _checkSignature(bytes32 hash, bytes memory signature) internal view returns (bool) {
        return ECDSA.recover(hash, signature) == address(this);
    }

    function _requireForExecute() internal view virtual override {
        require(msg.sender == address(this) || msg.sender == address(ENTRY_POINT), "not from self or EntryPoint");
    }

    function supportsInterface(bytes4 id) public pure override(ERC1155Holder, IERC165) returns (bool) {
        return id == type(IERC165).interfaceId || id == type(IAccount).interfaceId || id == type(IERC1271).interfaceId
            || id == type(IERC1155Receiver).interfaceId || id == type(IERC721Receiver).interfaceId;
    }

    // accept incoming calls (with or without value), to mimic an EOA.
    fallback() external payable {}

    receive() external payable {}
}

Last updated

Was this helpful?