DEV Community

ohmygod
ohmygod

Posted on

ERC-7702 Is Live — And It Broke Every DeFi Contract That Trusts tx.origin: The 5 Attack Surfaces Your Protocol Must Patch Before Pectra Eats Your Lunch

Ethereum's Pectra upgrade shipped ERC-7702 — native account abstraction that lets any EOA temporarily become a smart contract. The UX wins are real: transaction batching, gas sponsorship, social recovery. But here's what nobody's talking about loudly enough: ERC-7702 retroactively broke security assumptions in thousands of deployed DeFi contracts.

If your protocol uses tx.origin, relies on EOAs being "dumb" accounts, or assumes msg.sender == tx.origin means no reentrancy — you're exposed. This isn't theoretical. Exploits are already circulating.

Let's break down the five attack surfaces, show real code patterns, and give you the exact mitigations.


1. The tx.origin Identity Crisis

The Old Assumption

// "Only EOAs can call this" — wrong after ERC-7702
require(msg.sender == tx.origin, "No contracts allowed");
Enter fullscreen mode Exit fullscreen mode

Before Pectra, msg.sender == tx.origin was a reasonable (if imperfect) heuristic to ensure the caller wasn't a contract. Many protocols used this as an anti-reentrancy guard or to restrict access to "real users."

What Changed

With ERC-7702, an EOA can delegate to arbitrary contract code while keeping its address. Now tx.origin and msg.sender can both point to an address that's executing complex contract logic — including reentrant calls.

The Attack

// Attacker's EOA delegates to this contract via ERC-7702
contract MaliciousDelegation {
    function attack(address target) external {
        // msg.sender == tx.origin == attacker's EOA address
        // Passes the "no contracts" check
        IVault(target).withdrawAll(); // Reenters freely
    }
}
Enter fullscreen mode Exit fullscreen mode

The Fix

Stop using tx.origin for security logic. Period.

// Use proper reentrancy guards instead
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract SecureVault is ReentrancyGuard {
    function withdraw(uint256 amount) external nonReentrant {
        // No tx.origin check needed
        _processWithdrawal(msg.sender, amount);
    }
}
Enter fullscreen mode Exit fullscreen mode

Audit checklist item: grep -rn "tx.origin" contracts/ — every hit is now a potential vulnerability.


2. The One-Click Drain: Delegation Phishing

How It Works

ERC-7702 introduces AUTH — a new transaction type where users sign a delegation to a contract address. This is where phishing gets terrifying.

Before Pectra: A phishing attack could steal one token approval at a time. The user sees "Approve USDC for [contract]" and might catch it.

After Pectra: A single delegation signature grants a malicious contract full execution authority over the EOA. The contract can then:

  1. Drain all ERC-20 balances across every protocol
  2. Liquidate lending positions
  3. Revoke all existing approvals
  4. Bridge remaining assets to another chain
  5. All in one atomic transaction

The Anatomy of an Attack

User signs: AUTH(delegate=0xMalicious, nonce=42)

0xMalicious.execute():
  ├── USDC.transfer(attacker, balance)
  ├── WETH.transfer(attacker, balance)  
  ├── Aave.withdraw(all_deposits)
  ├── Compound.redeem(all_cTokens)
  ├── Uniswap.removeLiquidity(all_positions)
  └── Bridge.sendToL2(attacker_l2_addr, total)
Enter fullscreen mode Exit fullscreen mode

Mitigation for Wallet Developers

  • Simulate before signing: Show users the full execution trace of any delegation
  • Domain separation: Clearly distinguish delegation signatures from regular transaction signatures
  • Time-bound delegations: Encourage contracts that implement expiring delegation windows
  • Allowlisted delegates only: Maintain curated registries of audited delegation contracts

Mitigation for Protocol Teams

// Implement explicit delegation-aware access control
contract DelegationAwareVault {
    mapping(address => bool) public trustedDelegates;

    modifier onlyTrustedDelegate() {
        // Check if caller is executing delegated code
        if (_isDelegated(msg.sender)) {
            require(
                trustedDelegates[_getDelegateContract(msg.sender)],
                "Untrusted delegate"
            );
        }
        _;
    }
}
Enter fullscreen mode Exit fullscreen mode

3. The Front-Running Initialization Race

The Problem

Traditional smart contracts initialize via constructors, which run atomically at deployment. ERC-7702 delegated accounts don't have this luxury — initialization happens in a separate transaction after the delegation is set.

The Attack Window

Block N:   User sets delegation → AUTH(delegate=SafeWallet)
Block N+1: User calls initialize(owner=user) 

// But attacker sees the pending AUTH in the mempool...
Block N:   User sets delegation → AUTH(delegate=SafeWallet)  
Block N+1: ATTACKER calls initialize(owner=attacker)  ← front-runs
Block N+2: User's initialize() reverts — attacker owns the account
Enter fullscreen mode Exit fullscreen mode

Real Impact

If your delegation contract stores an owner variable that controls withdrawals, and initialization isn't protected, an attacker can claim ownership of any EOA that delegates to your contract.

The Fix

contract SecureDelegation {
    // Use ERC-7201 namespaced storage to prevent collisions
    bytes32 private constant STORAGE_SLOT = 
        keccak256("SecureDelegation.storage.v1");

    struct Storage {
        address owner;
        bool initialized;
    }

    function initialize() external {
        Storage storage s = _getStorage();
        // Only the EOA itself can initialize
        require(msg.sender == address(this), "Only self-init");
        require(!s.initialized, "Already initialized");
        s.owner = msg.sender;
        s.initialized = true;
    }

    function _getStorage() private pure returns (Storage storage s) {
        bytes32 slot = STORAGE_SLOT;
        assembly { s.slot := slot }
    }
}
Enter fullscreen mode Exit fullscreen mode

Key insight: Use require(msg.sender == address(this)) for initialization — this ensures only the account owner (via their signing key) can set up the delegation, because with ERC-7702, address(this) in delegated code resolves to the EOA's address.


4. Storage Collision Landmines

The Scenario

An EOA delegates to Contract A, which stores data at slot 0. Later, the user switches delegation to Contract B, which also uses slot 0 but for a different purpose.

// Contract A: slot 0 = owner (address)
contract WalletA {
    address public owner; // slot 0
}

// Contract B: slot 0 = balance (uint256)  
contract WalletB {
    uint256 public balance; // slot 0
}

// After switching A → B:
// balance = uint256(owner_address) = some huge number
// User appears to have billions in "balance"
Enter fullscreen mode Exit fullscreen mode

Why This Is Worse Than It Sounds

  • Cross-delegation state corruption can bypass balance checks, access controls, and invariants
  • Storage persists on the EOA even after delegation changes
  • No automatic cleanup mechanism exists
  • Different contracts with overlapping storage layouts will silently corrupt each other's state

Defensive Pattern

// Always use ERC-7201 namespaced storage
contract SafeDelegation {
    /// @custom:storage-location erc7201:SafeDelegation.main
    struct MainStorage {
        address owner;
        uint256 nonce;
        mapping(address => bool) authorized;
    }

    // keccak256("SafeDelegation.main") - 1
    bytes32 private constant MAIN_STORAGE = 0x...; 

    function _getMainStorage() private pure returns (MainStorage storage $) {
        assembly { $.slot := MAIN_STORAGE }
    }
}
Enter fullscreen mode Exit fullscreen mode

For auditors: Check every delegation contract for raw storage slot usage. If it's not using ERC-7201 namespaced storage, flag it as high severity.


5. The Whitelist Bypass via Delegation Chains

The Attack

Many DeFi protocols maintain whitelists of approved addresses. With ERC-7702, a whitelisted EOA can delegate to arbitrary code, effectively turning a single whitelisted address into a gateway for unlimited unauthorized actors.

Protocol whitelist: [Alice_EOA, Bob_EOA, ...]

Alice_EOA delegates to ProxyContract via ERC-7702
ProxyContract accepts calls from anyone
Attacker calls ProxyContract → executes as Alice_EOA → passes whitelist check
Enter fullscreen mode Exit fullscreen mode

Why Existing Checks Fail

// This check is now broken:
require(whitelist[msg.sender], "Not whitelisted");
// msg.sender = Alice's EOA address (still whitelisted)
// But the actual caller is the attacker, routing through Alice's delegation
Enter fullscreen mode Exit fullscreen mode

The Fix

Protocols using whitelists must now verify both the address and its delegation status:

function isGenuineEOA(address account) internal view returns (bool) {
    uint256 size;
    assembly { size := extcodesize(account) }
    if (size > 0) {
        return false;
    }
    return true;
}

modifier onlyWhitelistedEOA(address account) {
    require(whitelist[account], "Not whitelisted");
    require(isGenuineEOA(account), "Delegated accounts not allowed");
    _;
}
Enter fullscreen mode Exit fullscreen mode

The Audit Toolkit: What to Do Right Now

1. Scan Your Codebase

# Find all tx.origin usage
grep -rn "tx.origin" contracts/ --include="*.sol"

# Find msg.sender == tx.origin anti-reentrancy patterns
grep -rn "msg.sender.*==.*tx.origin" contracts/ --include="*.sol"

# Find extcodesize-based EOA checks (also broken)
grep -rn "extcodesize" contracts/ --include="*.sol"
Enter fullscreen mode Exit fullscreen mode

2. Run Static Analysis

# Slither has new ERC-7702 detectors
slither . --detect erc7702-unsafe-origin,delegation-reentrancy

# Aderyn (Cyfrin) also added Pectra-aware rules
aderyn . --rules pectra-compat
Enter fullscreen mode Exit fullscreen mode

3. Add Formal Verification for Critical Invariants

// Halmos symbolic test
function check_noReentrancy(uint256 amount) public {
    uint256 balanceBefore = vault.balanceOf(address(this));
    vault.withdraw(amount);
    uint256 balanceAfter = vault.balanceOf(address(this));
    assert(balanceBefore - balanceAfter == amount);
}
Enter fullscreen mode Exit fullscreen mode

4. Update Your Threat Model

Add these to your audit checklist:

  • [ ] No security-critical tx.origin checks
  • [ ] Initialization functions are front-running resistant
  • [ ] Storage uses ERC-7201 namespaced patterns
  • [ ] Whitelist logic accounts for delegation
  • [ ] Reentrancy guards don't rely on EOA assumptions
  • [ ] Integration tests with ERC-7702 delegated callers

The Bottom Line

ERC-7702 is the biggest change to Ethereum's execution model since EIP-1559. It fundamentally alters who msg.sender can be and what an "externally owned account" means. Every DeFi protocol deployed before Pectra should be re-audited with these five attack surfaces in mind.

The good news: the mitigations are straightforward. The bad news: most protocols haven't applied them yet.

Don't be the next postmortem.


This article is part of the DeFi Security Research series by DreamWork Security. We publish weekly deep-dives into smart contract vulnerabilities, audit methodologies, and defensive tooling. Follow for more.

Top comments (0)