BEVM Janus June 15, 2026 · 9 min read

The exploit hiding in dead code: orphan-block DELEGATECALL

Some of the most dangerous bytecode never appears in the source. It lives in a basic block that looks unreachable — until a computed jump lands on it at runtime. Static source tools don't see it. Here's how BEVM finds it and Janus turns it into a kill chain.

Illustrative
The contract and traces below are a representative, synthetic example used to teach the pattern. The technique — CFG reachability + computed-jump discovery + fork-replay confirmation — is exactly what BEVM runs on real targets.

Why source-level tools miss it

The EVM has no functions — only JUMP, JUMPI, and JUMPDEST. A Solidity compiler emits a jump table at the top of the contract that dispatches on the 4-byte selector. But jump targets can be computed at runtime, and not every reachable JUMPDEST corresponds to a public function in the source. When a block holds a powerful opcode — DELEGATECALL, SELFDESTRUCT, an unguarded SSTORE to slot 0 — but appears "orphaned" from the normal dispatcher, a source-only linter has nothing to flag. The bytecode tells a different story.

Step 1 — X-ray the bytecode

In BEVM you attach by address (or paste bytecode). Within a second you get the disassembly, the resolved selectors, and the control-flow graph. The left rail lists every basic block with a verb and a flag — and the ones we care about are tagged orphan:

# BEVM · CFG BLOCKS
0x2f2  PROXY   PATCH → JUMPI (46)   SSTORE   orphan / unreached
0x32b  PROXY   TRANSFER → JUMPI (36) CALL     orphan / unreached
0x4a1          OPAQUE → JUMP (12)     DELEGATECALL  orphan / unreached

Block 0x4a1 contains a DELEGATECALL and is marked orphan / unreached — the standard dispatcher never falls through to it. That is precisely the kind of block an attacker hunts for.

Step 2 — can it actually be reached?

"Unreached by the dispatcher" is not the same as "unreachable." BEVM builds a reachability map and looks for computed jumps — a JUMP whose destination is taken from calldata, storage, or a prior computation. If some path lets an attacker steer the program counter onto 0x4a1, the dead code is suddenly very much alive.

# attack-graph: who can write the jump target?
selector 0x5ec2dc8d  → writes slot 0x6 (jumpTarget)   ungated
block    0x4a1        ← JUMP [slot 0x6]               DELEGATECALL [slot 0x7]
slot 0x7 (impl) ← writable via selector 0x3d103b6d      ungated

Now the shape is clear: two ungated setters let an attacker control both the jump target and the DELEGATECALL implementation address. Point the jump at 0x4a1, point the implementation at an attacker contract, and the victim delegate-calls into attacker code with the victim's storage and balance. That is full takeover.

Step 3 — prove it on a fork

A graph is a hypothesis. BEVM's Intruder seeds the GPU fuzzer with the attack-graph routes (orphan-block PCs get priority), and any candidate that moves value is replayed on an Anvil mainnet fork. Only a strictly positive, attacker-controlled balance delta is reported:

# BEVM · fork replay verdict
chain: setJumpTarget(0x4a1) → setImpl(attacker) → trigger()
  step 1  ok   SSTORE slot 0x6 = 0x4a1
  step 2  ok   SSTORE slot 0x7 = 0xATTACKER
  step 3  ok   DELEGATECALL → attacker.sweep()
  ─────────────────────────────────────────────
  CONFIRMED  attacker_eth_delta = +142.3 ETH   (replayed on fork)

Step 4 — let Janus explain it

Findings still need to be understood and fixed. Paste the result into Janus and it reasons like an attacker, then writes it up for a human:

CRITICAL — Arbitrary DELEGATECALL via computed jump. Two ungated setters (0x5ec2dc8d, 0x3d103b6d) let any caller control the runtime jump target and the delegatecall implementation. An attacker steers execution into orphan block 0x4a1 and delegatecalls attacker-owned code, executing with the contract's storage and funds — equivalent to full ownership. Fix: gate both setters behind access control, and prefer a fixed jump table over storage-derived targets; never delegatecall to an address taken from unauthenticated state.

Why the combination matters

No single technique gets here. Static analysis flags the opcode but can't tell you it's reachable. A fuzzer without the CFG wastes its budget on the front door. Reachability + computed-jump discovery finds the path; GPU fuzzing + fork replay proves it; and Janus turns the proof into something a team can act on. That's the BEVM + Janus loop: find, prove, explain.

Illustrative example for education. Addresses, blocks, and the ETH figure are synthetic. Only run security testing against contracts you own or are explicitly authorized to test.

← All posts