EIP-7702 with Account Abstraction
Learn how to execute ERC-4337 UserOperations through an EIP-7702 delegated EOA using Tachyon.
EIP-7702 with Account Abstraction
This guide demonstrates how to combine EIP-7702 authorization with ERC-4337 Account Abstraction to execute UserOperations through an EOA delegated to a smart account implementation.
Overview
By leveraging EIP-7702, you can temporarily delegate your EOA to an ERC-4337 compatible smart account, enabling:
- Gas Sponsorship: Execute transactions without holding native tokens
- Batched Operations: Multiple contract calls in a single transaction
- Custom Validation: Advanced signature schemes and access controls
- No Migration Required: Use smart account features without deploying a new wallet
Prerequisites
- Node.js 18+
- A funded EOA wallet
- Tachyon API key
- Basic understanding of EIP-7702 and Account Abstraction
Step-by-Step Implementation
Install Dependencies
npm install viem @rathfi/tachyonImport Required Modules
import {
createWalletClient,
createPublicClient,
http,
encodeFunctionData,
encodePacked,
concat,
pad,
numberToHex,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";
import { Tachyon } from "@rathfi/tachyon";
import {
entryPoint07Abi,
entryPoint07Address,
getUserOperationHash,
} from "viem/account-abstraction";Configure Entry Point and Clients
Set up the ERC-4337 EntryPoint v0.7 configuration and create the necessary clients:
// Entry point configuration (v0.7)
const entryPoint = {
address: entryPoint07Address as `0x${string}`,
version: "0.7" as const,
abi: entryPoint07Abi,
};
// Public client for reading blockchain state
const publicClient = createPublicClient({
transport: http(),
chain: base,
});
// Wallet client with your EOA
const walletClient = createWalletClient({
account: privateKeyToAccount(process.env.PRIVATE_KEY! as `0x${string}`),
transport: http(),
chain: base,
});
// Contract addresses
const delegateContractAddress = "0xd6CEDDe84be40893d153Be9d467CD6aD37875a28"; // ERC-4337 Account implementation
const beneficiary = "0x4C16955d8A0DcB2e7826d50f4114990c787b21E7"; // Bundler beneficiary
const targetContract = "0xA7A833e6641D7901F30EaD6f27d4Ee2C9bb670a7"; // Your target contractCreate EIP-7702 Authorization (First Transaction Only)
Sign an authorization to delegate your EOA to the smart account implementation:
// Track delegation state - set to true after first successful delegation
const isAlreadyDelegated = false;
async function getAuthorizationList() {
if (isAlreadyDelegated) {
console.log("Skipping delegation - address already delegated");
return undefined;
}
// Sign the authorization - viem handles nonce automatically
const authorization = await walletClient.signAuthorization({
contractAddress: delegateContractAddress,
});
// Format authorization for Tachyon
const auth = {
chainId: authorization.chainId,
address: authorization.address,
nonce: Number(authorization.nonce),
r: authorization.r,
s: authorization.s,
v: Number(authorization.v),
yParity: Number(authorization.yParity) as 0 | 1,
};
console.log("Authorization created for delegation");
return [auth];
}After the first transaction with authorization, your EOA will be delegated to the smart account. Subsequent transactions can skip the authorization step by setting isAlreadyDelegated to true.
Encode the Target Call Data
Prepare the function call you want to execute:
// Example: Calling a sayHello function
const sayHelloAbi = [
{
inputs: [{ internalType: "string", name: "message", type: "string" }],
name: "sayHello",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
] as const;
const sayHelloCallData = encodeFunctionData({
abi: sayHelloAbi,
functionName: "sayHello",
args: ["Hello from Tachyon!"],
});Build the UserOperation Call Data
Encode the execution data for the smart account's execute function:
// Encode execution payload: target address + value + calldata
const executionPayload = encodePacked(
["address", "uint256", "bytes"],
[
targetContract as `0x${string}`,
BigInt(0), // No ETH value
sayHelloCallData as `0x${string}`,
]
);
// Encode the execute function call for the smart account
const executeAbi = [
{
inputs: [
{ internalType: "ExecMode", name: "execMode", type: "bytes32" },
{ internalType: "bytes", name: "executionCalldata", type: "bytes" },
],
name: "execute",
outputs: [],
stateMutability: "payable",
type: "function",
},
] as const;
const userOperationCallData = encodeFunctionData({
abi: executeAbi,
functionName: "execute",
args: [
"0x0000000000000000000000000000000000000000000000000000000000000000", // Default exec mode
executionPayload,
],
});Create and Sign the UserOperation
Build the UserOperation structure and sign it:
// Get nonce from EntryPoint
const nonce = await publicClient.readContract({
address: entryPoint.address,
abi: entryPoint.abi,
functionName: "getNonce",
args: [walletClient.account.address, BigInt(0)],
blockTag: "pending",
});
// Gas limits
const callGasLimit = BigInt(500_000);
const verificationGasLimit = BigInt(1_200_000);
const preVerificationGas = BigInt(100_000);
// Create UserOperation
const userOperation = {
sender: walletClient.account.address,
nonce: nonce,
initCode: "0x" as `0x${string}`,
callData: userOperationCallData,
callGasLimit: callGasLimit,
verificationGasLimit: verificationGasLimit,
preVerificationGas: preVerificationGas,
maxFeePerGas: BigInt(0),
maxPriorityFeePerGas: BigInt(0),
paymasterAndData: "0x" as `0x${string}`,
signature: "0x" as `0x${string}`,
};
// Get UserOperation hash
const userOperationHash = getUserOperationHash({
userOperation,
entryPointAddress: entryPoint.address,
entryPointVersion: entryPoint.version,
chainId: base.id,
});
// Sign the UserOperation hash with EOA
const signature = await walletClient.signMessage({
message: { raw: userOperationHash },
});
userOperation.signature = signature;
console.log("UserOperation Hash:", userOperationHash);
console.log("Signature:", signature);Encode the handleOps Call
Prepare the EntryPoint handleOps function call:
const handleOpsCallData = encodeFunctionData({
abi: entryPoint.abi,
functionName: "handleOps",
args: [
[
{
sender: userOperation.sender,
nonce: userOperation.nonce,
initCode: userOperation.initCode || "0x",
callData: userOperation.callData,
accountGasLimits: concat([
pad(numberToHex(userOperation.verificationGasLimit || BigInt(0)), {
size: 16,
}),
pad(numberToHex(userOperation.callGasLimit || BigInt(0)), {
size: 16,
}),
]),
preVerificationGas: userOperation.preVerificationGas,
gasFees: concat([
pad(numberToHex(BigInt(0)), { size: 16 }),
pad(numberToHex(BigInt(0)), { size: 16 }),
]),
paymasterAndData: userOperation.paymasterAndData || "0x",
signature: userOperation.signature,
},
],
beneficiary as `0x${string}`,
],
});Submit via Tachyon
Initialize the Tachyon SDK and submit the transaction:
// Calculate gas limit with buffer
const relayGasLimit =
(userOperation.callGasLimit +
userOperation.verificationGasLimit +
userOperation.preVerificationGas) *
BigInt(2);
// Initialize Tachyon SDK
const tachyon = new Tachyon({
apiKey: process.env.TACHYON_API_KEY!,
});
// Get authorization list (only needed for first transaction)
const authorizationList = await getAuthorizationList();
// Submit transaction
const taskId = await tachyon.relay({
chainId: base.id,
to: entryPoint.address,
callData: handleOpsCallData,
value: "0",
gasLimit: relayGasLimit.toString(),
...(authorizationList
? { authorizationList }
: { transactionType: "flash-blocks" }),
});
console.log("Task ID:", taskId);
// Wait for execution
const tx = await tachyon.waitForExecutionHash(taskId, 30_000);
console.log("Transaction executed:", tx);
console.log("Called sayHello via EIP-7702 delegated address through EntryPoint");Complete Example
Here's the full implementation combining all the steps:
import {
createWalletClient,
createPublicClient,
http,
encodeFunctionData,
encodePacked,
concat,
pad,
numberToHex,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";
import { Tachyon } from "@rathfi/tachyon";
import {
entryPoint07Abi,
entryPoint07Address,
getUserOperationHash,
} from "viem/account-abstraction";
// Configuration
const entryPoint = {
address: entryPoint07Address as `0x${string}`,
version: "0.7" as const,
abi: entryPoint07Abi,
};
const delegateContractAddress = "0xd6CEDDe84be40893d153Be9d467CD6aD37875a28";
const beneficiary = "0x4C16955d8A0DcB2e7826d50f4114990c787b21E7";
const targetContract = "0xA7A833e6641D7901F30EaD6f27d4Ee2C9bb670a7";
// Set to true after first successful delegation
const isAlreadyDelegated = false;
const publicClient = createPublicClient({
transport: http(),
chain: base,
});
const walletClient = createWalletClient({
account: privateKeyToAccount(process.env.PRIVATE_KEY! as `0x${string}`),
transport: http(),
chain: base,
});
async function executeEIP7702WithAA() {
// Step 1: Get authorization (only for first transaction)
let authorizationList = undefined;
if (!isAlreadyDelegated) {
const authorization = await walletClient.signAuthorization({
contractAddress: delegateContractAddress,
});
authorizationList = [
{
chainId: authorization.chainId,
address: authorization.address,
nonce: Number(authorization.nonce),
r: authorization.r,
s: authorization.s,
v: Number(authorization.v),
yParity: Number(authorization.yParity) as 0 | 1,
},
];
console.log("Authorization created");
}
// Step 2: Encode target call
const sayHelloCallData = encodeFunctionData({
abi: [
{
inputs: [{ internalType: "string", name: "message", type: "string" }],
name: "sayHello",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
],
functionName: "sayHello",
args: ["Hello from Tachyon!"],
});
// Step 3: Build execution payload
const executionPayload = encodePacked(
["address", "uint256", "bytes"],
[targetContract as `0x${string}`, BigInt(0), sayHelloCallData as `0x${string}`]
);
const userOperationCallData = encodeFunctionData({
abi: [
{
inputs: [
{ internalType: "ExecMode", name: "execMode", type: "bytes32" },
{ internalType: "bytes", name: "executionCalldata", type: "bytes" },
],
name: "execute",
outputs: [],
stateMutability: "payable",
type: "function",
},
],
functionName: "execute",
args: [
"0x0000000000000000000000000000000000000000000000000000000000000000",
executionPayload,
],
});
// Step 4: Get nonce and create UserOperation
const nonce = await publicClient.readContract({
address: entryPoint.address,
abi: entryPoint.abi,
functionName: "getNonce",
args: [walletClient.account.address, BigInt(0)],
blockTag: "pending",
});
const userOperation = {
sender: walletClient.account.address,
nonce: nonce,
initCode: "0x" as `0x${string}`,
callData: userOperationCallData,
callGasLimit: BigInt(500_000),
verificationGasLimit: BigInt(1_200_000),
preVerificationGas: BigInt(100_000),
maxFeePerGas: BigInt(0),
maxPriorityFeePerGas: BigInt(0),
paymasterAndData: "0x" as `0x${string}`,
signature: "0x" as `0x${string}`,
};
// Step 5: Sign UserOperation
const userOperationHash = getUserOperationHash({
userOperation,
entryPointAddress: entryPoint.address,
entryPointVersion: entryPoint.version,
chainId: base.id,
});
userOperation.signature = await walletClient.signMessage({
message: { raw: userOperationHash },
});
// Step 6: Encode handleOps
const handleOpsCallData = encodeFunctionData({
abi: entryPoint.abi,
functionName: "handleOps",
args: [
[
{
sender: userOperation.sender,
nonce: userOperation.nonce,
initCode: "0x",
callData: userOperation.callData,
accountGasLimits: concat([
pad(numberToHex(userOperation.verificationGasLimit), { size: 16 }),
pad(numberToHex(userOperation.callGasLimit), { size: 16 }),
]),
preVerificationGas: userOperation.preVerificationGas,
gasFees: concat([
pad(numberToHex(BigInt(0)), { size: 16 }),
pad(numberToHex(BigInt(0)), { size: 16 }),
]),
paymasterAndData: "0x",
signature: userOperation.signature,
},
],
beneficiary as `0x${string}`,
],
});
// Step 7: Submit via Tachyon
const relayGasLimit =
(userOperation.callGasLimit +
userOperation.verificationGasLimit +
userOperation.preVerificationGas) *
BigInt(2);
const tachyon = new Tachyon({
apiKey: process.env.TACHYON_API_KEY!,
});
const taskId = await tachyon.relay({
chainId: base.id,
to: entryPoint.address,
callData: handleOpsCallData,
value: "0",
gasLimit: relayGasLimit.toString(),
...(authorizationList
? { authorizationList }
: { transactionType: "flash-blocks" }),
});
console.log("Task ID:", taskId);
const tx = await tachyon.waitForExecutionHash(taskId, 30_000);
console.log("Transaction executed:", tx);
return tx;
}
executeEIP7702WithAA();Key Considerations
Security: Never expose your private key in client-side code. Use secure key management solutions in production.
- Delegation Persistence: After the first transaction with
authorizationList, your EOA remains delegated until you explicitly revoke it - Gas Estimation: The example uses fixed gas limits. In production, consider using gas estimation APIs for optimal values
- Nonce Management: The EntryPoint manages its own nonce system, separate from the EOA's transaction nonce
- Chain Support: EIP-7702 is supported on specific chains. Check supported networks for availability
Related Resources
- EIP-7702 Authorization - Learn more about EIP-7702 delegation
- Relay Transactions - Basic transaction relay guide
- Tachyon SDK Reference - Complete SDK documentation