-
Notifications
You must be signed in to change notification settings - Fork 6.3k
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
git clone https://github.com/ethereum-optimism/optimism.git
cd optimism/packages/contracts-bedrockReproduce 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 -vvExpected 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 -vvExpected 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 <= 10000for 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