Fix: EIP-7702 Set Code Authorization Replay Attacks & Cross-Chain Delegation Front-Running
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.
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:
- The EVM recovers the signer $A$ from the tuple $(y_parity, r, s)$ hashed against
keccak256(MAGIC || rlp([chain_id, address, nonce])). - The EVM checks that the signer’s on-chain nonce matches
nonce. - 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:
- The delegation bytecode
0xef0100...remains written in their account code slot. - If the delegated contract implementation contained an unprotected
selfdestruct(on legacy chains), uninitialized proxy storage, or an arbitraryexecute(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:
- If an attacker sees a pending Type
0x04transaction 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. - 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
| Risk | Exploit Mechanism | Architectural Remedy |
|---|---|---|
| Cross-Chain Replay | chainId = 0 in authorization tuple | Disallow chainId: 0 in client signer modules. |
| Zombie Delegation | Unrevoked bytecode left in EOA code slot | Automate 0x0 address revocation when session terminates. |
| Mempool Nonce Clash | Nonce increments prior to bundle mining | Broadcast authorizations through private RPC endpoints (e.g. Flashbots Protect). |
| Direct State Hijack | Delegate contract missing initializer guards | Ensure delegate implementation uses immutable storage or slot-isolated storage (ERC-7201). |
Fact-Checked by Victor Vance, Senior Smart Contract Security Analyst & Node Operator.