Opcode Summary

PropertyValue
Opcode0x5F
MnemonicPUSH0
Gas2
Stack Input(none)
Stack Output0 (zero, as a full 32-byte word)
BehaviorPushes the constant value 0 onto the stack. Introduced in the Shanghai hard fork via EIP-3855. PUSH0 is a single-byte instruction (opcode only, no immediate data), compared to PUSH1 0x00 which is two bytes (opcode + 0x00 immediate). PUSH0 costs 2 gas versus PUSH1’s 3 gas, saving both bytecode size and execution cost. The Solidity compiler (>= 0.8.20) emits PUSH0 by default when targeting the Shanghai EVM version or later.

Threat Surface

PUSH0 is a pure optimization opcode — it has no side effects, takes no inputs, and produces a deterministic constant. Unlike opcodes such as COINBASE or TIMESTAMP that expose externally-controlled values, PUSH0 itself cannot be manipulated. Its threat surface is therefore entirely indirect, arising not from what the opcode does at runtime, but from what it does to bytecode.

The threat surface centers on three properties:

  1. PUSH0 changes compiled bytecode, breaking cross-chain deployment assumptions. Solidity >= 0.8.20 defaults to the Shanghai EVM target, which emits PUSH0 instead of PUSH1 0x00. A contract compiled with 0.8.20 produces different bytecode than the same source compiled with 0.8.19. On any chain that has not adopted the Shanghai hard fork, the 0x5F byte is an undefined opcode and causes an immediate INVALID execution. This is not a theoretical risk — it caused widespread deployment failures across Arbitrum, Polygon, Avalanche, BSC, and other L2/EVM-compatible chains throughout 2023.

  2. Bytecode changes break deterministic address schemes. CREATE2 computes deployment addresses as keccak256(0xff ++ deployer ++ salt ++ keccak256(init_code)). When PUSH0 replaces PUSH1 0x00 in the init code, the bytecode hash changes, and the CREATE2 address changes. Multi-chain protocols that rely on identical contract addresses across chains (counterfactual deployments, cross-chain messaging, factory patterns) break silently: the contract deploys to a different address than expected, and any off-chain or on-chain system referencing the old address points to empty code.

  3. The failure mode is silent or deceptive on some chains. On chains without PUSH0 support, the 0x5F byte may be treated as INVALID (immediate revert), or the contract may deploy “successfully” but produce incorrect behavior — state reads return zero, function calls revert at arbitrary points, and boolean constants return false. The Solidity GitHub issue #14254 documents a case on Arbitrum where a contract compiled with 0.8.20 deployed without error but every function call produced “invalid opcode: PUSH0” at runtime.


Smart Contract Threats

T1: Cross-Chain Deployment Incompatibility (High)

Contracts compiled with Solidity >= 0.8.20 (default EVM target: Shanghai) contain the PUSH0 opcode. Deploying this bytecode to any chain that has not implemented EIP-3855 causes failure:

  • Hard failure: INVALID opcode. On chains where 0x5F is undefined, executing PUSH0 triggers the INVALID exception handler, consuming all remaining gas and reverting. The contract may deploy (the init code may not hit PUSH0 during constructor execution if the constructor path avoids it), but any runtime function that executes a PUSH0 instruction reverts.

  • Silent deployment with broken runtime. The Solidity issue #14254 documented a contract on Arbitrum (before Arbitrum adopted Shanghai) where deployment succeeded, but bool public constant MY_BOOL = true returned false, and function calls returned “invalid opcode: PUSH0”. The contract appeared to exist on-chain with valid code, but every interaction failed. This is worse than a deployment failure because it can go undetected in testing if tests only check deployment success.

  • Affected chains (as of mid-2023). At the time Solidity 0.8.20 was released (May 2023), only ~16% of EVM-compatible chains supported PUSH0. Chains without support included Arbitrum, Polygon PoS, Avalanche C-Chain, BNB Smart Chain, zkSync Era, Linea, and Mantle. Most of these chains added PUSH0 support by late 2023 or 2024, but the timeline varied. Any protocol deploying to multiple chains during the gap period was affected.

  • Compiler default change was the root cause. EIP-3855 itself is benign — it just adds a cheaper way to push zero. The security issue arose because Solidity 0.8.20 changed the default EVM target from Paris to Shanghai, meaning developers who upgraded the compiler version without explicitly setting evmVersion automatically got PUSH0 in their bytecode with no warning.

Why it matters: Multi-chain deployment is standard practice for DeFi protocols. A compiler upgrade that silently changes bytecode compatibility across chains is a systemic risk vector that affected hundreds of projects.

T2: CREATE2 Address Prediction Breakage (High)

CREATE2 addresses are deterministic: address = keccak256(0xff, deployer, salt, keccak256(init_code))[12:]. When PUSH0 replaces PUSH1 0x00 in the init code, the init code hash changes, producing a different deployment address:

  • Cross-chain address divergence. A factory contract that deploys child contracts via CREATE2 will produce different child addresses on Shanghai-enabled chains (init code contains PUSH0) versus pre-Shanghai chains (init code contains PUSH1 0x00). This breaks the fundamental assumption of counterfactual deployment: “the same contract has the same address everywhere.”

  • Off-chain address precomputation fails. Protocols that precompute CREATE2 addresses off-chain (for cross-chain messaging, bridge verification, or UI display) must use the exact init code that will be deployed. If the compilation environment uses a different Solidity version or EVM target than the deployment chain, the precomputed address is wrong.

  • Governance and upgrade systems. Proxy patterns that use CREATE2 to deploy implementation contracts at predictable addresses break when the implementation bytecode changes due to PUSH0. The proxy still points at the old address (compiled without PUSH0), but the new deployment goes to a different address.

Why it matters: Deterministic addressing is a security-critical property for cross-chain bridges, factory patterns (Uniswap V3 pool deployment), account abstraction wallet factories, and any system where contract addresses are computed rather than observed.

T3: Compiler Version Mismatch in Multi-Chain Protocols (Medium)

When a protocol must deploy to chains with varying PUSH0 support, developers face a configuration management challenge that introduces subtle risks:

  • Different Solidity versions for different chains. Compiling with 0.8.19 for pre-Shanghai chains and 0.8.20+ for Shanghai chains means the bytecode differs even beyond PUSH0 — other optimizer changes between versions may introduce behavioral differences. Maintaining two compilation pipelines increases the risk of deploying the wrong artifact to the wrong chain.

  • evmVersion override as a workaround. Setting evmVersion: "paris" in the compiler config prevents PUSH0 emission while allowing use of Solidity 0.8.20+ features. However, this setting is easy to forget, is not enforced by CI/CD by default, and a single developer compiling locally without the override can produce incompatible bytecode.

  • Foundry/Hardhat configuration drift. The Foundry issue #6187 documents the need to manually set evm_version = "paris" in foundry.toml for cross-chain compatibility. If this setting is omitted or overridden in a profile, tests may pass locally (targeting Shanghai) while the deployment to an unsupported chain fails.

Why it matters: Configuration-based security (where safety depends on a build setting rather than code logic) is fragile. The PUSH0 compatibility issue demonstrates how a single build configuration change can silently break production deployments.

T4: Verification and Audit Discrepancies (Medium)

PUSH0 changes the bytecode that block explorers and auditors verify against:

  • Source verification failure. When a contract is deployed on a Shanghai chain with PUSH0 and the source verification tool attempts to recompile with a different EVM target, the bytecodes won’t match. Etherscan and other explorers require exact bytecode matching, so the contract appears “unverified” even though the source is correct.

  • Audit scope confusion. If an audit is performed against bytecode compiled with Solidity 0.8.19 (no PUSH0) but the deployment uses 0.8.20 (with PUSH0), the audited bytecode differs from the deployed bytecode. While PUSH0 itself doesn’t change logic, the optimizer may produce different code paths around the changed instructions, and the audit’s bytecode-level analysis no longer applies exactly.

Why it matters: Bytecode verification is a trust anchor for DeFi users. Mismatches between audited, verified, and deployed bytecode undermine confidence even when the logical behavior is identical.

T5: L2 Chains with Delayed or Partial Opcode Support (Medium)

Different L2 architectures adopt EVM upgrades on different timelines, creating a moving target for PUSH0 compatibility:

  • zkEVM circuits must be updated. Zero-knowledge EVM implementations (zkSync Era, Polygon zkEVM, Scroll, Linea) require circuit updates to support new opcodes. PUSH0 is trivial to implement in a standard EVM, but adding it to a ZK circuit requires a prover update and may take months. zkSync Era initially did not support PUSH0 and required separate compilation targets.

  • Optimistic rollups adopt faster. OP Stack chains (Optimism, Base, Blast, Mode) and Arbitrum adopted PUSH0 relatively quickly because they run standard EVM interpreters. But “quickly” still meant a gap of weeks to months after Solidity 0.8.20’s release.

  • Support timeline is not always documented. Not all L2s publish clear compatibility tables for EVM opcode support. Developers deploying to a new L2 may not know whether PUSH0 is supported until deployment fails.

Why it matters: The L2 ecosystem is rapidly expanding, and new chains launch frequently. Each new chain represents a potential PUSH0 compatibility gap until explicitly confirmed.


Protocol-Level Threats

P1: Chain Compatibility Fragmentation (Medium)

At the protocol level, PUSH0 exposed a systemic issue in the EVM ecosystem: there is no standard mechanism for a smart contract or deployment tool to query which opcodes a target chain supports. Developers must rely on documentation, community reports, or trial-and-error deployment.

Security implications:

  • No on-chain capability detection. Unlike feature flags in traditional software, there is no EVM opcode that returns “which EIP is supported.” A contract cannot check at deployment time whether PUSH0 is available. The only detection method is attempting to execute PUSH0 and catching the failure — but if PUSH0 is in the constructor, the failure means the contract doesn’t deploy at all.

  • Compiler as a chain compatibility layer. The Solidity compiler’s evmVersion flag is the de facto mechanism for managing chain compatibility. This places an unusual security burden on a build tool: a misconfigured compiler produces bytecode that is valid Solidity but invalid on the target chain.

  • L2 upgrade coordination. When Ethereum L1 introduces a new opcode via hard fork, every L2 must independently adopt it. There is no protocol-level mechanism to coordinate this. PUSH0 was the first opcode to expose this coordination gap at scale because Solidity’s default target change pushed it into widespread use immediately.

P2: Gas Optimization Encouraging Adoption on Unsupported Chains (Low)

PUSH0 saves 1 gas per execution and 1 byte per occurrence compared to PUSH1 0x00. For gas-optimized contracts (MEV bots, high-frequency DeFi operations), these savings are material at scale. The gas optimization incentive encourages developers to use the latest compiler version, which in turn increases the likelihood of PUSH0 appearing in bytecode destined for unsupported chains.


Edge Cases

Edge CaseBehaviorSecurity Implication
PUSH0 on a pre-Shanghai chainExecutes as INVALID opcode (0x5F is undefined); consumes all gas and revertsContract may deploy but every function touching PUSH0 reverts. Silent failure if deployment doesn’t hit the PUSH0 path.
PUSH0 vs PUSH1 0x00 at runtimeIdentical stack effect: both push a 32-byte zero wordNo runtime difference on supported chains; the risk is purely about bytecode compatibility and bytecode identity.
Bytecode hash: PUSH0 vs PUSH1 0x00keccak256 of bytecode differs (0x5F vs 0x6000)CREATE2 addresses, EXTCODEHASH comparisons, and bytecode verification all break when one version uses PUSH0 and the other uses PUSH1 0x00.
PUSH0 in init code (constructor)If the constructor code path includes PUSH0 on an unsupported chain, deployment failsThe contract never gets created; CREATE/CREATE2 returns address(0). No runtime code is stored.
PUSH0 only in runtime code (not init code)Deployment succeeds on unsupported chains, but runtime calls failDeceptive: the contract exists on-chain with code, but is non-functional. Harder to diagnose than a deployment failure.
Solidity 0.8.20 with evmVersion: "paris"Compiler does not emit PUSH0; uses PUSH1 0x00 insteadSafe workaround; bytecode is compatible with all pre-Shanghai chains.
PUSH0 inside JUMPDEST analysis0x5F is recognized as a valid opcode (not immediate data) on Shanghai chainsOn pre-Shanghai chains, 0x5F within code is treated as undefined. If it appears as an immediate byte of another PUSH instruction, it is data, not code, and is harmless.
Stack overflow from PUSH0If stack depth is already 1024, PUSH0 causes a stack overflow exceptionSame behavior as any PUSH instruction at max stack depth; not PUSH0-specific.
PUSH0 gas cost compared to alternativesPUSH0: 2 gas, 1 byte. PUSH1 0x00: 3 gas, 2 bytes. CALLDATALOAD(large_offset): 3 gas, variable bytes. XOR(x, x): 3 gas, depends on stack.PUSH0 is the cheapest way to produce zero on Shanghai+ chains. Gas savings incentivize its use.

Real-World Exploits

Exploit 1: Arbitrum Deployment Failures — Contracts Deploy but Malfunction (May-August 2023)

Root cause: Solidity 0.8.20 (released May 10, 2023) changed the default EVM target to Shanghai, causing all newly compiled contracts to contain PUSH0. Arbitrum had not yet adopted EIP-3855 at that time.

Details: Multiple developers reported that contracts compiled with Solidity 0.8.20 deployed to Arbitrum without error but were completely non-functional at runtime. The canonical report is Solidity GitHub issue #14254, filed on June 26, 2023. The reporter deployed a contract where bool public constant MY_BOOL = true returned false when queried, and any function call produced “Error: Returned error: invalid opcode: PUSH0.”

The failure was deceptive because:

  1. The deployment transaction succeeded (the constructor code path did not execute PUSH0 in this case, or Arbitrum allowed deployment of code containing undefined opcodes).
  2. The contract address existed on-chain with stored runtime bytecode.
  3. Only when the runtime code executed a PUSH0 instruction did the “invalid opcode” error surface.

This affected every contract compiled with the default settings of Solidity 0.8.20+ deployed to Arbitrum, Polygon, Avalanche, BSC, and other pre-Shanghai chains during the gap period. The issue was compounded by the fact that local testing (typically targeting Shanghai or later) passed, and only on-chain deployment revealed the failure.

Arbitrum added PUSH0 support later in 2023 via its ArbOS upgrade, but the months-long gap affected production deployments.

PUSH0’s role: PUSH0 was the direct cause of the runtime failures. The 0x5F byte, interpreted as an undefined opcode on pre-Shanghai chains, triggered the EVM’s invalid instruction handler.

Impact: Hundreds of developers affected. Multiple audit platforms (AuditBase, Cyfrin CodeHawks) added PUSH0 compatibility as a standard finding. Foundry added explicit documentation for the evm_version configuration (issue #6187).

References:


Exploit 2: Cross-Chain Factory Deployments Producing Different Addresses (2023, Recurring)

Root cause: Protocols using CREATE2 factory patterns to deploy contracts at deterministic addresses across multiple chains found that upgrading to Solidity 0.8.20 changed the init code bytecode (PUSH0 vs PUSH1 0x00), producing different CREATE2 addresses on different chains.

Details: The standard pattern for multi-chain deployment uses a CREATE2 factory: deploy the same factory at the same address on every chain (using Nick’s method or a keyless deployment), then deploy child contracts through the factory with a fixed salt. Because CREATE2_address = keccak256(0xff ++ factory ++ salt ++ keccak256(init_code)), the child address is the same on every chain — as long as the init code is identical.

When protocols upgraded their Solidity version from 0.8.19 to 0.8.20, the init code changed because PUSH0 replaced PUSH1 0x00 at multiple points. The keccak256(init_code) hash changed, and the CREATE2 address changed. This broke:

  1. Cross-chain messaging protocols that hardcode contract addresses for verification.
  2. Bridge contracts that validate the counterpart contract’s address on the destination chain.
  3. Frontend applications that precompute contract addresses for user interactions.
  4. Account abstraction wallets (ERC-4337) where the wallet address is a CREATE2 address derived from the factory and the user’s public key. Changing the factory’s compilation would change every wallet address.

The Zokyo auditing tutorial on PUSH0 cross-chain compatibility explicitly documents this as a breaking change for counterfactual deployments.

PUSH0’s role: The bytecode change from PUSH1 0x00 (0x6000) to PUSH0 (0x5F) at each zero-push site changed the init code hash, which is a direct input to the CREATE2 address formula.

Impact: No single large exploit, but a systemic breakage pattern affecting any protocol relying on deterministic CREATE2 addresses across chains with different compiler versions or EVM targets.

References:


Exploit 3: zkSync Era and Linea — Prolonged PUSH0 Incompatibility (2023-2024)

Root cause: Zero-knowledge EVM implementations require circuit-level changes to support new opcodes. PUSH0 support lagged significantly behind optimistic rollups and standard EVM chains.

Details: zkSync Era, Linea, and Mantle did not support PUSH0 for months after Solidity 0.8.20’s release. Unlike optimistic rollups (which run standard EVM interpreters and can add opcodes with a software update), zkEVM chains must update their zero-knowledge proof circuits to handle each new opcode. This is a fundamentally more complex engineering task.

Developers deploying to zkSync Era encountered “opcode 0x5f not defined” errors. The StackExchange post documenting this issue notes that the error message was opaque and difficult to diagnose without knowing about the PUSH0/Shanghai relationship. Polygon zkEVM scheduled its “Dragon Fruit Upgrade” for September 2023 specifically to add PUSH0 support.

The Cyfrin CodeHawks audit contest for Beanstalk (2024) documented PUSH0 incompatibility with zkSync, Linea, and Mantle as a finding, noting that these chains still had not fully adopted the opcode at that point.

PUSH0’s role: PUSH0 was the first post-Merge opcode to expose the zkEVM adoption gap. Previous opcodes (BASEFEE, PREVRANDAO) were adopted more gradually because the Solidity compiler did not change its default EVM target to require them.

Impact: Projects targeting zkEVM chains had to maintain separate compilation configurations or pin to Solidity 0.8.19, fragmenting the development experience and increasing the risk of configuration errors.

References:


Attack Scenarios

Scenario A: Silent Deployment on Unsupported Chain

// Contract compiled with Solidity 0.8.20 (default Shanghai target).
// The compiler emits PUSH0 for every zero-push, e.g., in function
// dispatchers, memory initialization, and default return values.
pragma solidity ^0.8.20;
 
contract MultiChainVault {
    mapping(address => uint256) public balances;
 
    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }
 
    function withdraw(uint256 amount) external {
        require(balances[msg.sender] >= amount, "insufficient");
        balances[msg.sender] -= amount;
        // The PUSH0 in the compiled bytecode for "value: amount" or
        // the function selector dispatcher causes runtime failure
        // on pre-Shanghai chains.
        payable(msg.sender).transfer(amount);
    }
}
 
// Deployment to a pre-Shanghai L2:
// - deploy() transaction succeeds (constructor may not hit PUSH0)
// - deposit() may work (if its code path avoids PUSH0)
// - withdraw() reverts with "invalid opcode: PUSH0"
// - User funds are locked: deposits work, withdrawals don't

Scenario B: CREATE2 Address Divergence Across Chains

contract WalletFactory {
    function deployWallet(address owner, uint256 salt) external returns (address) {
        // CREATE2 address = keccak256(0xff ++ this ++ salt ++ keccak256(init_code))
        // If init_code contains PUSH0 (compiled with 0.8.20+), the address
        // differs from init_code compiled with 0.8.19 (PUSH1 0x00).
        bytes memory initCode = type(Wallet).creationCode;
        bytes memory initCodeWithArgs = abi.encodePacked(initCode, abi.encode(owner));
 
        address wallet;
        assembly {
            wallet := create2(0, add(initCodeWithArgs, 0x20), mload(initCodeWithArgs), salt)
        }
        return wallet;
    }
}
 
// Chain A (Shanghai-enabled): factory compiled with 0.8.20
//   -> Wallet deploys to 0xAAAA...
// Chain B (pre-Shanghai): factory compiled with 0.8.19 to avoid PUSH0
//   -> Wallet deploys to 0xBBBB... (different bytecode hash!)
//
// Cross-chain bridge that expects wallet at 0xAAAA... on Chain B
// finds empty code -> messages to Chain B fail silently.

Scenario C: CI/CD Configuration Drift

# foundry.toml -- developer forgot to set evm_version for L2 profile
[profile.default]
solc_version = "0.8.20"
evm_version = "paris"  # Correct: prevents PUSH0
 
[profile.l2-deploy]
solc_version = "0.8.20"
# BUG: evm_version not set -> inherits "shanghai" default from 0.8.20
# All contracts compiled with this profile contain PUSH0
# Deployment to pre-Shanghai L2 will fail at runtime
# CI pipeline deploys to L2 using the wrong profile
# forge build --profile l2-deploy
# forge create ... --rpc-url $L2_RPC
# Deployment succeeds but contract is non-functional

Scenario D: EXTCODEHASH Comparison Failure

contract RegistryGuard {
    mapping(address => bytes32) public trustedCodeHash;
 
    function registerTrusted(address target) external {
        // Stores EXTCODEHASH of a contract compiled with 0.8.19
        trustedCodeHash[target] = target.codehash;
    }
 
    function validateTrusted(address target) external view returns (bool) {
        // After redeployment with 0.8.20 (PUSH0 in bytecode),
        // the code hash changes even though the logic is identical.
        return target.codehash == trustedCodeHash[target]; // Returns false!
    }
}
 
// A legitimate contract upgrade (same source, new compiler) breaks
// the trust registry because PUSH0 changes the bytecode hash.

Mitigations

ThreatMitigationImplementation
T1: Cross-chain deployment failurePin EVM version to pre-Shanghai for multi-chain deploymentSet evmVersion: "paris" in Solidity compiler settings; evm_version = "paris" in foundry.toml; evmVersion: "paris" in Hardhat config
T1: Compiler default changeUse explicit EVM version in all build configurationsNever rely on compiler defaults; always specify evmVersion in project config and CI/CD
T1: Runtime PUSH0 failure detectionTest deployments on target chain testnets before mainnetDeploy to L2 testnets in CI; use cast call --rpc-url $RPC --create 0x5f to probe PUSH0 support
T2: CREATE2 address divergenceEnsure identical bytecode across all target chainsCompile once with a fixed Solidity version and EVM target; deploy the same artifact to all chains
T2: Init code hash mismatchUse CREATE3 pattern (CREATE2 + CREATE) to decouple address from init codeCREATE3 libraries (e.g., Solmate, 0xSequence) compute addresses from salt alone, not bytecode hash
T3: Compiler version mismatchLock Solidity version in foundry.toml / hardhat.config and CIUse exact version pinning (solc_version = "0.8.20") with explicit evm_version across all profiles
T4: Bytecode verification failurePublish exact compiler version and settings alongside verified sourceInclude evmVersion, optimizer settings, and compiler version in verification metadata
T5: L2 opcode support uncertaintyCheck target chain’s EVM compatibility before deploymentConsult evmdiff.com for chain-specific opcode support; test with cast call --create 0x5f
General: Future opcode additionsAbstract EVM version targeting into shared build configCentralize evmVersion in a single config file; use matrix builds in CI for multi-chain targets

Compiler/EIP-Based Protections

  • EIP-3855 (Shanghai, April 2023): Defines PUSH0 (0x5F) as a valid opcode with gas cost 2 and no immediate data. On chains that implement EIP-3855, PUSH0 is fully equivalent to PUSH1 0x00 in stack effect. The EIP is simple and unambiguous; implementation bugs are unlikely.
  • Solidity evmVersion flag: Setting evmVersion: "paris" (or any pre-Shanghai version) prevents the compiler from emitting PUSH0. This is the primary mitigation for cross-chain deployment. Available in Solidity >= 0.8.20, Foundry (via evm_version in foundry.toml), and Hardhat (via evmVersion in hardhat.config.js).
  • Solidity >= 0.8.25: Later Solidity versions improved documentation around the Shanghai default and the implications for cross-chain deployment. The compiler changelog explicitly notes when the default EVM target changes.
  • CREATE3 libraries: Libraries like Solmate’s CREATE3 and 0xSequence’s CREATE3 decouple the deployed address from the init code bytecode hash, making the address depend only on the deployer and salt. This eliminates the PUSH0/PUSH1 bytecode divergence problem for deterministic deployment.

Severity Summary

Threat IDCategorySeverityLikelihoodReal-World Precedent
T1Smart ContractHighHighWidespread Arbitrum/Polygon/BSC deployment failures (2023); Solidity issue #14254; hundreds of affected projects
T2Smart ContractHighMediumCross-chain factory address divergence; Zokyo tutorial documenting counterfactual deployment breakage
T3Smart ContractMediumMediumFoundry issue #6187; CI/CD configuration drift across multi-chain projects
T4Smart ContractMediumLowBytecode verification mismatches on block explorers after compiler upgrade
T5Smart ContractMediumMediumzkSync Era, Linea, Mantle prolonged incompatibility; Cyfrin CodeHawks audit findings
P1ProtocolMediumHighEVM ecosystem fragmentation; no on-chain opcode capability detection mechanism
P2ProtocolLowLowGas optimization incentivizing premature adoption of Shanghai-only bytecode

OpcodeRelationship
PUSH1 (0x60)PUSH1 0x00 is the pre-Shanghai equivalent of PUSH0. Two bytes (0x60 0x00) vs one byte (0x5F). PUSH1 costs 3 gas vs PUSH0’s 2 gas. Every PUSH0 in compiled bytecode replaces what was previously PUSH1 0x00, changing the bytecode hash and size.
ISZERO (0x15)Consumes a stack value and pushes 1 if it was zero, 0 otherwise. PUSH0 produces a zero value directly; ISZERO tests for zero. In some compiler patterns, PUSH0 + ISZERO replaces a comparison sequence.
POP (0x50)Removes the top stack element. PUSH0 followed by POP is a no-op that the optimizer should eliminate. If the optimizer fails to remove dead PUSH0+POP pairs, it wastes 5 gas (2+3) per occurrence.
INVALID (0xFE)The explicit INVALID opcode. On pre-Shanghai chains, PUSH0 (0x5F) is functionally equivalent to INVALID — both cause an exceptional halt consuming all remaining gas. The difference: INVALID is intentional, PUSH0 on an unsupported chain is an accidental deployment error.
CREATE2 (0xF5)Deterministic contract deployment. CREATE2 addresses depend on keccak256(init_code). PUSH0 changes init code bytecode, breaking CREATE2 address determinism across chains or compiler versions.