Skip to content

Bytecode mismatch with optimizer_runs=999999 for constants using keccak256(...) - 1 pattern #16217

@JosepBove

Description

@JosepBove

Description

The Solidity 0.8.15 compiler produces different bytecode for the same source code when compiling with optimizer_runs=999999. This causes bytecode verification to fail when comparing artifacts with deployed contract bytecode.

Specifically, the compiled artifact shows 2563 bytes while test deployment produces 2473 bytes (90-byte difference). The constant value type(uint256).max - 1 (used in storage slot calculations) is present in the artifact but missing from the deployed bytecode.

Environment

  • Compiler version: 0.8.15
  • Compilation pipeline: legacy
  • Target EVM version: cancun
  • Framework/IDE: Foundry (forge 0.2.0+)
  • EVM execution environment / backend / blockchain client: Foundry test environment
  • Operating system: macOS

Steps to Reproduce

Repository Setup

git clone https://github.com/ethereum-optimism/optimism.git
cd optimism/packages/contracts-bedrock

Reproduce the Bug (Bytecode Mismatch)

# Checkout commit before the fix
git checkout 9243bb0452efa3fd255556631688d1255723384a

# Run bytecode verification test
forge test --mc VerifyOPCM_Run_Test --mt test_run_succeeds -vv

Expected output: Test fails with bytecode mismatch

[FAIL: VerifyOPCM_Failed()] test_run_succeeds()

Checking Contract: protocolVersionsImpl
  [FAIL] ERROR: Bytecode length mismatch for ProtocolVersions
  Expected length: 2563
  Actual length:   2473
Status: [FAIL] Verification failed for ProtocolVersions

Verify the Fix

# Checkout commit with the fix
git checkout 82e928abc6b6d1405b3c2129c3661c552c785aaa

# Run the same test
forge test --mc VerifyOPCM_Run_Test --mt test_run_succeeds -vv

Expected output: Test passes

Checking Contract: protocolVersionsImpl
Status: [OK] Exact Match
Status: [OK] Verified ProtocolVersions

Overall Verification Status: SUCCESS

The Fix

The fix reduces optimizer runs for ProtocolVersions from 999999 to 5000 via compilation restrictions:

# foundry.toml
compilation_restrictions = [
  { paths = "src/L1/ProtocolVersions.sol", optimizer_runs = 5000 }
]

Source Code

The affected contract is packages/contracts-bedrock/src/L1/ProtocolVersions.sol:

pragma solidity 0.8.15;

import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import { Storage } from "src/libraries/Storage.sol";

type ProtocolVersion is uint256;

contract ProtocolVersions is OwnableUpgradeable, ISemver {
    uint256 public constant VERSION = 0;
    
    // Storage slots using keccak256(...) - 1 pattern
    bytes32 public constant REQUIRED_SLOT = bytes32(uint256(keccak256("protocolversion.required")) - 1);
    bytes32 public constant RECOMMENDED_SLOT = bytes32(uint256(keccak256("protocolversion.recommended")) - 1);

    string public constant version = "1.1.0";

    constructor() {
        _disableInitializers();
    }

    function initialize(
        address _owner,
        ProtocolVersion _required,
        ProtocolVersion _recommended
    ) external initializer {
        __Ownable_init();
        transferOwnership(_owner);
        _setRequired(_required);
        _setRecommended(_recommended);
    }

    function required() external view returns (ProtocolVersion out_) {
        out_ = ProtocolVersion.wrap(Storage.getUint(REQUIRED_SLOT));
    }

    function recommended() external view returns (ProtocolVersion out_) {
        out_ = ProtocolVersion.wrap(Storage.getUint(RECOMMENDED_SLOT));
    }

    function setRequired(ProtocolVersion _required) external onlyOwner {
        _setRequired(_required);
    }

    function setRecommended(ProtocolVersion _recommended) external onlyOwner {
        _setRecommended(_recommended);
    }

    function _setRequired(ProtocolVersion _required) internal {
        Storage.setUint(REQUIRED_SLOT, ProtocolVersion.unwrap(_required));
        bytes memory data = abi.encode(_required);
        emit ConfigUpdate(VERSION, UpdateType.REQUIRED_PROTOCOL_VERSION, data);
    }

    function _setRecommended(ProtocolVersion _recommended) internal {
        Storage.setUint(RECOMMENDED_SLOT, ProtocolVersion.unwrap(_recommended));
        bytes memory data = abi.encode(_recommended);
        emit ConfigUpdate(VERSION, UpdateType.RECOMMENDED_PROTOCOL_VERSION, data);
    }
}

Observed Behavior

At optimizer_runs=999999, the compiler produces:

  • Artifact bytecode: 2563 bytes
  • Test deployment bytecode: 2473 bytes
  • Missing constant: 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe (type(uint256).max - 1)
  • Different jump addresses in function dispatcher

Expected Behavior

The compiler should produce identical bytecode for identical source code with identical compiler settings. Bytecode should be deterministic across different compilation contexts (artifact generation vs test deployment).

Additional Context

  • The contracts are functionally equivalent (all tests pass)
  • This is a determinism issue, not a correctness bug
  • The pattern bytes32(uint256(keccak256("...")) - 1) appears to trigger the issue
  • Affects any project using high optimizer runs with bytecode verification
  • Workaround: Use optimizer_runs <= 10000 for deterministic bytecode

Commit References

  • Bug present: 9243bb0452efa3fd255556631688d1255723384a
  • Fix applied: 82e928abc6b6d1405b3c2129c3661c552c785aaa
  • Repository: https://github.com/ethereum-optimism/optimism
  • Affected file: packages/contracts-bedrock/src/L1/ProtocolVersions.sol

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions