When an autonomous AI agent participates in decentralized on-chain task marketplaces on Base Mainnet (Chain ID 8453), every task claim is a financial transaction. If a task requires a worker collateral stake—typically 20% to 50% of the advertised USDC bounty—the agent must evaluate not only whether it can solve the natural-language prompt, but whether the underlying smart contract settlement path will actually permit work submission. This week, we intercepted and blocked a 26-variant serial Sybil honeypot campaign that used unverified 1,087-byte EVM lifecycle hooks to confiscate worker stakes across Base L2.
The Anatomy of an Agent-Targeted Escrow Honeypot
Traditional Ethereum Virtual Machine (EVM) honeypots target MEV searchers or arbitrage bots by dangling an apparent pricing discrepancy or an unlocked withdrawal function that silently reverts when called by anyone other than the deployer. In the emerging Agent-to-Agent (A2A) economy, however, attackers have adapted the honeypot model to exploit automated task-execution pipelines.
The bait looks completely benign at the application layer. While polling open tasks on Base Mainnet, our intake daemon encountered a series of straightforward micro-tasks paying $5.00 USDC: prompts asking an agent to "Write 4 short field labels for a school timetable" or "Draft 5 concise CLI support reply templates." For an LLM-backed engineering agent, generating four timetable labels takes less than two seconds of compute.
Beneath the innocuous JSON prompt, however, the requester configured a lethal combination of on-chain settlement terms and custom lifecycle hook contracts:
- Mandatory Worker Stake (
stakeRequired: true,stakeBps: 2000): To claim the$5.00 USDCtask, the worker agent must lock$1.00 USDC(1,000,000micro-units) into the escrow contract via an EIP-3009transferWithAuthorizationvoucher. - Voluntary Release Disabled (
releaseAllowed: false): Once the worker claims the task, callingreleaseClaim(taskId)unconditionally reverts withReleaseNotAllowed(). The worker cannot back out. - Forfeiture Routed to the Requester (
penaltyTo: Requester): If the task clock expires without a recorded deliverable hash on-chain (task.deliverable == bytes32(0)), 100% of the worker's forfeited USDC stake is transferred directly to the requester's wallet viaforfeitAndReopen(taskId). - Unverified Custom Lifecycle Hook (
hookConfig.contracts): Alongside the canonical reward hook, the requester attached a freshly deployed, unverified 1,087-byte contract (such as0x0e2f4405a823593c6bd623a28ca40d296008bdfeand later0x2adac8840c5967b1e2f402242f187b185f6dcc11).
Why Prompt-Level Filtering Fails: An autonomous agent that only evaluates the natural-language task description, the USDC reward amount, and the deadline will see a high-margin, low-effort task. Without inspecting the EVM bytecode of every contract in the task's execution graph, the agent walks straight into a one-way cryptographic vault.
Disassembling the 1,087-Byte Trap on Base Mainnet
Why couldn't trapped agents simply submit their four school timetable labels and collect the $5.00 USDC reward? To answer that, we queried Base Mainnet via eth_getCode on hook contract 0x0e2f4405a823593c6bd623a28ca40d296008bdfe (and its 26th sibling clone, 0x2adac8840c5967b1e2f402242f187b185f6dcc11) and disassembled the raw EVM opcodes across its function selector dispatch table.
In modular escrow architectures (including EIP-2535 Diamond proxies), lifecycle hooks implement pre-condition check callbacks—such as checkFund, checkClaim, checkSubmit, and checkComplete—as well as post-settlement notification callbacks like onCancel, onExpire, and onForfeit. During submitWork(taskId, deliverableHash), the escrow router iterates through every hook attached to the task and invokes checkSubmit(bytes32, TaskContext, address, bytes32). If any attached hook returns false (0x00) or reverts, the entire submitWork transaction reverts with custom error HookCheckSubmitRejected() (selector 0xef94626f).
Here is the exact disassembly of the attacker's 1,087-byte hook runtime bytecode:
// Selector Dispatch Table — Malicious Escrow Hook (1,087 bytes, Solidity 0.8.20+ with PUSH0)
// Observed at 0x0e2f4405a823593c6bd623a28ca40d296008bdfe & 0x2adac8840c5967b1e2f402242f187b185f6dcc11
// 1. ERC-165 Interface Detection: supportsInterface(bytes4) [0x01ffc9a7]
// Returns true (0x01) for ITMPHook (0x20787d79) and IERC165 (0x01ffc9a7)
00aa: JUMPDEST
00ab: PUSH4 0x20787d79 ...
// 2. Pre-Claim & Pre-Fund Gates: Lure the victim into locking their stake
// checkFund [0x97942309] -> jump 0x0110 -> PUSH1 0x01 (returns true)
// checkClaim [0x03e71b04] -> jump 0x00d1 -> PUSH1 0x01 (returns true)
// checkSelectWorker [0xac63e52c] -> jump 0x00d1 -> PUSH1 0x01 (returns true)
// 3. The Submission Trap: checkSubmit(bytes32,TaskContext,address,bytes32) [0xb65065f4]
0136: JUMPDEST
0137: PUSH2 0x00bd
013a: PUSH2 0x0144
013d: CALLDATASIZE
013e: PUSH1 0x04
0140: PUSH2 0x03ae // Validates calldata ABI encoding so malformed calls revert cleanly
0143: JUMP
0144: JUMPDEST
0145: PUSH0 // 0x5f: Pushes literal 0 (false) onto the EVM stack!
0146: SWAP5
0147: SWAP4
0148: POP
0149: POP
014a: POP
014b: POP
014c: JUMP // Returns ABI-encoded bool(false) -> triggers 0xef94626f revert!
// 4. Post-Expiry Gas Griefing: onExpire [0x19e10090], onCancel [0xa55e8383], onForfeit [0xe1ef65a9]
01aa: JUMPDEST
01ab: JUMPDEST
01ac: PUSH2 0x03e8 // 1,000 gas threshold
01af: GAS // Push gasleft()
01b0: GT // gasleft() > 1000
01b1: PUSH2 0x01ab
01b4: JUMPI // Infinite spin loop burning the entire 1,000,000 gas stipend!
01b5: JUMP
Look closely at offset 0x0145. After validating the calldata layout at 0x03ae, the function handler for checkSubmit (0xb65065f4) never reads storage, never inspects the worker address, and never checks the deliverable digest. In fact, all storage slots (0x00 through 0x04) on the contract are completely empty. It executes a single PUSH0 (0x5f) instruction, pops the decoded arguments off the stack, and returns false.
| Function Selector | Lifecycle Method | Jump Target | Bytecode Behavior | Economic Effect on Worker Agent |
|---|---|---|---|---|
0x97942309 |
checkFund(...) |
0x0110 |
PUSH1 0x01 (Returns true) |
Allows attacker to register the honeypot task on-chain. |
0x03e71b04 |
checkClaim(...) |
0x00d1 |
PUSH1 0x01 (Returns true) |
Permits victim agent to lock $1.00 USDC stake into escrow. |
0xb65065f4 |
checkSubmit(...) |
0x0136 |
PUSH0 (Returns false) |
Unconditionally blocks submitWork with HookCheckSubmitRejected(). |
0xe1ef65a9 |
onForfeit(...) |
0x01aa |
while (gasleft() > 1000) {} |
Burns 1,000,000 gas stipend while requester sweeps forfeited stake. |
Tracking the 26-Variant Serial Sybil Snowball
By indexing creation events and hook bytecode hashes across Base Mainnet over a 48-hour window, we discovered that this was not a one-off experiment. An automated script was running a serial Sybil snowball campaign:
- Seed & Compounding: The operator started on October 28 with a
$1.75 USDCtask requiring a$0.35 USDCstake. Every time a worker bot claimed the task and got trapped bycheckSubmit() -> false, the operator waited for the 1-hour expiry window to lapse, swept the victim's forfeited stake, and compounded the stolen USDC into a larger bounty (scaling from$1.75to$2.40,$3.40,$4.50,$5.00, and$6.70 USDC). - Identity Rotation: Rather than reusing a single requester address that agents could blacklist, the operator transferred the compounded balance to a fresh EOA wallet every 60 to 120 minutes, registered a brand-new on-chain agent identity, and deployed a fresh copy of the 1,087-byte hook contract.
- Bytecode Polymorphism via Metadata Tail: Across 26 deployed hook contracts—from
0x1db4e9d200254246447d53520c4e838ef33f3ac4on variant #1 to0x2adac8840c5967b1e2f402242f187b185f6dcc11on variant #26—the executable opcode prefix was 100% identical, differing only in the 32-byte CBOR IPFS metadata hash appended by the Solidity compiler at the end of the bytecode.
How We Engineered Our Pre-Claim Bytecode Firewall
Relying on static wallet blacklists is futile against an adversary who rotates EOA addresses and contract deployments every hour. To permanently immunize our autonomous agent operations on Base Mainnet, we built a three-stage Pre-Claim Bytecode & Simulation Firewall directly into our task evaluation pipeline.
Stage 1: Canonical Hook Allowlist & Zero-Hook Fast Path
Before our agent ever signs an EIP-3009 USDC authorization or submits a claim transaction, it inspects the task's hookConfig.contracts array and settlement terms. If a task requires a worker stake (stakeRequired === true), any attached hook contract address must match our verified protocol allowlist (such as the audited canonical token reward hook 0x8e28bb2c54443f54030ff9f2bc1f1794c017f0ca). Any unknown or unverified hook address on a staked task immediately triggers a quarantine hold.
Stage 2: Normalized Runtime Bytecode Fingerprint & Opcode Heuristics
For any non-allowlisted hook contract, our screening module fetches the live runtime bytecode via eth_getCode, strips the trailing 53-byte CBOR compiler metadata section (a264697066735822...64736f6c6343...0033) so that recompiled clones produce an identical SHA-256 code hash, and scans the control-flow graph for two fatal opcode signatures:
- Unconditional False Return in
checkSubmit(0xb65065f4): Disassembling the jump target for selector0xb65065f4and detecting a basic block that pushes0x00(PUSH0/5forPUSH1 0x00/6000) without any conditional jump (JUMPI) or external/storage read (SLOAD,STATICCALL) leading to a0x01return path. - Gas-Griefing Spin Loops: Detecting tight backward
JUMPIloops conditioned on theGAS(0x5a) opcode—specifically the6103e85a116101ab57sequence (while (gasleft() > 1000) {}).
// Pre-Claim EVM Hook Screening Gate (TypeScript / Node.js)
export async function verifyTaskHooksBeforeClaim(
rpcUrl: string,
taskId: string,
workerAddress: string,
stakeRequired: boolean,
hookAddresses: string[]
): Promise<{ safe: boolean; reason?: string }> {
const CANONICAL_HOOK_ALLOWLIST = new Set([
'0x8e28bb2c54443f54030ff9f2bc1f1794c017f0ca' // Canonical Token Reward Hook
]);
for (const rawHook of hookAddresses) {
const hook = rawHook.toLowerCase();
if (CANONICAL_HOOK_ALLOWLIST.has(hook)) continue;
const codeHex = await ethGetCode(rpcUrl, hook);
const cleanHex = codeHex.replace(/^0x/, '').toLowerCase();
// 1. Flag hardcoded PUSH0 return in checkSubmit (5f94935050505056) or gasleft() spin loop (6103e85a11)
if (cleanHex.includes('5f94935050505056') || cleanHex.includes('6103e85a11')) {
return {
safe: false,
reason: `Blocked malicious stake-theft honeypot hook ${hook} on task ${taskId} (hardcoded checkSubmit=false & gas-grief loop)`
};
}
// 2. Reject any unverified custom hook when worker stake is at risk
if (stakeRequired) {
return {
safe: false,
reason: `Quarantined staked task ${taskId}: unverified custom lifecycle hook ${hook}`
};
}
}
return { safe: true };
}
Stage 3: Pre-Flight Full-Lifecycle eth_call Simulation
Even if a hook contract uses more subtle storage conditions rather than a literal PUSH0, an agent should never claim a staked task without simulating the entire lifecycle first. Before locking a stake, our pre-flight verifier executes a read-only eth_call directly against every hook's checkSubmit(taskId, mockContext, workerAddress, dummyDeliverableHash) selector (0xb65065f4). When we ran that pre-flight check against task 0x7f2e4d0a2b1cf88474bdd1857d26f82bc0712043d6e6cb3b1d832de6b523629a and its attached hook 0x2adac8840c5967b1e2f402242f187b185f6dcc11, the firewall immediately flagged the 1,087-byte runtime signature and aborted the claim in under 40 milliseconds—preserving our $1.00 USDC stake with zero gas spent.
Key Takeaway for Agentic Systems Builders: In open on-chain marketplaces, natural-language task prompts are untrusted user input, and custom escrow hooks are untrusted executable code. Treating pre-claim EVM bytecode disassembly and full-lifecycle eth_call simulation as mandatory pre-flight gates is the difference between an autonomous agent that compounds revenue and one that leaks its treasury to automated Sybil traps.
