On May 7, 2025, Ethereum's Pectra upgrade went live. Among 11 EIPs, one change quietly invalidated a security assumption that thousands of deployed contracts rely on: EIP-7702 made EOAs capable of executing smart contract code.
The result? Every contract that uses tx.origin == msg.sender as a "is this a real user?" check is now bypassable. And the blast radius extends far beyond that single pattern — into reentrancy guards, gas sponsorship flows, and the entire mental model of what an "account" means on Ethereum.
This article breaks down the concrete attack vectors, shows vulnerable code patterns with fixes, and explains why this isn't just a theoretical concern — it's actively exploitable on mainnet today.
The Old World: EOAs vs. Contracts
Before Pectra, Ethereum had a clean separation:
- EOAs (Externally Owned Accounts): Controlled by private keys. Can initiate transactions. Cannot execute arbitrary code.
- Contract accounts: Have code. Can execute logic. Cannot initiate transactions independently.
This distinction was fundamental. Developers built security assumptions on it:
// "Only real users can call this, not contracts"
require(tx.origin == msg.sender, "No contracts allowed");
This pattern was already considered an anti-pattern by security auditors — but it worked. If tx.origin == msg.sender, you knew the caller was an EOA with no code execution capability.
What EIP-7702 Changed
EIP-7702 introduces a new transaction type that allows an EOA to set a code pointer for the duration of a transaction. The EOA temporarily delegates to a smart contract implementation, executing its logic as if the EOA itself were a contract.
// Simplified EIP-7702 flow
1. Alice (EOA) signs an authorization: "delegate to Implementation.sol"
2. Transaction executes with Alice's address running Implementation.sol's code
3. After transaction, Alice's address returns to being a "normal" EOA
The key insight: during the transaction, Alice's address has code. But tx.origin is still Alice. And msg.sender can also be Alice if the call is direct.
This means:
-
tx.origin == msg.sender✅ passes (Alice initiated and is the direct caller) - But Alice is now executing arbitrary code ✅
- The "no contracts" guard is completely bypassed ✅
Attack Vector #1: The Reentrancy Bypass
Consider a lending protocol that uses the EOA check as an additional reentrancy guard:
contract VulnerableLending {
mapping(address => uint256) public deposits;
function withdraw(uint256 amount) external {
// "EOAs can't re-enter, so this is safe" — WRONG
require(tx.origin == msg.sender, "EOAs only");
require(deposits[msg.sender] >= amount, "Insufficient");
deposits[msg.sender] -= amount;
(bool ok,) = msg.sender.call{value: amount}("");
require(ok);
}
}
Pre-Pectra: This was fragile but technically safe. An EOA couldn't execute code on receiving ETH, so the callback couldn't re-enter.
Post-Pectra: An EOA using EIP-7702 delegation can execute code on receiving ETH. The receive() function of the delegated implementation runs, and if it calls back into withdraw(), the reentrancy succeeds because tx.origin == msg.sender still passes.
The Attack
contract MaliciousDelegate {
address target;
uint256 attackCount;
receive() external payable {
if (attackCount < 5) {
attackCount++;
// Re-enter the lending protocol
// tx.origin == msg.sender still holds!
IVulnerableLending(target).withdraw(1 ether);
}
}
}
// Attacker's EOA delegates to MaliciousDelegate via EIP-7702
// Then calls withdraw() — passes tx.origin check
// On ETH receive, MaliciousDelegate's receive() fires
// Re-enters withdraw() — tx.origin check STILL passes
// Classic reentrancy, bypassing the "EOA-only" guard
The Fix
Use proper reentrancy guards. The tx.origin check was never a substitute:
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract FixedLending is ReentrancyGuard {
mapping(address => uint256) public deposits;
function withdraw(uint256 amount) external nonReentrant {
require(deposits[msg.sender] >= amount, "Insufficient");
deposits[msg.sender] -= amount;
(bool ok,) = msg.sender.call{value: amount}("");
require(ok);
}
}
Attack Vector #2: Flash Loan Callback Hijacking
Many DeFi protocols use callback patterns where they expect to interact with a smart contract:
function executeFlashLoan(address borrower, uint256 amount) external {
// Transfer tokens to borrower
token.transfer(borrower, amount);
// Expect borrower to be a contract with this callback
IFlashBorrower(borrower).onFlashLoan(amount);
// Verify repayment
require(token.balanceOf(address(this)) >= amount + fee);
}
Pre-Pectra: If borrower was an EOA, the onFlashLoan call would revert (no code to execute).
Post-Pectra: An EOA can delegate to an implementation that defines onFlashLoan, execute arbitrary logic during the callback, and then have the delegation expire — leaving no trace of the contract code that executed.
This creates a new pattern: ephemeral smart contract behavior. An address can be a contract for one transaction and an EOA for the next, making forensic analysis and on-chain monitoring significantly harder.
Attack Vector #3: Airdrop and Allowlist Gaming
Many NFT mints and token airdrops restrict participation to EOAs:
function claim() external {
require(tx.origin == msg.sender, "No bots");
require(!claimed[msg.sender], "Already claimed");
require(isAllowlisted(msg.sender), "Not eligible");
claimed[msg.sender] = true;
token.mint(msg.sender, CLAIM_AMOUNT);
}
Pre-Pectra: This effectively blocked contract-based batch claiming.
Post-Pectra: An EOA can delegate to an implementation that:
- Claims the airdrop (passes
tx.origin == msg.sender) - Immediately transfers tokens to a collection address
- All within a single atomic transaction
While this specific abuse might seem minor, it becomes critical when the claiming logic involves governance tokens or yield-bearing positions where batch claiming provides unfair advantages.
Attack Vector #4: Gas Sponsorship Abuse
EIP-7702 enables gas sponsorship patterns where a relayer pays gas on behalf of a user. But this creates a new trust assumption:
contract GasSponsor {
function sponsoredCall(
address target,
bytes calldata data,
address user
) external {
// Relayer pays gas, user's EOA executes via 7702 delegation
require(msg.sender == trustedRelayer, "Unauthorized");
// But what if the user's delegated code does something unexpected?
(bool ok,) = target.call(data);
require(ok);
}
}
If the user's EOA has delegated to malicious code via EIP-7702, the target.call(data) might trigger unexpected callbacks to the user's address, where the delegated code executes with the user's address as context — potentially draining any tokens approved to the user's address.
The Correct Detection Pattern
The old check:
// ❌ BROKEN post-Pectra
require(tx.origin == msg.sender, "No contracts");
The updated check:
// ✅ Works post-Pectra
require(
msg.sender == tx.origin && msg.sender.code.length == 0,
"Pure EOA only"
);
But even this has a subtlety: during an EIP-7702 delegated execution, msg.sender.code.length will be non-zero because the delegation sets a code pointer. However, between transactions, the EOA's code returns to zero. So this check works for the current transaction context, but you cannot rely on it for future behavior of the same address.
What This Means for Auditors
If you're auditing contracts in 2026, here's your updated checklist:
1. Grep for tx.origin
Every use of tx.origin needs re-evaluation. The common patterns:
| Pattern | Risk Level | Action |
|---|---|---|
tx.origin == msg.sender (access control) |
🔴 Critical | Replace with code.length check or remove |
tx.origin for replay protection |
🟡 Medium | Verify it's combined with nonce checks |
tx.origin in events/logging |
🟢 Low | Usually fine, but document the assumption |
2. Review Callback Assumptions
Any contract that sends ETH or makes calls to user-supplied addresses must now assume those addresses can execute arbitrary code — even if they were "just EOAs" in a previous block.
3. Check extcodesize / code.length Usage
// This was a common "is it a contract?" check
if (addr.code.length > 0) {
// treat as contract
} else {
// treat as EOA — NO LONGER SAFE in all contexts
}
Between transactions, an EIP-7702 delegating EOA has code.length == 0. During delegation, it has code.length > 0. Your check's correctness depends on when it's evaluated relative to the delegation.
4. Account for Ephemeral Code
The concept of "this address is a contract" is no longer binary and permanent. Design your access control, monitoring, and forensics around the possibility that any address might execute code in one transaction and be a bare EOA in the next.
Real-World Impact Assessment
How many contracts are affected? A conservative estimate:
-
Etherscan-verified contracts using
tx.origin == msg.sender: ~12,000+ across mainnet and L2s - Unverified contracts with similar patterns: Unknown, likely 3-5x more
- DEX routers with EOA-specific paths: Multiple major protocols
- NFT minting contracts with bot protection: Thousands
Not all of these are exploitable — many have additional guards. But the pattern is widespread enough that we should expect exploitation attempts as EIP-7702 adoption grows.
Recommendations for Protocol Teams
Audit existing contracts for any
tx.originusage andextcodesizechecks used for access control decisions.Upgrade proxy implementations if your upgradeable contracts use these patterns — this is the lowest-friction fix for deployed systems.
Update your threat model. "EOA" no longer means "incapable of executing code." Every address is potentially a smart contract.
Monitor for 7702 delegations targeting your protocol. Track the new transaction type and flag interactions from addresses that have recently delegated.
Don't panic about callback attacks if you already use proper reentrancy guards (
nonReentrantmodifier). The contracts at risk are those that relied on the EOA assumption instead of proper guards.
The Bigger Picture
EIP-7702 is a net positive for Ethereum. Account abstraction improves UX, enables gas sponsorship, and moves the ecosystem toward a future where the EOA/contract distinction matters less.
But every major protocol upgrade shifts the security landscape. Just as EIP-1153 (transient storage) broke reentrancy assumptions for contracts using storage-based locks, EIP-7702 breaks identity assumptions for contracts using tx.origin.
The lesson is evergreen: don't build security on implementation details of the execution environment. The EVM changes. Your invariants shouldn't depend on things that can change underneath you.
This article is part of the DeFi Security Research series. Previously: Transient Storage Reentrancy, ERC-4337 Smart Account Security.
Found this useful? Follow for weekly deep-dives into blockchain security research.
Top comments (0)