Ethereum's Pectra upgrade brought EIP-7702 — a game-changer for account abstraction that lets externally owned accounts (EOAs) temporarily delegate execution to smart contracts. Better UX, batched transactions, gas sponsorship.
One problem: over 97% of EIP-7702 delegations in the wild pointed to malicious sweeper contracts. The biggest family? A contract dubbed CrimeEnjoyor by Wintermute.
This article breaks down the attack mechanics, explains why traditional phishing defenses fail against delegation-based attacks, and provides concrete detection patterns for defenders.
What Changed With EIP-7702
Before Pectra, EOAs were simple: one private key, one signer, direct transaction execution. Smart contract wallets (ERC-4337) offered programmability but required deploying a new contract and migrating assets.
EIP-7702 introduced type 0x04 transactions with an authorization tuple:
authorization = (chain_id, address, nonce, y_parity, r, s)
When signed, this tuple tells the network: "For this transaction, treat my EOA as if it delegates to the contract at address." The EOA's code pointer temporarily redirects to the delegate contract.
This is powerful. It's also a single signature away from total account compromise.
The Anatomy of a CrimeEnjoyor Attack
Step 1: Social Engineering the Delegation Signature
The attacker crafts a malicious authorization tuple pointing to a CrimeEnjoyor sweeper contract. Unlike traditional token approvals (which are visible and revocable), this delegation:
- Is not an ERC-20 approval — it won't show up in Revoke.cash or similar tools
- Grants code execution authority — the delegate contract runs as if it is the victim's account
- Persists across transactions — once delegated, every future interaction routes through the malicious contract
The victim sees what looks like a normal signature request. Maybe a "free mint," a "gas refund claim," or a "wallet upgrade." One click.
Step 2: The Sweeper Activates
The CrimeEnjoyor contract family implements a minimal but devastating pattern:
// Simplified CrimeEnjoyor logic
fallback() external payable {
// Any ETH received → immediately forwarded to attacker
(bool s,) = ATTACKER.call{value: address(this).balance}("");
require(s);
}
function execute(address token, uint256 amount) external {
// Drain any ERC-20 tokens
IERC20(token).transfer(ATTACKER, amount);
}
The real contracts are slightly more sophisticated, but the core mechanic is the same: any asset that touches the wallet gets swept instantly.
Step 3: The Trap Is Set
Here's what makes this devastating:
- Victim signs the delegation → wallet is now compromised
- Attacker monitors the address for incoming transactions
- Someone sends ETH/tokens to the victim → swept within the same block
- Victim tries to "rescue" funds by sending gas → also swept
- Even airdrop claims or protocol rewards → intercepted
The wallet looks normal on Etherscan. The balance shows zero, but there's no obvious "drain" transaction — just a series of what look like outgoing transfers the user "made."
Why Traditional Defenses Fail
Token Approvals ≠ Delegation
Tools like Revoke.cash monitor approve() and permit() calls. EIP-7702 delegations bypass this entirely — they operate at the account level, not the token level.
Simulation Misses the Persistence
Transaction simulators (Tenderly, Blowfish) can flag a single malicious transaction. But the delegation persists across future transactions that haven't been submitted yet. The attack surface is everything that will ever happen to this wallet.
tx.origin Checks Are Broken
Pre-Pectra, require(tx.origin == msg.sender) was a reliable (if discouraged) way to ensure only EOAs called a function. With EIP-7702, an EOA with delegated code can now execute arbitrary logic while still being tx.origin. Reentrancy guards and EOA-only checks that rely on this assumption are bypassed.
Detection Patterns for Defenders
On-Chain: Identifying CrimeEnjoyor Contracts
The CrimeEnjoyor family shares bytecode signatures. Here's how to detect them:
Pattern 1: Code similarity analysis
from web3 import Web3
def is_crimeenjoyor_family(w3, address):
code = w3.eth.get_code(address)
# Known CrimeEnjoyor bytecode prefixes
known_prefixes = [
bytes.fromhex("6080604052"), # Standard prefix
]
# Check for attacker address hardcoded in bytecode
# CrimeEnjoyor contracts embed the drain destination
# Look for PUSH20 followed by known attacker addresses
attacker_addresses = [
# Add known attacker addresses here
]
for addr in attacker_addresses:
if addr.lower().replace("0x", "") in code.hex():
return True
return False
Pattern 2: Behavioral analysis
def analyze_delegation_target(w3, delegate_address):
"""Flag suspicious delegation targets"""
code = w3.eth.get_code(delegate_address)
red_flags = 0
# Flag 1: Very small bytecode (sweeper contracts are minimal)
if len(code) < 200:
red_flags += 1
# Flag 2: Contains hardcoded address (fund destination)
# PUSH20 opcode = 0x73
push20_count = code.hex().count("73")
if push20_count >= 1 and len(code) < 500:
red_flags += 1
# Flag 3: Has payable fallback but minimal other logic
# Check for CALLDATASIZE near start (fallback routing)
if b"\x36" in code[:20]: # CALLDATASIZE early = fallback-heavy
red_flags += 1
# Flag 4: No access control patterns
# Absence of CALLER (0x33) checks
if b"\x33" not in code:
red_flags += 1
return red_flags >= 2
Off-Chain: Monitoring for Delegation Events
Track type 0x04 transactions targeting your protocol's users:
def monitor_delegations(w3, watched_addresses):
"""Monitor for suspicious EIP-7702 delegations"""
latest = w3.eth.block_number
block = w3.eth.get_block(latest, full_transactions=True)
for tx in block.transactions:
# Type 4 = EIP-7702 delegation transaction
if tx.get('type') == '0x4' and tx['from'] in watched_addresses:
authorization_list = tx.get('authorizationList', [])
for auth in authorization_list:
delegate = auth['address']
if analyze_delegation_target(w3, delegate):
alert(f"CRITICAL: {tx['from']} delegated to suspicious contract {delegate}")
Wallet-Level: Pre-Signing Validation
If you're building a wallet or dApp, validate delegation targets before presenting them to users:
async function validateDelegation(
delegateAddress: string,
provider: ethers.Provider
): Promise<{safe: boolean, reason: string}> {
// Check 1: Is the delegate verified on Etherscan?
// Verified source code = at least some transparency
// Check 2: Contract age and interaction count
const code = await provider.getCode(delegateAddress);
if (code === '0x') {
return { safe: false, reason: 'Delegate has no code deployed' };
}
// Check 3: Cross-reference against known malicious registries
// Wintermute's CrimeEnjoyor database
// GoPlus Security API
// Forta alert feeds
// Check 4: Does the contract implement standard interfaces?
// Legitimate delegates typically implement ERC-165
const iface = new ethers.Interface([
'function supportsInterface(bytes4) view returns (bool)'
]);
try {
const contract = new ethers.Contract(delegateAddress, iface, provider);
await contract.supportsInterface('0x01ffc9a7');
} catch {
return { safe: false, reason: 'Delegate does not implement ERC-165' };
}
return { safe: true, reason: 'Passed basic validation' };
}
Defense Playbook for Protocol Developers
1. Don't Trust tx.origin — Ever
This was already bad practice. EIP-7702 made it exploitable:
// ❌ BROKEN: Can be bypassed by delegated EOA
require(tx.origin == msg.sender, "No contracts");
// ✅ BETTER: Check code size at call time
require(msg.sender.code.length == 0, "No contracts");
// ⚠️ BUT: Even this is unreliable with EIP-7702
// EOAs with active delegation WILL have code
// Consider: do you actually need this check?
2. Implement Delegation-Aware Access Control
// Check if an address has an active EIP-7702 delegation
function hasDelegation(address account) internal view returns (bool) {
bytes memory code = account.code;
// EIP-7702 delegated accounts have a specific code prefix
// 0xef0100 followed by the delegate address
return code.length == 23 &&
code[0] == 0xef &&
code[1] == 0x01 &&
code[2] == 0x00;
}
function secureFunction() external {
if (hasDelegation(msg.sender)) {
// Additional verification for delegated accounts
// e.g., require signature over function-specific data
// or check delegate against allowlist
}
}
3. Rate-Limit High-Value Operations
If a previously dormant wallet suddenly starts batching transactions through a delegate, that's a signal:
mapping(address => uint256) public lastActivity;
mapping(address => uint256) public activityCount;
modifier rateLimited() {
if (block.timestamp - lastActivity[msg.sender] < 1 hours) {
activityCount[msg.sender]++;
require(activityCount[msg.sender] <= 5, "Rate limited");
} else {
activityCount[msg.sender] = 1;
}
lastActivity[msg.sender] = block.timestamp;
_;
}
For Users: How to Check If You're Compromised
Quick Check
cast code <YOUR_ADDRESS> --rpc-url https://eth.llamarpc.com
Revoking a Delegation
To revoke an EIP-7702 delegation, submit a new type 0x04 transaction delegating to address(0) or to a known-safe delegate:
cast send --auth <YOUR_ADDRESS>=0x0000000000000000000000000000000000000000 \
--rpc-url https://eth.llamarpc.com \
--private-key <YOUR_KEY>
Critical caveat: If your private key is compromised (not just phished into signing a delegation), revoking won't help — the attacker will simply re-delegate. In this case, your only option is to transfer all assets to a fresh wallet as quickly as possible.
The Bigger Picture
EIP-7702 is genuinely useful technology. Batched transactions, gas sponsorship, and programmable EOAs are real improvements. But the 97% malicious delegation rate reveals a fundamental UX problem: users cannot distinguish legitimate delegation requests from malicious ones.
The fix isn't technical — it's informational:
- Wallets must surface delegation details prominently — not buried in "advanced" tabs
- Delegate contract registries (like Wintermute's CrimeEnjoyor database) need to become standard infrastructure
- Protocol developers must stop assuming EOAs can't execute code
- Users need education — "signing a delegation" is as dangerous as "exporting your private key"
The CrimeEnjoyor epidemic isn't over. As long as delegation requests look like harmless signatures, attackers will exploit the gap between what users see and what they're actually authorizing.
Security research by the author. Follow for more DeFi security analysis, vulnerability breakdowns, and audit tooling guides.
Tags: ethereum, security, defi, web3
Top comments (0)