argotorg / argotorg/solidity

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

Open
#16,217 0 comments 0 reactions 0 assignees View on GitHub
bug :bug:
Dominant language
C++
Stars
25.7k
Forks
6.2k
Avg merge
2d 19h
Merged PRs (30d)
29

Description

## 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

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

### Reproduce the Bug (Bytecode Mismatch)

```bash
# 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

```bash
# 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:

```toml
# 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`:

```solidity
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`

Contributor guide

Open the contributing guide

Research direction

Start in packages/contracts-bedrock/src/L1/ProtocolVersions.sol and reproduce with forge test --mc VerifyOPCM_Run_Test --mt test_run_succeeds -vv at the bug-present commit. Compare artifact and deployment bytecode at optimizer_runs=999999, then inspect the compiler behavior behind the keccak256(...) - 1 constants. Done means identical bytecode and a passing verification test without relying on the optimizer restriction.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, solidity
Domain
blockchain, compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.