diff --git a/.circleci/config.yml b/.circleci/config.yml index 281f4f4336b..54f03b188bf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1823,7 +1823,6 @@ workflows: op-e2e/actions op-e2e/faultproofs packages/contracts-bedrock/scripts/checks - packages/contracts-bedrock/scripts/verify op-dripper devnet-sdk op-acceptance-tests diff --git a/op-chain-ops/solc/types.go b/op-chain-ops/solc/types.go index 8f9cdaf7808..0b2c7bbbb59 100644 --- a/op-chain-ops/solc/types.go +++ b/op-chain-ops/solc/types.go @@ -123,10 +123,11 @@ type CompilerOutputEvm struct { // Object must be a string because its not guaranteed to be // a hex string type CompilerOutputBytecode struct { - Object string `json:"object"` - Opcodes string `json:"opcodes"` - SourceMap string `json:"sourceMap"` - LinkReferences LinkReferences `json:"linkReferences"` + Object string `json:"object"` + Opcodes string `json:"opcodes"` + SourceMap string `json:"sourceMap"` + LinkReferences LinkReferences `json:"linkReferences"` + ImmutableReferences ImmutableReferences `json:"immutableReferences"` } type LinkReferences map[string]LinkReference @@ -137,6 +138,13 @@ type LinkReferenceOffset struct { Start uint `json:"start"` } +type ImmutableReferences map[string][]ImmutableReference + +type ImmutableReference struct { + Start uint `json:"start"` + Length uint `json:"length"` +} + type CompilerOutputSources map[string]CompilerOutputSource type CompilerOutputSource struct { diff --git a/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol new file mode 100644 index 00000000000..f403efadf0c --- /dev/null +++ b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol @@ -0,0 +1,627 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +// Foundry +import { Script } from "forge-std/Script.sol"; +import { console2 as console } from "forge-std/console2.sol"; +import { stdJson } from "forge-std/StdJson.sol"; + +// Libraries +import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; +import { LibString } from "@solady/utils/LibString.sol"; +import { Process } from "scripts/libraries/Process.sol"; +import { Config } from "scripts/libraries/Config.sol"; +import { Bytes } from "src/libraries/Bytes.sol"; + +// Interfaces +import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; + +/// @title VerifyOPCM +/// @notice Verifies the bytecode of an OPContractsManager instance and all associated blueprints +/// and implementations against locally built artifacts. +contract VerifyOPCM is Script { + using stdJson for string; + + /// @notice Thrown when the top-level verification fails. + error VerifyOPCM_Failed(); + + /// @notice Thrown when no properties are found in the OPCM. + error VerifyOPCM_NoProperties(); + + /// @notice Thrown when no implementations are found in the OPCM. + error VerifyOPCM_NoImplementations(); + + /// @notice Thrown when no blueprints are found in the OPCM. + error VerifyOPCM_NoBlueprints(); + + /// @notice Thrown when an unexpected part number is found in the blueprint. + error VerifyOPCM_UnexpectedPart(); + + /// @notice Thrown when an artifact file is empty. + error VerifyOPCM_EmptyArtifactFile(string _artifactPath); + + /// @notice Thrown when the creation bytecode is not found in an artifact file. + error VerifyOPCM_CreationBytecodeNotFound(string _artifactPath); + + /// @notice Thrown when the runtime bytecode is not found in an artifact file. + error VerifyOPCM_RuntimeBytecodeNotFound(string _artifactPath); + + /// @notice Preamble used for blueprint contracts. + bytes constant BLUEPRINT_PREAMBLE = hex"FE7100"; + + /// @notice Maximum init code size for blueprints. + uint256 constant MAX_INIT_CODE_SIZE = 23500; + + /// @notice Represents a contract name and its corresponding address. + /// @param field Name of the field the address was extracted from. + /// @param name Name of the contract. + /// @param addr Address of the contract. + struct OpcmContractRef { + string field; + string name; + address addr; + bool blueprint; + } + + /// @notice Represents an immutable reference within bytecode. + /// @param length Length of the immutable reference in bytes. + /// @param offset Offset of the immutable reference within the bytecode. + struct ImmutableRef { + uint256 length; + uint256 offset; + } + + /// @notice Represents info loaded from a contract artifact JSON file. + /// @param bytecode The creation bytecode. + /// @param deployedBytecode The runtime bytecode. + /// @param immutableRefs Array of immutable references found in the deployed bytecode. + struct ArtifactInfo { + bytes bytecode; + bytes deployedBytecode; + ImmutableRef[] immutableRefs; + } + + /// @notice Maps OPCM field names (as strings) to an overriding contract name. + mapping(string => string) internal fieldNameOverrides; + + /// @notice Maps contract names to an overriding source file name. + mapping(string => string) internal sourceNameOverrides; + + /// @notice Setup flag. + bool internal ready; + + /// @notice Populates override mappings. + function setUp() public { + // Overrides for situations where field names do not cleanly map to contract names. + fieldNameOverrides["optimismPortalImpl"] = "OptimismPortal2"; + fieldNameOverrides["mipsImpl"] = "MIPS64"; + fieldNameOverrides["ethLockboxImpl"] = "ETHLockbox"; + fieldNameOverrides["permissionlessDisputeGame1"] = "FaultDisputeGame"; + fieldNameOverrides["permissionlessDisputeGame2"] = "FaultDisputeGame"; + fieldNameOverrides["permissionedDisputeGame1"] = "PermissionedDisputeGame"; + fieldNameOverrides["permissionedDisputeGame2"] = "PermissionedDisputeGame"; + fieldNameOverrides["superPermissionlessDisputeGame1"] = "SuperFaultDisputeGame"; + fieldNameOverrides["superPermissionlessDisputeGame2"] = "SuperFaultDisputeGame"; + fieldNameOverrides["superPermissionedDisputeGame1"] = "SuperPermissionedDisputeGame"; + fieldNameOverrides["superPermissionedDisputeGame2"] = "SuperPermissionedDisputeGame"; + fieldNameOverrides["opcmGameTypeAdder"] = "OPContractsManagerGameTypeAdder"; + fieldNameOverrides["opcmDeployer"] = "OPContractsManagerDeployer"; + fieldNameOverrides["opcmUpgrader"] = "OPContractsManagerUpgrader"; + fieldNameOverrides["opcmInteropMigrator"] = "OPContractsManagerInteropMigrator"; + + // Overrides for situations where contracts have differently named source files. + sourceNameOverrides["OPContractsManagerGameTypeAdder"] = "OPContractsManager"; + sourceNameOverrides["OPContractsManagerDeployer"] = "OPContractsManager"; + sourceNameOverrides["OPContractsManagerUpgrader"] = "OPContractsManager"; + sourceNameOverrides["OPContractsManagerInteropMigrator"] = "OPContractsManager"; + + // Mark as ready. + ready = true; + } + + /// @notice Entry point for the script when run via `forge script`, reads the OPCM address from + /// the environment variable OPCM_ADDRESS. Use run(address) if you want to specify the + /// address as an argument instead. Running in this mode will not allow you to skip + /// constructor verification. + function run() external { + // nosemgrep: sol-style-vm-env-only-in-config-sol + run(vm.envAddress("OPCM_ADDRESS"), false); + } + + /// @notice Entry point for the script when trying to verify a single contract by name. + /// @param _name Name of the contract to verify. + /// @param _addr Address of the contract to verify. + /// @param _skipConstructorVerification Whether to skip constructor verification. + function runSingle(string memory _name, address _addr, bool _skipConstructorVerification) public { + _verifyOpcmContractRef( + OpcmContractRef({ field: _name, name: _name, addr: _addr, blueprint: false }), _skipConstructorVerification + ); + } + + /// @notice Main verification logic. + /// @param _opcmAddress Address of the OPContractsManager contract to verify. + /// @param _skipConstructorVerification Whether to skip constructor verification. + function run(address _opcmAddress, bool _skipConstructorVerification) public { + // Make sure the setup function has been called. + if (!ready) { + setUp(); + } + + // Log a warning if constructor verification is being skipped. + if (_skipConstructorVerification) { + console.log("WARNING: Constructor verification is being skipped"); + console.log(" ONLY to be used in test environments"); + console.log(" Do NOT do this in production"); + } + + // Fetch Implementations & Blueprints from OPCM + IOPContractsManager opcm = IOPContractsManager(_opcmAddress); + + // Collect all the references. + OpcmContractRef[] memory refs = _collectOpcmContractRefs(opcm); + + // Verify each reference. + bool success = true; + for (uint256 i = 0; i < refs.length; i++) { + success = _verifyOpcmContractRef(refs[i], _skipConstructorVerification) && success; + } + + // Final Result + console.log(); + if (success) { + console.log("Overall Verification Status: SUCCESS"); + } else { + console.log("Overall Verification Status: FAILED"); + revert VerifyOPCM_Failed(); + } + } + + /// @notice Collects all the references from the OPCM contract. + /// @param _opcm The live OPCM contract. + /// @return Array of OpcmContractRef structs containing contract names/addresses. + function _collectOpcmContractRefs(IOPContractsManager _opcm) internal returns (OpcmContractRef[] memory) { + // Collect property references. + OpcmContractRef[] memory propRefs = _getOpcmPropertyRefs(_opcm); + if (propRefs.length == 0) { + revert VerifyOPCM_NoProperties(); + } + + // Collect implementation references. + OpcmContractRef[] memory implRefs = _getOpcmContractRefs(_opcm, "implementations", false); + if (implRefs.length == 0) { + revert VerifyOPCM_NoImplementations(); + } + + // Collect blueprint references. + OpcmContractRef[] memory bpRefs = _getOpcmContractRefs(_opcm, "blueprints", true); + if (bpRefs.length == 0) { + revert VerifyOPCM_NoBlueprints(); + } + + // Create a single array to join everything together. + uint256 extraRefs = 1; + OpcmContractRef[] memory refs = + new OpcmContractRef[](propRefs.length + implRefs.length + bpRefs.length + extraRefs); + + // References for OPCM and linked contracts. + refs[0] = OpcmContractRef({ field: "opcm", name: "OPContractsManager", addr: address(_opcm), blueprint: false }); + + // Add the property references. + for (uint256 i = 0; i < propRefs.length; i++) { + refs[i + extraRefs] = propRefs[i]; + } + + // Add the implementation references. + for (uint256 i = 0; i < implRefs.length; i++) { + refs[i + extraRefs + propRefs.length] = implRefs[i]; + } + + // Add the blueprint references. + for (uint256 i = 0; i < bpRefs.length; i++) { + refs[i + extraRefs + propRefs.length + implRefs.length] = bpRefs[i]; + } + + // Return the combined references. + return refs; + } + + /// @notice Verifies a single OPCM contract reference (implementation or bytecode). + /// @param _target The target contract reference to verify. + /// @param _skipConstructorVerification Whether to skip constructor verification. + /// @return True if the contract reference is verified, false otherwise. + function _verifyOpcmContractRef( + OpcmContractRef memory _target, + bool _skipConstructorVerification + ) + internal + returns (bool) + { + console.log(); + console.log(string.concat("Checking Contract: ", _target.field)); + console.log(string.concat(" Type: ", _target.blueprint ? "Blueprint" : "Implementation")); + console.log(string.concat(" Contract: ", _target.name)); + console.log(string.concat(" Address: ", vm.toString(_target.addr))); + + // Build the expected path to the artifact file. + string memory artifactPath = _buildArtifactPath(_target.name); + console.log(string.concat(" Expected Runtime Artifact: ", artifactPath)); + + // Load artifact information (bytecode, immutable refs) for detailed comparison + ArtifactInfo memory artifact = _loadArtifactInfo(artifactPath); + + // Grab the actual code. + bytes memory actualCode = _target.addr.code; + + // Figure out expected code. + bytes memory expectedCode; + if (_target.blueprint) { + // Determine which part of the blueprint this is using final digit as signifier. + uint8 partNumber = 1; + bytes memory fieldBytes = bytes(_target.field); + if (fieldBytes.length > 0) { + uint8 lastChar = uint8(fieldBytes[fieldBytes.length - 1]); + if (lastChar >= uint8(bytes1("1")) && lastChar <= uint8(bytes1("9"))) { + partNumber = lastChar - uint8(bytes1("0")); + } + } + + // Split the creation code. + bytes memory creationCodePart; + if (partNumber == 1) { + // First part: take initial MAX_INIT_CODE_SIZE bytes. + creationCodePart = + Bytes.slice(artifact.bytecode, 0, Math.min(MAX_INIT_CODE_SIZE, artifact.bytecode.length)); + } else if (partNumber == 2) { + // Second part: take remaining bytes. + creationCodePart = + Bytes.slice(artifact.bytecode, MAX_INIT_CODE_SIZE, artifact.bytecode.length - MAX_INIT_CODE_SIZE); + } else { + // We don't support >2 parts for now, this is an explicit error. + revert VerifyOPCM_UnexpectedPart(); + } + + // Create expected blueprint code for this part. + expectedCode = abi.encodePacked(BLUEPRINT_PREAMBLE, creationCodePart); + } else { + expectedCode = artifact.deployedBytecode; + } + + // Perform detailed bytecode comparison. + bool success = _compareBytecode(actualCode, expectedCode, _target.name, artifact, !_target.blueprint); + + // If requested and this is not a blueprint, we also need to check the creation code. + if (!_target.blueprint && !_skipConstructorVerification) { + // Use the Etherscan API to get the creation code. + bytes memory actualCreationCode = bytes( + Process.bash( + string.concat( + "curl -s 'https://api.etherscan.io/v2/api?chainid=", + vm.toString(block.chainid), + "&module=contract&action=getcontractcreation&contractaddresses=", + vm.toString(_target.addr), + "&apikey=", + Config.etherscanApiKey(), + "' | jq -r '.result[0].creationBytecode'" + ) + ) + ); + + // If we got a creation code, try to grab the constructor arguments from etherscan too. + if (actualCreationCode.length > 0) { + // Now try to grab the constructor arguments from etherscan too. + bytes memory constructorArgs = bytes( + Process.bash( + string.concat( + "curl -s 'https://api.etherscan.io/v2/api?chainid=", + vm.toString(block.chainid), + "&module=contract&action=getsourcecode&address=", + vm.toString(_target.addr), + "&apikey=", + Config.etherscanApiKey(), + "' | jq -r '.result[0].ConstructorArguments'" + ) + ) + ); + + // Constructor args might be empty, so we check regardless of the result. + success = _compareBytecode( + actualCreationCode, + bytes.concat(artifact.bytecode, constructorArgs), + _target.name, + artifact, + !_target.blueprint + ); + } else { + console.log(string.concat("[FAIL] ERROR: Failed to retrieve creation code for ", _target.name)); + success = false; + } + } + + // Log final status for this field. + if (success) { + console.log(string.concat("Status: [OK] Verified ", _target.name)); + } else { + console.log(string.concat("Status: [FAIL] Verification failed for ", _target.name)); + } + + return success; + } + + /// @notice Loads artifact info from a JSON file using Foundry's parsing capabilities. + /// @param _artifactPath Path to the artifact JSON file. + /// @return info The parsed artifact information containing bytecode and immutable references. + function _loadArtifactInfo(string memory _artifactPath) internal view returns (ArtifactInfo memory) { + // Read and parse the artifact file. + string memory artifactJson = vm.readFile(_artifactPath); + if (bytes(artifactJson).length == 0) { + revert VerifyOPCM_EmptyArtifactFile(_artifactPath); + } + + // Parse the creation bytecode. + bytes memory bytecode = vm.parseBytes(artifactJson.readString(".bytecode.object")); + if (bytecode.length == 0) { + revert VerifyOPCM_CreationBytecodeNotFound(_artifactPath); + } + + // Parse the runtime bytecode. + bytes memory deployedBytecode = vm.parseBytes(artifactJson.readString(".deployedBytecode.object")); + if (deployedBytecode.length == 0) { + revert VerifyOPCM_RuntimeBytecodeNotFound(_artifactPath); + } + + // Put together the artifact info struct. + return ArtifactInfo({ + bytecode: bytecode, + deployedBytecode: deployedBytecode, + immutableRefs: _parseImmutableRefs(artifactJson) + }); + } + + /// @notice Parses immutable references from the artifact JSON. + /// @param _artifactJson Complete artifact JSON string. + /// @return Array of parsed immutable reference structs {offset, length}. + function _parseImmutableRefs(string memory _artifactJson) internal view returns (ImmutableRef[] memory) { + // Check if immutableReferences exists, skip if not. + if (!vm.keyExistsJson(_artifactJson, ".deployedBytecode.immutableReferences")) { + return new ImmutableRef[](0); + } + + // Grab all keys (AST node IDs) from the immutableReferences object. + string[] memory keys = vm.parseJsonKeys(_artifactJson, ".deployedBytecode.immutableReferences"); + if (keys.length == 0) { + return new ImmutableRef[](0); + } + + // Count the total number of individual references across all keys. + uint256 totalRefs = 0; + for (uint256 i = 0; i < keys.length; i++) { + string memory key = keys[i]; + string memory refsPath = string.concat(".deployedBytecode.immutableReferences.", key); + ImmutableRef[] memory positions = abi.decode(vm.parseJson(_artifactJson, refsPath), (ImmutableRef[])); + totalRefs += positions.length; + } + + // Allocate the final array to hold all references. + ImmutableRef[] memory refs = new ImmutableRef[](totalRefs); + uint256 refIdx = 0; + + // Populate the final array with references from each key. + for (uint256 i = 0; i < keys.length; i++) { + string memory key = keys[i]; + string memory refsPath = string.concat(".deployedBytecode.immutableReferences.", key); + ImmutableRef[] memory positions = abi.decode(vm.parseJson(_artifactJson, refsPath), (ImmutableRef[])); + for (uint256 j = 0; j < positions.length; j++) { + refs[refIdx++] = positions[j]; + } + } + + return refs; + } + + /// @notice Compares two bytecode arrays for differences. + /// @param _actual The actual bytecode obtained from the chain. + /// @param _expected The expected bytecode from the local artifact. + /// @param _contractName The name of the contract being compared (for logging). + /// @param _artifact Additional artifact info (used for immutable reference checking). + /// @param _allowImmutables True if immutables are allowed to be different, false otherwise. + /// @return True if bytecodes match exactly or if differences only occur within known immutables. + function _compareBytecode( + bytes memory _actual, + bytes memory _expected, + string memory _contractName, + ArtifactInfo memory _artifact, + bool _allowImmutables + ) + internal + pure + returns (bool) + { + // Basic length check + if (_actual.length != _expected.length) { + console.log(string.concat("[FAIL] ERROR: Bytecode length mismatch for ", _contractName)); + console.log(string.concat(" Expected length: ", vm.toString(_expected.length))); + console.log(string.concat(" Actual length: ", vm.toString(_actual.length))); + return false; + } + + // Simplified logic, compare each byte individually, check if that difference falls within + // an immutable range (if immutables are allowed) or if it's a code difference. + for (uint256 i = 0; i < _actual.length; i++) { + if (_actual[i] != _expected[i] && (!_allowImmutables || !_posInsideImmutable(i, _artifact))) { + console.log(string.concat("[FAIL] ERROR: Bytecode difference found for ", _contractName)); + console.log(string.concat(" Offset: ", vm.toString(i))); + console.log(string.concat(" Expected: ", vm.toString(_expected[i]))); + console.log(string.concat(" Actual: ", vm.toString(_actual[i]))); + return false; + } + } + + // If we're here, the bytecode is identical. + console.log("Status: [OK] Exact Match"); + return true; + } + + /// @notice Uses the OPContractsManager ABI JSON and the live OPCM contract to extract a list + /// of contract names and their corresonding addresses for the various immutable + /// references to other OPCM contracts. + /// @param _opcm The live OPCM contract. + /// @return Array of OpcmContractRef structs containing contract names/addresses. + function _getOpcmPropertyRefs(IOPContractsManager _opcm) internal returns (OpcmContractRef[] memory) { + // Find all functions that start with "opcm". + string[] memory functionNames = abi.decode( + vm.parseJson( + Process.bash( + string.concat( + "jq -r '[.abi[] | select(.name? and (.name | type == \"string\") and (.name | startswith(\"opcm\"))) | .name]' ", + _buildArtifactPath("OPContractsManager") + ) + ) + ), + (string[]) + ); + + // For each of these, turn into a contract reference. + OpcmContractRef[] memory refs = new OpcmContractRef[](functionNames.length); + for (uint256 i = 0; i < functionNames.length; i++) { + // Get the function name. + string memory functionName = functionNames[i]; + + // Call the function to retrieve the encoded address. + // nosemgrep: sol-style-use-abi-encodecall + (bool callSuccess, bytes memory returnedData) = + address(_opcm).staticcall(abi.encodeWithSignature(string.concat(functionName, "()"))); + if (!callSuccess) { + console.log(string.concat("[FAIL] ERROR: Failed to call ", functionName, "() function on OPCM.")); + return new OpcmContractRef[](0); + } + + // Decode as an address. + address implAddress = abi.decode(returnedData, (address)); + + // Add to the list. + string memory contractName = _getContractNameFromFieldName(functionName); + refs[i] = OpcmContractRef({ field: functionName, name: contractName, addr: implAddress, blueprint: false }); + } + + // Return the results. + return refs; + } + + /// @notice Uses the OPContractsManager ABI JSON and the live OPCM contract to extract a list + /// of contract names and their corresponding addresses for a given property/struct on + /// the OPCM contract. + /// @param _opcm The live OPCM contract. + /// @param _property The property/struct to extract contract names and addresses from. + /// @param _blueprint Whether this is a blueprint or an implementation. + /// @return Array of OpcmContractRef structs containing contract names/addresses. + function _getOpcmContractRefs( + IOPContractsManager _opcm, + string memory _property, + bool _blueprint + ) + internal + returns (OpcmContractRef[] memory) + { + // Use jq to grab the field names from the ABI. + string[] memory fieldNames = abi.decode( + vm.parseJson( + Process.bash( + string.concat( + "jq -r '[.abi[] | select(.name == \"", + _property, + "\") | .outputs[0].components[].name]' ", + _buildArtifactPath("OPContractsManager") + ) + ) + ), + (string[]) + ); + + // Call the corresponding function on the OPCM contract. + // nosemgrep: sol-style-use-abi-encodecall + (bool callSuccess, bytes memory returnedData) = + address(_opcm).staticcall(abi.encodeWithSignature(string.concat(_property, "()"))); + if (!callSuccess) { + console.log(string.concat("[FAIL] ERROR: Failed to call ", _property, "() function on OPCM.")); + return new OpcmContractRef[](0); + } + + // Expected length check: numFields * 32 bytes/address. + uint256 expectedDataLength = fieldNames.length * 32; + if (returnedData.length != expectedDataLength) { + console.log(string.concat("[FAIL] ERROR: Returned data length mismatch from ", _property, "() call.")); + console.log(string.concat(" Expected length: ", vm.toString(expectedDataLength))); + console.log(string.concat(" Actual length: ", vm.toString(returnedData.length))); + return new OpcmContractRef[](0); + } + + // Extract the addresses from the returned data. + OpcmContractRef[] memory opcmContractRefs = new OpcmContractRef[](fieldNames.length); + for (uint256 i = 0; i < fieldNames.length; i++) { + string memory fieldName = fieldNames[i]; + uint256 offset = i * 32; + address implAddress = abi.decode(Bytes.slice(returnedData, offset, 32), (address)); + string memory contractName = _getContractNameFromFieldName(fieldName); + opcmContractRefs[i] = + OpcmContractRef({ field: fieldName, name: contractName, addr: implAddress, blueprint: _blueprint }); + } + + // Return the extracted addresses. + return opcmContractRefs; + } + + /// @notice Converts an OPCM field name to a contract name. Not 100% reliable, so use overrides + /// if necessary. Works most of the time though. + /// @param _fieldName The field name to convert. + /// @return The contract name. + function _getContractNameFromFieldName(string memory _fieldName) internal view returns (string memory) { + // Check for an explicit override + string memory overrideName = fieldNameOverrides[_fieldName]; + if (bytes(overrideName).length > 0) { + return overrideName; + } + + // Make a copy of the field name. + string memory fieldName = LibString.slice(_fieldName, 0, bytes(_fieldName).length); + + // Uppercase the first character + bytes memory fieldBytes = bytes(fieldName); + fieldBytes[0] = bytes1(uint8(bytes1("A")) + uint8(fieldBytes[0]) - uint8(bytes1("a"))); + + // If it ends in impl, strip that. + if (LibString.endsWith(_fieldName, "Impl")) { + fieldBytes = Bytes.slice(fieldBytes, 0, fieldBytes.length - 4); + } + + // Return the field name with the first character uppercase + return string(fieldBytes); + } + + /// @notice Checks if a position is inside an immutable reference. + /// @param _pos The position to check. + /// @param _artifact The artifact info. + /// @return True if the position is inside an immutable reference, false otherwise. + function _posInsideImmutable(uint256 _pos, ArtifactInfo memory _artifact) internal pure returns (bool) { + for (uint256 i = 0; i < _artifact.immutableRefs.length; i++) { + ImmutableRef memory ref = _artifact.immutableRefs[i]; + if (_pos >= ref.offset && _pos < ref.offset + ref.length) { + return true; + } + } + return false; + } + + /// @notice Constructs the expected path to Foundry artifact JSON file based on contract name. + /// @param _contractName The simple contract name (e.g., "SystemConfig", "FaultDisputeGame"). + /// @return Path to the artifact file. + function _buildArtifactPath(string memory _contractName) internal view returns (string memory) { + // Potentially need to override the source name if multiple contracts are defined in the same file. + string memory sourceName = _contractName; + if (bytes(sourceNameOverrides[_contractName]).length > 0) { + sourceName = sourceNameOverrides[_contractName]; + } + + // Return computed path, relative to the contracts-bedrock directory. + return string.concat("forge-artifacts/", sourceName, ".sol/", _contractName, ".json"); + } +} diff --git a/packages/contracts-bedrock/scripts/libraries/Config.sol b/packages/contracts-bedrock/scripts/libraries/Config.sol index c4e062d7d63..de18623857e 100644 --- a/packages/contracts-bedrock/scripts/libraries/Config.sol +++ b/packages/contracts-bedrock/scripts/libraries/Config.sol @@ -123,6 +123,11 @@ library Config { env_ = vm.envUint("DRIPPIE_OWNER_PRIVATE_KEY"); } + /// @notice Returns the API key for the Etherscan API. + function etherscanApiKey() internal view returns (string memory env_) { + env_ = vm.envString("ETHERSCAN_API_KEY"); + } + /// @notice Returns the OutputMode for genesis allocs generation. /// It reads the mode from the environment variable OUTPUT_MODE. /// If it is unset, OutputMode.ALL is returned. diff --git a/packages/contracts-bedrock/scripts/verify/verify-bytecode/main.go b/packages/contracts-bedrock/scripts/verify/verify-bytecode/main.go deleted file mode 100644 index 3119cd9fcb7..00000000000 --- a/packages/contracts-bedrock/scripts/verify/verify-bytecode/main.go +++ /dev/null @@ -1,541 +0,0 @@ -package main - -import ( - "context" - "encoding/hex" - "encoding/json" - "flag" - "fmt" - "os" - "strconv" - "strings" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/ethclient" - "github.com/fatih/color" -) - -// ImmutableReference represents an immutable reference in the contract bytecode -type ImmutableReference struct { - Offset int - Length int - Value string -} - -// BytecodeDifference represents a difference between expected and actual bytecode -type BytecodeDifference struct { - Start int - Length int - Expected string - Actual string - InImmutable bool - ImmutableName string -} - -// currentDiff is a helper struct for tracking differences during comparison -type currentDiff struct { - Start int - Expected []string - Actual []string - InImmutable bool - ImmutableName string -} - -func main() { - // Parse command line arguments - address := flag.String("address", "", "Contract address to check") - artifactPath := flag.String("artifact", "", "Path to the contract artifact JSON file") - rpcURL := flag.String("rpc", "", "RPC URL for the network") - flag.Parse() - - if *rpcURL == "" { - color.Red("Error: RPC URL is required") - flag.Usage() - os.Exit(1) - } - - color.Cyan("Comparing contract at %s with artifact %s", *address, *artifactPath) - - // Load the artifact - artifact, err := loadArtifact(*artifactPath) - if err != nil { - color.Red("Error loading artifact: %v", err) - os.Exit(1) - } - - // Get expected bytecode from artifact - expectedBytecode, err := getDeployedBytecode(artifact) - if err != nil { - color.Red("Error: %v", err) - os.Exit(1) - } - - // Get immutable references - immutableRefs, err := getImmutableReferences(artifact) - if err != nil { - color.Red("Error: %v", err) - os.Exit(1) - } - - // Get actual bytecode from the network - actualBytecode, err := getOnchainBytecode(*address, *rpcURL) - if err != nil { - color.Red("Error: %v", err) - os.Exit(1) - } - - // Find differences - differences, err := findDifferences(expectedBytecode, actualBytecode, immutableRefs) - if err != nil { - color.Red("Error: %v", err) - os.Exit(1) - } - - // Print results - printDifferences(differences, immutableRefs) - - // Exit with error code if there are non-immutable differences - for _, diff := range differences { - if !diff.InImmutable { - os.Exit(1) - } - } - - fmt.Println() - color.Green("✓ Contract bytecode matches the artifact (accounting for immutable references).") -} - -func loadArtifact(path string) (map[string]any, error) { - if path == "" { - return nil, fmt.Errorf("artifact path is required") - } - - data, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("failed to read artifact file: %w", err) - } - - var artifact map[string]any - if err := json.Unmarshal(data, &artifact); err != nil { - return nil, fmt.Errorf("failed to parse JSON: %w", err) - } - - return artifact, nil -} - -func getDeployedBytecode(artifact map[string]any) (string, error) { - // Check for Forge/Foundry artifact format - if deployedBytecode, ok := artifact["deployedBytecode"].(map[string]any); ok { - if object, ok := deployedBytecode["object"].(string); ok { - return object, nil - } - } - - // Check for standard artifact formats - if deployedBytecode, ok := artifact["deployedBytecode"].(string); ok { - return deployedBytecode, nil - } - - // Check for bytecode field - if bytecode, ok := artifact["bytecode"].(map[string]any); ok { - if object, ok := bytecode["object"].(string); ok { - return object, nil - } - } else if bytecode, ok := artifact["bytecode"].(string); ok { - return bytecode, nil - } - - return "", fmt.Errorf("could not find deployedBytecode in artifact") -} - -func getVariableNameFromAST(artifact map[string]any, varID string) string { - // Remove any prefix from the ID (sometimes IDs are prefixed with a path) - cleanID := varID - if strings.Contains(varID, ":") { - parts := strings.Split(varID, ":") - cleanID = parts[len(parts)-1] - } - - // Try to convert to int - idInt, err := strconv.Atoi(cleanID) - if err != nil { - return varID - } - - // Try to find the AST node - if ast, ok := artifact["ast"].(map[string]any); ok { - // Recursively search for the node with matching ID - name := findNodeName(ast, idInt) - if name != "" { - return name - } - } - - // Fallback to using the ID if we can't find the name - return varID -} - -func findNodeName(node any, targetID int) string { - switch n := node.(type) { - case map[string]any: - // Check if this is the node we're looking for - if id, ok := n["id"].(float64); ok && int(id) == targetID { - if name, ok := n["name"].(string); ok { - return name - } - } - - // Recursively search in all child nodes - for _, value := range n { - result := findNodeName(value, targetID) - if result != "" { - return result - } - } - case []any: - // Search in list items - for _, item := range n { - result := findNodeName(item, targetID) - if result != "" { - return result - } - } - } - return "" -} - -func getImmutableReferences(artifact map[string]any) (map[string][]ImmutableReference, error) { - references := make(map[string][]ImmutableReference) - - var immutableRefs map[string]any - - // Handle Forge/Foundry artifact format - if deployedBytecode, ok := artifact["deployedBytecode"].(map[string]any); ok { - if refs, ok := deployedBytecode["immutableReferences"].(map[string]any); ok { - immutableRefs = refs - } else { - return references, nil // No immutable references found - } - } else if refs, ok := artifact["immutableReferences"].(map[string]any); ok { - // Handle standard artifact format - immutableRefs = refs - } else { - return references, nil // No immutable references found - } - - // Process the references - for varID, refs := range immutableRefs { - // Get the variable name from AST - varName := getVariableNameFromAST(artifact, varID) - references[varName] = []ImmutableReference{} - - refsList, ok := refs.([]any) - if !ok { - continue - } - - for _, ref := range refsList { - var start, length int - - // Handle different formats of immutable references - if refMap, ok := ref.(map[string]any); ok { - if startVal, ok := refMap["start"].(float64); ok { - start = int(startVal) - } - if lengthVal, ok := refMap["length"].(float64); ok { - length = int(lengthVal) - } - } else if refArray, ok := ref.([]any); ok && len(refArray) >= 2 { - // Some formats use [start, length] array - if startVal, ok := refArray[0].(float64); ok { - start = int(startVal) - } - if lengthVal, ok := refArray[1].(float64); ok { - length = int(lengthVal) - } - } else { - color.Yellow("Warning: Unrecognized immutable reference format: %v", ref) - continue - } - - references[varName] = append(references[varName], ImmutableReference{ - Offset: start, - Length: length, - Value: "", - }) - } - } - - return references, nil -} - -func getOnchainBytecode(address string, rpcURL string) (string, error) { - if address == "" { - return "", fmt.Errorf("contract address is required") - } - - client, err := ethclient.Dial(rpcURL) - if err != nil { - return "", fmt.Errorf("failed to connect to RPC at %s: %w", rpcURL, err) - } - - code, err := client.CodeAt(context.Background(), common.HexToAddress(address), nil) - if err != nil { - return "", fmt.Errorf("failed to get code at address %s: %w", address, err) - } - - if len(code) == 0 { - return "", fmt.Errorf("no code found at address %s", address) - } - - return "0x" + hex.EncodeToString(code), nil -} - -func isInImmutableReference( - position int, - immutableRefs map[string][]ImmutableReference, -) (bool, string, *ImmutableReference) { - for varName, refs := range immutableRefs { - for i := range refs { - ref := &refs[i] - if ref.Offset <= position && position < ref.Offset+ref.Length { - return true, varName, ref - } - } - } - return false, "", nil -} - -func findDifferences( - expectedBytecode string, - actualBytecode string, - immutableRefs map[string][]ImmutableReference, -) ([]BytecodeDifference, error) { - // Remove '0x' prefix if present - expected := strings.TrimPrefix(expectedBytecode, "0x") - actual := strings.TrimPrefix(actualBytecode, "0x") - - // Convert to bytes for comparison - expectedBytes, err := hex.DecodeString(expected) - if err != nil { - return nil, fmt.Errorf("failed to decode expected bytecode: %w", err) - } - - actualBytes, err := hex.DecodeString(actual) - if err != nil { - return nil, fmt.Errorf("failed to decode actual bytecode: %w", err) - } - - // Check length differences - if len(expectedBytes) != len(actualBytes) { - color.Yellow("Warning: Bytecode length mismatch. Expected: %d, Actual: %d", - len(expectedBytes), len(actualBytes)) - } - - // Use the shorter length for comparison - compareLength := min(len(expectedBytes), len(actualBytes)) - - // Initialize all immutable reference values - for _, refs := range immutableRefs { - for i := range refs { - refs[i].Value = "" - } - } - - differences := []BytecodeDifference{} - var currDiff *currentDiff = nil - - for i := 0; i < compareLength; i++ { - inImmutable, varName, ref := isInImmutableReference(i, immutableRefs) - - // If we're in an immutable reference, collect the value - if inImmutable && ref != nil { - // Add this byte to the immutable value - ref.Value += fmt.Sprintf("%02x", actualBytes[i]) - - // If bytes differ and we're in an immutable reference, that's expected - if expectedBytes[i] != actualBytes[i] { - if currDiff == nil { - currDiff = ¤tDiff{ - Start: i, - Expected: []string{}, - Actual: []string{}, - InImmutable: true, - ImmutableName: varName, - } - } else if !currDiff.InImmutable { - // We were tracking a non-immutable diff, finish it and start a new one - differences = append(differences, BytecodeDifference{ - Start: currDiff.Start, - Length: len(currDiff.Expected), - Expected: strings.Join(currDiff.Expected, ""), - Actual: strings.Join(currDiff.Actual, ""), - InImmutable: currDiff.InImmutable, - ImmutableName: currDiff.ImmutableName, - }) - currDiff = ¤tDiff{ - Start: i, - Expected: []string{}, - Actual: []string{}, - InImmutable: true, - ImmutableName: varName, - } - } - - currDiff.Expected = append(currDiff.Expected, fmt.Sprintf("%02x", expectedBytes[i])) - currDiff.Actual = append(currDiff.Actual, fmt.Sprintf("%02x", actualBytes[i])) - } else if currDiff != nil && currDiff.InImmutable { - // End of a difference section within an immutable reference - differences = append(differences, BytecodeDifference{ - Start: currDiff.Start, - Length: len(currDiff.Expected), - Expected: strings.Join(currDiff.Expected, ""), - Actual: strings.Join(currDiff.Actual, ""), - InImmutable: currDiff.InImmutable, - ImmutableName: currDiff.ImmutableName, - }) - currDiff = nil - } - } else { - // Not in an immutable reference - any difference is an error - if expectedBytes[i] != actualBytes[i] { - if currDiff == nil { - currDiff = ¤tDiff{ - Start: i, - Expected: []string{}, - Actual: []string{}, - InImmutable: false, - ImmutableName: "", - } - } else if currDiff.InImmutable { - // We were tracking an immutable diff, finish it and start a new one - differences = append(differences, BytecodeDifference{ - Start: currDiff.Start, - Length: len(currDiff.Expected), - Expected: strings.Join(currDiff.Expected, ""), - Actual: strings.Join(currDiff.Actual, ""), - InImmutable: currDiff.InImmutable, - ImmutableName: currDiff.ImmutableName, - }) - currDiff = ¤tDiff{ - Start: i, - Expected: []string{}, - Actual: []string{}, - InImmutable: false, - ImmutableName: "", - } - } - - currDiff.Expected = append(currDiff.Expected, fmt.Sprintf("%02x", expectedBytes[i])) - currDiff.Actual = append(currDiff.Actual, fmt.Sprintf("%02x", actualBytes[i])) - } else if currDiff != nil && !currDiff.InImmutable { - // End of a difference section outside immutable reference - differences = append(differences, BytecodeDifference{ - Start: currDiff.Start, - Length: len(currDiff.Expected), - Expected: strings.Join(currDiff.Expected, ""), - Actual: strings.Join(currDiff.Actual, ""), - InImmutable: currDiff.InImmutable, - ImmutableName: currDiff.ImmutableName, - }) - currDiff = nil - } - } - } - - // Don't forget the last difference if we reached the end - if currDiff != nil { - differences = append(differences, BytecodeDifference{ - Start: currDiff.Start, - Length: len(currDiff.Expected), - Expected: strings.Join(currDiff.Expected, ""), - Actual: strings.Join(currDiff.Actual, ""), - InImmutable: currDiff.InImmutable, - ImmutableName: currDiff.ImmutableName, - }) - } - - return differences, nil -} - -func printDifferences( - differences []BytecodeDifference, - immutableRefs map[string][]ImmutableReference, -) { - // Separate immutable and non-immutable differences - var nonImmutableDiffs []BytecodeDifference - var immutableDiffs []BytecodeDifference - - for _, diff := range differences { - if diff.InImmutable { - immutableDiffs = append(immutableDiffs, diff) - } else { - nonImmutableDiffs = append(nonImmutableDiffs, diff) - } - } - - // Print summary - color.Cyan("\n=== Bytecode Comparison Summary ===") - fmt.Printf("Total differences: %d\n", len(differences)) - fmt.Printf(" - In immutable references: %d\n", len(immutableDiffs)) - fmt.Printf(" - In code: %d\n", len(nonImmutableDiffs)) - - // Print non-immutable differences (these are errors) - if len(nonImmutableDiffs) > 0 { - color.Red("\n=== Unexpected Differences in Code ===") - for _, diff := range nonImmutableDiffs { - color.Red("Position %d-%d:", diff.Start, diff.Start+diff.Length-1) - fmt.Printf(" Expected: 0x%s\n", diff.Expected) - fmt.Printf(" Actual: 0x%s\n", diff.Actual) - } - color.Red("\n⚠️ The contract bytecode does not match the artifact!") - } else { - color.Green("\n✓ No unexpected differences in code.") - } - - // Print immutable references - color.Cyan("\n=== Immutable References ===") - if len(immutableRefs) == 0 { - fmt.Println("No immutable references found in the artifact.") - } else { - for varName, refs := range immutableRefs { - color.Yellow("\n%s:", varName) - - // Check if all values for this variable are the same - allSameValue := true - var firstValue string - var nonEmptyValueFound bool - - for _, ref := range refs { - if ref.Value != "" { - if !nonEmptyValueFound { - firstValue = ref.Value - nonEmptyValueFound = true - } else if ref.Value != firstValue { - allSameValue = false - break - } - } - } - - // Print each reference position on separate lines - for i, ref := range refs { - fmt.Printf(" [%d] Offset: %d, Length: %d\n", i, ref.Offset, ref.Length) - if ref.Value != "" { - fmt.Printf(" Value: 0x%s\n", ref.Value) - } else { - fmt.Printf(" Value: (not modified)\n") - } - } - - // Print consistency status - if nonEmptyValueFound { - if allSameValue { - color.Green(" ✓ All values for this variable are consistent.") - } else { - color.Red(" ⚠️ WARNING: Different values found for the same immutable variable!") - } - } - } - } -} diff --git a/packages/contracts-bedrock/scripts/verify/verify-bytecode/main_test.go b/packages/contracts-bedrock/scripts/verify/verify-bytecode/main_test.go deleted file mode 100644 index bc07b31a836..00000000000 --- a/packages/contracts-bedrock/scripts/verify/verify-bytecode/main_test.go +++ /dev/null @@ -1,646 +0,0 @@ -package main - -import ( - "encoding/json" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestLoadArtifact(t *testing.T) { - // Create a temporary artifact file - tempDir := t.TempDir() - artifactPath := filepath.Join(tempDir, "artifact.json") - - // Test case 1: Valid artifact - validArtifact := map[string]interface{}{ - "deployedBytecode": map[string]interface{}{ - "object": "0x1234", - }, - } - artifactJSON, err := json.Marshal(validArtifact) - require.NoError(t, err) - err = os.WriteFile(artifactPath, artifactJSON, 0644) - require.NoError(t, err) - - artifact, err := loadArtifact(artifactPath) - require.NoError(t, err) - assert.Equal(t, "0x1234", artifact["deployedBytecode"].(map[string]interface{})["object"]) - - // Test case 2: Empty path - _, err = loadArtifact("") - assert.Error(t, err) - assert.Contains(t, err.Error(), "artifact path is required") - - // Test case 3: Non-existent file - _, err = loadArtifact(filepath.Join(tempDir, "nonexistent.json")) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to read artifact file") - - // Test case 4: Invalid JSON - err = os.WriteFile(artifactPath, []byte("invalid json"), 0644) - require.NoError(t, err) - _, err = loadArtifact(artifactPath) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to parse JSON") -} - -func TestGetDeployedBytecode(t *testing.T) { - tests := []struct { - name string - artifact map[string]interface{} - want string - wantErr bool - }{ - { - name: "Forge/Foundry format", - artifact: map[string]interface{}{ - "deployedBytecode": map[string]interface{}{ - "object": "0x1234", - }, - }, - want: "0x1234", - wantErr: false, - }, - { - name: "Standard format with string", - artifact: map[string]interface{}{ - "deployedBytecode": "0x5678", - }, - want: "0x5678", - wantErr: false, - }, - { - name: "Bytecode object format", - artifact: map[string]interface{}{ - "bytecode": map[string]interface{}{ - "object": "0xabcd", - }, - }, - want: "0xabcd", - wantErr: false, - }, - { - name: "Bytecode string format", - artifact: map[string]interface{}{ - "bytecode": "0xef01", - }, - want: "0xef01", - wantErr: false, - }, - { - name: "No bytecode", - artifact: map[string]interface{}{}, - want: "", - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := getDeployedBytecode(tt.artifact) - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.want, got) - } - }) - } -} - -func TestGetVariableNameFromAST(t *testing.T) { - tests := []struct { - name string - artifact map[string]interface{} - varID string - want string - }{ - { - name: "Find variable by ID", - artifact: map[string]interface{}{ - "ast": map[string]interface{}{ - "nodes": []interface{}{ - map[string]interface{}{ - "id": float64(123), - "name": "testVar", - }, - }, - }, - }, - varID: "123", - want: "testVar", - }, - { - name: "Find variable with path prefix", - artifact: map[string]interface{}{ - "ast": map[string]interface{}{ - "nodes": []interface{}{ - map[string]interface{}{ - "id": float64(456), - "name": "prefixedVar", - }, - }, - }, - }, - varID: "path:to:456", - want: "prefixedVar", - }, - { - name: "Variable not found", - artifact: map[string]interface{}{ - "ast": map[string]interface{}{ - "nodes": []interface{}{ - map[string]interface{}{ - "id": float64(789), - "name": "otherVar", - }, - }, - }, - }, - varID: "999", - want: "999", // Returns the ID if not found - }, - { - name: "Non-numeric ID", - artifact: map[string]interface{}{ - "ast": map[string]interface{}{ - "nodes": []interface{}{}, - }, - }, - varID: "abc", - want: "abc", // Returns the ID if not numeric - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := getVariableNameFromAST(tt.artifact, tt.varID) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestFindNodeName(t *testing.T) { - tests := []struct { - name string - node interface{} - targetID int - want string - }{ - { - name: "Find node in map", - node: map[string]interface{}{ - "id": float64(123), - "name": "testNode", - }, - targetID: 123, - want: "testNode", - }, - { - name: "Find node in nested map", - node: map[string]interface{}{ - "child": map[string]interface{}{ - "id": float64(456), - "name": "nestedNode", - }, - }, - targetID: 456, - want: "nestedNode", - }, - { - name: "Find node in array", - node: map[string]interface{}{ - "children": []interface{}{ - map[string]interface{}{ - "id": float64(789), - "name": "arrayNode", - }, - }, - }, - targetID: 789, - want: "arrayNode", - }, - { - name: "Node not found", - node: map[string]interface{}{ - "id": float64(111), - "name": "wrongNode", - }, - targetID: 999, - want: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := findNodeName(tt.node, tt.targetID) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestGetImmutableReferences(t *testing.T) { - tests := []struct { - name string - artifact map[string]interface{} - want map[string][]ImmutableReference - wantLen int - }{ - { - name: "Forge/Foundry format", - artifact: map[string]interface{}{ - "deployedBytecode": map[string]interface{}{ - "immutableReferences": map[string]interface{}{ - "123": []interface{}{ - map[string]interface{}{ - "start": float64(10), - "length": float64(32), - }, - }, - }, - }, - "ast": map[string]interface{}{ - "nodes": []interface{}{ - map[string]interface{}{ - "id": float64(123), - "name": "testVar", - }, - }, - }, - }, - want: map[string][]ImmutableReference{ - "testVar": { - { - Offset: 10, - Length: 32, - Value: "", - }, - }, - }, - wantLen: 1, - }, - { - name: "Standard format", - artifact: map[string]interface{}{ - "immutableReferences": map[string]interface{}{ - "456": []interface{}{ - []interface{}{float64(20), float64(16)}, - }, - }, - "ast": map[string]interface{}{ - "nodes": []interface{}{ - map[string]interface{}{ - "id": float64(456), - "name": "anotherVar", - }, - }, - }, - }, - want: map[string][]ImmutableReference{ - "anotherVar": { - { - Offset: 20, - Length: 16, - Value: "", - }, - }, - }, - wantLen: 1, - }, - { - name: "No immutable references", - artifact: map[string]interface{}{ - "deployedBytecode": map[string]interface{}{}, - }, - want: map[string][]ImmutableReference{}, - wantLen: 0, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := getImmutableReferences(tt.artifact) - assert.NoError(t, err) - assert.Equal(t, tt.wantLen, len(got)) - - // Check specific values for non-empty cases - if tt.wantLen > 0 { - for k, v := range tt.want { - assert.Contains(t, got, k) - assert.Equal(t, v[0].Offset, got[k][0].Offset) - assert.Equal(t, v[0].Length, got[k][0].Length) - } - } - }) - } -} - -func TestIsInImmutableReference(t *testing.T) { - immutableRefs := map[string][]ImmutableReference{ - "var1": { - {Offset: 10, Length: 5, Value: ""}, - }, - "var2": { - {Offset: 20, Length: 10, Value: ""}, - {Offset: 40, Length: 5, Value: ""}, - }, - } - - tests := []struct { - name string - position int - wantIn bool - wantVarName string - wantRef bool - }{ - { - name: "Inside first variable", - position: 12, - wantIn: true, - wantVarName: "var1", - wantRef: true, - }, - { - name: "At start of first variable", - position: 10, - wantIn: true, - wantVarName: "var1", - wantRef: true, - }, - { - name: "At end of first variable (exclusive)", - position: 15, - wantIn: false, - wantVarName: "", - wantRef: false, - }, - { - name: "Inside second variable, first reference", - position: 25, - wantIn: true, - wantVarName: "var2", - wantRef: true, - }, - { - name: "Inside second variable, second reference", - position: 42, - wantIn: true, - wantVarName: "var2", - wantRef: true, - }, - { - name: "Outside any variable", - position: 30, - wantIn: false, - wantVarName: "", - wantRef: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - inImmutable, varName, ref := isInImmutableReference(tt.position, immutableRefs) - assert.Equal(t, tt.wantIn, inImmutable) - assert.Equal(t, tt.wantVarName, varName) - if tt.wantRef { - assert.NotNil(t, ref) - } else { - assert.Nil(t, ref) - } - }) - } -} - -func TestFindDifferences(t *testing.T) { - tests := []struct { - name string - expectedBytecode string - actualBytecode string - immutableRefs map[string][]ImmutableReference - wantDiffs int - wantImmutable int - wantErr bool - }{ - { - name: "No differences", - expectedBytecode: "0x1234567890abcdef", - actualBytecode: "0x1234567890abcdef", - immutableRefs: map[string][]ImmutableReference{}, - wantDiffs: 0, - wantImmutable: 0, - wantErr: false, - }, - { - name: "Difference in immutable reference", - expectedBytecode: "0x1234000000abcdef", - actualBytecode: "0x1234fffffeabcdef", - immutableRefs: map[string][]ImmutableReference{ - "testVar": { - {Offset: 2, Length: 3, Value: ""}, - }, - }, - wantDiffs: 1, - wantImmutable: 1, - wantErr: false, - }, - { - name: "Difference outside immutable reference", - expectedBytecode: "0x1234567890abcdef", - actualBytecode: "0x1234567890abcdee", // Last byte different - immutableRefs: map[string][]ImmutableReference{}, - wantDiffs: 1, - wantImmutable: 0, - wantErr: false, - }, - { - name: "Multiple differences", - expectedBytecode: "0x1234000000abcdef", - actualBytecode: "0x1234fffffeabcdee", // Immutable and non-immutable differences - immutableRefs: map[string][]ImmutableReference{ - "testVar": { - {Offset: 2, Length: 3, Value: ""}, - }, - }, - wantDiffs: 2, - wantImmutable: 1, - wantErr: false, - }, - { - name: "Invalid expected bytecode", - expectedBytecode: "0xZZZZ", - actualBytecode: "0x1234", - immutableRefs: map[string][]ImmutableReference{}, - wantDiffs: 0, - wantImmutable: 0, - wantErr: true, - }, - { - name: "Invalid actual bytecode", - expectedBytecode: "0x1234", - actualBytecode: "0xZZZZ", - immutableRefs: map[string][]ImmutableReference{}, - wantDiffs: 0, - wantImmutable: 0, - wantErr: true, - }, - { - name: "Different lengths", - expectedBytecode: "0x1234", - actualBytecode: "0x123456", - immutableRefs: map[string][]ImmutableReference{}, - wantDiffs: 0, // No differences in the common part - wantImmutable: 0, - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - diffs, err := findDifferences(tt.expectedBytecode, tt.actualBytecode, tt.immutableRefs) - - if tt.wantErr { - assert.Error(t, err) - return - } - - assert.NoError(t, err) - assert.Equal(t, tt.wantDiffs, len(diffs)) - - // Count immutable differences - immutableCount := 0 - for _, diff := range diffs { - if diff.InImmutable { - immutableCount++ - } - } - assert.Equal(t, tt.wantImmutable, immutableCount) - }) - } -} - -func TestFindDifferencesDetailed(t *testing.T) { - // Test with specific bytecode patterns to verify exact difference detection - expected := "0x1234567890abcdef" - actual := "0x1234FF7890abFFef" - - immutableRefs := map[string][]ImmutableReference{ - "testVar": { - {Offset: 2, Length: 1, Value: ""}, // Covers the "FF" difference - }, - } - - diffs, err := findDifferences(expected, actual, immutableRefs) - require.NoError(t, err) - - // Should find 2 differences: one in immutable ref, one outside - assert.Equal(t, 2, len(diffs)) - - // First difference should be in immutable reference - assert.True(t, diffs[0].InImmutable) - assert.Equal(t, "testVar", diffs[0].ImmutableName) - assert.Equal(t, 2, diffs[0].Start) // 0-based index after 0x prefix - assert.Equal(t, 1, diffs[0].Length) - assert.Equal(t, "56", diffs[0].Expected) - assert.Equal(t, "ff", diffs[0].Actual) - - // Second difference should be outside immutable reference - assert.False(t, diffs[1].InImmutable) - assert.Equal(t, "", diffs[1].ImmutableName) - assert.Equal(t, 6, diffs[1].Start) // 0-based index after 0x prefix - assert.Equal(t, 1, diffs[1].Length) - assert.Equal(t, "cd", diffs[1].Expected) - assert.Equal(t, "ff", diffs[1].Actual) - - // Check that immutable reference value was captured - assert.Equal(t, "ff", immutableRefs["testVar"][0].Value) -} - -// TestPrintDifferences doesn't test the actual output (which goes to stdout) -// but ensures the function doesn't panic with various inputs -func TestPrintDifferences(t *testing.T) { - differences := []BytecodeDifference{ - { - Start: 10, - Length: 2, - Expected: "1234", - Actual: "5678", - InImmutable: true, - ImmutableName: "testVar", - }, - { - Start: 20, - Length: 1, - Expected: "ab", - Actual: "cd", - InImmutable: false, - ImmutableName: "", - }, - } - - immutableRefs := map[string][]ImmutableReference{ - "testVar": { - {Offset: 10, Length: 2, Value: "5678"}, - }, - } - - // This should not panic - printDifferences(differences, immutableRefs) - - // Test with empty differences - printDifferences([]BytecodeDifference{}, immutableRefs) - - // Test with empty immutable references - printDifferences(differences, map[string][]ImmutableReference{}) -} - -// Test handling of bytecode with and without 0x prefix -func TestBytecodePrefix(t *testing.T) { - expected := "0x1234" - actual := "1234" // No prefix - - diffs, err := findDifferences(expected, actual, map[string][]ImmutableReference{}) - require.NoError(t, err) - assert.Equal(t, 0, len(diffs), "Should handle different prefixes correctly") - - // Test the reverse - diffs, err = findDifferences(actual, expected, map[string][]ImmutableReference{}) - require.NoError(t, err) - assert.Equal(t, 0, len(diffs), "Should handle different prefixes correctly") -} - -// Test consecutive differences are properly grouped -func TestConsecutiveDifferences(t *testing.T) { - expected := "0x123456789a" - actual := "0x12FFFF789a" // Two consecutive bytes different - - diffs, err := findDifferences(expected, actual, map[string][]ImmutableReference{}) - require.NoError(t, err) - - // Should group consecutive differences - assert.Equal(t, 1, len(diffs), "Consecutive differences should be grouped") - assert.Equal(t, 2, diffs[0].Length, "Difference should span 2 bytes") - assert.Equal(t, "3456", diffs[0].Expected) - assert.Equal(t, "ffff", diffs[0].Actual) -} - -// Test with empty bytecode -func TestEmptyBytecode(t *testing.T) { - _, err := findDifferences("0x", "0x", map[string][]ImmutableReference{}) - assert.NoError(t, err, "Should handle empty bytecode") - - _, err = findDifferences("", "", map[string][]ImmutableReference{}) - assert.NoError(t, err, "Should handle empty bytecode without prefix") -} - -// Test with invalid hex characters -func TestInvalidHex(t *testing.T) { - _, err := findDifferences("0x123Z", "0x1234", map[string][]ImmutableReference{}) - assert.Error(t, err, "Should detect invalid hex in expected bytecode") - - _, err = findDifferences("0x1234", "0x123Z", map[string][]ImmutableReference{}) - assert.Error(t, err, "Should detect invalid hex in actual bytecode") -} diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index 00bffbe6b9f..707bd192590 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -12,8 +12,10 @@ import { DelegateCaller } from "test/mocks/Callers.sol"; import { DeployOPChainInput } from "scripts/deploy/DeployOPChain.s.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; import { Deploy } from "scripts/deploy/Deploy.s.sol"; +import { VerifyOPCM } from "scripts/deploy/VerifyOPCM.s.sol"; import { Config } from "scripts/libraries/Config.sol"; import { StandardConstants } from "scripts/deploy/StandardConstants.sol"; + // Libraries import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { Blueprint } from "src/libraries/Blueprint.sol"; @@ -642,6 +644,20 @@ contract OPContractsManager_Upgrade_Test is OPContractsManager_Upgrade_Harness { runUpgradeTestAndChecks(upgrader); } + function test_verifyOpcmCorrectness_succeeds() public { + skipIfNotOpFork("test_verifyOpcmCorrectness_succeeds"); + skipIfCoverage(); // Coverage changes bytecode and breaks the verification script. + + // Run the upgrade test and checks + runUpgradeTestAndChecks(upgrader); + + // Run the verification script without etherscan verificatin. Hard to run with etherscan + // verification in these tests, can do it but means we add even more dependencies to the + // test environment. + VerifyOPCM verify = new VerifyOPCM(); + verify.run(address(opcm), true); + } + function test_isRcFalseAfterCalledByUpgrader_works() public { skipIfNotOpFork("test_isRcFalseAfterCalledByUpgrader_works"); assertTrue(opcm.isRC()); diff --git a/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol b/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol new file mode 100644 index 00000000000..4be8e6fbae2 --- /dev/null +++ b/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Foundry +import { VmSafe } from "forge-std/Vm.sol"; + +// Tests +import { OPContractsManager_TestInit } from "test/L1/OPContractsManager.t.sol"; + +// Scripts +import { VerifyOPCM } from "scripts/deploy/VerifyOPCM.s.sol"; + +// Interfaces +import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; + +contract VerifyOPCM_Harness is VerifyOPCM { + function loadArtifactInfo(string memory _artifactPath) public view returns (ArtifactInfo memory) { + return _loadArtifactInfo(_artifactPath); + } + + function getOpcmPropertyRefs(IOPContractsManager _opcm) public returns (OpcmContractRef[] memory) { + return _getOpcmPropertyRefs(_opcm); + } + + function getOpcmContractRefs( + IOPContractsManager _opcm, + string memory _property, + bool _blueprint + ) + public + returns (OpcmContractRef[] memory) + { + return _getOpcmContractRefs(_opcm, _property, _blueprint); + } + + function buildArtifactPath(string memory _contractName) public view returns (string memory) { + return _buildArtifactPath(_contractName); + } +} + +contract VerifyOPCM_TestInit is OPContractsManager_TestInit { + VerifyOPCM_Harness internal harness; + + function setUp() public override { + super.setUp(); + harness = new VerifyOPCM_Harness(); + harness.setUp(); + } + + /// @notice Skips if running in coverage mode. + function skipIfCoverage() public { + if (vm.isContext(VmSafe.ForgeContext.Coverage)) { + vm.skip(true); + } + } +} + +contract VerifyOPCM_run_Test is VerifyOPCM_TestInit { + /// @notice Tests that the script succeeds when no changes are introduced. + function test_run_succeeds() public { + // Coverage changes bytecode and causes failures, skip. + skipIfCoverage(); + + // Run the script. + harness.run(address(opcm), true); + } + + /// @notice Tests that the script succeeds when differences are introduced into the immutable + /// variables of implementation contracts. Fuzzing is too slow here, randomness is good + /// enough. + function test_run_implementationDifferentInsideImmutable_succeeds() public { + // Coverage changes bytecode and causes failures, skip. + skipIfCoverage(); + + // Grab the list of implementations. + VerifyOPCM.OpcmContractRef[] memory refs = harness.getOpcmContractRefs(opcm, "implementations", false); + + // Change 256 bytes at random. + for (uint8 i = 0; i < 255; i++) { + // Pick a random implementation to change. + uint256 randomImplIndex = vm.randomUint(0, refs.length - 1); + VerifyOPCM.OpcmContractRef memory ref = refs[randomImplIndex]; + + // Get the code for the implementation. + bytes memory implCode = ref.addr.code; + + // Grab the artifact info for the implementation. + VerifyOPCM.ArtifactInfo memory artifact = harness.loadArtifactInfo(harness.buildArtifactPath(ref.name)); + + // Skip, no immutable references. Will make some fuzz runs useless but it's not worth + // the extra complexity to handle this properly. + if (artifact.immutableRefs.length == 0) { + continue; + } + + // Find a random byte that's inside an immutable reference. + bool inImmutable = false; + uint256 randomDiffPosition; + while (!inImmutable) { + randomDiffPosition = vm.randomUint(0, implCode.length - 1); + inImmutable = false; + for (uint256 j = 0; j < artifact.immutableRefs.length; j++) { + VerifyOPCM.ImmutableRef memory immRef = artifact.immutableRefs[j]; + if (randomDiffPosition >= immRef.offset && randomDiffPosition < immRef.offset + immRef.length) { + inImmutable = true; + break; + } + } + } + + // Change the byte to something new. + bytes1 existingByte = implCode[randomDiffPosition]; + bytes1 newByte = bytes1(uint8(vm.randomUint(0, 255))); + while (newByte == existingByte) { + newByte = bytes1(uint8(vm.randomUint(0, 255))); + } + + // Write the new byte to the code. + implCode[randomDiffPosition] = newByte; + vm.etch(ref.addr, implCode); + } + + // Run the script. + // No revert expected. + harness.run(address(opcm), true); + } + + /// @notice Tests that the script reverts when differences are introduced into the code of + /// implementation contracts that are not inside immutable references. Fuzzing is too + /// slow here, randomness is good enough. + function test_run_implementationDifferentOutsideImmutable_reverts() public { + // Coverage changes bytecode and causes failures, skip. + skipIfCoverage(); + + // Grab the list of implementations. + VerifyOPCM.OpcmContractRef[] memory refs = harness.getOpcmContractRefs(opcm, "implementations", false); + + // Change 256 bytes at random. + for (uint8 i = 0; i < 255; i++) { + // Pick a random implementation to change. + uint256 randomImplIndex = vm.randomUint(0, refs.length - 1); + VerifyOPCM.OpcmContractRef memory ref = refs[randomImplIndex]; + + // Get the code for the implementation. + bytes memory implCode = ref.addr.code; + + // Grab the artifact info for the implementation. + VerifyOPCM.ArtifactInfo memory artifact = harness.loadArtifactInfo(harness.buildArtifactPath(ref.name)); + + // Find a random byte that isn't in an immutable reference. + bool inImmutable = true; + uint256 randomDiffPosition; + while (inImmutable) { + randomDiffPosition = vm.randomUint(0, implCode.length - 1); + inImmutable = false; + for (uint256 j = 0; j < artifact.immutableRefs.length; j++) { + VerifyOPCM.ImmutableRef memory immRef = artifact.immutableRefs[j]; + if (randomDiffPosition >= immRef.offset && randomDiffPosition < immRef.offset + immRef.length) { + inImmutable = true; + break; + } + } + } + + // Change the byte to something new. + bytes1 existingByte = implCode[randomDiffPosition]; + bytes1 newByte = bytes1(uint8(vm.randomUint(0, 255))); + while (newByte == existingByte) { + newByte = bytes1(uint8(vm.randomUint(0, 255))); + } + + // Write the new byte to the code. + implCode[randomDiffPosition] = newByte; + vm.etch(ref.addr, implCode); + } + + // Run the script. + vm.expectRevert(VerifyOPCM.VerifyOPCM_Failed.selector); + harness.run(address(opcm), true); + } + + /// @notice Tests that the script reverts when differences are introduced into the code of + /// blueprints. Unlike immutables, any difference anywhere in the blueprint should + /// cause the script to revert. Fuzzing is too slow here, randomness is good enough. + function test_run_blueprintAnyDifference_reverts() public { + // Coverage changes bytecode and causes failures, skip. + skipIfCoverage(); + + // Grab the list of blueprints. + VerifyOPCM.OpcmContractRef[] memory refs = harness.getOpcmContractRefs(opcm, "blueprints", true); + + // Change 256 bytes at random. + for (uint8 i = 0; i < 255; i++) { + // Pick a random blueprint to change. + uint256 randomBlueprintIndex = vm.randomUint(0, refs.length - 1); + VerifyOPCM.OpcmContractRef memory ref = refs[randomBlueprintIndex]; + + // Get the code for the blueprint. + address blueprint = ref.addr; + bytes memory blueprintCode = blueprint.code; + + // We don't care about immutable references for blueprints. + // Pick a random position. + uint256 randomDiffPosition = vm.randomUint(0, blueprintCode.length - 1); + + // Change the byte to something new. + bytes1 existingByte = blueprintCode[randomDiffPosition]; + bytes1 newByte = bytes1(uint8(vm.randomUint(0, 255))); + while (newByte == existingByte) { + newByte = bytes1(uint8(vm.randomUint(0, 255))); + } + + // Write the new byte to the code. + blueprintCode[randomDiffPosition] = newByte; + vm.etch(blueprint, blueprintCode); + } + + // Run the script. + vm.expectRevert(VerifyOPCM.VerifyOPCM_Failed.selector); + harness.run(address(opcm), true); + } +}