LayerZeroFault
wallet security-fixes Updated:

EIP-7702 Delegation Replay Vulnerability

VV

Written by

Updated:

Technical Guide

To prevent EIP-7702 authorization replay attacks across different chains and execution contexts, always enforce explicit chainId domain binding and strictly monotone nonce validation in every signed authorization tuple. Authorizations signed with chainId = 0 are universally valid across all EVM forks, exposing EOAs to cross-chain delegation hijacking. Always sign tuples with the exact target EIP-155 chain ID and verify nonce synchronization through private RPC endpoints.

Ethereum’s Prague/Electra upgrade introduced EIP-7702 (Set Code for EOA at Transaction), revolutionizing Account Abstraction by allowing standard Externally Owned Accounts (EOAs) to temporarily delegate their code execution to a smart contract without deploying a separate ERC-4337 smart account contract.

While EIP-7702 solves key onboarding and batching hurdles, improper verification of the authorization signature tuple introduces lethal attack vectors: cross-chain authorization replay, perpetual delegation hijacking, and mempool front-running.

If you are securing decentralized application communications or embedded iframe messaging, also inspect our companion analysis on WalletConnect postMessage Missing event.origin Validation.


[!TIP] Diagnostic Verification: Decode and verify cross-chain UserOperations and signature parameters in our ERC-4337 UserOp Decoder. For recovery steps if delegation code is compromised, see our EIP-7702 Revocation to Zero Address Guide.

Technical Architecture of EIP-7702 (Transaction Type 0x04)

Under EIP-7702, a new transaction envelope type 0x04 is defined. The transaction payload contains a list of authorizations:

$$\text{authorization_list} = [[\text{chain_id}, \text{address}, \text{nonce}, y_parity, r, s], \dots]$$

When the EVM processes an authorization:

  1. The EVM recovers the signer $A$ from the tuple $(y_parity, r, s)$ hashed against keccak256(MAGIC || rlp([chain_id, address, nonce])).
  2. The EVM checks that the signer’s on-chain nonce matches nonce.
  3. If valid, the EVM sets the code of account $A$ to the delegation designator bytecode:

$$\texttt{0xef0100} ,|, \text{address}_{20}$$ Any subsequent calls made to account $A$ during or after the transaction will execute the runtime code deployed at address via a context-preserving delegatecall into the target contract.


Threat Vector 1: Cross-Chain Replay via chain_id = 0

The Vulnerability

EIP-7702 allows chain_id in the authorization tuple to be set to 0 to signify wildcard / multi-chain authorization:

// DANGEROUS PATTERN: Wildcard chain_id authorization
const dangerousAuth = {
  chainId: 0n, // Valid on EVERY EVM chain!
  address: "0xDef1Ca1000000000000000000000000000000000",
  nonce: currentNonce,
};

If an EOA signs this tuple to test batch transactions on Sepolia or a local rollup, any observer extracting the tuple from the public mempool can replay the identical tuple on Ethereum Mainnet, Base, Arbitrum, or Polygon.

If the implementation contract at 0xDef1Ca... on Sepolia does not exist or has different bytecode on Mainnet (or worse, is deployed by an adversary via CREATE2), the attacker gains arbitrary execution privileges over the user’s mainnet wallet.

Mitigation: Strict Chain ID Binding

Never permit client-side signing of chainId = 0 in production dApps or wallet SDKs:

// SECURE PATTERN: Enforce explicit active chain ID
import { getChainId } from 'viem/actions';

async function signEip7702Authorization(client: any, targetDelegate: `0x${string}`) {
  const activeChainId = await client.getChainId();
  if (activeChainId === 0) {
    throw new SecurityError("Wildcard chain_id (0) is strictly disallowed under safety protocol.");
  }

  const nonce = await client.getTransactionCount({ address: client.account.address });

  return await client.signAuthorization({
    contractAddress: targetDelegate,
    chainId: activeChainId,
    nonce: nonce,
  });
}

Threat Vector 2: The “Perpetual Zombie Delegation” Trap

Unlike early drafts where delegation expired at the end of the transaction, EIP-7702 preserves code designations across blocks until explicitly overwritten or revoked. If an EOA delegates to an account abstraction implementation (e.g., a modular multisig or session key validator) and later resumes using their wallet as a normal EOA:

  1. The delegation bytecode 0xef0100... remains written in their account code slot.
  2. If the delegated contract implementation contained an unprotected selfdestruct (on legacy chains), uninitialized proxy storage, or an arbitrary execute(address to, uint256 value, bytes data) function without access controls, attackers can drain any native ETH or ERC-20 tokens subsequently sent to that EOA.

Defensive Code: Emergency Revocation Script

To revoke an existing code delegation, an EOA must submit a new Type 0x04 transaction targeting address 0x0:

import { createWalletClient, http } from 'viem';
import { mainnet } from 'viem/chains';

const client = createWalletClient({
  chain: mainnet,
  transport: http(),
});

async function revokeDelegation(account: any) {
  const currentNonce = await client.getTransactionCount({ address: account.address });

  // Target 0x0000000000000000000000000000000000000000 clears the code designator
  const revokeAuth = await client.signAuthorization({
    account,
    contractAddress: '0x0000000000000000000000000000000000000000',
    chainId: mainnet.id,
    nonce: currentNonce,
  });

  const txHash = await client.sendTransaction({
    account,
    to: account.address,
    value: 0n,
    authorizationList: [revokeAuth],
  });

  console.log("Delegation revoked in tx:", txHash);
}

Threat Vector 3: Nonce Invalidation Front-Running

Because an EIP-7702 authorization requires auth.nonce == account.nonce:

  1. If an attacker sees a pending Type 0x04 transaction in the public mempool that they wish to censor or disrupt, they can front-run it by submitting a zero-value transaction with higher priority fee from the victim’s account (if private key is partially leaked) or causing a state clash.
  2. Conversely, if a dApp user signs an authorization tuple and concurrently executes a standard transfer before the bundler submits the authorization, the user’s nonce increments from $N$ to $N+1$, permanently invalidating the signed authorization tuple and causing bundler simulation failures (nonce mismatch).

Developer Best Practice: Atomic Bundling

Bundlers and smart account SDKs must enforce that the transaction initiating the EIP-7702 set code operation is the exact transaction that increments the signer’s nonce:

// Smart Contract Implementation Guard
abstract contract Safe7702Implementation {
    // Prevent uninitialized implementation hijacking
    address private immutable _deployer;

    constructor() {
        _deployer = msg.sender;
    }

    modifier onlySelf() {
        // Under EIP-7702 delegatecall, address(this) is the delegating EOA
        require(msg.sender == address(this), "7702: Caller not self");
        _;
    }
}

Defensive Engineering Audit Summary

RiskExploit MechanismArchitectural Remedy
Cross-Chain ReplaychainId = 0 in authorization tupleDisallow chainId: 0 in client signer modules.
Zombie DelegationUnrevoked bytecode left in EOA code slotAutomate 0x0 address revocation when session terminates.
Mempool Nonce ClashNonce increments prior to bundle miningBroadcast authorizations through private RPC endpoints (e.g. Flashbots Protect).
Direct State HijackDelegate contract missing initializer guardsEnsure delegate implementation uses immutable storage or slot-isolated storage (ERC-7201).

Fact-Checked by Victor Vance, Senior Smart Contract Security Analyst & Node Operator.

Interactive Diagnostic Sandbox

Local UserOp & Protocol Security Sandbox

Decode UserOperation calldata, simulate prefund accounting, and test bundler validation rules in your local browser.

Companion Diagnostic Guide WalletConnect postMessage Listener Missing event.origin Validation →

Resolve the critical security vulnerability in WalletConnect where postMessage event.origin is not validated. Learn how to secure your iframe communications.

Read Guide
Contextual Architecture

Related Diagnostic Guides

View All in Silo →

Related Inquiries

Why does EIP-7702 permit cross-chain replay attacks?

When an authorization tuple is signed with chainId = 0, the signature is mathematically valid on any EVM network where the signer's nonce matches. Attackers can replay the authorization on alternative L2s to hijack identical EOA addresses.

How do you bind an EIP-7702 authorization to a single chain?

Specify the explicit EIP-155 chain ID (e.g., 1 for Ethereum Mainnet, 8453 for Base, 42161 for Arbitrum) in the authorization tuple before signing, ensuring client verification rejects the tuple on conflicting networks.

What is the security risk of nonce desynchronization across rollups?

If an EOA maintains nonces 5 on Mainnet and 2 on Base, an authorization signed for Mainnet at nonce 5 cannot be immediately replayed on Base until the Base account naturally advances to nonce 5, creating an unmonitored future exploit window.

How can I verify if an account is currently delegated under EIP-7702?

Perform an eth_getCode call against the target address. An active EIP-7702 delegation returns bytecode prefixed with 0xef0100 followed by the 20-byte address of the delegate contract.