Account abstraction with EIP-7702
This guide applies EIP-7702 to an EOA and uses the delegation to batch several USDT0 transfers into one atomic transaction. The EOA keeps its address and private key throughout. For session keys and spending limits, see Session keys with EIP-7702, which uses an audited smart account instead of a custom delegate.
Prerequisites
- Understanding of EOA vs. smart contract accounts (EOAs have no code by default).
- Familiarity with EVM transaction types (EIP-2718).
- Foundry to deploy the delegate contract.
Overview
EIP-7702 introduces a new transaction type (0x04) that carries an authorizationList. Each authorization designates a smart contract whose code the EOA executes. The flow is:
- Choose or deploy a delegate contract: a standard Solidity contract implementing the logic you want EOAs to use. Use an audited contract whenever possible.
- Sign an authorization: the EOA owner signs a message authorizing the delegate contract.
- Submit an EIP-7702 transaction: the transaction includes the authorization, and the EOA runs the delegate's code during execution.
Use case: batch transactions
Batching runs several calls (token transfers, approvals, or any mix) in one atomic transaction. Batch payments are one example: instead of sending ten separate transactions for a payroll run, the EOA executes them all at once.
Step 1: Deploy a batch executor
BatchExecutor runs a list of calls in order and reverts the whole batch if any call fails. It accepts calls only from the EOA itself, so only the owner's key can trigger a batch.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @title BatchExecutor (example, unaudited)
/// @notice EIP-7702 delegate that executes a batch of calls from the EOA.
contract BatchExecutor {
struct Call {
address target;
uint256 value;
bytes data;
}
function execute(Call[] calldata calls) external payable {
// SAFE: only the EOA itself, signing with its own key, can run a batch.
require(msg.sender == address(this), "only self");
for (uint256 i = 0; i < calls.length; i++) {
(bool success,) = calls[i].target.call{value: calls[i].value}(calls[i].data);
require(success, "call failed");
}
}
}Deploy it once. Any number of EOAs can delegate to the same deployment.
forge create src/BatchExecutor.sol:BatchExecutor \
--rpc-url https://rpc.testnet.stable.xyz \
--private-key $PRIVATE_KEY \
--broadcastDeployer: 0xYourAddress...
Deployed to: 0xBatchExecutor...
Transaction hash: 0x...Step 2: Sign an authorization
The EOA owner signs an authorization that designates the delegate contract. This authorization is included in the EIP-7702 transaction.
// config.ts
import { ethers } from "ethers";
export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz";
export const STABLE_TESTNET_CHAIN_ID = 2201;
export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9";
export const DELEGATE_ADDRESS = "0xBatchExecutor..."; // from Step 1
export const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC);
export const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);// signAuthorization.ts
import { DELEGATE_ADDRESS, STABLE_TESTNET_CHAIN_ID, provider, wallet } from "./config";
export async function signAuthorization() {
const txNonce = await provider.getTransactionCount(wallet.address);
return wallet.authorize({
chainId: STABLE_TESTNET_CHAIN_ID,
address: DELEGATE_ADDRESS,
// The EOA sends this transaction itself. Its nonce increments before the
// authorization is checked, so the authorization must use the next nonce.
nonce: txNonce + 1,
});
}Step 3: Submit an EIP-7702 transaction
Combine the authorization with a call to execute. This example batches three USDT0 transfers in a single transaction.
import { ethers } from "ethers";
import { wallet, USDT0_ADDRESS } from "./config";
import { signAuthorization } from "./signAuthorization";
const usdt0Interface = new ethers.Interface([
"function transfer(address to, uint256 amount)",
]);
const batchInterface = new ethers.Interface([
"function execute((address target, uint256 value, bytes data)[] calls)",
]);
async function main() {
const recipients = [
{ to: "0xAlice...", amount: ethers.parseUnits("100", 6) },
{ to: "0xBob...", amount: ethers.parseUnits("200", 6) },
{ to: "0xCarol...", amount: ethers.parseUnits("150", 6) },
];
const batchData = batchInterface.encodeFunctionData("execute", [
recipients.map(({ to, amount }) => ({
target: USDT0_ADDRESS,
value: 0n,
data: usdt0Interface.encodeFunctionData("transfer", [to, amount]),
})),
]);
const signedAuth = await signAuthorization();
const tx = await wallet.sendTransaction({
type: 4, // EIP-7702 transaction type
to: wallet.address, // call is directed at the EOA itself
data: batchData, // execute() call to run
authorizationList: [signedAuth],
maxPriorityFeePerGas: 0n,
});
const receipt = await tx.wait(1);
console.log("Batch transactions executed in tx:", receipt.hash);
}
main();Batch transactions executed in tx: 0x...The EOA executes all three transfers in a single atomic transaction. The delegation persists, so later batches only need a regular transaction to wallet.address with execute calldata and no authorizationList.
Spending limits and session keys
Spending limits and session keys let a second key act for the EOA within rules the owner sets. Use an audited smart account for them rather than a custom delegate:
- Access control is the whole feature. A missed caller check in a custom delegate exposes the EOA to anyone, not only to the session key.
- Storage outlives the delegate. Delegate state lives in the EOA's storage. Re-delegating to a contract with a different storage layout can misread or overwrite it.
Session keys with EIP-7702 delegates the EOA to Modular Account v2 and grants a key an expiring USDT0 allowance. That allowance is the spending limit.
Important considerations
- Persistent delegation: the delegation persists until the EOA explicitly changes or clears it. It is not limited to a single transaction.
- Gas costs: EIP-7702 transactions have slightly higher base gas due to authorization processing, offset when the delegate batches multiple calls.
- Use audited delegates: a malicious or unguarded delegate contract can drain the EOA's assets.
BatchExecutorabove is an unaudited example. Only delegate to audited contracts in production.
Key takeaways
- EIP-7702 lets EOAs execute smart contract logic without migrating to a new account type.
- On Stable, EIP-7702 enables batch payments, spending limits, and scoped session keys on existing EOAs.
- Every function on the delegate is callable on the EOA by anyone, so the delegate must check the caller.
- The delegation persists until explicitly changed. Always use an audited delegate contract.
Next recommended
- Session keys with EIP-7702: Grant a scoped, expiring key that sends transactions without the owner key.
- Subscribe and collect: Apply EIP-7702 to recurring subscription payments with a SubscriptionManager.
- EIP-7702 concept: Understand the delegation model before you ship it.
- EIP-7702 reference: Look up the
0x04transaction format and authorization fields.

