Opcode Summary
| Property | Value |
|---|---|
| Opcode | 0x5B |
| Mnemonic | JUMPDEST |
| Gas | 1 |
| Stack Input | (none) |
| Stack Output | (none) |
| Behavior | Marks a valid destination for JUMP (0x56) and JUMPI (0x57). Executes as a no-op — it does not modify the stack, memory, or storage. Before execution, the EVM performs JUMPDEST analysis to build a bitmap of all valid jump destinations. A byte at offset n is a valid JUMPDEST only if (a) the byte value is 0x5B and (b) offset n is not within the immediate data of a PUSH instruction. Jumping to any offset not marked as a valid JUMPDEST causes an exceptional halt (transaction revert). |
Threat Surface
JUMPDEST is the simplest opcode in the EVM by computation — a 1-gas no-op that does nothing at execution time. Its entire security significance lies in what it represents: the only valid target for control flow transfers. The EVM’s “JUMPDEST analysis” pass, which runs before execution to build a bitmap of valid jump targets, is the actual mechanism that enforces control flow integrity. This analysis and the semantic gap it creates between how different tools interpret the byte 0x5B is the core of JUMPDEST’s threat surface.
Three properties drive the threat surface:
-
Phantom JUMPDESTs create an analysis divergence. The byte 0x5B can appear inside the immediate data of PUSH instructions (e.g.,
PUSH2 0x5B5B). The EVM correctly identifies these as data, not valid JUMPDESTs, because its JUMPDEST analysis walks the bytecode instruction-by-instruction, skipping PUSH immediates. However, naive bytecode scanners, decompilers, and on-chain verification contracts that perform byte-by-byte scanning (or use different reachability heuristics) may interpret these 0x5B bytes as valid jump targets. This divergence between “what the EVM sees” and “what analysis tools see” has been directly exploited to bypass on-chain security verification (Dharma IndestructibleRegistry, 2019). -
JUMPDEST analysis is a linear-time pre-execution cost. Every time a contract is called, the EVM must scan the entire bytecode to build the JUMPDEST bitmap. For contract creation (initcode), this analysis was historically unmetered and unbounded, creating a DoS vector where large initcode forced nodes to perform expensive analysis at minimal gas cost. EIP-3860 (Shanghai, 2023) added metering (2 gas per 32-byte chunk) and a 49,152-byte size cap. For deployed code, analysis cost is subsumed by the code-loading gas, but it still scales linearly with contract size.
-
Spurious JUMPDESTs enable bytecode obfuscation. Attackers or obfuscators can insert unreachable JUMPDEST instructions throughout bytecode to confuse decompilers and static analysis tools. Since JUMPDEST is a valid opcode that costs only 1 gas when executed sequentially, scattering them through dead code paths inflates the apparent control flow graph, making audit and reverse engineering significantly harder.
Smart Contract Threats
T1: Phantom JUMPDEST Confusion for Auditors and On-Chain Verifiers (High)
The byte 0x5B appearing inside PUSH immediate data creates “phantom JUMPDESTs” — locations that look like valid jump destinations to naive analysis but are not recognized by the EVM. This divergence has concrete security consequences:
-
On-chain bytecode verifiers. Contracts that scan bytecode to verify security properties (e.g., “does this contract contain SELFDESTRUCT?”) must walk bytecode instruction-by-instruction to correctly skip PUSH data. If they instead perform byte-by-byte scanning or use incorrect reachability heuristics when resuming analysis after unreachable code, they will misidentify phantom JUMPDESTs as real ones. This was the exact vulnerability in Dharma’s
IndestructibleRegistry(December 2019): the registry’s analysis resumed scanning at a phantom JUMPDEST inside PUSH data, causing it to miss a SELFDESTRUCT that followed. -
Decompiler and static analysis tool confusion. Tools that reconstruct control flow graphs from bytecode must correctly handle phantom JUMPDESTs. A decompiler that treats 0x5B bytes inside PUSH data as valid basic block starts will produce incorrect CFGs, potentially hiding malicious logic or showing false code paths.
-
Auditor misdirection. When reviewing raw bytecode (e.g., unverified contracts on Etherscan), auditors may be misled by phantom JUMPDESTs into believing certain code paths are reachable when they are not, or vice versa. Malicious contract authors can deliberately embed 0x5B bytes in PUSH data to obscure the true control flow.
Why it matters: Any system that analyzes bytecode for security properties must implement JUMPDEST analysis identically to the EVM. A single divergence in how PUSH immediates are skipped can invalidate the entire analysis.
T2: JUMPDEST Analysis Overhead During Deployment and Validation (Medium)
JUMPDEST analysis must scan every byte of the bytecode to build the validity bitmap. For contract creation, this presents a DoS surface:
-
Pre-EIP-3860 (before Shanghai, 2023). Initcode had no enforced size limit and JUMPDEST analysis was completely unmetered. An attacker could submit a creation transaction with hundreds of kilobytes of initcode, forcing nodes to perform linear-time JUMPDEST analysis at negligible gas cost. A documented transaction (0x48342552…) deployed 116,262 bytes of initcode — 237% of the current limit — using 25.38M gas, demonstrating the pre-metering vulnerability.
-
Post-EIP-3860. Initcode is capped at 49,152 bytes and charged 2 gas per 32-byte chunk (max ~3,072 gas for JUMPDEST analysis). This bounds the worst case but does not eliminate the computational work. Nodes must still perform the full linear scan.
-
Deployed code at call time. Each time a contract is called, the client must perform JUMPDEST analysis (or use a cached bitmap). For contracts near the 24,576-byte EIP-170 limit, this is ~24K of bytecode to scan per call. While typically fast (microseconds), adversarial contracts stuffed with PUSH32 instructions maximize the number of bytes the scanner must process per instruction.
Why it matters: EIP-3860 significantly reduced this threat, but JUMPDEST analysis remains a protocol-level cost that scales linearly with code size. The SEC-49 vulnerability (2015) showed that even the data structure choice for storing JUMPDEST results can cause 2GB+ memory consumption under adversarial conditions.
T3: Code Injection via Constructor Arguments Containing 0x5B (Medium)
When a contract is deployed, the constructor arguments are ABI-encoded and appended to the initcode. If these arguments contain 0x5B bytes (which is common — 0x5B is just the integer 91 or part of any address/hash containing that byte), the JUMPDEST analysis of the initcode will correctly classify them as non-instruction data (they’re beyond the RETURN in the constructor). However, this creates risks for tooling:
-
Bytecode analysis of creation transactions. Tools analyzing creation transaction input data may incorrectly identify 0x5B bytes in constructor arguments as JUMPDESTs if they don’t properly identify where the initcode ends and the arguments begin. Since ABI-encoded arguments are not delineated by an opcode boundary, the only way to distinguish them is to execute the initcode or parse the ABI encoding.
-
Crafted constructor arguments for metadata confusion. An attacker deploying a contract through a factory can supply constructor arguments specifically crafted to contain bytecode-like sequences (including 0x5B markers) that confuse any tool analyzing the factory’s creation transaction. This won’t affect EVM execution but can produce misleading results in block explorers, decompilers, and security scanners.
-
CODECOPY of constructor args into runtime code. If a contract’s constructor uses CODECOPY to copy portions of the creation bytecode (including arguments) into the deployed runtime code, carefully crafted arguments containing 0x5B could introduce valid JUMPDESTs into the deployed code at attacker-controlled offsets. This is a theoretical vector requiring specific constructor patterns.
Why it matters: The boundary between initcode and constructor arguments is semantically meaningful but not syntactically marked in the bytecode, creating an analysis gap that 0x5B bytes can exploit.
T4: Obfuscation Using Spurious JUMPDESTs (Medium)
JUMPDEST costs only 1 gas and is a no-op, making it the cheapest possible instruction to scatter through bytecode for obfuscation:
-
Control flow graph explosion. Inserting JUMPDESTs at many offsets inflates the number of potential basic block starts. A decompiler or static analysis tool must consider each JUMPDEST as a possible entry point, exponentially increasing the complexity of CFG reconstruction if it cannot prove which JUMPDESTs are actually targeted by JUMP/JUMPI instructions.
-
Dead code camouflage. Unreachable code regions peppered with JUMPDESTs appear as legitimate (if confusing) code to analysis tools. Malicious contracts can hide their true logic among dozens of fake code paths, each beginning with a JUMPDEST, making manual audit impractical.
-
Strategy hiding in DeFi. MEV bots and proprietary trading contracts frequently use JUMPDEST-based obfuscation to prevent competitors from reverse-engineering their strategies. While not a vulnerability per se, this practice makes it harder for security researchers and auditors to identify malicious behavior.
-
Minimal gas overhead. A contract with 100 spurious JUMPDESTs scattered through dead code pays 0 extra gas at runtime (dead code is never executed) and only marginally increases deployment cost (100 extra bytes × 200 gas per byte = 20,000 gas, ~$0.50 at typical gas prices).
Why it matters: JUMPDEST obfuscation is cheap, effective, and widely used. It directly undermines the auditability of smart contracts, which is a foundational security assumption of the ecosystem.
T5: EOF Transition Removing JUMPDEST Requirement (Low)
The EVM Object Format (EOF, EIP-3540 and related EIPs) fundamentally changes how jump destinations work:
-
JUMPDEST becomes invalid in EOF containers. EOF contracts use a JUMPDEST table (EIP-3690) stored in a dedicated section, or eliminate dynamic jumps entirely in favor of structured control flow (
CALLF/RETFper EIP-4750). The 0x5B byte in EOF code is not a valid opcode. -
Transition period risk. During the transition from legacy to EOF, the ecosystem will have two classes of contracts with fundamentally different control flow validation. Tools must handle both, and assumptions valid for one format may be wrong for the other. A tool that assumes “0x5B = JUMPDEST” will produce incorrect results for EOF contracts.
-
Legacy contracts persist indefinitely. Existing deployed contracts using JUMPDEST will never be migrated; they remain on-chain forever. The JUMPDEST analysis infrastructure must be maintained in perpetuity for legacy contracts even as EOF becomes the default.
Why it matters: EOF eliminates the JUMPDEST threat class for new contracts but creates a long transition period where both paradigms coexist, increasing implementation complexity and the risk of analysis errors.
Protocol-Level Threats
P1: JUMPDEST Analysis as DoS Vector During Contract Creation (Medium)
JUMPDEST analysis during contract creation has been a recurring protocol-level concern:
-
SEC-49 (2015, pre-mainnet). A bug bounty finding demonstrated that 1,024 recursive calls followed by 15,000 JUMPDEST opcodes consumed ~2GB of RAM in go-ethereum due to an inefficient map-based data structure for storing JUMPDEST positions. Each recursive call duplicated the analysis for the same code, and the map overhead per entry was significant. The fix switched to caching the JUMPDEST bitmap across recursive calls and using a compact bitvector instead of a map, reducing memory usage to ~700KB. As the go-ethereum developer noted, the attack was “basically an Ethereum version of the classic XML entity bomb.”
-
Unmetered initcode (pre-Shanghai, 2023). Before EIP-3860, initcode JUMPDEST analysis was not charged gas. A creation transaction with 100KB of initcode forced ~100KB of linear scanning at zero incremental gas cost beyond the base transaction fee. Combined with the 63/64 gas forwarding rule from EIP-150, an attacker could create deeply nested CREATE calls, each triggering fresh JUMPDEST analysis.
-
Post-mitigation residual risk. EIP-3860 caps initcode at 49,152 bytes and charges 2 gas per 32-byte chunk. EIP-7907 proposes extending per-word metering to deployed code loading beyond 24KB. These changes bound the worst case but do not eliminate the computational work. Pathological contracts (e.g., alternating PUSH32 and JUMPDEST) maximize the ratio of bytes scanned to actual executable instructions.
P2: EOF Migration and Protocol Complexity (Low)
The EOF transition (EIP-3540, EIP-3690, EIP-4750) will eventually eliminate JUMPDEST analysis for new contracts, but introduces protocol complexity:
-
Dual validation paths. Consensus clients must maintain both legacy JUMPDEST analysis (for pre-EOF contracts) and EOF validation (for new contracts) indefinitely. This increases the attack surface for consensus divergence — a bug in either path could cause chain splits.
-
EIP-7921 alternative. An alternative proposal (EIP-7921) suggests allowing all 0x5B bytes to serve as valid JUMPDESTs regardless of whether they appear inside PUSH data. This would eliminate the linear JUMPDEST analysis pass entirely but change the semantics of existing bytecode (some previously-invalid jump targets would become valid). This is a breaking change that could only be safely applied alongside EOF adoption.
-
Cross-client consistency. The JUMPDEST analysis algorithm is deceptively simple but has produced divergences between clients and testing frameworks. Foundry/Anvil diverged from mainnet behavior in handling JUMPDEST validity within the same transaction as SELFDESTRUCT, illustrating how subtle the edge cases are.
P3: Consensus Safety of JUMPDEST Analysis (Low)
The JUMPDEST analysis algorithm (walk bytecode, skip PUSH immediates, mark 0x5B bytes at non-PUSH offsets) is deterministic and well-specified in the Yellow Paper (section 9.4.3). All major clients (geth, Nethermind, Besu, Erigon, Reth) implement it identically. No mainnet consensus failures have been attributed to JUMPDEST analysis divergence. The algorithm’s simplicity is its strength — it requires no state access, no external data, and no floating-point math.
Edge Cases
| Edge Case | Behavior | Security Implication |
|---|---|---|
0x5B byte inside PUSH immediate data (e.g., PUSH2 0x5B5B) | Not a valid JUMPDEST — the EVM skips PUSH immediates during analysis | Phantom JUMPDEST: naive bytecode scanners may misidentify as a valid jump target, diverging from EVM behavior |
Consecutive JUMPDESTs (5B 5B 5B 5B) | Each is a valid, independent JUMPDEST; execution falls through as sequential no-ops | No computational effect, but inflates the CFG for analysis tools; wastes 1 gas per JUMPDEST if executed |
| JUMPDEST at bytecode offset 0 | Valid JUMPDEST at position 0; PUSH1 0x00 JUMP will land here | Unusual but legal; some obfuscation techniques use offset-0 JUMPDESTs to confuse decompilers expecting a function dispatcher |
| Thousands of JUMPDESTs in a single contract | All valid; JUMPDEST analysis bitmap grows linearly with code size | Pre-SEC-49 fix: 15,000 JUMPDESTs with recursive calls consumed 2GB RAM. Post-fix: bitmap is compact (1 bit per byte offset) |
| JUMPDEST immediately after a PUSH instruction’s last data byte | Valid JUMPDEST if it’s at a non-PUSH-data offset | Boundary case that tests correct PUSH immediate length handling; off-by-one errors here produce phantom JUMPDESTs |
| JUMPDEST in unreachable code (after STOP/RETURN/REVERT) | Still a valid JUMPDEST — the EVM does not perform reachability analysis | Can be jumped to from any reachable JUMP/JUMPI; “unreachable” is a static analysis concept, not an EVM one |
| 0x5B in code deployed via CODECOPY from constructor | Valid JUMPDEST if not inside PUSH data in the deployed code | JUMPDEST analysis runs on deployed bytecode independently; the initcode analysis is separate |
| JUMPDEST in EOF container code | Invalid opcode — EOF replaces JUMPDEST with a dedicated table | Tools must detect EOF containers (magic bytes 0xEF00) before applying legacy JUMPDEST analysis |
Real-World Exploits
Exploit 1: Dharma IndestructibleRegistry Bypass — Phantom JUMPDEST (December 2019)
Root cause: Divergence between how Dharma’s IndestructibleRegistry contract analyzed bytecode for dangerous opcodes and how the EVM validates JUMPDEST positions. The registry’s reachability-based scanning algorithm resumed analysis at phantom JUMPDESTs inside PUSH data, missing a hidden SELFDESTRUCT.
Details: Dharma’s IndestructibleRegistry was an on-chain contract that verified whether a given contract’s bytecode was provably indestructible — free of SELFDESTRUCT, CALLCODE, and DELEGATECALL opcodes. The verification algorithm worked as follows:
- Walk the bytecode opcode by opcode
- Skip PUSH immediate data
- When encountering a terminating opcode (JUMP, RETURN, STOP), enter “unreachable mode”
- In unreachable mode, scan byte-by-byte looking for JUMPDEST (0x5B) to resume normal analysis
The critical flaw was in step 4: the byte-by-byte scan in unreachable mode does not skip PUSH immediates. This differs from how the EVM determines valid JUMPDESTs. The EVM walks the entire bytecode linearly, skipping PUSH immediates, regardless of reachability.
Steve Marx (ConsenSys Diligence) constructed an exploit using this 9-byte bytecode:
0x00: PUSH1 0x06 // push jump target
0x02: JUMP // jump to offset 6; registry enters "unreachable mode" for bytes 3-5
0x03: PUSH2 // registry: skip 2 bytes of push data in unreachable scan
0x04: [0x5B] // registry: finds JUMPDEST, exits unreachable mode
0x05: PUSH3 // registry: skips next 3 bytes as push data → misses SELFDESTRUCT
0x06: [0x5B] // EVM: this is the real JUMPDEST (offset 6, valid)
0x07: CALLVALUE // push 0 onto stack (selfdestruct target)
0x08: SELFDESTRUCT // destroy the contract
The registry saw: PUSH1, JUMP, (unreachable) PUSH2, JUMPDEST at 0x04, PUSH3 (skip 3 bytes including the real JUMPDEST and SELFDESTRUCT). It concluded the contract was safe.
The EVM saw: PUSH1(0x06), JUMP, PUSH2(0x5B, 0x5B), 0x5B at offset 4 is PUSH data (not a JUMPDEST), JUMPDEST at offset 6 (valid), CALLVALUE, SELFDESTRUCT. Execution jumps to offset 6 and self-destructs.
Marx deployed the contract, registered it as indestructible, then destroyed it on mainnet.
JUMPDEST’s role: The entire exploit hinged on the phantom JUMPDEST at offset 0x04 — a 0x5B byte inside PUSH2’s immediate data that the registry incorrectly identified as a valid JUMPDEST, causing it to resume analysis at the wrong position and miss the SELFDESTRUCT.
Impact: Low direct financial impact (Dharma was not using the registry for security-critical decisions). High conceptual impact: demonstrated that on-chain bytecode analysis must replicate the EVM’s exact JUMPDEST validation algorithm. A fixed version was promptly deployed.
References:
Exploit 2: SEC-49 JUMPDEST Memory Exhaustion DoS (May 2015)
Root cause: go-ethereum used a map[uint64]struct{} to store JUMPDEST positions, and re-computed the JUMPDEST map for each recursive call without caching. Adversarial bytecode with many JUMPDESTs and deep recursion caused quadratic memory growth.
Details: A bug bounty hunter submitted a state test with bytecode containing 1,024 recursive CALLs to itself followed by a STOP and 15,000 consecutive JUMPDEST opcodes. The transaction cost only ~80,000 gas but caused go-ethereum to:
- Perform JUMPDEST analysis for each of the 1,024 recursive call frames
- Allocate a new map with 15,000 entries for each frame
- The Go map overhead (~128 bytes per entry) resulted in ~2GB total RAM consumption
- Processing took ~18 seconds
The C++ client (cpp-ethereum) had the same issue but used a std::set, consuming ~1.2GB. Both were well beyond acceptable resource usage for an 80K-gas transaction.
The fix in go-ethereum (PR #1150) cached the JUMPDEST bitmap across recursive calls to the same contract and switched to a compact bitvector representation. Post-fix, the same test used ~700KB of memory and completed in under a second.
JUMPDEST’s role: The attack is a direct consequence of the JUMPDEST analysis requirement. Every call to a contract triggers JUMPDEST analysis, and without caching, recursive calls multiply the cost. The 15,000 JUMPDEST opcodes maximized the number of entries in the data structure.
Impact: Found pre-mainnet through the bug bounty program. Had it reached production, a low-gas transaction could have crashed or severely degraded Ethereum nodes. The geth developer noted it was “basically an Ethereum version of the classic XML entity bomb.”
References:
- go-ethereum Issue #1147: SEC-49 JUMPDEST vulnerability
- go-ethereum PR #1150: Improve JUMPDEST analysis
Exploit 3: Unmetered Initcode JUMPDEST Analysis DoS (Pre-Shanghai, Ongoing Until 2023)
Root cause: Before EIP-3860 (Shanghai hard fork, April 2023), contract creation initcode had no enforced size limit and the JUMPDEST analysis work was not metered in gas. Attackers could force nodes to perform expensive linear-time analysis at minimal cost.
Details: When a contract is created via CREATE or CREATE2, the EVM must perform JUMPDEST analysis on the initcode before execution begins. This analysis is O(n) in the initcode length. Before EIP-3860:
- No protocol-enforced maximum initcode size existed
- JUMPDEST analysis cost was not charged to the transaction
- The only cost was the per-byte calldata cost (16 gas per non-zero byte, 4 gas per zero byte)
A documented transaction (0x48342552…) created 11 contracts with 116,262 bytes of total initcode, consuming 25.38M gas. This was 237% of the current EIP-3860 limit and demonstrated that real-world transactions were exploiting the lack of metering.
The worst case was a creation transaction with maximal initcode: at the 30M block gas limit and 16 gas per byte, an attacker could submit ~1.87MB of initcode, forcing ~1.87MB of JUMPDEST analysis per node per block. Combined with CREATE-within-initcode patterns that trigger nested JUMPDEST analysis, the amplification factor was significant.
EIP-3860 (Shanghai, April 2023) fixed this by capping initcode at 49,152 bytes and charging 2 * ceil(len(initcode) / 32) gas for JUMPDEST analysis. The maximum analysis cost is now ~3,072 gas, properly metering the work.
JUMPDEST’s role: The entire cost being metered is specifically the JUMPDEST analysis pass. Without JUMPDESTs in the EVM, this analysis would be unnecessary and the DoS vector would not exist.
Impact: Network-wide performance degradation over multiple years. EIP-3860 was specifically motivated by the need to meter JUMPDEST analysis cost during contract creation.
References:
Attack Scenarios
Scenario A: Phantom JUMPDEST Bytecode Verification Bypass
// On-chain bytecode verifier that checks for dangerous opcodes
contract NaiveBytecodeVerifier {
function isSafe(address target) external view returns (bool) {
bytes memory code;
assembly {
let size := extcodesize(target)
code := mload(0x40)
mstore(0x40, add(code, add(size, 0x20)))
mstore(code, size)
extcodecopy(target, add(code, 0x20), 0, size)
}
for (uint256 i = 0; i < code.length; i++) {
uint8 op = uint8(code[i]);
// Skip PUSH data
if (op >= 0x60 && op <= 0x7F) {
i += (op - 0x5F);
continue;
}
// BUG: After a terminating opcode, scanner enters
// byte-by-byte mode looking for JUMPDEST to resume.
// This mode does NOT skip PUSH immediates, creating
// phantom JUMPDEST divergence from EVM.
if (op == 0x56 || op == 0x00 || op == 0xF3 || op == 0xFD) {
// Skip to next JUMPDEST
i++;
while (i < code.length && uint8(code[i]) != 0x5B) {
i++;
}
continue;
}
// Flag dangerous opcodes
if (op == 0xFF) return false; // SELFDESTRUCT
if (op == 0xF2) return false; // CALLCODE
if (op == 0xF4) return false; // DELEGATECALL
}
return true;
}
}
// Attacker deploys bytecode that passes the verifier but self-destructs:
// PUSH1 0x06, JUMP, PUSH2, [0x5B], [PUSH3], JUMPDEST, CALLVALUE, SELFDESTRUCT
// The verifier sees PUSH3 starting at the phantom JUMPDEST offset,
// skipping over the real JUMPDEST + SELFDESTRUCT.Scenario B: JUMPDEST Analysis Memory Bomb (Pre-Fix Pattern)
// Adversarial bytecode pattern from SEC-49
// 1024 recursive self-calls followed by 15,000 JUMPDESTs
//
// Pseudobytecode (simplified):
//
// [setup: push own address, push gas, CALL to self] × 1024 nesting
// STOP
// JUMPDEST × 15,000
//
// With map-based JUMPDEST storage (pre-fix):
// - Each call frame: new map with 15,000 entries
// - Go map overhead: ~128 bytes/entry
// - 1024 frames × 15,000 entries × 128 bytes ≈ 1.96 GB
// - Transaction cost: ~80,000 gas
//
// Post-fix (bitvector + caching):
// - Single bitvector: ceil(code_length / 8) bytes
// - Cached across recursive calls to same contract
// - Memory: ~700 KB total
Scenario C: JUMPDEST-Based Bytecode Obfuscation
// A contract that uses spurious JUMPDESTs to confuse decompilers.
// The real logic is a simple token transfer, but the bytecode
// contains 50+ dead-code JUMPDESTs creating fake basic blocks.
// Deployed bytecode (conceptual):
//
// 0x00: [real dispatcher - routes to actual functions]
// 0x80: JUMPDEST // fake block 1
// 0x81: PUSH1 0x00
// 0x83: PUSH1 0x00
// 0x85: REVERT
// 0x86: JUMPDEST // fake block 2
// 0x87: PUSH20 0xdead...
// 0xA2: SELFDESTRUCT // looks scary but unreachable
// ...
// (50 more fake blocks with various dangerous-looking opcodes)
// ...
// 0x200: JUMPDEST // real function: transfer()
// 0x201: [legitimate transfer logic]
//
// Decompilers must prove which JUMPDESTs are actually targeted by
// JUMP/JUMPI to exclude fake blocks. This is PSPACE-hard in the
// general case (requires solving the halting problem for stack values).
// A human auditor sees 50+ code paths including SELFDESTRUCT
// and DELEGATECALL, but all the dangerous opcodes are in
// unreachable dead code. Proving this requires full dataflow
// analysis of every JUMP target.Scenario D: Initcode JUMPDEST Analysis Cost Amplification
// Pre-EIP-3860: attacker submits a creation transaction with maximum initcode
// to waste node computation on JUMPDEST analysis
contract JumpdestAnalysisDOS {
function attack() external {
// Pre-EIP-3860: no size limit on initcode
// Build initcode that is mostly JUMPDEST (0x5B) bytes
// followed by STOP, to maximize analysis work
bytes memory initcode = new bytes(200_000);
for (uint256 i = 0; i < initcode.length - 1; i++) {
initcode[i] = 0x5B; // JUMPDEST
}
initcode[initcode.length - 1] = 0x00; // STOP
// Each CREATE triggers fresh JUMPDEST analysis on 200KB
// Cost: ~3.2M gas for calldata, but forces 200KB linear scan
assembly {
let addr := create(0, add(initcode, 0x20), mload(initcode))
}
// Post-EIP-3860: this reverts because initcode > 49,152 bytes
// and initcode_cost = 2 * ceil(200000/32) = 12,500 gas is charged
}
}Mitigations
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: Phantom JUMPDEST confusion | Implement JUMPDEST analysis identically to the EVM | Walk bytecode instruction-by-instruction, skip PUSH(n) immediates exactly n bytes; never use byte-by-byte scanning or reachability heuristics for JUMPDEST identification |
| T1: On-chain bytecode verification | Use the EVM’s own JUMPDEST analysis, not custom scanners | Deploy verification contracts that replicate geth’s codeBitmap() algorithm exactly; or use EXTCODEHASH to verify against known-good code hashes |
| T2: Initcode JUMPDEST analysis DoS | EIP-3860 metering and size limits | Upgrade to Shanghai-compatible clients; initcode capped at 49,152 bytes, charged 2 gas per 32-byte chunk |
| T2: JUMPDEST analysis memory consumption | Use compact bitvector + caching | All major clients now use bitvector-based JUMPDEST bitmaps cached across recursive calls (fixed since SEC-49) |
| T3: Constructor argument confusion | Separate initcode from constructor args in analysis | Use ABI decoding to identify the initcode/argument boundary; do not perform opcode analysis on constructor arguments |
| T4: JUMPDEST obfuscation | Use advanced decompilation with dataflow analysis | Tools like Dedaub, Heimdall, and Panoramix resolve JUMP targets via symbolic execution to exclude unreachable JUMPDESTs |
| T4: Audit of obfuscated contracts | Require source code verification for trust | Only interact with contracts verified on Etherscan/Sourcify; treat unverified contracts as untrusted regardless of bytecode analysis |
| T5: EOF transition | Support both legacy and EOF validation | Implement EOF container detection (magic bytes 0xEF00) and route to appropriate validation path; maintain legacy JUMPDEST analysis for pre-EOF contracts |
| General | Adopt EOF for new deployments | EOF eliminates JUMPDEST entirely, replacing it with compile-time validated jump tables or structured control flow (CALLF/RETF) |
EIP/Protocol-Based Protections
- EIP-3860 (Shanghai, 2023): Meters initcode JUMPDEST analysis at 2 gas per 32-byte chunk and caps initcode at 49,152 bytes. The most direct mitigation for P1.
- EIP-170 (Spurious Dragon, 2016): Caps deployed code at 24,576 bytes, bounding the JUMPDEST analysis work per call.
- EIP-3540 (EOF v1): Introduces structured bytecode containers that separate code from data, enabling compile-time JUMPDEST validation.
- EIP-3690 (EOF JUMPDEST Table): Moves valid jump destinations into a dedicated table section, eliminating runtime JUMPDEST analysis entirely for EOF contracts.
- EIP-4750 (EOF Functions): Replaces dynamic JUMP/JUMPI with structured
CALLF/RETF, removing the need for JUMPDEST altogether in EOF. - EIP-7921 (Skip JUMPDEST immediate argument check): Proposes allowing all 0x5B bytes as valid JUMPDESTs, eliminating the analysis pass but changing semantics. Draft status.
Severity Summary
| Threat ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | High | Medium | Dharma IndestructibleRegistry bypass (2019) |
| T2 | Smart Contract | Medium | Low (post-EIP-3860) | SEC-49 memory DoS (2015), unmetered initcode (pre-2023) |
| T3 | Smart Contract | Medium | Low | Tooling confusion with constructor arguments |
| T4 | Smart Contract | Medium | High | Widespread MEV bot and malicious contract obfuscation |
| T5 | Smart Contract | Low | Low | EOF not yet deployed on mainnet |
| P1 | Protocol | Medium | Low (post-EIP-3860) | SEC-49 (2015), unmetered initcode (pre-Shanghai) |
| P2 | Protocol | Low | Low | No consensus failures attributed to JUMPDEST |
| P3 | Protocol | Low | Low | No mainnet divergences; minor testing framework inconsistencies |
Related Opcodes
| Opcode | Relationship |
|---|---|
| JUMP (0x56) | Unconditional jump — the target offset must be a valid JUMPDEST. JUMP and JUMPDEST are co-dependent: JUMP is useless without JUMPDEST targets, and JUMPDEST is meaningless without JUMP/JUMPI instructions that reference it. |
| JUMPI (0x57) | Conditional jump — same JUMPDEST validation requirement as JUMP. JUMPI enables branching (if/else, loops) and its target must be a valid JUMPDEST. Dynamic JUMPI targets with stack-computed offsets make static analysis of which JUMPDESTs are reachable undecidable in general. |
| PC (0x58) | Returns the current program counter value. Combined with JUMP, allows self-referential jumps (PC JUMPDEST at the same offset creates an infinite loop). PC is deprecated in EOF in favor of relative jumps. |
| PUSH1-PUSH32 (0x60-0x7F) | PUSH instructions’ immediate data is where phantom JUMPDESTs hide. The JUMPDEST analysis algorithm’s core job is correctly skipping PUSH immediates. A PUSH instruction’s byte count (1-32 data bytes) determines how many bytes are excluded from JUMPDEST candidacy. |