Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

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:

  1. Choose or deploy a delegate contract: a standard Solidity contract implementing the logic you want EOAs to use. Use an audited contract whenever possible.
  2. Sign an authorization: the EOA owner signs a message authorizing the delegate contract.
  3. 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 \
  --broadcast
Deployer: 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. BatchExecutor above 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