Opcode Summary

PropertyValue
Opcodes0x60 (PUSH1) through 0x7F (PUSH32)
MnemonicPUSHn, where n = 1..32
Gas3 (all variants)
Stack Input(none)
Stack Outputvalue (the n-byte immediate, right-aligned and zero-padded to 32 bytes)
BehaviorReads the next n bytes of bytecode (the immediate data) following the opcode and pushes them onto the stack as a single 32-byte word, right-aligned and left-padded with zeros. The program counter advances by 1 + n bytes (opcode + immediate). If the immediate extends past the end of the bytecode, the missing bytes are zero-padded per the Yellow Paper. PUSHn is the only instruction family in legacy EVM that consumes bytes beyond the opcode itself, making the boundary between “code” and “data” ambiguous at the bytecode level.

Threat Surface

The PUSH family is the primary mechanism for embedding constant data in EVM bytecode. Every literal value, address, function selector, storage slot, and jump destination in a compiled contract originates from a PUSHn instruction. This ubiquity makes PUSH both the most frequent opcode family and the one most deeply entangled with bytecode analysis, disassembly, and code verification.

The threat surface centers on four properties:

  1. PUSH data is embedded inline in the code section, blurring the code/data boundary. The EVM does not have separate code and data segments. PUSH immediates live inline alongside executable opcodes. Any byte sequence that appears inside PUSH data — including bytes that look like valid opcodes — is treated as data by the EVM during sequential execution but appears as code to naive disassemblers, decompilers, and static analysis tools. This ambiguity is the foundation for bytecode obfuscation, JUMPDEST confusion, and immutable executable injection attacks.

  2. PUSH data containing 0x5B (JUMPDEST) confuses bytecode analysis. The EVM’s JUMPDEST analysis pass distinguishes real JUMPDEST markers from 0x5B bytes embedded in PUSH data by tracking instruction boundaries. But any tool that performs linear disassembly without PUSH-aware boundary tracking will misidentify PUSH data containing 0x5B as a valid jump destination. This creates “phantom JUMPDESTs” that mislead security tooling, enable obfuscation of control flow, and can cause auditors to misunderstand a contract’s reachable code paths.

  3. Immutable variables and constructor arguments are injected into bytecode as PUSH32 data. The Solidity compiler embeds immutable variable values directly into deployed bytecode using PUSH32. These values are set at deployment time from constructor arguments and become permanent, unmodifiable bytecode. If an attacker controls constructor arguments (e.g., deploying a contract through a factory), they can inject arbitrary 32-byte sequences into the deployed code. When those sequences begin with 0x5B and contain valid EVM opcodes, the “data” becomes executable code if the contract can be induced to jump to it.

  4. PUSHn reading past the end of bytecode produces zero-padded values, not errors. If a PUSHn instruction’s immediate data extends beyond the last byte of the code, the EVM zero-pads the missing bytes. This is well-defined behavior per the Yellow Paper, but it means truncated or malformed bytecode does not halt execution — it silently produces different values than expected. Client implementations that fail to implement this correctly (as seen in EthereumJS’s 2024-2025 consensus bug) produce divergent execution results.


Smart Contract Threats

T1: Immutable Executable Injection via PUSH32 Data (High)

Solidity immutable variables are embedded in deployed bytecode as PUSH32 immediates. The compiler patches these values into the runtime bytecode during deployment, replacing placeholder slots with the actual constructor argument values. If an attacker controls the constructor arguments — by deploying through a factory, governance proposal, or social engineering — they can inject a 32-byte payload that constitutes valid EVM instructions:

  • Payload structure. A 32-byte immutable value like 0x5b63CAFEBABE60005260206000f3... decodes to a complete EVM program: JUMPDEST, PUSH4 0xCAFEBABE, PUSH1 0, MSTORE, PUSH1 32, PUSH1 0, RETURN. The Solidity compiler places this in the deployed bytecode preceded by a PUSH32 opcode. During normal execution, the 32 bytes are treated as a constant. But if the contract contains an assembly block that computes a jump target pointing to the immutable’s offset (where the leading 0x5B sits), the “data” executes as code.

  • Arithmetic trigger. The jump to the payload need not be an obvious jump(constant). An attacker can design the contract’s assembly to compute the target offset from seemingly innocuous arithmetic on user input. To an auditor, the assembly looks like “validation logic” or “gas optimization.” The actual computation resolves to the byte offset of the immutable’s JUMPDEST.

  • Audit evasion. Etherscan source verification passes because the source code compiles to the exact deployed bytecode. The malicious behavior exists only in the constructor arguments, which auditors typically treat as “configuration data,” not executable code. Static analysis tools do not flag immutable values as potential instruction sequences.

  • Chaining immutables. A single PUSH32 provides 32 bytes of payload. Multiple bytes32 immutable declarations yield consecutive payload regions in bytecode, enabling multi-segment hidden programs sufficient for balance transfers, ownership changes, or proxy upgrades.

Why it matters: A working proof-of-concept (Bombadil Systems, 2025) demonstrated end-to-end injection: a deployed contract that passes Etherscan verification, looks clean to auditors, and executes hidden code when triggered with a specific input. The technique targets the gap between “what was audited” (source) and “what was deployed” (bytecode + constructor args).

T2: JUMPDEST Confusion from 0x5B in PUSH Data (Medium)

The EVM’s JUMPDEST analysis correctly identifies that a 0x5B byte within PUSH immediate data is not a valid jump destination. A JUMP to such an offset causes an exceptional halt. However, the confusion arises at the tooling layer:

  • Linear disassembly errors. Disassemblers that scan bytecode byte-by-byte without tracking PUSHn instruction boundaries will interpret a 0x5B inside PUSH data as a standalone JUMPDEST instruction. This produces a corrupted disassembly where subsequent bytes are misidentified, creating phantom instructions that don’t exist in actual execution. If an auditor or automated tool relies on this disassembly, they may analyze code paths that are unreachable or miss code paths that are reachable.

  • Deliberate obfuscation. Malicious contract authors can craft PUSH immediates that contain 0x5B to confuse analysis tools. By embedding fake JUMPDESTs in PUSH data, they create a “shadow” control flow graph that misleads tools into reporting benign behavior while the real execution path performs malicious operations. This is analogous to “return-oriented programming” confusion in traditional binary exploitation.

  • Audit misdirection. A contract with dense PUSH data containing multiple 0x5B bytes forces auditors relying on bytecode-level analysis to carefully distinguish real from phantom JUMPDESTs. The cognitive overhead benefits attackers who embed malicious logic in the genuine instruction stream while filling PUSH data with distracting 0x5B bytes.

Why it matters: EIP-3690 (EOF JUMPDEST Table) was specifically proposed to address this problem by moving JUMPDEST validation to deployment time. Until EOF adoption, every bytecode analysis tool must implement its own PUSH-boundary tracking, and bugs in this tracking directly produce incorrect security analysis.

T3: PUSH Past End of Code — Zero-Padding Behavior (Medium)

When a PUSHn instruction appears near the end of the bytecode and its n-byte immediate extends past the last byte, the EVM fills the missing bytes with zeros. For example, PUSH8 (0x67) followed by a single byte 0x01 at the very end of the code pushes 0x0100000000000000 onto the stack — seven zero bytes are appended:

  • Silent value corruption. A contract whose bytecode is truncated (e.g., due to an initcode size calculation error, a deployment tool bug, or an intentional attack on the deployment pipeline) will not revert on the truncated PUSHn. Instead, it pushes a zero-padded value that differs from what the compiler intended. If this value is a jump target, the jump lands at an unexpected offset. If it’s an address, it resolves to a different (likely non-existent) contract. If it’s a comparison constant, security checks pass or fail incorrectly.

  • Client implementation divergence. EthereumJS had a consensus-critical bug (Issue #3861, fixed February 2025) where PUSHn instructions reading past the end of bytecode were not zero-padded correctly. The client pushed the raw truncated value instead of the right-padded value, causing execution results to diverge from Geth, Reth, and REVM. Hyperledger Besu had a similar bug fixed in November 2024. These divergences are consensus-breaking: a chain using the buggy client would accept or reject different transactions than the rest of the network.

  • Intentional exploitation. An attacker who can influence the deployed bytecode length (e.g., by manipulating initcode in a factory pattern) can cause a trailing PUSHn to read past the boundary, changing a critical constant from its intended value to a zero-padded variant.

Why it matters: The zero-padding behavior is well-defined but surprising. Multiple production Ethereum clients had bugs in this exact behavior, demonstrating that even implementers find the edge case non-obvious.

T4: Stack Overflow via Excessive PUSH Operations (Low)

The EVM stack has a hard limit of 1024 items. A sequence of more than 1024 PUSH instructions without corresponding POP or consuming operations causes a stack overflow, which triggers an exceptional halt:

  • Deployment-time DoS. A malicious initcode sequence consisting of 1025+ consecutive PUSH1 instructions will overflow the stack during contract creation, causing the CREATE/CREATE2 to fail. While this wastes the deployer’s gas rather than attacking others, it’s relevant for factory contracts that deploy user-supplied initcode: if the factory doesn’t validate initcode before executing it, an attacker can force failed deployments that consume gas.

  • Gas-bounded scope. Even with zero-gas PUSH1 instructions (hypothetically), 1025 PUSH1 operations cost 1025 * 3 = 3075 gas — trivially cheap. The stack overflow is a hard safety limit, not an economic one. The real constraint is that useful programs need to consume stack values, so exceeding 1024 depth requires pathological code. Compilers never produce such sequences.

  • Stack depth attacks in nested calls. Historically (pre-EIP-150), attackers exploited the 1024 call-depth limit to cause failures. The 1024 stack depth within a single execution frame is a separate limit that prevents unbounded memory growth in EVM implementations.

Why it matters: Stack overflow from PUSH instructions is a theoretical completeness concern rather than a practical exploit vector. The gas cost and call-depth changes (EIP-150) make it impractical to weaponize.

T5: Secret and Key Exposure in PUSH32 Bytecode (High)

All deployed contract bytecode is publicly readable. Any value embedded via PUSH32 is visible to anyone who reads the chain:

  • Hardcoded secrets. Developers who store private keys, API secrets, encryption keys, or passwords as bytes32 constant or bytes32 immutable values make those secrets permanently public. The Solidity compiler embeds these as PUSH32 instructions in the deployed bytecode, and tools like eth_getCode, block explorers, and decompilers trivially extract them.

  • Constructor argument exposure. Even if the source code is not verified on Etherscan, the constructor arguments (which populate immutable values via PUSH32) are visible in the deployment transaction’s input data. An attacker can read the deployment transaction, extract the constructor arguments, and identify any secrets passed at deployment time.

  • Immutable “private” variables. Solidity’s private visibility modifier only restricts access at the Solidity language level — it does not hide values from bytecode analysis. A bytes32 private immutable secret is just as visible in bytecode as a bytes32 public immutable config. The private keyword creates a false sense of confidentiality.

  • Bytecode scanning at scale. Automated tools scan all newly deployed contracts for patterns in PUSH32 data — known key formats, high-entropy byte sequences, and recognizable private key prefixes. Contracts containing exploitable secrets are identified and drained within minutes of deployment.

Why it matters: Every year, developers lose funds by embedding secrets in contract bytecode. The on-chain transparency model means there is no mechanism to hide PUSH data. Any value in bytecode is public, permanent, and extractable.


Protocol-Level Threats

P1: EOF Changes to PUSH Semantics (Medium)

The Ethereum Object Format (EOF) introduces structural changes that affect how PUSH instructions are validated and executed:

  • Deployment-time code validation. EOF validates bytecode structure at deployment rather than execution. This includes verifying that all PUSHn immediates are fully contained within the code section, eliminating the zero-padding edge case (T3). Code that would trigger past-end-of-code reads is rejected at deployment.

  • JUMPDEST table replaces runtime analysis. EIP-3690 proposes an explicit JUMPDEST table in the EOF container, removing the need for runtime JUMPDEST analysis. This eliminates T2 entirely for EOF contracts: the table explicitly lists valid jump destinations, so 0x5B bytes in PUSH data cannot confuse analysis. The EVM no longer needs to distinguish data from code at runtime.

  • Backward compatibility gap. Legacy (non-EOF) contracts continue to use runtime JUMPDEST analysis and support past-end-of-code zero-padding. The two execution models will coexist indefinitely. Tools and auditors must handle both, and the security properties guaranteed by EOF do not apply to legacy contracts.

  • New immediate-bearing opcodes. EOF introduces RJUMP, RJUMPI, CALLF, and other instructions with immediate data. Unlike PUSHn, these immediates encode offsets and indices rather than arbitrary values, and their structure is validated at deployment. The PUSH family remains the only way to embed arbitrary constant data in EOF code sections.

P2: Initcode Size Limits — EIP-3860 (Low)

EIP-3860 (Shanghai, April 2023) limits initcode to 49,152 bytes (2x the 24,576-byte runtime code limit from EIP-170) and meters initcode at 2 gas per 32-byte word:

  • JUMPDEST analysis cost. The initcode metering exists specifically because the EVM must perform JUMPDEST analysis on initcode before executing it. This analysis must scan every byte to identify PUSHn instruction boundaries and mark valid JUMPDESTs. Without a size limit, an attacker could submit massive initcode that is cheap to create but expensive for nodes to analyze.

  • PUSHn-heavy initcode. Initcode that is predominantly PUSH32 instructions (33 bytes each: 1 opcode + 32 data) can embed ~1,490 arbitrary 32-byte values within the 49,152-byte limit. This is more than sufficient for any legitimate deployment but constrains the payload size for exotic initcode-based attacks.

  • EOF relaxation. EIP-7830 proposes raising the code size limit to 65,536 bytes for EOF contracts, since EOF performs validation at deployment and doesn’t need runtime JUMPDEST analysis. This means EOF contracts can embed more PUSH data, but the deployment-time validation prevents the analysis-cost attack that EIP-3860 was designed to prevent.


Edge Cases

Edge CaseBehaviorSecurity Implication
PUSHn at end of code (immediate extends past boundary)Missing bytes are zero-padded per Yellow Paper. PUSH8 0x01 at the last byte pushes 0x0100000000000000.Silent value corruption. Multiple client implementations had bugs in this behavior, causing consensus divergence (EthereumJS #3861, Besu #7834).
PUSH32 with all zeros (0x7F + 32 zero bytes)Pushes 0x0000...0000 onto the stack. Functionally identical to PUSH0 (0x5F).No direct security risk, but wastes 32 bytes of bytecode. Pre-Shanghai compilers emit this pattern instead of PUSH0. Cross-chain bytecode matching may break if one compilation uses PUSH0 and another uses PUSH32(0).
Stack overflow from 1025 consecutive PUSH operations1025th PUSH causes an exceptional halt (stack overflow). Transaction reverts.Theoretical DoS in factory contracts that execute user-supplied initcode without validation. Gas cost is trivial (3075 gas for 1025 PUSH1 ops).
PUSH1 0x5B (data byte is the JUMPDEST opcode value)The 0x5B is PUSH data, not an instruction. JUMPDEST analysis correctly excludes it. Jumping to this offset reverts.Creates a “phantom JUMPDEST” that confuses linear disassemblers. Audit tools that don’t track PUSHn boundaries misidentify it as a valid jump target.
PUSH32 containing a valid function selectorThe 32 bytes are a constant value, not callable code. But decompilers may flag it as a “known selector.”False positive in decompiler output. Auditors may investigate a non-existent function call.
Consecutive PUSHn with no consuming opcodesEach PUSH adds one item to the stack. Stack grows until 1024 limit or a consuming opcode is reached.Compilers always pair PUSH with consuming instructions. Hand-crafted bytecode can exploit this for stack overflow (see T4).
PUSHn inside unreachable code (after STOP/RETURN/REVERT)Bytes are present in bytecode but never executed. JUMPDEST analysis still scans them.Hidden code regions. Immutable values typically land in unreachable code sections (after the contract’s final STOP). Jumping into these regions is the basis of T1 (immutable executable injection).
PUSH0 (0x5F) vs PUSH1 0x00 (0x60 0x00)Semantically identical (push zero). PUSH0 costs 2 gas, PUSH1 0x00 costs 3 gas. PUSH0 is 1 byte, PUSH1 0x00 is 2 bytes.Bytecode hash difference breaks CREATE2 address determinism across compiler versions. See PUSH0 threat model for full analysis.

Real-World Exploits

Exploit 1: Immutable Executable Injection — Hidden Backdoor via PUSH32 Data (PoC, Bombadil Systems, 2025)

Root cause: Solidity immutable variables are embedded in bytecode as PUSH32 data. Constructor arguments containing valid EVM opcodes (starting with 0x5B JUMPDEST) become hidden executable code that passes Etherscan source verification.

Details: A security researcher at Bombadil Systems built a working proof-of-concept demonstrating end-to-end exploitation. The attack contract declares bytes32 public immutable variables disguised as “gas optimization lookup tables.” At deployment, the constructor receives a payload like 0x5b63CAFEBABE60005260206000f3... — which decodes to a complete EVM subroutine (JUMPDEST, PUSH4, PUSH1, MSTORE, PUSH1, PUSH1, RETURN).

The contract includes an assembly block with arithmetic that computes a jump target. The attacker calls a function with a specific trigger value (e.g., 0xdeadface017b), which decomposes into an authentication token (0xDEADFACE) and a bytecode offset (379 — the exact position of the immutable’s leading 0x5B in the deployed bytecode). The assembly verifies the token and jumps to the offset, transferring execution from the “normal” code path into the hidden payload.

The contract passes Etherscan verification because the source compiles to the exact deployed bytecode. The payload hides in plain sight in constructor arguments, which block explorers display as raw hex. No standard audit workflow interprets constructor parameters as executable code.

PUSH32’s role: The Solidity compiler embeds immutable values using PUSH32 (0x7F). The payload starts at byte position PUSH32_offset + 1 in the deployed code. The leading 0x5B of the payload is valid PUSH32 data during normal execution (the EVM reads all 32 bytes as a constant). But when explicitly jumped to, the 0x5B byte is a valid JUMPDEST at a proper instruction boundary (it’s the start of the immutable region, not inside another PUSHn), and the subsequent bytes execute as a standalone program.

Impact: Proof-of-concept demonstrated successful hidden code execution. A weaponized version could drain funds, change ownership, manipulate proxy implementations, or exfiltrate storage values. The technique targets the 99% of deployed contracts that receive source-level audits without deployment-transaction review.

References:


Exploit 2: EthereumJS PUSH Zero-Padding Bug — Consensus Divergence (2024-2025)

Root cause: EthereumJS did not correctly zero-pad PUSHn immediates that extend past the end of the bytecode, producing execution results divergent from all other Ethereum clients.

Details: The Ethereum Yellow Paper specifies that when a PUSHn instruction reads past the end of bytecode, the missing bytes are filled with zeros and the value is right-aligned. For example, bytecode 0x6801 (PUSH8 followed by a single byte 0x01) should push 0x0100000000000000 — the 0x01 byte followed by seven zero bytes. EthereumJS instead pushed just 0x01, treating the truncated immediate as a complete value without padding.

This caused execution results to differ from Geth, Reth, REVM, and every other compliant client. The divergence was consensus-critical: the Lodestar beacon chain client, which uses EthereumJS as its execution engine, would compute different state roots than the rest of the network for any transaction that triggered a past-end-of-code PUSHn. The bug also affected Linea’s zkrollup arithmetization pipeline, which depends on byte-accurate EVM execution.

A similar zero-padding bug was independently discovered and fixed in Hyperledger Besu (PR #7834, November 2024) and traced in Nethermind’s EOF handling (Immunefi Attackathon, December 2024), demonstrating that this edge case is a recurring implementation hazard.

PUSH’s role: The PUSHn family is the only EVM instruction where the opcode consumes a variable number of following bytes. This unique property creates the edge case: no other opcode reads past its own byte, so no other opcode has this zero-padding requirement. The bug existed specifically because PUSHn’s “read n bytes of data” behavior requires explicit handling of the code-boundary edge case.

Impact: Potential consensus split for any network using EthereumJS or Lodestar. Fixed in EthereumJS PR #3863 (merged February 10, 2025). No on-chain exploit occurred because the specific bytecode pattern (PUSHn at the very end of code) is rare in compiler-generated contracts, but hand-crafted or adversarial bytecode could have triggered it.

References:


Exploit 3: JUMPDEST Confusion in Bytecode Analysis Tooling (Ongoing, Systemic)

Root cause: Bytecode disassemblers and static analysis tools that perform linear scanning without PUSHn boundary tracking misidentify 0x5B bytes in PUSH data as valid JUMPDEST instructions, producing incorrect control flow graphs.

Details: The EVM executes bytecode sequentially, advancing the program counter by 1 for regular opcodes and by 1+n for PUSHn opcodes. A 0x5B byte at offset k is a valid JUMPDEST only if k is at a proper instruction boundary — not within the immediate data of a preceding PUSHn. The EVM enforces this via JUMPDEST analysis, which scans the entire bytecode at execution time to build a validity bitmap.

However, many bytecode analysis tools, educational resources, and audit frameworks have historically performed “linear disassembly” — scanning byte-by-byte and interpreting each byte as an opcode. This approach fails catastrophically on PUSHn instructions because it does not skip the n immediate data bytes. When PUSH data contains 0x5B, the linear scan emits a phantom JUMPDEST, corrupting the disassembly of all subsequent bytes.

Malicious contract authors deliberately exploit this by embedding 0x5B in PUSH immediates to create shadow control flow graphs. The real execution path (which the EVM follows) performs malicious operations, while the phantom disassembly (which naive tools show) appears benign. The technique is a bytecode-level analog of opcode-level obfuscation in native binary exploitation.

EIP-3690 (EOF JUMPDEST Table) addresses this by making jump destinations explicit in the contract’s metadata, eliminating the need for runtime analysis. Until EOF is widely adopted, the problem persists for all legacy contracts.

PUSH’s role: PUSHn is the only legacy EVM instruction with multi-byte encoding. Every other opcode is a single byte. The variable-length encoding of PUSHn creates the code/data ambiguity that enables JUMPDEST confusion. Without PUSHn’s inline data, the EVM bytecode would be a flat sequence of single-byte instructions with no analysis ambiguity.

Impact: Ongoing risk for any tool or audit that relies on bytecode disassembly without proper PUSHn boundary tracking. No single large financial exploit attributed solely to this vector, but it is a documented obfuscation technique used in malicious contracts and a recurring source of false positives/negatives in security tooling.

References:


Attack Scenarios

Scenario A: Immutable Executable Injection (Hidden Backdoor)

contract GasOptimizedVault {
    bytes32 public immutable lut0;
    bytes32 public immutable lut1;
    mapping(address => uint256) public balances;
 
    constructor(bytes32 _lut0, bytes32 _lut1) {
        lut0 = _lut0;
        lut1 = _lut1;
    }
 
    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }
 
    function withdraw(uint256 amount, uint256 validationProof) external {
        require(balances[msg.sender] >= amount, "insufficient");
        balances[msg.sender] -= amount;
 
        // "Gas optimization path selection" -- actually a hidden trigger
        assembly {
            let targetOffset := and(validationProof, 0xFFFF)
            let authBits := shr(16, validationProof)
 
            if eq(authBits, 0xDEADFACE) {
                if lt(targetOffset, codesize()) {
                    let mem := mload(0x40)
                    codecopy(mem, 0, codesize())
                    let op := byte(0, mload(add(mem, targetOffset)))
                    if eq(op, 0x5b) {
                        // Jumps to hidden payload in immutable data
                        jump(targetOffset)
                    }
                }
            }
        }
 
        payable(msg.sender).transfer(amount);
    }
}
 
// Deployment: attacker passes hidden EVM payload as constructor args
// lut0 = 0x5b63CAFEBABE60005260206000f3000000000000000000000000000000000000
//   Decodes to: JUMPDEST, PUSH4 0xCAFEBABE, PUSH1 0, MSTORE, PUSH1 32,
//               PUSH1 0, RETURN
//
// Attack call: withdraw(0, 0xdeadface017b)
//   authBits = 0xDEADFACE (trigger token)
//   targetOffset = 379 (byte offset of lut0's 0x5B in deployed bytecode)
//   Result: hidden code executes instead of normal withdrawal logic

Scenario B: Phantom JUMPDEST Obfuscation

// Bytecode (hex):
// 7f 5b600160025560...  60 01 56
//
// What a naive linear disassembler sees:
//   0x00: PUSH32 0x5b600160025560...   (correctly identified)
//   ... but if the disassembler loses sync:
//   0x01: JUMPDEST                      (phantom -- this is PUSH32 data!)
//   0x02: PUSH1 0x01                    (phantom)
//   0x04: PUSH1 0x02                    (phantom)
//   0x06: SSTORE                        (phantom)
//
// What the EVM actually executes:
//   0x00: PUSH32 (reads 32 bytes as data, including the 0x5B)
//   0x21: PUSH1 0x01
//   0x23: JUMP -> jumps to a REAL JUMPDEST elsewhere
//
// The attacker plants "harmless-looking" phantom instructions in PUSH data
// while the real code path (after the PUSH32) performs malicious SSTORE
// or CALL operations that don't appear in the phantom disassembly.

Scenario C: PUSH Past End of Code Exploiting Zero-Padding

// Scenario: Factory deploys a minimal contract from user-supplied bytecode.
// Attacker submits truncated bytecode where a critical PUSH8 is at the end.

// Intended bytecode (complete):
//   PUSH8 0xDEADBEEFCAFEBABE   =>  0x67 DEADBEEFCAFEBABE
//   PUSH1 0x00                  =>  0x60 00
//   SSTORE                      =>  0x55
//
// Truncated bytecode (only first 2 bytes of PUSH8 data):
//   0x67 DE AD
//
// EVM execution of truncated version:
//   PUSH8 reads 2 available bytes + 6 zero-padded bytes
//   Stack value: 0xDEAD000000000000 (not 0xDEADBEEFCAFEBABE)
//
// If this value is a storage key, slot pointer, or address:
//   The contract writes to / reads from the WRONG location silently.
//   No revert. No error. Just a different value than intended.

Scenario D: Secret Exposure in PUSH32

contract VulnerableOracle {
    // "Private" API key embedded as immutable -- visible to everyone
    bytes32 private immutable apiKey;
 
    constructor(bytes32 _key) {
        apiKey = _key;
    }
 
    function query(bytes32 data) external view returns (bytes32) {
        require(keccak256(abi.encodePacked(data)) != apiKey, "blocked");
        // ... oracle logic ...
    }
 
    // Attack: Read deployed bytecode via eth_getCode.
    // Find the PUSH32 (0x7F) instruction followed by 32 bytes.
    // Those 32 bytes ARE the apiKey, despite "private" visibility.
    //
    // Alternatively, read the deployment transaction's input data.
    // Constructor arguments are appended after the initcode,
    // ABI-encoded and trivially decodable.
    //
    // Even without Etherscan source verification, the key is exposed.
}

Mitigations

ThreatMitigationImplementation
T1: Immutable executable injectionConstrain immutable types; validate constructor argumentsUse uint256, address, or tightly bounded types instead of bytes32 for immutables. Validate in constructor: require(uint8(bytes1(_param)) != 0x5b) to reject payloads starting with JUMPDEST.
T1: Assembly-based triggersAudit deployment transactions, not just sourceReview constructor arguments for executable patterns. Flag any assembly using codecopy, codesize, or computed jump targets.
T2: JUMPDEST confusion in toolingUse PUSHn-aware disassemblersEmploy tools that track instruction boundaries (e.g., Heimdall, Panoramix, Dedaub). Verify disassembly against multiple tools. Never rely on linear byte scanning.
T2: Bytecode obfuscationCross-reference disassembly with execution tracesUse Foundry/Hardhat trace output or Tenderly to verify actual execution paths. Disassembly alone is insufficient for obfuscated contracts.
T3: PUSH past end of codeValidate bytecode completeness before deploymentFactory contracts should verify that all PUSHn immediates are fully contained: require(pc + n <= codeLength) during initcode validation.
T3: Client implementation bugsFuzz-test PUSHn boundary conditionsClient teams should include edge-case tests: PUSHn at last byte, PUSHn reading 0 to n bytes past end, all PUSHn variants (PUSH1 through PUSH32).
T4: Stack overflowValidate initcode before factory executionCap initcode instruction count or simulate execution before committing gas. Reject initcode with pathological PUSH density.
T5: Secrets in bytecodeNever embed secrets in contract codeUse off-chain secret management with on-chain commitments (commit-reveal). Store hashes, not plaintext. Use Chainlink Functions or SUAVE for confidential computation.
T5: “Private” variable false confidenceEducate developers on EVM transparencyAll on-chain data is public. private is a Solidity access modifier, not an encryption mechanism. Bytecode scanning extracts all PUSH32 constants trivially.
General: EOF adoptionMigrate to EOF contracts when availableEOF eliminates T2 (JUMPDEST confusion) and T3 (past-end-of-code reads) via deployment-time validation. Monitor EOF EIP status and compiler support.

Compiler/EIP-Based Protections

  • EIP-3860 (Initcode Limits, Shanghai 2023): Caps initcode at 49,152 bytes and meters it at 2 gas per 32-byte word. Prevents JUMPDEST analysis DoS via oversized initcode containing dense PUSHn sequences.
  • EIP-3690 (EOF JUMPDEST Table): Replaces runtime JUMPDEST analysis with an explicit deployment-time table. Eliminates phantom JUMPDEST confusion for EOF contracts. 0x5B bytes in PUSH data cannot be misidentified as jump targets.
  • EIP-3855 (PUSH0, Shanghai 2023): Adds a dedicated opcode for pushing zero, replacing the common PUSH1 0x00 pattern. Reduces bytecode size and gas cost but does not change security semantics of PUSHn.
  • EIP-7830 (EOF Code Size Increase): Proposes raising code size limits for EOF contracts to 65,536 bytes, enabled by EOF’s deployment-time validation removing the need for runtime JUMPDEST analysis costs.
  • Solidity >= 0.8.20: Defaults to Shanghai EVM target, emitting PUSH0 instead of PUSH1 0x00. Cross-chain bytecode hash differences require explicit --evm-version flags for chains without PUSH0 support.

Severity Summary

Threat IDCategorySeverityLikelihoodReal-World Precedent
T1Smart ContractHighMediumBombadil Systems PoC (2025); immutable injection passes Etherscan verification
T2Smart ContractMediumMediumOngoing; EIP-3690 proposed specifically to address JUMPDEST confusion
T3Smart ContractMediumLowEthereumJS consensus bug (#3861), Besu (#7834), Nethermind trace discrepancy
T4Smart ContractLowLowTheoretical; gas cost makes exploitation impractical
T5Smart ContractHighHighRecurring; developers regularly expose secrets in PUSH32 bytecode
P1ProtocolMediumMediumEOF EIPs in active development; legacy/EOF coexistence creates dual threat models
P2ProtocolLowN/AEIP-3860 deployed (Shanghai); no bypass known

OpcodeRelationship
JUMPDEST (0x5B)Marks valid jump destinations. The JUMPDEST analysis pass must track PUSHn instruction boundaries to distinguish real JUMPDESTs from 0x5B bytes in PUSH data. Phantom JUMPDESTs from PUSH data are the basis of T2.
JUMP (0x56)Unconditional jump to a stack-provided offset. Only succeeds if the target is a valid JUMPDEST. The immutable executable injection attack (T1) requires a JUMP instruction to redirect execution into PUSH32 data.
PUSH0 (0x5F)Pushes zero onto the stack. Functionally equivalent to PUSH1 0x00 but 1 byte shorter and 1 gas cheaper. Cross-chain bytecode differences between PUSH0 and PUSH1 0x00 break CREATE2 address determinism.
DUP1-DUP16 (0x80-0x8F)Duplicates stack items. Often follows PUSHn to reuse a constant. No immediate data, so no code/data ambiguity.
POP (0x50)Removes the top stack item. Counterbalances PUSH operations. Without POP or consuming opcodes, excessive PUSHes lead to stack overflow (T4).
CODECOPY (0x39)Copies the current contract’s bytecode to memory. Used in the immutable injection PoC to read bytecode and verify the payload offset before jumping.
CODESIZE (0x38)Returns the size of the current contract’s bytecode. Used alongside CODECOPY for bytecode self-inspection. The T3 edge case depends on the relationship between PUSHn offset and CODESIZE.