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