From a42740254e9475dc1bd99f9c1de77be042e6ad2a Mon Sep 17 00:00:00 2001 From: Adrian Sutton Date: Thu, 18 Dec 2025 06:59:29 +1100 Subject: [PATCH 01/13] op-chain-ops: Add command to calculate an output root from an EL endpoint. (#18626) --- op-chain-ops/cmd/check-output-root/README.md | 30 +++++++ op-chain-ops/cmd/check-output-root/main.go | 91 ++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 op-chain-ops/cmd/check-output-root/README.md create mode 100644 op-chain-ops/cmd/check-output-root/main.go diff --git a/op-chain-ops/cmd/check-output-root/README.md b/op-chain-ops/cmd/check-output-root/README.md new file mode 100644 index 00000000000..2e5dc983a5c --- /dev/null +++ b/op-chain-ops/cmd/check-output-root/README.md @@ -0,0 +1,30 @@ +# Overview + +Generates an output root for a given block using only the execution client RPC endpoint. + +## Prerequisites: + +1. git clone or pull the latest develop branch of the optimism repo +2. Go Installed + - You can follow the instructions in the [CONTRIBUTING.md](http://CONTRIBUTING.md) to install all software dependencies of the repo +3. RPC URL for the **L2** chain execution client you want to generate a output root for. + - **Important**: The RPC endpoint must be trusted as it provide the chain state used to compute the output root. + +## Usage: + +```bash +go run op-chain-ops/cmd/check-output-root/ --l2-eth-rpc $RPC_URL --block-num $BLOCK_NUM +``` + +Output: + +```text +0xfefc68b1c0aa7f6e744a8c74084142cf3daa8692179fd5b9ff46c6eacdffe9aa +``` + +## Environment Variables + +Alternatively, you can use environment variables to configure the script: + +- `CHECK_OUTPUT_ROOT_L2_ETH_RPC`: L2 execution client RPC endpoint. +- `CHECK_OUTPUT_ROOT_BLOCK_NUM`: Block number to calculate the output root for. diff --git a/op-chain-ops/cmd/check-output-root/main.go b/op-chain-ops/cmd/check-output-root/main.go new file mode 100644 index 00000000000..6b34bcaaa02 --- /dev/null +++ b/op-chain-ops/cmd/check-output-root/main.go @@ -0,0 +1,91 @@ +package main + +import ( + "context" + "fmt" + "math/big" + "os" + + "github.com/ethereum-optimism/optimism/op-service/ctxinterrupt" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/log" + "github.com/urfave/cli/v2" + + oplog "github.com/ethereum-optimism/optimism/op-service/log" +) + +const ( + // L2EthRpcFlagname defines the flag name for the l2 eth RPC endpoint. + L2EthRpcFlagName = "l2-eth-rpc" + // BlockNumberFlagName defines the flag name for the L2 block number. + BlockNumberFlagName = "block-num" +) + +// Flags contains the list of configuration options available to the binary. +var Flags = []cli.Flag{ + &cli.StringFlag{ + Name: L2EthRpcFlagName, + Usage: "Required: L2 execution client RPC endpoint (e.g., http://host:port).", + Required: true, + EnvVars: []string{"CHECK_OUTPUT_ROOT_L2_ETH_RPC"}, + }, + &cli.Uint64Flag{ + Name: BlockNumberFlagName, + Usage: "Required: L2 block number to calculate the output root for.", + EnvVars: []string{"CHECK_OUTPUT_ROOT_BLOCK_NUM"}, + }, +} + +func main() { + oplog.SetupDefaults() + + app := cli.NewApp() + app.Name = "check-output-root" + app.Usage = "Calculates a output root from an L2 EL endpoint." + // Combine specific flags with log flags + app.Flags = append(Flags, oplog.CLIFlags("CHECK_OUTPUT_ROOT")...) + + app.Action = func(c *cli.Context) error { + ctx := ctxinterrupt.WithCancelOnInterrupt(c.Context) + rpcUrl := c.String(L2EthRpcFlagName) + blockNum := c.Uint64(BlockNumberFlagName) + root, err := CalculateOutputRoot(ctx, rpcUrl, blockNum) + if err != nil { + return err + } + fmt.Println(root.Hex()) + return nil + } + + if err := app.Run(os.Args); err != nil { + log.Crit("Application failed", "err", err) + } +} + +func CalculateOutputRoot(ctx context.Context, rpcUrl string, blockNum uint64) (common.Hash, error) { + client, err := ethclient.DialContext(ctx, rpcUrl) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to connect to L2 RPC endpoint: %w", err) + } + header, err := client.HeaderByNumber(ctx, new(big.Int).SetUint64(blockNum)) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to get L2 block header: %w", err) + } + // Isthmus assumes WithdrawalsHash is present in the header. + if header.WithdrawalsHash == nil { + return common.Hash{}, fmt.Errorf("target block %d (%s) is missing withdrawals hash, required for Isthmus output root calculation", + header.Number.Uint64(), header.Hash()) + } + + // Construct OutputV0 using StateRoot, WithdrawalsHash (as MessagePasserStorageRoot), and BlockHash + output := ð.OutputV0{ + StateRoot: eth.Bytes32(header.Root), + MessagePasserStorageRoot: eth.Bytes32(*header.WithdrawalsHash), + BlockHash: header.Hash(), + } + + // Calculate the output root hash + return common.Hash(eth.OutputRoot(output)), nil +} From f3e6c8c2809777339242762d30ed073bdbbaa0c6 Mon Sep 17 00:00:00 2001 From: Michael Amadi Date: Wed, 17 Dec 2025 21:09:46 +0100 Subject: [PATCH 02/13] verify opcm v2 (#18590) * verify opcm v2 * fixes * fixes * some fixes * some fixes * some fixes * fix verifyOPCM v1 tests * fix verifyOPCM v1 tests * fix ci checks * better approach to tests, less diff * improve test coverage * fix typo * fix semver * Update packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> --------- Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> --- .../opcm/IOPContractsManagerUtilsCaller.sol | 2 +- .../L1/opcm/IOPContractsManagerV2.sol | 9 ++- .../scripts/deploy/VerifyOPCM.s.sol | 65 +++++++++++++++++-- .../snapshots/abi/OPContractsManagerV2.json | 43 +++++++----- .../snapshots/semver-lock.json | 4 +- .../L1/opcm/OPContractsManagerUtilsCaller.sol | 8 +-- .../src/L1/opcm/OPContractsManagerV2.sol | 26 +++++--- .../OPContractsManagerStandardValidator.t.sol | 2 +- .../test/L1/opcm/OPContractsManagerV2.t.sol | 17 ++++- .../test/scripts/VerifyOPCM.t.sol | 26 ++++++-- 10 files changed, 153 insertions(+), 49 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerUtilsCaller.sol b/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerUtilsCaller.sol index f061d52afdc..05bb1010c97 100644 --- a/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerUtilsCaller.sol +++ b/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerUtilsCaller.sol @@ -5,5 +5,5 @@ import { IOPContractsManagerUtils } from "interfaces/L1/opcm/IOPContractsManager interface IOPContractsManagerUtilsCaller { function __constructor__(IOPContractsManagerUtils _utils) external; - function utils() external view returns (IOPContractsManagerUtils); + function opcmUtils() external view returns (IOPContractsManagerUtils); } diff --git a/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerV2.sol b/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerV2.sol index 9dcc891cc99..94e5ad3ff75 100644 --- a/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerV2.sol +++ b/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerV2.sol @@ -130,11 +130,11 @@ interface IOPContractsManagerV2 { function contractsContainer() external view returns (IOPContractsManagerContainer); - function standardValidator() external view returns (IOPContractsManagerStandardValidator); + function opcmStandardValidator() external view returns (IOPContractsManagerStandardValidator); - function thisOPCM() external view returns (IOPContractsManagerV2); + function opcmV2() external view returns (IOPContractsManagerV2); - function utils() external view returns (IOPContractsManagerUtils); + function opcmUtils() external view returns (IOPContractsManagerUtils); function version() external view returns (string memory); @@ -152,4 +152,7 @@ interface IOPContractsManagerV2 { /// @notice Checks if the upgrade sequence from the last used OPCM to this OPCM is permitted. function isPermittedUpgradeSequence(ISystemConfig _systemConfig) external view returns (bool); + + /// @notice Returns the development feature bitmap. + function devFeatureBitmap() external view returns (bytes32); } diff --git a/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol index ea8374a9519..11c209ee151 100644 --- a/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol @@ -13,6 +13,7 @@ import { Process } from "scripts/libraries/Process.sol"; import { Config } from "scripts/libraries/Config.sol"; import { Bytes } from "src/libraries/Bytes.sol"; import { DevFeatures } from "src/libraries/DevFeatures.sol"; +import { SemverComp } from "src/libraries/SemverComp.sol"; // Interfaces import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; @@ -129,8 +130,18 @@ contract VerifyOPCM is Script { fieldNameOverrides["opcmUpgrader"] = "OPContractsManagerUpgrader"; fieldNameOverrides["opcmInteropMigrator"] = "OPContractsManagerInteropMigrator"; fieldNameOverrides["opcmStandardValidator"] = "OPContractsManagerStandardValidator"; + + // Since both OPCM V1 and V2 have contractsContainer var and they point to different contract file names, + // in the code logic, we rename any occurrences of it to "contractsContainerV1" or "contractsContainerV2" before + // using it to read the mapping. + fieldNameOverrides["contractsContainerV1"] = "OPContractsManagerContractsContainer"; + fieldNameOverrides["contractsContainerV2"] = "OPContractsManagerContainer"; + + // OPCM V2 Specific field name overrides. + fieldNameOverrides["standardValidator"] = "OPContractsManagerStandardValidator"; + fieldNameOverrides["storageSetterImpl"] = "StorageSetter"; fieldNameOverrides["opcmV2"] = "OPContractsManagerV2"; - fieldNameOverrides["contractsContainer"] = "OPContractsManagerContractsContainer"; + fieldNameOverrides["opcmUtils"] = "OPContractsManagerUtils"; // Overrides for situations where contracts have differently named source files. sourceNameOverrides["OPContractsManagerGameTypeAdder"] = "OPContractsManager"; @@ -157,11 +168,16 @@ contract VerifyOPCM is Script { expectedGetters["opcmInteropMigrator"] = "SKIP"; // Address verified via bytecode comparison expectedGetters["opcmStandardValidator"] = "SKIP"; // Address verified via bytecode comparison expectedGetters["opcmUpgrader"] = "SKIP"; // Address verified via bytecode comparison + + // OPCM V2 Specific expected getters overrides expectedGetters["opcmV2"] = "SKIP"; // Address verified via bytecode comparison + expectedGetters["opcmUtils"] = "SKIP"; // Address verified via bytecode comparison + expectedGetters["contractsContainer"] = "SKIP"; // Address verified via bytecode comparison // Getters that don't need any sort of verification expectedGetters["devFeatureBitmap"] = "SKIP"; expectedGetters["isDevFeatureEnabled"] = "SKIP"; + expectedGetters["version"] = "SKIP"; // Mark as ready. ready = true; @@ -277,10 +293,11 @@ contract VerifyOPCM is Script { 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 }); + refs[0] = OpcmContractRef({ field: "opcm", name: _opcmContractName(), addr: address(_opcm), blueprint: false }); refs[1] = OpcmContractRef({ field: "contractsContainer", - name: "OPContractsManagerContractsContainer", + // nosemgrep: sol-style-vm-env-only-in-config-sol + name: _isOPCMV2() ? "OPContractsManagerContainer" : "OPContractsManagerContractsContainer", addr: contractsContainerAddr, blueprint: false }); @@ -756,7 +773,7 @@ contract VerifyOPCM is Script { } /// @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 + /// of contract names and their corresponding 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. @@ -767,7 +784,7 @@ contract VerifyOPCM is Script { Process.bash( string.concat( "jq -r '[.abi[] | select(.name? and (.name | type == \"string\") and (.name | startswith(\"opcm\"))) | .name]' ", - _buildArtifactPath("OPContractsManager") + _buildArtifactPath(_opcmContractName()) ) ) ), @@ -824,7 +841,7 @@ contract VerifyOPCM is Script { "jq -r '[.abi[] | select(.name == \"", _property, "\") | .outputs[0].components[].name]' ", - _buildArtifactPath("OPContractsManager") + _buildArtifactPath(_opcmContractName()) ) ) ), @@ -869,6 +886,10 @@ contract VerifyOPCM is Script { /// @param _fieldName The field name to convert. /// @return The contract name. function _getContractNameFromFieldName(string memory _fieldName) internal view returns (string memory) { + if (LibString.eq(_fieldName, "contractsContainer")) { + _fieldName = _isOPCMV2() ? "contractsContainerV2" : "contractsContainerV1"; + } + // Check for an explicit override string memory overrideName = fieldNameOverrides[_fieldName]; if (bytes(overrideName).length > 0) { @@ -993,7 +1014,7 @@ contract VerifyOPCM is Script { Process.bash( string.concat( "jq -r '[.abi[] | select(.type == \"function\" and .stateMutability == \"view\" and (.inputs | length) == 0) | .name]' ", - _buildArtifactPath("OPContractsManager") + _buildArtifactPath(_opcmContractName()) ) ) ), @@ -1046,4 +1067,34 @@ contract VerifyOPCM is Script { revert VerifyOPCM_UnaccountedGetters(trimmedUnaccounted); } } + + /// @notice Returns the name of the OPCM contract depending on whether the OPCM is V2. + /// @return The name of the OPCM contract. + function _opcmContractName() internal view returns (string memory) { + return _isOPCMV2() ? "OPContractsManagerV2" : "OPContractsManager"; + } + + /// @notice Checks if the OPCM is V2. + /// @dev If the OPCM address is not set, default to false. + /// @return True if the OPCM is V2, false otherwise. + function _isOPCMV2() internal view returns (bool) { + // Get the OPCM contract address from the environment variables. + address opcmAddress = _getOPCMAddress(); + + // If the OPCM contract address is not set, default to V1. + if (opcmAddress == address(0)) { + return false; + } + + // If the OPCM contract version is greater than or equal to 7.0.0, then it is OPCM V2. + return SemverComp.gte(IOPContractsManager(opcmAddress).version(), "7.0.0"); + } + + /// @notice Gets the address of the OPCM contract from the environment variables. + /// @dev If not set, default to address(0). + /// @return The address of the OPCM contract. + function _getOPCMAddress() internal view returns (address) { + // nosemgrep: sol-style-vm-env-only-in-config-sol + return vm.envOr("OPCM_ADDRESS", address(0)); + } } diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerV2.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerV2.json index 59886e85dc8..17555aea061 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerV2.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerV2.json @@ -297,6 +297,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "devFeatureBitmap", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "implementations", @@ -447,7 +460,7 @@ }, { "inputs": [], - "name": "standardValidator", + "name": "opcmStandardValidator", "outputs": [ { "internalType": "contract IOPContractsManagerStandardValidator", @@ -460,7 +473,20 @@ }, { "inputs": [], - "name": "thisOPCM", + "name": "opcmUtils", + "outputs": [ + { + "internalType": "contract IOPContractsManagerUtils", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "opcmV2", "outputs": [ { "internalType": "contract OPContractsManagerV2", @@ -653,19 +679,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [], - "name": "utils", - "outputs": [ - { - "internalType": "contract IOPContractsManagerUtils", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "version", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 68fd89aff3d..782af2b6756 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -52,8 +52,8 @@ "sourceCodeHash": "0xb3184aa5d95a82109e7134d1f61941b30e25f655b9849a0e303d04bbce0cde0b" }, "src/L1/opcm/OPContractsManagerV2.sol:OPContractsManagerV2": { - "initCodeHash": "0x7648333338c6ca7e7e39421259b7c78174771263d84d03f9e363a8e0f9ba74f9", - "sourceCodeHash": "0x4c4f6079034ed5dfd0bf242a57a353583ed9ff948b6f9018d91b091a555574db" + "initCodeHash": "0x4d2822fdc1f81c51f843d027ccece5c4e847f7c5870bb068a05bd568f7354c22", + "sourceCodeHash": "0xdb47dbef330b9a8c6e644d5791ed3414796381d90b22078db682b14de30ed16d" }, "src/L2/BaseFeeVault.sol:BaseFeeVault": { "initCodeHash": "0x838bbd7f381e84e21887f72bd1da605bfc4588b3c39aed96cbce67c09335b3ee", diff --git a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerUtilsCaller.sol b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerUtilsCaller.sol index 232deac1dac..22e4ce10ce1 100644 --- a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerUtilsCaller.sol +++ b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerUtilsCaller.sol @@ -15,11 +15,11 @@ import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; /// this is much easier for humans to read and for us to validate offchain. abstract contract OPContractsManagerUtilsCaller { /// @notice Address of the OPContractsManagerUtils contract. - IOPContractsManagerUtils public immutable utils; + IOPContractsManagerUtils public immutable opcmUtils; /// @param _utils Address of the OPContractsManagerUtils contract. constructor(IOPContractsManagerUtils _utils) { - utils = _utils; + opcmUtils = _utils; } /// @notice Maps an L2 chain ID to an L1 batch inbox address as defined by the standard @@ -185,7 +185,7 @@ abstract contract OPContractsManagerUtilsCaller { /// @param _data Calldata to send to the utils contract. /// @return Result of the call. function _delegatecall(bytes memory _data) internal returns (bytes memory) { - (bool success, bytes memory result) = address(utils).delegatecall(_data); + (bool success, bytes memory result) = address(opcmUtils).delegatecall(_data); if (!success) { assembly { revert(add(result, 0x20), mload(result)) @@ -198,7 +198,7 @@ abstract contract OPContractsManagerUtilsCaller { /// @param _data Calldata to send to the utils contract. /// @return Result of the call. function _staticcall(bytes memory _data) internal view returns (bytes memory) { - (bool success, bytes memory result) = address(utils).staticcall(_data); + (bool success, bytes memory result) = address(opcmUtils).staticcall(_data); if (!success) { assembly { revert(add(result, 0x20), mload(result)) diff --git a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol index d59a081cf25..3b937106b27 100644 --- a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol +++ b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol @@ -158,20 +158,20 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { IOPContractsManagerContainer public immutable contractsContainer; /// @notice Address of the Standard Validator for this OPCM release. - IOPContractsManagerStandardValidator public immutable standardValidator; + IOPContractsManagerStandardValidator public immutable opcmStandardValidator; /// @notice Immutable reference to this OPCM contract so that the address of this contract can /// be used when this contract is DELEGATECALLed. - OPContractsManagerV2 public immutable thisOPCM; + OPContractsManagerV2 public immutable opcmV2; /// @notice The version of the OPCM contract. /// WARNING: OPCM versioning rules differ from other contracts: /// - Major bump: New required sequential upgrade /// - Minor bump: Replacement OPCM for same upgrade /// - Patch bump: Development changes (expected for normal dev work) - /// @custom:semver 7.0.0 + /// @custom:semver 7.0.1 function version() public pure returns (string memory) { - return "7.0.0"; + return "7.0.1"; } /// @param _contractsContainer The container of blueprint and implementation contract addresses. @@ -185,8 +185,8 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { OPContractsManagerUtilsCaller(_utils) { contractsContainer = _contractsContainer; - standardValidator = _standardValidator; - thisOPCM = this; + opcmStandardValidator = _standardValidator; + opcmV2 = this; } /////////////////////////////////////////////////////////////////////////// @@ -905,7 +905,7 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { optimismPortal: address(_cts.optimismPortal), optimismMintableERC20Factory: address(_cts.optimismMintableERC20Factory), delayedWETH: address(_cts.delayedWETH), - opcm: address(thisOPCM) + opcm: address(opcmV2) }); // Generate the initializer arguments. @@ -1022,7 +1022,7 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { // 1. Address of the last used OPCM is identical to the address of this OPCM (re-running). // 2. This OPCM version is the same major version but a greater minor version (patch). // 3. This OPCM version is the next major version (sequential upgrade). - bool isSameOPCM = address(lastUsedOPCM) == address(thisOPCM); + bool isSameOPCM = address(lastUsedOPCM) == address(opcmV2); bool isNextMajor = thisSemver.major == lastUsedSemver.major + 1; bool isSameMajorHigherMinor = thisSemver.major == lastUsedSemver.major && thisSemver.minor > lastUsedSemver.minor; @@ -1052,10 +1052,16 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { /////////////////////////////////////////////////////////////////////////// /// @notice Helper for retrieving the version of the OPCM contract. - /// @dev We use thisOPCM.version() because it allows us to properly mock the version function + /// @dev We use opcmV2.version() because it allows us to properly mock the version function /// in tests without running into issues because this contract is being DELEGATECALLed. /// @return The version of the OPCM contract. function _version() internal view returns (string memory) { - return thisOPCM.version(); + return opcmV2.version(); + } + + /// @notice Returns the development feature bitmap. + /// @return The development feature bitmap. + function devFeatureBitmap() public view returns (bytes32) { + return contractsContainer.devFeatureBitmap(); } } diff --git a/packages/contracts-bedrock/test/L1/OPContractsManagerStandardValidator.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManagerStandardValidator.t.sol index 9ca73cd450f..eceb10760a5 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManagerStandardValidator.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManagerStandardValidator.t.sol @@ -167,7 +167,7 @@ abstract contract OPContractsManagerStandardValidator_TestInit is CommonTest, Di preimageOracle = IPreimageOracle(artifacts.mustGetAddress("PreimageOracle")); if (isDevFeatureEnabled(DevFeatures.OPCM_V2)) { - standardValidator = opcmV2.standardValidator(); + standardValidator = opcmV2.opcmStandardValidator(); } else { standardValidator = opcm.opcmStandardValidator(); } diff --git a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol index bf994e4ed25..917e82b3921 100644 --- a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol +++ b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol @@ -118,7 +118,7 @@ contract OPContractsManagerV2_TestInit is CommonTest, DisputeGames { }); // Grab the validator before we do the error assertion. - IOPContractsManagerStandardValidator validator = _opcm.standardValidator(); + IOPContractsManagerStandardValidator validator = _opcm.opcmStandardValidator(); // Expect validator errors if the user provides them. if (bytes(_expectedValidatorErrors).length > 0) { @@ -373,7 +373,7 @@ contract OPContractsManagerV2_Upgrade_TestInit is OPContractsManagerV2_TestInit // Grab the validator before we do the error assertion because otherwise the assertion will // try to apply to this function call instead. - IOPContractsManagerStandardValidator validator = _opcm.standardValidator(); + IOPContractsManagerStandardValidator validator = _opcm.opcmStandardValidator(); // Expect validator errors if the user provides them. We always expect the L1PAOMultisig // and Challenger overrides so we don't need to repeat them here. @@ -1128,3 +1128,16 @@ contract OPContractsManagerV2_Deploy_Test is OPContractsManagerV2_TestInit { ); } } + +/// @title OPContractsManagerV2_DevFeatureBitmap_Test +/// @notice Tests OPContractsManagerV2.devFeatureBitmap +contract OPContractsManagerV2_DevFeatureBitmap_Test is OPContractsManagerV2_TestInit { + /// @notice Tests that the devFeatureBitmap returned by opcmV2 matches the contractsContainer address's own. + function test_devFeatureBitmap_succeeds() public view { + assertEq( + opcmV2.devFeatureBitmap(), + opcmV2.contractsContainer().devFeatureBitmap(), + "devFeatureBitmap on opcmV2 does not match contractsContainer bitmap" + ); + } +} diff --git a/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol b/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol index 8455c09462f..48407923434 100644 --- a/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol +++ b/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol @@ -6,7 +6,7 @@ import { LibString } from "@solady/utils/LibString.sol"; import { DevFeatures } from "src/libraries/DevFeatures.sol"; // Tests -import { OPContractsManager_TestInit } from "test/L1/OPContractsManager.t.sol"; +import { CommonTest } from "test/setup/CommonTest.sol"; // Scripts import { VerifyOPCM } from "scripts/deploy/VerifyOPCM.s.sol"; @@ -61,7 +61,7 @@ contract VerifyOPCM_Harness is VerifyOPCM { /// @title VerifyOPCM_TestInit /// @notice Reusable test initialization for `VerifyOPCM` tests. -abstract contract VerifyOPCM_TestInit is OPContractsManager_TestInit { +abstract contract VerifyOPCM_TestInit is CommonTest { VerifyOPCM_Harness internal harness; function setUp() public virtual override { @@ -69,6 +69,12 @@ abstract contract VerifyOPCM_TestInit is OPContractsManager_TestInit { harness = new VerifyOPCM_Harness(); harness.setUp(); } + + /// @notice Sets up the environment variables for the VerifyOPCM test. + function setupEnvVars() public { + vm.setEnv("EXPECTED_SUPERCHAIN_CONFIG", vm.toString(address(opcm.superchainConfig()))); + vm.setEnv("EXPECTED_PROTOCOL_VERSIONS", vm.toString(address(opcm.protocolVersions()))); + } } /// @title VerifyOPCM_Run_Test @@ -76,8 +82,17 @@ abstract contract VerifyOPCM_TestInit is OPContractsManager_TestInit { contract VerifyOPCM_Run_Test is VerifyOPCM_TestInit { function setUp() public override { super.setUp(); - skipIfDevFeatureEnabled(DevFeatures.OPCM_V2); - setupEnvVars(); + + // If OPCM V2 is enabled, set up the test environment for OPCM V2. + // nosemgrep: sol-style-vm-env-only-in-config-sol + if (vm.envOr("DEV_FEATURE__OPCM_V2", false)) { + opcm = IOPContractsManager(address(opcmV2)); + } else { + setupEnvVars(); + } + + // Set the OPCM address so that runSingle also runs for V2 OPCM if the dev feature is enabled. + vm.setEnv("OPCM_ADDRESS", vm.toString(address(opcm))); } /// @notice Tests that the script succeeds when no changes are introduced. @@ -468,6 +483,9 @@ contract VerifyOPCM_Run_Test is VerifyOPCM_TestInit { // Coverage changes bytecode and causes failures, skip. skipIfCoverage(); + // If OPCM V2 is enabled because we do not use environment variables for OPCM V2. + skipIfDevFeatureEnabled(DevFeatures.OPCM_V2); + // Set expected addresses via environment variables address expectedSuperchainConfig = address(0x1111); address expectedProtocolVersions = address(0x2222); From a6d1906145052c06cf2f2a46f2145a4b17cab399 Mon Sep 17 00:00:00 2001 From: serpixel <5087962+serpixel@users.noreply.github.com> Date: Wed, 17 Dec 2025 21:40:26 +0100 Subject: [PATCH 03/13] codeowners(op-deployer): add platforms team as codeowners (#18635) --- .github/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 15c6b3be40d..6c5d44c4384 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -13,6 +13,9 @@ /op-devstack @ethereum-optimism/op-stack @ethereum-optimism/go-reviewers # Expert areas +/op-deployer @ethereum-optimism/platforms-team +/op-validator @ethereum-optimism/platforms-team + /op-node/rollup @ethereum-optimism/consensus @ethereum-optimism/go-reviewers /op-supervisor @ethereum-optimism/interop @ethereum-optimism/go-reviewers From 0b1901c0421b6035bc60c18d9ba4a038f1571de4 Mon Sep 17 00:00:00 2001 From: Inphi Date: Wed, 17 Dec 2025 15:58:47 -0500 Subject: [PATCH 04/13] ci: Fix contracts test-upgrade test junit output (#18637) * ci: Fix contracts test-upgrade test junit output * fix test-upgrade xml output --- .circleci/config.yml | 6 ++++-- packages/contracts-bedrock/justfile | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6ae6249ad25..5f3707c0ada 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1041,9 +1041,10 @@ jobs: name: Run heavy fuzz tests command: | mkdir -p results - just test --junit > results/results.xml + just test environment: FOUNDRY_PROFILE: ciheavy + JUNIT_TEST_PATH: results/results.xml working_directory: packages/contracts-bedrock no_output_timeout: 90m - run: @@ -1248,8 +1249,9 @@ jobs: name: Run tests command: | mkdir -p results - just test-upgrade --junit > results/results.xml + just test-upgrade environment: + JUNIT_TEST_PATH: results/results.xml FOUNDRY_FUZZ_SEED: 42424242 FOUNDRY_FUZZ_RUNS: 1 FOUNDRY_PROFILE: ci diff --git a/packages/contracts-bedrock/justfile b/packages/contracts-bedrock/justfile index cff45827452..94aab76a70b 100644 --- a/packages/contracts-bedrock/justfile +++ b/packages/contracts-bedrock/justfile @@ -73,7 +73,12 @@ clean: # Runs standard contract tests. test *ARGS: build-go-ffi - forge test {{ARGS}} + #!/bin/bash + if [ -n "$JUNIT_TEST_PATH" ]; then + forge test {{ARGS}} --junit > "$JUNIT_TEST_PATH" + else + forge test {{ARGS}} + fi # Runs standard contract tests (developer mode). test-dev *ARGS: build-go-ffi @@ -115,7 +120,12 @@ prepare-upgrade-env *ARGS : build-go-ffi # Runs upgrade path variant of contract tests. test-upgrade *ARGS: - just prepare-upgrade-env "forge test {{ARGS}}" + #!/bin/bash + if [ -n "$JUNIT_TEST_PATH" ]; then + just prepare-upgrade-env "forge test {{ARGS}} --junit > \"$JUNIT_TEST_PATH\"" + else + just prepare-upgrade-env "forge test {{ARGS}}" + fi test-upgrade-rerun *ARGS: build-go-ffi just test-upgrade {{ARGS}} --rerun -vvvv From 752feafbe79fc453f8c772da9958ef20a7579d62 Mon Sep 17 00:00:00 2001 From: Inphi Date: Wed, 17 Dec 2025 22:04:48 -0500 Subject: [PATCH 05/13] proofs: Add provable output roots to super DGs (#18605) * proofs: Add provable output roots to super DGs This patch updates the SuperFaultDisputeGame to explicitly prove output roots contained in the super root. This is accomplished by requiring proposers to provide the preimage of the super root when creating a SuperFaultDisputeGame. The Super DG is then extended with a rootClaim(uint256 _l2ChainId) method that returns specific output roots contained in the proposal. This allows withdrawals to be proven using only an output root. * review comments * fix nits; update interfaces * update snapshots * fix panic in non-l2oo proposals --- .../tests/interop/proofs/challenger_test.go | 3 +- .../dsl/proofs/dispute_game_factory.go | 32 ++- op-e2e/actions/helpers/l2_proposer.go | 4 + op-proposer/contracts/disputegamefactory.go | 4 +- .../contracts/disputegamefactory_test.go | 2 +- op-proposer/proposer/driver.go | 33 +-- op-proposer/proposer/driver_test.go | 2 +- op-proposer/proposer/source/source.go | 28 +- .../proposer/source/source_supervisor.go | 14 + .../proposer/source/source_supervisor_test.go | 7 +- packages/contracts-bedrock/foundry.toml | 4 + .../dispute/ISuperFaultDisputeGame.sol | 6 + .../dispute/ISuperPermissionedDisputeGame.sol | 6 + .../snapshots/abi/SuperFaultDisputeGame.json | 39 +++ .../abi/SuperPermissionedDisputeGame.json | 39 +++ .../snapshots/semver-lock.json | 8 +- .../src/dispute/SuperFaultDisputeGame.sol | 130 +++++++-- .../dispute/SuperPermissionedDisputeGame.sol | 16 +- .../src/dispute/lib/Errors.sol | 7 + .../src/libraries/Encoding.sol | 45 ++++ .../test/L1/OPContractsManager.t.sol | 59 +++- .../test/dispute/SuperFaultDisputeGame.t.sol | 241 ++++++++++++----- .../SuperPermissionedDisputeGame.t.sol | 57 +++- .../invariants/SuperFaultDisputeGame.t.sol | 16 +- .../test/libraries/Encoding.t.sol | 253 ++++++++++++++++++ .../test/setup/DisputeGames.sol | 23 +- 26 files changed, 919 insertions(+), 159 deletions(-) diff --git a/op-acceptance-tests/tests/interop/proofs/challenger_test.go b/op-acceptance-tests/tests/interop/proofs/challenger_test.go index 4d3bf8b21e8..83ee70bd888 100644 --- a/op-acceptance-tests/tests/interop/proofs/challenger_test.go +++ b/op-acceptance-tests/tests/interop/proofs/challenger_test.go @@ -25,8 +25,7 @@ func TestChallengerPlaysGame(gt *testing.T) { badClaim := common.HexToHash("0xdeadbeef00000000000000000000000000000000000000000000000000000000") attacker := sys.FunderL1.NewFundedEOA(eth.Ether(15)) dgf := sys.DisputeGameFactory() - - game := dgf.StartSuperCannonGame(attacker, proofs.WithRootClaim(badClaim)) + game := dgf.StartSuperCannonGame(attacker, proofs.WithSuperRootFrom(eth.Bytes32(badClaim), eth.Bytes32(badClaim))) claim := game.RootClaim() // This is the bad claim from attacker counterClaim := claim.WaitForCounterClaim() // This is the counter-claim from the challenger diff --git a/op-devstack/dsl/proofs/dispute_game_factory.go b/op-devstack/dsl/proofs/dispute_game_factory.go index d8247b4c1d5..a26052d6ffc 100644 --- a/op-devstack/dsl/proofs/dispute_game_factory.go +++ b/op-devstack/dsl/proofs/dispute_game_factory.go @@ -19,6 +19,7 @@ import ( safetyTypes "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" "github.com/stretchr/testify/require" @@ -84,6 +85,7 @@ type GameCfg struct { l2SequenceNumberSet bool rootClaimSet bool rootClaim common.Hash + superOutputRoots []eth.Bytes32 } type GameOpt interface { Apply(cfg *GameCfg) @@ -120,6 +122,14 @@ func WithL2SequenceNumber(seqNum uint64) GameOpt { }) } +// WithSuperRootFrom sets the output roots to use in a super root game. +// The length of outputRoots must match the number of chains in the super root. +func WithSuperRootFrom(outputRoots ...eth.Bytes32) GameOpt { + return gameOptFn(func(c *GameCfg) { + c.superOutputRoots = outputRoots + }) +} + func NewGameCfg(opts ...GameOpt) *GameCfg { cfg := &GameCfg{} for _, opt := range opts { @@ -181,6 +191,10 @@ func (f *DisputeGameFactory) StartSuperCannonGame(eoa *dsl.EOA, opts ...GameOpt) func (f *DisputeGameFactory) startSuperCannonGameOfType(eoa *dsl.EOA, gameType gameTypes.GameType, opts ...GameOpt) *SuperFaultDisputeGame { cfg := NewGameCfg(opts...) + if len(cfg.superOutputRoots) != 0 && cfg.rootClaimSet { + f.t.Error("cannot set both super output roots and root claim in super game") + f.t.FailNow() + } timestamp := cfg.l2SequenceNumber if !cfg.l2SequenceNumberSet { timestamp = f.supervisor.FetchSyncStatus().SafeTimestamp @@ -188,9 +202,7 @@ func (f *DisputeGameFactory) startSuperCannonGameOfType(eoa *dsl.EOA, gameType g extraData := f.createSuperGameExtraData(timestamp, cfg) rootClaim := cfg.rootClaim if !cfg.rootClaimSet { - // Default to the correct root claim - response := f.supervisor.FetchSuperRootAtTimestamp(timestamp) - rootClaim = common.Hash(response.SuperRoot) + rootClaim = crypto.Keccak256Hash(extraData) } game, addr := f.createNewGame(eoa, gameType, rootClaim, extraData) @@ -202,8 +214,18 @@ func (f *DisputeGameFactory) createSuperGameExtraData(timestamp uint64, cfg *Gam if !cfg.allowFuture { f.supervisor.AwaitMinCrossSafeTimestamp(timestamp) } - extraData := make([]byte, 32) - binary.BigEndian.PutUint64(extraData[24:], timestamp) + super, err := f.supervisor.FetchSuperRootAtTimestamp(timestamp).ToSuper() + f.require.NoError(err, "Failed to fetch super root for timestamp %v", timestamp) + + superV1, ok := super.(*eth.SuperV1) + f.require.Truef(ok, "Unsupported super type %T", super) + if len(cfg.superOutputRoots) != 0 { + f.require.Len(cfg.superOutputRoots, len(superV1.Chains), "Super output roots length mismatch") + for i := range superV1.Chains { + superV1.Chains[i].Output = cfg.superOutputRoots[i] + } + } + extraData := superV1.Marshal() return extraData } diff --git a/op-e2e/actions/helpers/l2_proposer.go b/op-e2e/actions/helpers/l2_proposer.go index 2ba93d7e7c3..82404117b6b 100644 --- a/op-e2e/actions/helpers/l2_proposer.go +++ b/op-e2e/actions/helpers/l2_proposer.go @@ -4,6 +4,7 @@ import ( "context" "crypto/ecdsa" "encoding/binary" + "errors" "math/big" "time" @@ -195,6 +196,9 @@ func (p *L2Proposer) fetchNextOutput(t Testing) (source.Proposal, bool, error) { if err != nil || !shouldPropose { return source.Proposal{}, false, err } + if output.IsSuperRootProposal() { + return source.Proposal{}, false, errors.New("unexpected super root proposal") + } encodedBlockNumber := make([]byte, 32) binary.BigEndian.PutUint64(encodedBlockNumber[24:], output.SequenceNum) game, err := p.disputeGameFactory.Games(&bind.CallOpts{}, p.driver.Cfg.DisputeGameType, output.Root, encodedBlockNumber) diff --git a/op-proposer/contracts/disputegamefactory.go b/op-proposer/contracts/disputegamefactory.go index 838887209e1..527d0e50160 100644 --- a/op-proposer/contracts/disputegamefactory.go +++ b/op-proposer/contracts/disputegamefactory.go @@ -94,7 +94,7 @@ func (f *DisputeGameFactory) HasProposedSince(ctx context.Context, proposer comm } } -func (f *DisputeGameFactory) ProposalTx(ctx context.Context, gameType uint32, outputRoot common.Hash, l2BlockNum uint64) (txmgr.TxCandidate, error) { +func (f *DisputeGameFactory) ProposalTx(ctx context.Context, gameType uint32, outputRoot common.Hash, extraData []byte) (txmgr.TxCandidate, error) { cCtx, cancel := context.WithTimeout(ctx, f.networkTimeout) defer cancel() result, err := f.caller.SingleCall(cCtx, rpcblock.Latest, f.contract.Call(methodInitBonds, gameType)) @@ -102,7 +102,7 @@ func (f *DisputeGameFactory) ProposalTx(ctx context.Context, gameType uint32, ou return txmgr.TxCandidate{}, fmt.Errorf("failed to fetch init bond: %w", err) } initBond := result.GetBigInt(0) - call := f.contract.Call(methodCreateGame, gameType, outputRoot, common.BigToHash(big.NewInt(int64(l2BlockNum))).Bytes()) + call := f.contract.Call(methodCreateGame, gameType, outputRoot, extraData) candidate, err := call.ToTxCandidate() if err != nil { return txmgr.TxCandidate{}, err diff --git a/op-proposer/contracts/disputegamefactory_test.go b/op-proposer/contracts/disputegamefactory_test.go index 84c7dae1a98..dfd89a10900 100644 --- a/op-proposer/contracts/disputegamefactory_test.go +++ b/op-proposer/contracts/disputegamefactory_test.go @@ -210,7 +210,7 @@ func TestProposalTx(t *testing.T) { bond := big.NewInt(49284294829) stubRpc.SetResponse(factoryAddr, methodInitBonds, rpcblock.Latest, []interface{}{gameType}, []interface{}{bond}) stubRpc.SetResponse(factoryAddr, methodCreateGame, rpcblock.Latest, []interface{}{gameType, outputRoot, l2BlockNum}, nil) - tx, err := factory.ProposalTx(context.Background(), gameType, outputRoot, uint64(456)) + tx, err := factory.ProposalTx(context.Background(), gameType, outputRoot, l2BlockNum) require.NoError(t, err) stubRpc.VerifyTxCandidate(tx) require.NotNil(t, tx.Value) diff --git a/op-proposer/proposer/driver.go b/op-proposer/proposer/driver.go index 1e5e96bf39c..cb5a5757e88 100644 --- a/op-proposer/proposer/driver.go +++ b/op-proposer/proposer/driver.go @@ -48,7 +48,7 @@ type L2OOContract interface { type DGFContract interface { Version(ctx context.Context) (string, error) HasProposedSince(ctx context.Context, proposer common.Address, cutoff time.Time, gameType uint32) (bool, time.Time, common.Hash, error) - ProposalTx(ctx context.Context, gameType uint32, outputRoot common.Hash, l2BlockNum uint64) (txmgr.TxCandidate, error) + ProposalTx(ctx context.Context, gameType uint32, outputRoot common.Hash, extraData []byte) (txmgr.TxCandidate, error) } type RollupClient interface { @@ -237,21 +237,24 @@ func (l *L2OutputSubmitter) FetchL2OOOutput(ctx context.Context) (source.Proposa return source.Proposal{}, false, nil } - output, err := l.FetchOutput(ctx, nextCheckpointBlock) + proposal, err := l.FetchOutput(ctx, nextCheckpointBlock) if err != nil { return source.Proposal{}, false, fmt.Errorf("fetching output: %w", err) } + if proposal.IsSuperRootProposal() { + panic("L2 Output Submitter should not be configured for Super Root proposals") + } // Always propose if it's part of the Finalized L2 chain. Or if allowed, if it's part of the safe L2 chain. - if output.SequenceNum > output.Legacy.FinalizedL2.Number && (!l.Cfg.AllowNonFinalized || output.SequenceNum > output.Legacy.SafeL2.Number) { + if proposal.SequenceNum > proposal.Legacy.FinalizedL2.Number && (!l.Cfg.AllowNonFinalized || proposal.SequenceNum > proposal.Legacy.SafeL2.Number) { l.Log.Debug("Not proposing yet, L2 block is not ready for proposal", - "l2_proposal", output.SequenceNum, - "l2_safe", output.Legacy.SafeL2, - "l2_finalized", output.Legacy.FinalizedL2, + "l2_proposal", proposal.SequenceNum, + "l2_safe", proposal.Legacy.SafeL2, + "l2_finalized", proposal.Legacy.FinalizedL2, "allow_non_finalized", l.Cfg.AllowNonFinalized) - return output, false, nil + return proposal, false, nil } - return output, true, nil + return proposal, true, nil } // FetchDGFOutput queries the DGF for the latest game and infers whether it is time to make another proposal @@ -313,14 +316,14 @@ func (l *L2OutputSubmitter) FetchCurrentBlockNumber(ctx context.Context) (uint64 } func (l *L2OutputSubmitter) FetchOutput(ctx context.Context, block uint64) (source.Proposal, error) { - output, err := l.ProposalSource.ProposalAtSequenceNum(ctx, block) + proposal, err := l.ProposalSource.ProposalAtSequenceNum(ctx, block) if err != nil { - return source.Proposal{}, fmt.Errorf("fetching output at block %d: %w", block, err) + return source.Proposal{}, fmt.Errorf("fetching proposal at block %d: %w", block, err) } - if onum := output.SequenceNum; onum != block { // sanity check, e.g. in case of bad RPC caching - return source.Proposal{}, fmt.Errorf("output block number %d mismatches requested %d", output.SequenceNum, block) + if onum := proposal.SequenceNum; onum != block && !proposal.IsSuperRootProposal() { // sanity check, e.g. in case of bad RPC caching + return source.Proposal{}, fmt.Errorf("proposal block number %d mismatches requested %d", proposal.SequenceNum, block) } - return output, nil + return proposal, nil } // ProposeL2OutputTxData creates the transaction data for the ProposeL2Output function @@ -341,7 +344,7 @@ func proposeL2OutputTxData(abi *abi.ABI, output source.Proposal) ([]byte, error) func (l *L2OutputSubmitter) ProposeL2OutputDGFTxCandidate(ctx context.Context, output source.Proposal) (txmgr.TxCandidate, error) { cCtx, cancel := context.WithTimeout(ctx, l.Cfg.NetworkTimeout) defer cancel() - return l.dgfContract.ProposalTx(cCtx, l.Cfg.DisputeGameType, output.Root, output.SequenceNum) + return l.dgfContract.ProposalTx(cCtx, l.Cfg.DisputeGameType, output.Root, output.ExtraData()) } // We wait until l1head advances beyond blocknum. This is used to make sure proposal tx won't @@ -374,7 +377,7 @@ func (l *L2OutputSubmitter) waitForL1Head(ctx context.Context, blockNum uint64) // sendTransaction creates & sends transactions through the underlying transaction manager. func (l *L2OutputSubmitter) sendTransaction(ctx context.Context, output source.Proposal) error { - l.Log.Info("Proposing output root", "output", output.Root, "block", output.SequenceNum) + l.Log.Info("Proposing output root", "output", output.Root, "sequenceNum", output.SequenceNum, "extraData", output.ExtraData()) var receipt *types.Receipt if l.Cfg.DisputeGameFactoryAddr != nil { candidate, err := l.ProposeL2OutputDGFTxCandidate(ctx, output) diff --git a/op-proposer/proposer/driver_test.go b/op-proposer/proposer/driver_test.go index 1e34910d0af..58e49e002a0 100644 --- a/op-proposer/proposer/driver_test.go +++ b/op-proposer/proposer/driver_test.go @@ -48,7 +48,7 @@ func (m *StubDGFContract) HasProposedSince(_ context.Context, _ common.Address, return false, time.Unix(1000, 0), common.Hash{0xdd}, nil } -func (m *StubDGFContract) ProposalTx(_ context.Context, _ uint32, _ common.Hash, _ uint64) (txmgr.TxCandidate, error) { +func (m *StubDGFContract) ProposalTx(_ context.Context, _ uint32, _ common.Hash, _ []byte) (txmgr.TxCandidate, error) { panic("not implemented") } diff --git a/op-proposer/proposer/source/source.go b/op-proposer/proposer/source/source.go index 2c4e5e4d810..0aae82ce47e 100644 --- a/op-proposer/proposer/source/source.go +++ b/op-proposer/proposer/source/source.go @@ -2,6 +2,7 @@ package source import ( "context" + "encoding/binary" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum/go-ethereum/common" @@ -10,17 +11,36 @@ import ( type Proposal struct { // Root is the proposal hash Root common.Hash - // SequenceNum identifies the position in the overall state transition. - // For output roots this is the L2 block number. - // For super roots this is the timestamp. + + // SequenceNum represents the L2 Block number or Super Root L2 timestamp SequenceNum uint64 - CurrentL1 eth.BlockID + + // Super is present if, and only if, this Proposal is a Super Root proposal + Super eth.Super + + CurrentL1 eth.BlockID // Legacy provides data that is only available when retrieving data from a single rollup node. // It should only be used for L2OO proposals. Legacy LegacyProposalData } +// IsSuperRootProposal returns true if the proposal is a Super Root proposal. +func (p *Proposal) IsSuperRootProposal() bool { + return p.Super != nil +} + +// ExtraData returns the Dispute Game extra data as appropriate for the proposal type. +func (p *Proposal) ExtraData() []byte { + if p.Super != nil { + return p.Super.Marshal() + } else { + var extraData [32]byte + binary.BigEndian.PutUint64(extraData[24:], p.SequenceNum) + return extraData[:] + } +} + type LegacyProposalData struct { HeadL1 eth.L1BlockRef SafeL2 eth.L2BlockRef diff --git a/op-proposer/proposer/source/source_supervisor.go b/op-proposer/proposer/source/source_supervisor.go index 7253c03b37a..508dc19617b 100644 --- a/op-proposer/proposer/source/source_supervisor.go +++ b/op-proposer/proposer/source/source_supervisor.go @@ -95,9 +95,23 @@ func (s *SupervisorProposalSource) ProposalAtSequenceNum(ctx context.Context, ti s.log.Warn("Failed to retrieve proposal from supervisor", "idx", i, "err", err) continue } + + super, err := output.ToSuper() + if err != nil { + errs = append(errs, err) + s.log.Warn("Failed to construct super from supervisor", "idx", i, "err", err) + continue + } + if ver := super.Version(); ver != eth.SuperRootVersionV1 { + errs = append(errs, fmt.Errorf("unsupported super type %d from supervisor idx %d", ver, i)) + s.log.Warn("Unsupported super type from supervisor", "idx", i) + continue + } + return Proposal{ Root: common.Hash(output.SuperRoot), SequenceNum: output.Timestamp, + Super: super, CurrentL1: output.CrossSafeDerivedFrom, // Unsupported by super root proposals diff --git a/op-proposer/proposer/source/source_supervisor_test.go b/op-proposer/proposer/source/source_supervisor_test.go index 75c7bbf817b..6c77a513d05 100644 --- a/op-proposer/proposer/source/source_supervisor_test.go +++ b/op-proposer/proposer/source/source_supervisor_test.go @@ -232,14 +232,17 @@ func TestSupervisorSource_ProposalAtSequenceNum(t *testing.T) { }, Timestamp: 59298244, SuperRoot: eth.Bytes32{0xaa, 0xbb}, - Version: 3, + Version: 1, Chains: nil, } + responseSuper, err := response.ToSuper() + require.NoError(t, err) expected := Proposal{ Root: common.Hash(response.SuperRoot), - SequenceNum: response.Timestamp, + SequenceNum: 59298244, CurrentL1: response.CrossSafeDerivedFrom, Legacy: LegacyProposalData{}, + Super: responseSuper, } sequenceNum := uint64(599) t.Run("Single-Success", func(t *testing.T) { diff --git a/packages/contracts-bedrock/foundry.toml b/packages/contracts-bedrock/foundry.toml index b27dbe8207b..f03a6bac0c9 100644 --- a/packages/contracts-bedrock/foundry.toml +++ b/packages/contracts-bedrock/foundry.toml @@ -26,6 +26,8 @@ compilation_restrictions = [ { paths = "src/dispute/PermissionedDisputeGame.sol", optimizer_runs = 5000 }, { paths = "src/dispute/v2/PermissionedDisputeGameV2.sol", optimizer_runs = 5000 }, { paths = "src/dispute/zk/OPSuccinctFaultDisputeGame.sol", optimizer_runs = 5000 }, + { paths = "src/dispute/SuperFaultDisputeGame.sol", optimizer_runs = 5000 }, + { paths = "src/dispute/SuperPermissionedDisputeGame.sol", optimizer_runs = 5000 }, { paths = "src/L1/OPContractsManager.sol", optimizer_runs = 5000 }, { paths = "src/L1/OPContractsManagerStandardValidator.sol", optimizer_runs = 5000 }, { paths = "src/L1/opcm/OPContractsManagerV2.sol", optimizer_runs = 5000 }, @@ -156,6 +158,8 @@ compilation_restrictions = [ { paths = "src/dispute/PermissionedDisputeGame.sol", optimizer_runs = 0 }, { paths = "src/dispute/v2/PermissionedDisputeGameV2.sol", optimizer_runs = 0 }, { paths = "src/dispute/zk/OPSuccinctFaultDisputeGame.sol", optimizer_runs = 0 }, + { paths = "src/dispute/SuperFaultDisputeGame.sol", optimizer_runs = 0 }, + { paths = "src/dispute/SuperPermissionedDisputeGame.sol", optimizer_runs = 0 }, { paths = "src/L1/OPContractsManager.sol", optimizer_runs = 0 }, { paths = "src/L1/OPContractsManagerStandardValidator.sol", optimizer_runs = 0 }, { paths = "src/L1/opcm/OPContractsManagerV2.sol", optimizer_runs = 0 }, diff --git a/packages/contracts-bedrock/interfaces/dispute/ISuperFaultDisputeGame.sol b/packages/contracts-bedrock/interfaces/dispute/ISuperFaultDisputeGame.sol index b9633919fce..40f7b0e87ca 100644 --- a/packages/contracts-bedrock/interfaces/dispute/ISuperFaultDisputeGame.sol +++ b/packages/contracts-bedrock/interfaces/dispute/ISuperFaultDisputeGame.sol @@ -64,6 +64,10 @@ interface ISuperFaultDisputeGame is IDisputeGame { error GameNotFinalized(); error GameNotResolved(); error GamePaused(); + error UnknownChainId(); + error Encoding_EmptySuperRoot(); + error Encoding_InvalidSuperRootVersion(); + error Encoding_InvalidSuperRootEncoding(); event Move(uint256 indexed parentIndex, Claim indexed claim, address indexed claimant); event GameClosed(BondDistributionMode bondDistributionMode); @@ -117,6 +121,8 @@ interface ISuperFaultDisputeGame is IDisputeGame { function vm() external view returns (IBigStepper vm_); function wasRespectedGameTypeWhenCreated() external view returns (bool); function weth() external view returns (IDelayedWETH weth_); + // TODO(#18516): Remove once IDisputeGame includes this interface + function rootClaimByChainId(uint256 _chainId) external view returns (Claim outputRootClaim_); function __constructor__(GameConstructorParams memory _params) external; } diff --git a/packages/contracts-bedrock/interfaces/dispute/ISuperPermissionedDisputeGame.sol b/packages/contracts-bedrock/interfaces/dispute/ISuperPermissionedDisputeGame.sol index edaf24d5244..06e9d2cbd25 100644 --- a/packages/contracts-bedrock/interfaces/dispute/ISuperPermissionedDisputeGame.sol +++ b/packages/contracts-bedrock/interfaces/dispute/ISuperPermissionedDisputeGame.sol @@ -59,6 +59,10 @@ interface ISuperPermissionedDisputeGame is IDisputeGame { error GameNotResolved(); error BadAuth(); error GamePaused(); + error UnknownChainId(); + error Encoding_EmptySuperRoot(); + error Encoding_InvalidSuperRootVersion(); + error Encoding_InvalidSuperRootEncoding(); event Move(uint256 indexed parentIndex, Claim indexed claim, address indexed claimant); event GameClosed(BondDistributionMode bondDistributionMode); @@ -115,6 +119,8 @@ interface ISuperPermissionedDisputeGame is IDisputeGame { function vm() external view returns (IBigStepper vm_); function wasRespectedGameTypeWhenCreated() external view returns (bool); function weth() external view returns (IDelayedWETH weth_); + // TODO(#18516): Remove once IDisputeGame includes this interface + function rootClaimByChainId(uint256 _chainId) external view returns (Claim outputRootClaim_); function __constructor__(ISuperFaultDisputeGame.GameConstructorParams memory _params) external; } diff --git a/packages/contracts-bedrock/snapshots/abi/SuperFaultDisputeGame.json b/packages/contracts-bedrock/snapshots/abi/SuperFaultDisputeGame.json index 598292e2f78..20554462ddb 100644 --- a/packages/contracts-bedrock/snapshots/abi/SuperFaultDisputeGame.json +++ b/packages/contracts-bedrock/snapshots/abi/SuperFaultDisputeGame.json @@ -659,6 +659,25 @@ "stateMutability": "pure", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_chainId", + "type": "uint256" + } + ], + "name": "rootClaimByChainId", + "outputs": [ + { + "internalType": "Claim", + "name": "outputRootClaim_", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, { "inputs": [], "name": "splitDepth", @@ -939,6 +958,21 @@ "name": "DuplicateStep", "type": "error" }, + { + "inputs": [], + "name": "Encoding_EmptySuperRoot", + "type": "error" + }, + { + "inputs": [], + "name": "Encoding_InvalidSuperRootEncoding", + "type": "error" + }, + { + "inputs": [], + "name": "Encoding_InvalidSuperRootVersion", + "type": "error" + }, { "inputs": [], "name": "GameDepthExceeded", @@ -1045,6 +1079,11 @@ "name": "UnexpectedRootClaim", "type": "error" }, + { + "inputs": [], + "name": "UnknownChainId", + "type": "error" + }, { "inputs": [], "name": "ValidStep", diff --git a/packages/contracts-bedrock/snapshots/abi/SuperPermissionedDisputeGame.json b/packages/contracts-bedrock/snapshots/abi/SuperPermissionedDisputeGame.json index 829a145f051..26743d7e383 100644 --- a/packages/contracts-bedrock/snapshots/abi/SuperPermissionedDisputeGame.json +++ b/packages/contracts-bedrock/snapshots/abi/SuperPermissionedDisputeGame.json @@ -685,6 +685,25 @@ "stateMutability": "pure", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_chainId", + "type": "uint256" + } + ], + "name": "rootClaimByChainId", + "outputs": [ + { + "internalType": "Claim", + "name": "outputRootClaim_", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, { "inputs": [], "name": "splitDepth", @@ -970,6 +989,21 @@ "name": "DuplicateStep", "type": "error" }, + { + "inputs": [], + "name": "Encoding_EmptySuperRoot", + "type": "error" + }, + { + "inputs": [], + "name": "Encoding_InvalidSuperRootEncoding", + "type": "error" + }, + { + "inputs": [], + "name": "Encoding_InvalidSuperRootVersion", + "type": "error" + }, { "inputs": [], "name": "GameDepthExceeded", @@ -1076,6 +1110,11 @@ "name": "UnexpectedRootClaim", "type": "error" }, + { + "inputs": [], + "name": "UnknownChainId", + "type": "error" + }, { "inputs": [], "name": "ValidStep", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 782af2b6756..eb0bc8f4b61 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -204,12 +204,12 @@ "sourceCodeHash": "0x335a503a4cc02dd30d88d163393680f3fd89168e0faa4fa4b0ae5da399656f91" }, "src/dispute/SuperFaultDisputeGame.sol:SuperFaultDisputeGame": { - "initCodeHash": "0x388bba903940171322e271b37a9359d4da100afb809a6d6b6b9e798532fca743", - "sourceCodeHash": "0xc29624464bce5382394b281b1e444a248f463f3879381bc7cf4f80c08c5c98ea" + "initCodeHash": "0xb5ce71bc56109055cd0dc71fc63015443bbdb29c5975e049802cd1b5188f06ca", + "sourceCodeHash": "0x3096a447574168528555f091a357a120b1dee6f35b50634b7d705ace1ef9c0ad" }, "src/dispute/SuperPermissionedDisputeGame.sol:SuperPermissionedDisputeGame": { - "initCodeHash": "0x4686e904225a7bce9b7d6b7e9827f557881cf4fab375df3a565b5667062db923", - "sourceCodeHash": "0x00e899175371a254ff49f3b485f60a4e8db9ade04d1807df5934c8b3fe9b2816" + "initCodeHash": "0xa080730728e812e8b02d03b5857b23d16ade46f1656f26f22274835a3100edd7", + "sourceCodeHash": "0x314b6e0412f698ce3531e8176ce8e5b8a3976cc3fa9d7ecb1f3278612f90ed4e" }, "src/dispute/v2/FaultDisputeGameV2.sol:FaultDisputeGameV2": { "initCodeHash": "0x6fc59e2da083c9e2093e42b0fda705e8215cc216e4dcedbf728c08f69ec2d3bd", diff --git a/packages/contracts-bedrock/src/dispute/SuperFaultDisputeGame.sol b/packages/contracts-bedrock/src/dispute/SuperFaultDisputeGame.sol index 05578687eb2..d34297c51db 100644 --- a/packages/contracts-bedrock/src/dispute/SuperFaultDisputeGame.sol +++ b/packages/contracts-bedrock/src/dispute/SuperFaultDisputeGame.sol @@ -19,6 +19,7 @@ import { LocalPreimageKey, VMStatuses } from "src/dispute/lib/Types.sol"; +import { Types } from "src/libraries/Types.sol"; import { Position, LibPosition } from "src/dispute/lib/LibPosition.sol"; import { InvalidParent, @@ -50,8 +51,11 @@ import { InvalidBondDistributionMode, GameNotResolved, GamePaused, - BadExtraData + BadExtraData, + UnknownChainId } from "src/dispute/lib/Errors.sol"; +import { Hashing } from "src/libraries/Hashing.sol"; +import { Encoding } from "src/libraries/Encoding.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; @@ -142,9 +146,9 @@ contract SuperFaultDisputeGame is Clone, ISemver { Position internal constant ROOT_POSITION = Position.wrap(1); /// @notice Semantic version. - /// @custom:semver 0.6.0 + /// @custom:semver 0.7.0 function version() public pure virtual returns (string memory) { - return "0.6.0"; + return "0.7.0"; } /// @notice The starting timestamp of the game @@ -247,7 +251,15 @@ contract SuperFaultDisputeGame is Clone, ISemver { // This is to prevent adding extra or omitting bytes from to `extraData` that result in a different game UUID // in the factory, but are not used by the game, which would allow for multiple dispute games for the same // super proposal to be created. - if (msg.data.length != expectedInitCallDataLength()) revert BadExtraData(); + if (!_verifyInitCallDataLength()) revert BadExtraData(); + + // Sanity check to prevent initializing right away with an invalid claim state that is used as convention + // Should be impossible to find a valid Super preimage of INVALID_ROOT_CLAIM + if (rootClaim().raw() == INVALID_ROOT_CLAIM) revert SuperFaultDisputeGameInvalidRootClaim(); + + // Revert if the super root proof in extraData does not match the root claim. + Types.SuperRootProof memory superRootProof = Encoding.decodeSuperRootProof(extraData()); + if (Hashing.hashSuperRootProof(superRootProof) != rootClaim().raw()) revert BadExtraData(); // Grab the latest anchor root. (Hash root, uint256 rootL2SequenceNumber) = anchorStateRegistry().getAnchorRoot(); @@ -255,9 +267,6 @@ contract SuperFaultDisputeGame is Clone, ISemver { // Should only happen if this is a new game type that hasn't been set up yet. if (root.raw() == bytes32(0)) revert AnchorRootNotFound(); - // Prevent initializing right away with an invalid claim state that is used as convention - if (rootClaim().raw() == INVALID_ROOT_CLAIM) revert SuperFaultDisputeGameInvalidRootClaim(); - // Set the starting Proposal. startingProposal = Proposal({ l2SequenceNumber: rootL2SequenceNumber, root: root }); @@ -314,29 +323,79 @@ contract SuperFaultDisputeGame is Clone, ISemver { GameType.unwrap(anchorStateRegistry().respectedGameType()) == GameType.unwrap(gameType()); } - /// @notice Returns the expected calldata length for the initialize method - function expectedInitCallDataLength() internal pure returns (uint256) { - // Expected length: 6 bytes + immutable args byte count - // - 4 bytes: selector - // - 2 bytes: CWIA length prefix - // - n bytes: Immutable args data - return 6 + immutableArgsByteCount(); + /// @notice Validates the expected length of msg.data for the initialize() call. + /// @dev This function must only be called by initialize(). + /// Note that implementations may override the expected game impl args (see SuperPermissionedDisputeGame). + /// + /// Expected msg.data structure: + /// ┌────────────────────────────────────────────────────────────────────┐ + /// │ 4 bytes │ Function selector (initialize()) │ + /// │ 2 bytes │ CWIA length prefix │ + /// │===================│ ============ pre extra data ================== │ + /// │ 20 bytes │ creator address │ + /// │ 32 bytes │ root claim │ + /// │ 32 bytes │ l1 head │ + /// │ 4 bytes │ game type │ + /// │===================│ ============ extra data ====================== │ + /// │ 1 byte │ super version │ + /// │ 8 bytes │ super timestamp (seqnr) │ + /// │ n * (32+32) bytes │ (chainId, outputRoot) tuples │ + /// │===================│ ============ end extra data ================== │ + /// │ 124 bytes │ game impl args │ + /// └────────────────────────────────────────────────────────────────────┘ + function _verifyInitCallDataLength() internal pure returns (bool) { + uint256 preExtraDataLen = 4 + 2 + _preExtraDataByteCount(); + if (msg.data.length < preExtraDataLen) { + return false; + } + + uint256 postExtraDataLen = gameImplArgsByteCount(); + uint256 extraDataAndGameArgsLength = msg.data.length - preExtraDataLen; + // ensure we have enough data for the game impl args + if (extraDataAndGameArgsLength < postExtraDataLen) { + return false; + } + + uint256 superLen = extraDataAndGameArgsLength - postExtraDataLen; + if (superLen < 9) { + return false; + } + uint256 rem = superLen - 9; + // there must be at least one (chainId, outputRoot) tuple + if (rem == 0) { + return false; + } + return (rem % 64) == 0; + } + + /// @notice Returns the length of the super extra data in the initialize() call. + /// @dev Precondition: msg.data has a valid length. + function _extraDataByteCount() internal pure returns (uint256) { + // The CWIA runtime appends the immutable args and a 2-byte length suffix to every call; + // strip the original calldata and suffix so offsets stay correct for functions with params. + uint256 immutableArgsLength = msg.data.length - _getImmutableArgsOffset() - 2; + return immutableArgsLength - _preExtraDataByteCount() - gameImplArgsByteCount(); } - /// @notice Returns the byte count of the immutable args for this contract. - function immutableArgsByteCount() internal pure virtual returns (uint256) { - // Expected length: 244 bytes + /// @notice Returns the byte count of the data before the extra data (super) in the initialize() call. + function _preExtraDataByteCount() internal pure returns (uint256) { + // Expected length: 88 bytes // - 20 bytes: creator address // - 32 bytes: root claim // - 32 bytes: l1 head - // - 4 bytes: game type - // - 32 bytes: extraData + // - 4 bytes: game type + return 88; + } + + /// @notice Returns the byte count of the game implementation args for this contract. + function gameImplArgsByteCount() internal pure virtual returns (uint256) { + // Expected length: 124 bytes // - 32 bytes: absolutePrestate // - 20 bytes: vm address // - 20 bytes: anchorStateRegistry address // - 20 bytes: weth address // - 32 bytes: l2ChainId (unused) - return 244; + return 124; } //////////////////////////////////////////////////////////////// @@ -608,7 +667,7 @@ contract SuperFaultDisputeGame is Clone, ISemver { /// @notice The l2SequenceNumber (timestamp) of the disputed super root in game root claim. function l2SequenceNumber() public pure returns (uint256 l2SequenceNumber_) { - l2SequenceNumber_ = _getArgUint256(88); + l2SequenceNumber_ = _getArgUint64(89); } /// @notice Only the starting sequence number (timestamp) of the game. @@ -764,6 +823,21 @@ contract SuperFaultDisputeGame is Clone, ISemver { rootClaim_ = Claim.wrap(_getArgBytes32(20)); } + /// @notice Returns the output root in the root claim for the specified L2 chain ID. + /// @param _chainId The L2 chain ID to get the output root claim for. + /// @return outputRootClaim_ The output root claim for the specified L2 chain ID. + function rootClaimByChainId(uint256 _chainId) public pure returns (Claim outputRootClaim_) { + Types.SuperRootProof memory superRootProof = Encoding.decodeSuperRootProof(extraData()); + Types.OutputRootWithChainId[] memory outputRoots = superRootProof.outputRoots; + + for (uint256 i = 0; i < outputRoots.length; i++) { + if (outputRoots[i].chainId == _chainId) { + return Claim.wrap(outputRoots[i].root); + } + } + revert UnknownChainId(); + } + /// @notice Getter for the parent hash of the L1 block when the dispute game was created. /// @dev `clones-with-immutable-args` argument #3 /// @return l1Head_ The parent hash of the L1 block when the dispute game was created. @@ -782,44 +856,42 @@ contract SuperFaultDisputeGame is Clone, ISemver { /// @dev `clones-with-immutable-args` argument #4 /// @return extraData_ Any extra data supplied to the dispute game contract by the creator. function extraData() public pure returns (bytes memory extraData_) { - // The extra data starts at the second word within the cwia calldata and - // is 32 bytes long. - extraData_ = _getArgBytes(88, 32); + extraData_ = _getArgBytes(88, _extraDataByteCount()); } /// @notice Getter for the absolute prestate of the instruction trace. /// @dev `clones-with-immutable-args` argument #6 /// @return absolutePrestate_ The absolute prestate of the instruction trace. function absolutePrestate() public pure returns (Claim absolutePrestate_) { - absolutePrestate_ = Claim.wrap(_getArgBytes32(120)); + absolutePrestate_ = Claim.wrap(_getArgBytes32(_preExtraDataByteCount() + _extraDataByteCount())); } /// @notice Getter for the VM implementation. /// @dev `clones-with-immutable-args` argument #7 /// @return vm_ The onchain VM implementation address. function vm() public pure returns (IBigStepper vm_) { - vm_ = IBigStepper(_getArgAddress(152)); + vm_ = IBigStepper(_getArgAddress(_preExtraDataByteCount() + _extraDataByteCount() + 32)); } /// @notice Getter for the anchor state registry. /// @dev `clones-with-immutable-args` argument #8 /// @return registry_ The anchor state registry contract address. function anchorStateRegistry() public pure returns (IAnchorStateRegistry registry_) { - registry_ = IAnchorStateRegistry(_getArgAddress(172)); + registry_ = IAnchorStateRegistry(_getArgAddress(_preExtraDataByteCount() + _extraDataByteCount() + 52)); } /// @notice Getter for the WETH contract. /// @dev `clones-with-immutable-args` argument #9 /// @return weth_ The WETH contract for holding ETH. function weth() public pure returns (IDelayedWETH weth_) { - weth_ = IDelayedWETH(payable(_getArgAddress(192))); + weth_ = IDelayedWETH(payable(_getArgAddress(_preExtraDataByteCount() + _extraDataByteCount() + 72))); } /// @notice Getter for the L2 chain ID. /// @dev `clones-with-immutable-args` argument #10 /// @return l2ChainId_ The L2 chain ID. function _l2ChainId() internal pure returns (uint256 l2ChainId_) { - l2ChainId_ = _getArgUint256(212); + l2ChainId_ = _getArgUint256(_preExtraDataByteCount() + _extraDataByteCount() + 92); } /// @notice A compliant implementation of this interface should return the components of the diff --git a/packages/contracts-bedrock/src/dispute/SuperPermissionedDisputeGame.sol b/packages/contracts-bedrock/src/dispute/SuperPermissionedDisputeGame.sol index ae64b35dd3b..a8c754f8a2f 100644 --- a/packages/contracts-bedrock/src/dispute/SuperPermissionedDisputeGame.sol +++ b/packages/contracts-bedrock/src/dispute/SuperPermissionedDisputeGame.sol @@ -27,9 +27,9 @@ contract SuperPermissionedDisputeGame is SuperFaultDisputeGame { } /// @notice Semantic version. - /// @custom:semver 0.6.0 + /// @custom:semver 0.7.0 function version() public pure override returns (string memory) { - return "0.6.0"; + return "0.7.0"; } /// @param _params Parameters for creating a new FaultDisputeGame. @@ -76,11 +76,12 @@ contract SuperPermissionedDisputeGame is SuperFaultDisputeGame { if (tx.origin != proposer()) revert BadAuth(); } - function immutableArgsByteCount() internal pure override returns (uint256) { + /// @notice Returns the byte count of the game implementation args for this contract. + function gameImplArgsByteCount() internal pure override returns (uint256) { // Extend expected data length to account for proposer and challenger addresses // - 20 bytes: proposer address // - 20 bytes: challenger address - return super.immutableArgsByteCount() + 40; + return super.gameImplArgsByteCount() + 40; } //////////////////////////////////////////////////////////////// @@ -90,11 +91,14 @@ contract SuperPermissionedDisputeGame is SuperFaultDisputeGame { /// @notice Returns the proposer address. The proposer role is allowed to create proposals and participate in the /// dispute game. function proposer() public pure returns (address proposer_) { - proposer_ = _getArgAddress(super.immutableArgsByteCount()); + proposer_ = + _getArgAddress(super._preExtraDataByteCount() + super._extraDataByteCount() + super.gameImplArgsByteCount()); } /// @notice Returns the challenger address. The challenger role is allowed to participate in the dispute game. function challenger() public pure returns (address challenger_) { - challenger_ = _getArgAddress(super.immutableArgsByteCount() + 20); + challenger_ = _getArgAddress( + super._preExtraDataByteCount() + super._extraDataByteCount() + super.gameImplArgsByteCount() + 20 + ); } } diff --git a/packages/contracts-bedrock/src/dispute/lib/Errors.sol b/packages/contracts-bedrock/src/dispute/lib/Errors.sol index 223bf6281f8..fa23f8d7d86 100644 --- a/packages/contracts-bedrock/src/dispute/lib/Errors.sol +++ b/packages/contracts-bedrock/src/dispute/lib/Errors.sol @@ -177,3 +177,10 @@ error InvalidProposalStatus(); /// @notice Thrown when the game is initialized by an incorrect factory. error IncorrectDisputeGameFactory(); + +//////////////////////////////////////////////////////////////// +// `SuperFaultDisputeGame` Errors // +//////////////////////////////////////////////////////////////// + +/// @notice Thrown when an unknown chain ID is encountered. +error UnknownChainId(); diff --git a/packages/contracts-bedrock/src/libraries/Encoding.sol b/packages/contracts-bedrock/src/libraries/Encoding.sol index ce31d43c0b0..483efc5bdcf 100644 --- a/packages/contracts-bedrock/src/libraries/Encoding.sol +++ b/packages/contracts-bedrock/src/libraries/Encoding.sol @@ -15,6 +15,9 @@ library Encoding { /// @notice Thrown when a provided Super Root proof has no Output Roots. error Encoding_EmptySuperRoot(); + /// @notice Thrown when attempting to decode an invalid Super Root Proof encoding. + error Encoding_InvalidSuperRootEncoding(); + /// @notice RLP encodes the L2 transaction that would be generated when a given deposit is sent /// to the L2 system. Useful for searching for a deposit in the L2 system. The /// transaction is prefixed with 0x7e to identify its EIP-2718 type. @@ -298,4 +301,46 @@ library Encoding { return encoded; } + + /// @notice Decodes a super root proof from the preimage of a Super Root. + /// @param _super Encoded super root proof. + /// @return Decoded super root proof. + function decodeSuperRootProof(bytes memory _super) internal pure returns (Types.SuperRootProof memory) { + if (_super.length < 9) { + revert Encoding_InvalidSuperRootEncoding(); + } + uint8 version = uint8(_super[0]); + if (version != 0x01) { + revert Encoding_InvalidSuperRootVersion(); + } + + uint256 offset = 1; + uint64 superTimestamp; + assembly { + superTimestamp := shr(192, mload(add(_super, add(32, offset)))) + } + offset += 8; + + if (_super.length <= offset) { + revert Encoding_EmptySuperRoot(); + } + if ((_super.length - offset) % 64 != 0) { + revert Encoding_InvalidSuperRootEncoding(); + } + + Types.OutputRootWithChainId[] memory outputRoots = + new Types.OutputRootWithChainId[]((_super.length - offset) / 64); + for (uint256 i = 0; i < outputRoots.length; i++) { + uint256 chainId; + bytes32 root; + assembly { + chainId := mload(add(_super, add(32, offset))) + root := mload(add(_super, add(32, add(offset, 0x20)))) + } + offset += 64; + outputRoots[i] = Types.OutputRootWithChainId({ chainId: chainId, root: root }); + } + + return Types.SuperRootProof({ version: bytes1(version), timestamp: superTimestamp, outputRoots: outputRoots }); + } } diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index ce5457ead99..5dd8a9e9dc4 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -22,6 +22,9 @@ import { GameType, Duration, Hash, Claim } from "src/dispute/lib/LibUDT.sol"; import { Proposal, GameTypes } from "src/dispute/lib/Types.sol"; import { LibGameArgs } from "src/dispute/lib/LibGameArgs.sol"; import { DevFeatures } from "src/libraries/DevFeatures.sol"; +import { Types as LibTypes } from "src/libraries/Types.sol"; +import { Encoding } from "src/libraries/Encoding.sol"; +import { Hashing } from "src/libraries/Hashing.sol"; // Interfaces import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; @@ -831,13 +834,24 @@ contract OPContractsManager_AddGameType_Test is OPContractsManager_TestInit { returns (IFaultDisputeGame) { // Create a game so we can assert on game args which aren't baked into the implementation contract - Claim claim = Claim.wrap(bytes32(uint256(9876))); - uint256 l2SequenceNumber = uint256(123); + Claim claim; + bytes memory extraData; + if (isSuperGame(agi.disputeGameType)) { + LibTypes.OutputRootWithChainId[] memory outputRoots = new LibTypes.OutputRootWithChainId[](1); + outputRoots[0] = LibTypes.OutputRootWithChainId({ chainId: 100, root: keccak256(abi.encode(gasleft())) }); + LibTypes.SuperRootProof memory superRootProof; + superRootProof.version = bytes1(uint8(1)); + superRootProof.timestamp = uint64(123); + superRootProof.outputRoots = outputRoots; + extraData = Encoding.encodeSuperRootProof(superRootProof); + claim = Claim.wrap(Hashing.hashSuperRootProof(superRootProof)); + } else { + claim = Claim.wrap(bytes32(uint256(9876))); + extraData = abi.encode(uint256(123)); // l2BlockNumber + } IFaultDisputeGame game = IFaultDisputeGame( payable( - createGame( - chainDeployOutput1.disputeGameFactoryProxy, agi.disputeGameType, proposer, claim, l2SequenceNumber - ) + createGame(chainDeployOutput1.disputeGameFactoryProxy, agi.disputeGameType, proposer, claim, extraData) ) ); @@ -850,7 +864,7 @@ contract OPContractsManager_AddGameType_Test is OPContractsManager_TestInit { assertEq(game.gameCreator(), proposer, "Game creator should match"); assertEq(game.rootClaim().raw(), claim.raw(), "Claim should match"); assertEq(game.l1Head().raw(), blockhash(block.number - 1), "L1 head should match"); - assertEq(game.l2SequenceNumber(), l2SequenceNumber, "L2 sequence number should match"); + assertEq(game.l2SequenceNumber(), 123, "L2 sequence number should match"); assertEq( game.absolutePrestate().raw(), agi.disputeAbsolutePrestate.raw(), "Absolute prestate should match input" ); @@ -1785,6 +1799,28 @@ contract OPContractsManager_Migrate_Test is OPContractsManager_TestInit { assertEq(_dgf.gameArgs(_gameType), hex"", string.concat("Game args should be empty: ", _label)); } + /// @notice Creates a dummy super root proof consisting of all chains being migrated + function _createSuperRootProof( + IOPContractsManagerInteropMigrator.MigrateInput memory _input, + uint64 _l2SequenceNumber + ) + internal + view + returns (LibTypes.SuperRootProof memory super_) + { + LibTypes.OutputRootWithChainId[] memory outputRoots = + new LibTypes.OutputRootWithChainId[](_input.opChainConfigs.length); + for (uint256 j; j < _input.opChainConfigs.length; j++) { + outputRoots[j] = LibTypes.OutputRootWithChainId({ + chainId: uint32(_input.opChainConfigs[j].systemConfigProxy.l2ChainId()), + root: keccak256(abi.encode(gasleft())) + }); + } + super_.version = bytes1(uint8(1)); + super_.timestamp = uint64(_l2SequenceNumber); + super_.outputRoots = outputRoots; + } + /// @notice Runs some tests after opcm.migrate function _runPostMigrateSmokeTests(IOPContractsManagerInteropMigrator.MigrateInput memory _input) internal { IDisputeGameFactory dgf = IDisputeGameFactory(chainDeployOutput1.systemConfigProxy.disputeGameFactory()); @@ -1817,12 +1853,19 @@ contract OPContractsManager_Migrate_Test is OPContractsManager_TestInit { assertEq(gameArgs.weth, permissionlessWeth, "gameArgs weth mismatch"); } - Claim rootClaim = Claim.wrap(bytes32(uint256(1))); + LibTypes.SuperRootProof memory superRootProof = _createSuperRootProof(_input, uint64(l2SequenceNumber)); uint256 bondAmount = dgf.initBonds(gameTypes[i]); vm.deal(address(proposer), bondAmount); vm.prank(proposer, proposer); + ISuperPermissionedDisputeGame game = ISuperPermissionedDisputeGame( - address(dgf.create{ value: bondAmount }(gameTypes[i], rootClaim, abi.encode(l2SequenceNumber))) + address( + dgf.create{ value: bondAmount }( + gameTypes[i], + Claim.wrap(Hashing.hashSuperRootProof(superRootProof)), + Encoding.encodeSuperRootProof(superRootProof) + ) + ) ); assertEq(game.gameType().raw(), gameTypes[i].raw(), "Super Cannon game type not set properly"); diff --git a/packages/contracts-bedrock/test/dispute/SuperFaultDisputeGame.t.sol b/packages/contracts-bedrock/test/dispute/SuperFaultDisputeGame.t.sol index 9138d9d47bc..c3a78ce42f8 100644 --- a/packages/contracts-bedrock/test/dispute/SuperFaultDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/dispute/SuperFaultDisputeGame.t.sol @@ -17,6 +17,7 @@ import { DisputeActor, HonestDisputeActor } from "test/actors/FaultDisputeActors // Libraries import { Types } from "src/libraries/Types.sol"; import { Hashing } from "src/libraries/Hashing.sol"; +import { Encoding } from "src/libraries/Encoding.sol"; import { RLPWriter } from "src/libraries/rlp/RLPWriter.sol"; import { LibClock } from "src/dispute/lib/LibUDT.sol"; import { LibPosition } from "src/dispute/lib/LibPosition.sol"; @@ -78,14 +79,14 @@ abstract contract BaseSuperFaultDisputeGame_TestInit is DisputeGameFactory_TestI event GameClosed(BondDistributionMode bondDistributionMode); event ReceiveETH(uint256 amount); - function init(Claim _rootClaim, Claim _absolutePrestate, uint256 _l2SequenceNumber) public { + function init(Claim _rootClaim, Claim _absolutePrestate, Types.SuperRootProof memory _super) public { // Set the time to a realistic date. if (!isForkTest()) { vm.warp(1690906994); } // Set the extra data for the game creation - extraData = abi.encode(_l2SequenceNumber); + extraData = Encoding.encodeSuperRootProof(_super); (address _impl, AlphabetVM _vm,) = setupSuperFaultDisputeGame(_absolutePrestate); gameImpl = ISuperFaultDisputeGame(_impl); @@ -115,6 +116,11 @@ abstract contract BaseSuperFaultDisputeGame_TestInit is DisputeGameFactory_TestI assertEq(address(gameProxy.weth()), address(delayedWeth)); assertEq(address(gameProxy.anchorStateRegistry()), address(anchorStateRegistry)); assertEq(address(gameProxy.vm()), address(_vm)); + // Check extra data + assertEq(gameProxy.l2SequenceNumber(), _super.timestamp); + for (uint256 i; i < _super.outputRoots.length; i++) { + assertEq(gameProxy.rootClaimByChainId(_super.outputRoots[i].chainId).raw(), _super.outputRoots[i].root); + } // Label the proxy vm.label(address(gameProxy), "SuperFaultDisputeGame_Clone"); @@ -131,8 +137,8 @@ abstract contract SuperFaultDisputeGame_TestInit is BaseSuperFaultDisputeGame_Te /// @dev The root claim of the game. Claim internal ROOT_CLAIM; - /// @dev An arbitrary root claim for testing. - Claim internal arbitaryRootClaim = Claim.wrap(bytes32(uint256(123))); + /// @dev The super root preimage of the game + Types.SuperRootProof SUPER_ROOT_PROOF; /// @dev The preimage of the absolute prestate claim bytes internal absolutePrestateData; @@ -150,8 +156,8 @@ abstract contract SuperFaultDisputeGame_TestInit is BaseSuperFaultDisputeGame_Te super.setUp(); // Get the actual anchor roots - (Hash root, uint256 l2Bn) = anchorStateRegistry.getAnchorRoot(); - validl2SequenceNumber = l2Bn + 1; + (, uint256 l2Seqno) = anchorStateRegistry.getAnchorRoot(); + validl2SequenceNumber = l2Seqno + 1; if (isForkTest()) { // Set the init bond of anchor game type 4 to be 0. @@ -160,8 +166,13 @@ abstract contract SuperFaultDisputeGame_TestInit is BaseSuperFaultDisputeGame_Te ); } - ROOT_CLAIM = Claim.wrap(Hash.unwrap(root)); - init({ _rootClaim: ROOT_CLAIM, _absolutePrestate: absolutePrestate, _l2SequenceNumber: validl2SequenceNumber }); + SUPER_ROOT_PROOF.version = bytes1(uint8(1)); + SUPER_ROOT_PROOF.timestamp = uint64(validl2SequenceNumber); + SUPER_ROOT_PROOF.outputRoots.push(Types.OutputRootWithChainId({ chainId: 5, root: keccak256(abi.encode(5)) })); + SUPER_ROOT_PROOF.outputRoots.push(Types.OutputRootWithChainId({ chainId: 6, root: keccak256(abi.encode(6)) })); + ROOT_CLAIM = Claim.wrap(Hashing.hashSuperRootProof(SUPER_ROOT_PROOF)); + + init({ _rootClaim: ROOT_CLAIM, _absolutePrestate: absolutePrestate, _super: SUPER_ROOT_PROOF }); } /// @notice Helper to generate a mock RLP encoded header (with only a real block number) & an @@ -205,6 +216,45 @@ abstract contract SuperFaultDisputeGame_TestInit is BaseSuperFaultDisputeGame_Te bond_ = gameProxy.getRequiredBond(pos); } + /// @notice Helper to return a pseudo-random super root proof + function _dummySuper() internal view returns (Types.SuperRootProof memory superRootProof_) { + return _dummySuper(uint64(validl2SequenceNumber)); + } + + /// @notice Helper to return a pseudo-random super root proof with the specified l2SequenceNumber + function _dummySuper(uint64 _l2SequenceNumber) + internal + view + returns (Types.SuperRootProof memory superRootProof_) + { + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](1); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: 5, root: keccak256(abi.encode(gasleft())) }); + superRootProof_.version = bytes1(uint8(1)); + superRootProof_.timestamp = uint64(_l2SequenceNumber); + superRootProof_.outputRoots = outputRoots; + } + + /// @notice Helper to return a pseudo-random super root with the specified l2 sequence number + function _dummySuperRoot(uint64 _l2SequenceNumber) internal view returns (bytes32 superRoot_________) { + return Hashing.hashSuperRootProof(_dummySuper(_l2SequenceNumber)); + } + + /// @notice Helper to return a pseudo-random root claim with the specified l2 sequence number + function _dummyRootClaim(uint64 _l2SequenceNumber) + internal + view + returns (Claim rootClaim_, bytes memory extraData_) + { + Types.SuperRootProof memory superRootProof = _dummySuper(_l2SequenceNumber); + rootClaim_ = Claim.wrap(Hashing.hashSuperRootProof(superRootProof)); + extraData_ = Encoding.encodeSuperRootProof(superRootProof); + } + + /// @notice Helper to return a pseudo-random root claim + function _dummyRootClaim() internal view returns (Claim rootClaim_, bytes memory extraData_) { + return _dummyRootClaim(uint64(validl2SequenceNumber)); + } + /// @notice Helper to return a pseudo-random claim function _dummyClaim() internal view returns (Claim) { return Claim.wrap(keccak256(abi.encode(gasleft()))); @@ -345,14 +395,17 @@ contract SuperFaultDisputeGame_Constructor_Test is SuperFaultDisputeGame_TestIni contract SuperFaultDisputeGame_Initialize_Test is SuperFaultDisputeGame_TestInit { /// @notice Tests that the game cannot be initialized with an output root that commits to /// <= the configured starting block number - function testFuzz_initialize_cannotProposeGenesis_reverts(uint256 _blockNumber) public { + function testFuzz_initialize_cannotProposeGenesis_reverts(uint64 _blockNumber) public { (, uint256 startingL2Block) = gameProxy.startingProposal(); - _blockNumber = bound(_blockNumber, 0, startingL2Block); + _blockNumber = uint64(bound(_blockNumber, 0, uint64(startingL2Block))); + + Types.SuperRootProof memory superRootProof = _dummySuper(_blockNumber); + Claim claim = Claim.wrap(Hashing.hashSuperRootProof(superRootProof)); + bytes memory extraData = Encoding.encodeSuperRootProof(superRootProof); - Claim claim = _dummyClaim(); vm.expectRevert(abi.encodeWithSelector(UnexpectedRootClaim.selector, claim)); gameProxy = ISuperFaultDisputeGame( - payable(address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, claim, abi.encode(_blockNumber)))) + payable(address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, claim, extraData))) ); } @@ -361,15 +414,10 @@ contract SuperFaultDisputeGame_Initialize_Test is SuperFaultDisputeGame_TestInit uint256 _value = disputeGameFactory.initBonds(GAME_TYPE); vm.deal(address(this), _value); + (Claim rootClaim, bytes memory extraData) = _dummyRootClaim(); assertEq(address(gameProxy).balance, 0); gameProxy = ISuperFaultDisputeGame( - payable( - address( - disputeGameFactory.create{ value: _value }( - GAME_TYPE, arbitaryRootClaim, abi.encode(validl2SequenceNumber) - ) - ) - ) + payable(address(disputeGameFactory.create{ value: _value }(GAME_TYPE, rootClaim, extraData))) ); assertEq(address(gameProxy).balance, 0); assertEq(delayedWeth.balanceOf(address(gameProxy)), _value); @@ -395,7 +443,8 @@ contract SuperFaultDisputeGame_Initialize_Test is SuperFaultDisputeGame_TestInit // bytecode. // [0 bytes, 31 bytes] u [33 bytes, 23.5 KB] _extraDataLen = bound(_extraDataLen, 0, 23_500); - if (_extraDataLen == 32) { + + if (_extraDataLen > 9 && (_extraDataLen - 9) % 64 == 0) { _extraDataLen++; } bytes memory _extraData = new bytes(_extraDataLen); @@ -451,11 +500,10 @@ contract SuperFaultDisputeGame_Initialize_Test is SuperFaultDisputeGame_TestInit ); // Creation should fail. + (Claim rootClaim, bytes memory extraData) = _dummyRootClaim(); vm.expectRevert(AnchorRootNotFound.selector); gameProxy = ISuperFaultDisputeGame( - payable( - address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, _dummyClaim(), new bytes(uint256(32)))) - ) + payable(address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, rootClaim, extraData))) ); } @@ -478,17 +526,11 @@ contract SuperFaultDisputeGame_Initialize_Test is SuperFaultDisputeGame_TestInit address(vm_.oracle()), abi.encodeCall(IPreimageOracle.challengePeriod, ()), abi.encode(_challengePeriod) ); - // Create game via factory - initialize() is called automatically and should revert - (, uint256 anchorSeqNo) = anchorStateRegistry.getAnchorRoot(); - // Expect the initialize call to revert with InvalidChallengePeriod + (Claim rootClaim, bytes memory extraData) = _dummyRootClaim(); vm.expectRevert(InvalidChallengePeriod.selector); gameProxy = ISuperFaultDisputeGame( - payable( - address( - disputeGameFactory.create{ value: initBond }(GAME_TYPE, _dummyClaim(), abi.encode(anchorSeqNo + 1)) - ) - ) + payable(address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, rootClaim, extraData))) ); } @@ -500,11 +542,10 @@ contract SuperFaultDisputeGame_Initialize_Test is SuperFaultDisputeGame_TestInit ByteUtils.overwriteAtOffset(gameArgs, l2ChainIdOffset, abi.encodePacked(uint256(1))); disputeGameFactory.setImplementation(GAME_TYPE, impl, gameArgs); + (Claim claim, bytes memory extraData) = _dummyRootClaim(); vm.expectRevert(ISuperFaultDisputeGame.NoChainIdNeeded.selector); gameProxy = ISuperFaultDisputeGame( - payable( - address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, _dummyClaim(), new bytes(uint256(32)))) - ) + payable(address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, claim, extraData))) ); } } @@ -1200,14 +1241,12 @@ contract SuperFaultDisputeGame_AddLocalData_Test is SuperFaultDisputeGame_TestIn /// @notice Tests that the L2 block number claim is favored over the bisected-to block when /// adding data. function test_addLocalData_l2SequenceNumberExtension_succeeds() public { + (Claim rootClaim, bytes memory extraData) = _dummyRootClaim(uint64(validl2SequenceNumber)); + // Deploy a new dispute game with a L2 block number claim of 8. This is directly in the // middle of the leaves in our output bisection test tree, at SPLIT_DEPTH = 2 ** 2 ISuperFaultDisputeGame game = ISuperFaultDisputeGame( - address( - disputeGameFactory.create{ value: initBond }( - GAME_TYPE, Claim.wrap(bytes32(uint256(0xFF))), abi.encode(uint256(validl2SequenceNumber)) - ) - ) + address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, rootClaim, extraData)) ); // Get a claim below the split depth so that we can add local data for an execution trace @@ -1239,7 +1278,7 @@ contract SuperFaultDisputeGame_AddLocalData_Test is SuperFaultDisputeGame_TestIn // Expected start/disputed claims bytes32 startingClaim = bytes32(uint256(3)); Position startingPos = LibPosition.wrap(4, 14); - bytes32 disputedClaim = bytes32(uint256(0xFF)); + bytes32 disputedClaim = Claim.unwrap(rootClaim); Position disputedPos = LibPosition.wrap(0, 0); // Expected local data. This should be `l2SequenceNumber`, and not the actual bisected-to @@ -1862,6 +1901,21 @@ contract SuperFaultDisputeGame_RootClaim_Test is SuperFaultDisputeGame_TestInit function test_rootClaim_succeeds() public view { assertEq(gameProxy.rootClaim().raw(), ROOT_CLAIM.raw()); } + + /// @notice Tests that the game's root claim for each output root is set correctly. + function test_rootClaimForOutputRoot_succeeds() public view { + for (uint256 i = 0; i < SUPER_ROOT_PROOF.outputRoots.length; i++) { + uint256 chainId = SUPER_ROOT_PROOF.outputRoots[i].chainId; + assertEq(gameProxy.rootClaimByChainId(chainId).raw(), SUPER_ROOT_PROOF.outputRoots[i].root); + } + } + + /// @notice Tests that requesting the root claim for an unknown chain ID reverts. + function test_rootClaimForOutputRoot_unknownChainId_reverts() public { + uint256 invalidChainId = 9999; + vm.expectRevert(UnknownChainId.selector); + gameProxy.rootClaimByChainId(invalidChainId); + } } /// @title SuperFaultDisputeGame_ExtraData_Test @@ -2379,6 +2433,35 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { super.setUp(); } + /// @notice Helper to create a dummy super root proof with two output roots. + function createSuper(uint256 _timestamp) internal pure returns (Types.SuperRootProof memory) { + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](2); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: 5, root: keccak256(abi.encode(5)) }); + outputRoots[1] = Types.OutputRootWithChainId({ chainId: 6, root: keccak256(abi.encode(6)) }); + Types.SuperRootProof memory superRootProof; + superRootProof.version = bytes1(uint8(1)); + superRootProof.timestamp = uint64(_timestamp); + superRootProof.outputRoots = outputRoots; + return superRootProof; + } + + /// @notice Helper to create a dummy super root proof with arbitrary number of output roots. + function createSuper( + uint256 _timestamp, + uint256[] memory _l2Outputs + ) + internal + pure + returns (Types.SuperRootProof memory) + { + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](_l2Outputs.length); + for (uint256 i = 0; i < _l2Outputs.length; i++) { + outputRoots[i] = Types.OutputRootWithChainId({ chainId: i + 110101, root: bytes32(_l2Outputs[i]) }); + } + return + Types.SuperRootProof({ version: bytes1(uint8(1)), timestamp: uint64(_timestamp), outputRoots: outputRoots }); + } + /// @notice Static unit test for a 1v1 output bisection dispute. function test_static_1v1dishonestRootGenesisAbsolutePrestate_succeeds() public { // The honest l2 outputs are from [1, 16] in this game. @@ -2407,7 +2490,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { // Run the actor test _actorTest({ - _rootClaim: 17, + _super: createSuper(17), _absolutePrestateData: 0, _honestTrace: honestTrace, _honestL2Outputs: honestL2Outputs, @@ -2419,11 +2502,15 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { /// @notice Static unit test for a 1v1 output bisection dispute. function test_static_1v1honestRoot_succeeds() public { + Types.SuperRootProof memory honestSuper = _dummySuper(uint64(validl2SequenceNumber)); // The honest l2 outputs are from [1, 16] in this game. uint256[] memory honestL2Outputs = new uint256[](16); - for (uint256 i; i < honestL2Outputs.length; i++) { + for (uint256 i; i < honestL2Outputs.length - 1; i++) { honestL2Outputs[i] = i + 1; } + // to ensure that the trace ancestor at split depth is the root claim + honestL2Outputs[15] = uint256(Hashing.hashSuperRootProof(honestSuper)); + // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, // consisting of bytes [0, 255]. bytes memory honestTrace = new bytes(256); @@ -2442,7 +2529,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { // Run the actor test _actorTest({ - _rootClaim: 16, + _super: honestSuper, _absolutePrestateData: 0, _honestTrace: honestTrace, _honestL2Outputs: honestL2Outputs, @@ -2477,7 +2564,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { // Run the actor test _actorTest({ - _rootClaim: 17, + _super: createSuper(17), _absolutePrestateData: 0, _honestTrace: honestTrace, _honestL2Outputs: honestL2Outputs, @@ -2489,11 +2576,15 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { /// @notice Static unit test for a 1v1 output bisection dispute. function test_static_1v1correctRootHalfWay_succeeds() public { + Types.SuperRootProof memory honestSuper = _dummySuper(uint64(validl2SequenceNumber)); // The honest l2 outputs are from [1, 16] in this game. uint256[] memory honestL2Outputs = new uint256[](16); - for (uint256 i; i < honestL2Outputs.length; i++) { + for (uint256 i; i < honestL2Outputs.length - 1; i++) { honestL2Outputs[i] = i + 1; } + // to ensure that the trace ancestor at split depth is the root claim + honestL2Outputs[15] = uint256(Hashing.hashSuperRootProof(honestSuper)); + // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, // consisting of bytes [0, 255]. bytes memory honestTrace = new bytes(256); @@ -2504,7 +2595,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { // The dishonest l2 outputs are half correct, half incorrect. uint256[] memory dishonestL2Outputs = new uint256[](16); for (uint256 i; i < dishonestL2Outputs.length; i++) { - dishonestL2Outputs[i] = i > 7 ? 0xFF : i + 1; + dishonestL2Outputs[i] = i > 7 ? 0xFF : honestL2Outputs[i]; } // The dishonest trace is half correct, half incorrect. bytes memory dishonestTrace = new bytes(256); @@ -2514,7 +2605,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { // Run the actor test _actorTest({ - _rootClaim: 16, + _super: honestSuper, _absolutePrestateData: 0, _honestTrace: honestTrace, _honestL2Outputs: honestL2Outputs, @@ -2538,11 +2629,16 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { honestTrace[i] = bytes1(uint8(i)); } + Types.SuperRootProof memory dishonestSuper = createSuper(0xFF); + // The dishonest l2 outputs are half correct, half incorrect. uint256[] memory dishonestL2Outputs = new uint256[](16); - for (uint256 i; i < dishonestL2Outputs.length; i++) { - dishonestL2Outputs[i] = i > 7 ? 0xFF : i + 1; + for (uint256 i; i < dishonestL2Outputs.length - 1; i++) { + dishonestL2Outputs[i] = i > 7 ? 0xFF : honestL2Outputs[i]; } + // to ensure that the trace ancestor at split depth is the root claim + dishonestL2Outputs[15] = uint256(Hashing.hashSuperRootProof(dishonestSuper)); + // The dishonest trace is half correct, half incorrect. bytes memory dishonestTrace = new bytes(256); for (uint256 i; i < dishonestTrace.length; i++) { @@ -2551,7 +2647,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { // Run the actor test _actorTest({ - _rootClaim: 0xFF, + _super: dishonestSuper, _absolutePrestateData: 0, _honestTrace: honestTrace, _honestL2Outputs: honestL2Outputs, @@ -2563,11 +2659,17 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { /// @notice Static unit test for a 1v1 output bisection dispute. function test_static_1v1dishonestAbsolutePrestate_succeeds() public { + Types.SuperRootProof memory honestSuper = createSuper(validl2SequenceNumber, new uint256[](16)); + bytes32 honestRootClaimHash = Hashing.hashSuperRootProof(honestSuper); + // The honest l2 outputs are from [1, 16] in this game. uint256[] memory honestL2Outputs = new uint256[](16); - for (uint256 i; i < honestL2Outputs.length; i++) { + for (uint256 i; i < honestL2Outputs.length - 1; i++) { honestL2Outputs[i] = i + 1; } + // to ensure that the trace ancestor at split depth is the root claim + honestL2Outputs[15] = uint256(honestRootClaimHash); + // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, // consisting of bytes [0, 255]. bytes memory honestTrace = new bytes(256); @@ -2578,7 +2680,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { // The dishonest l2 outputs are half correct, half incorrect. uint256[] memory dishonestL2Outputs = new uint256[](16); for (uint256 i; i < dishonestL2Outputs.length; i++) { - dishonestL2Outputs[i] = i > 7 ? 0xFF : i + 1; + dishonestL2Outputs[i] = i > 7 ? 0xFF : honestL2Outputs[i]; } // The dishonest trace correct is half correct, half incorrect. bytes memory dishonestTrace = new bytes(256); @@ -2588,7 +2690,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { // Run the actor test _actorTest({ - _rootClaim: 0xFF, + _super: honestSuper, _absolutePrestateData: 0, _honestTrace: honestTrace, _honestL2Outputs: honestL2Outputs, @@ -2600,11 +2702,16 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { /// @notice Static unit test for a 1v1 output bisection dispute. function test_static_1v1honestRootFinalInstruction_succeeds() public { + Types.SuperRootProof memory honestSuper = _dummySuper(uint64(validl2SequenceNumber)); + // The honest l2 outputs are from [1, 16] in this game. uint256[] memory honestL2Outputs = new uint256[](16); - for (uint256 i; i < honestL2Outputs.length; i++) { + for (uint256 i; i < honestL2Outputs.length - 1; i++) { honestL2Outputs[i] = i + 1; } + // to ensure that the trace ancestor at split depth is the root claim + honestL2Outputs[15] = uint256(Hashing.hashSuperRootProof(honestSuper)); + // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, // consisting of bytes [0, 255]. bytes memory honestTrace = new bytes(256); @@ -2615,7 +2722,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { // The dishonest l2 outputs are half correct, half incorrect. uint256[] memory dishonestL2Outputs = new uint256[](16); for (uint256 i; i < dishonestL2Outputs.length; i++) { - dishonestL2Outputs[i] = i > 7 ? 0xFF : i + 1; + dishonestL2Outputs[i] = i > 7 ? 0xFF : honestL2Outputs[i]; } // The dishonest trace is half correct, and correct all the way up to the final instruction // of the exec subgame. @@ -2626,7 +2733,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { // Run the actor test _actorTest({ - _rootClaim: 16, + _super: honestSuper, _absolutePrestateData: 0, _honestTrace: honestTrace, _honestL2Outputs: honestL2Outputs, @@ -2650,11 +2757,16 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { honestTrace[i] = bytes1(uint8(i)); } + Types.SuperRootProof memory dishonestSuper = createSuper(0xFF); + // The dishonest l2 outputs are half correct, half incorrect. uint256[] memory dishonestL2Outputs = new uint256[](16); - for (uint256 i; i < dishonestL2Outputs.length; i++) { - dishonestL2Outputs[i] = i > 7 ? 0xFF : i + 1; + for (uint256 i; i < dishonestL2Outputs.length - 1; i++) { + dishonestL2Outputs[i] = i > 7 ? 0xFF : honestL2Outputs[i]; } + // to ensure that the trace ancestor at split depth is the root claim + dishonestL2Outputs[15] = uint256(Hashing.hashSuperRootProof(dishonestSuper)); + // The dishonest trace is half correct, and correct all the way up to the final instruction // of the exec subgame. bytes memory dishonestTrace = new bytes(256); @@ -2664,7 +2776,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { // Run the actor test _actorTest({ - _rootClaim: 0xFF, + _super: dishonestSuper, _absolutePrestateData: 0, _honestTrace: honestTrace, _honestL2Outputs: honestL2Outputs, @@ -2680,7 +2792,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { /// @notice Helper to run a 1v1 actor test function _actorTest( - uint256 _rootClaim, + Types.SuperRootProof memory _super, uint256 _absolutePrestateData, bytes memory _honestTrace, uint256[] memory _honestL2Outputs, @@ -2701,8 +2813,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { } // Setup the environment - bytes memory absolutePrestateData = - _setup({ _absolutePrestateData: _absolutePrestateData, _rootClaim: _rootClaim }); + bytes memory absolutePrestateData = _setup({ _absolutePrestateData: _absolutePrestateData, _super: _super }); // Create actors _createActors({ @@ -2725,7 +2836,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { /// @notice Helper to setup the 1v1 test function _setup( uint256 _absolutePrestateData, - uint256 _rootClaim + Types.SuperRootProof memory _super ) internal returns (bytes memory absolutePrestateData_) @@ -2733,8 +2844,8 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { absolutePrestateData_ = abi.encode(_absolutePrestateData); Claim absolutePrestateExec = _changeClaimStatus(Claim.wrap(keccak256(absolutePrestateData_)), VMStatuses.UNFINISHED); - Claim rootClaim = Claim.wrap(bytes32(uint256(_rootClaim))); - init({ _rootClaim: rootClaim, _absolutePrestate: absolutePrestateExec, _l2SequenceNumber: _rootClaim }); + Claim rootClaim = Claim.wrap(Hashing.hashSuperRootProof(_super)); + init({ _rootClaim: rootClaim, _absolutePrestate: absolutePrestateExec, _super: _super }); } /// @notice Helper to create actors for the 1v1 dispute. diff --git a/packages/contracts-bedrock/test/dispute/SuperPermissionedDisputeGame.t.sol b/packages/contracts-bedrock/test/dispute/SuperPermissionedDisputeGame.t.sol index 5a4361dec01..c3d0dd5412b 100644 --- a/packages/contracts-bedrock/test/dispute/SuperPermissionedDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/dispute/SuperPermissionedDisputeGame.t.sol @@ -8,6 +8,9 @@ import { AlphabetVM } from "test/mocks/AlphabetVM.sol"; // Libraries import "src/dispute/lib/Types.sol"; import "src/dispute/lib/Errors.sol"; +import { Types } from "src/libraries/Types.sol"; +import { Hashing } from "src/libraries/Hashing.sol"; +import { Encoding } from "src/libraries/Encoding.sol"; // Interfaces import { ISuperPermissionedDisputeGame } from "interfaces/dispute/ISuperPermissionedDisputeGame.sol"; @@ -36,8 +39,9 @@ abstract contract SuperPermissionedDisputeGame_TestInit is DisputeGameFactory_Te /// @notice The root claim of the game. Claim internal rootClaim; - /// @notice An arbitrary root claim for testing. - Claim internal arbitaryRootClaim = Claim.wrap(bytes32(uint256(123))); + /// @notice The Super Root Proof of the rootClaim + Types.SuperRootProof internal superRootProof; + /// @notice Minimum bond value that covers all possible moves. uint256 internal constant MIN_BOND = 50 ether; @@ -50,7 +54,7 @@ abstract contract SuperPermissionedDisputeGame_TestInit is DisputeGameFactory_Te event Move(uint256 indexed parentIndex, Claim indexed pivot, address indexed claimant); - function init(Claim _rootClaim, Claim _absolutePrestate, uint256 _l2BlockNumber) public { + function init(Claim _rootClaim, Claim _absolutePrestate, Types.SuperRootProof memory _super) public { // Set the time to a realistic date. if (!isForkTest()) { vm.warp(1690906994); @@ -60,7 +64,7 @@ abstract contract SuperPermissionedDisputeGame_TestInit is DisputeGameFactory_Te vm.deal(PROPOSER, 100 ether); // Set the extra data for the game creation - extraData = abi.encode(_l2BlockNumber); + extraData = Encoding.encodeSuperRootProof(_super); (address _impl, AlphabetVM _vm,) = setupSuperPermissionedDisputeGame(_absolutePrestate, PROPOSER, CHALLENGER); gameImpl = ISuperPermissionedDisputeGame(_impl); @@ -89,6 +93,11 @@ abstract contract SuperPermissionedDisputeGame_TestInit is DisputeGameFactory_Te assertEq(address(gameProxy.anchorStateRegistry()), address(anchorStateRegistry)); assertEq(address(gameProxy.vm()), address(_vm)); assertEq(address(gameProxy.gameCreator()), PROPOSER); + // Check extra data + assertEq(gameProxy.l2SequenceNumber(), _super.timestamp); + for (uint256 i; i < _super.outputRoots.length; i++) { + assertEq(gameProxy.rootClaimByChainId(_super.outputRoots[i].chainId).raw(), _super.outputRoots[i].root); + } // Label the proxy vm.label(address(gameProxy), "FaultDisputeGame_Clone"); @@ -101,10 +110,16 @@ abstract contract SuperPermissionedDisputeGame_TestInit is DisputeGameFactory_Te super.setUp(); // Get the actual anchor roots - (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); - validL2BlockNumber = l2BlockNumber + 1; - rootClaim = Claim.wrap(Hash.unwrap(root)); - init({ _rootClaim: rootClaim, _absolutePrestate: absolutePrestate, _l2BlockNumber: validL2BlockNumber }); + (, uint256 l2Seqno) = anchorStateRegistry.getAnchorRoot(); + validL2BlockNumber = l2Seqno + 1; + + superRootProof.version = bytes1(uint8(1)); + superRootProof.timestamp = uint64(validL2BlockNumber); + superRootProof.outputRoots.push(Types.OutputRootWithChainId({ chainId: 5, root: keccak256(abi.encode(5)) })); + superRootProof.outputRoots.push(Types.OutputRootWithChainId({ chainId: 6, root: keccak256(abi.encode(6)) })); + rootClaim = Claim.wrap(Hashing.hashSuperRootProof(superRootProof)); + + init({ _rootClaim: rootClaim, _absolutePrestate: absolutePrestate, _super: superRootProof }); } /// @notice Helper to return a pseudo-random claim @@ -126,6 +141,16 @@ abstract contract SuperPermissionedDisputeGame_TestInit is DisputeGameFactory_Te } } + /// @notice Helper to create an arbitrary SuperRootProof + function _arbitraryRootClaim() internal view returns (Types.SuperRootProof memory super_) { + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](1); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: 99, root: keccak256(abi.encode(5)) }); + super_.version = bytes1(uint8(1)); + super_.timestamp = uint64(validL2BlockNumber + 100); + super_.outputRoots = outputRoots; + return super_; + } + fallback() external payable { } receive() external payable { } @@ -209,8 +234,13 @@ contract SuperPermissionedDisputeGame_Uncategorized_Test is SuperPermissionedDis /// @notice Tests that the proposer can create a permissioned dispute game. function test_createGame_proposer_succeeds() public { uint256 bondAmount = disputeGameFactory.initBonds(GAME_TYPE); + Types.SuperRootProof memory arbitrarySuperRootProof = _arbitraryRootClaim(); vm.prank(PROPOSER, PROPOSER); - disputeGameFactory.create{ value: bondAmount }(GAME_TYPE, arbitaryRootClaim, abi.encode(validL2BlockNumber)); + disputeGameFactory.create{ value: bondAmount }( + GAME_TYPE, + Claim.wrap(Hashing.hashSuperRootProof(arbitrarySuperRootProof)), + Encoding.encodeSuperRootProof(arbitrarySuperRootProof) + ); } /// @notice Tests that the permissioned game cannot be created by any address other than the @@ -218,11 +248,16 @@ contract SuperPermissionedDisputeGame_Uncategorized_Test is SuperPermissionedDis function testFuzz_createGame_notProposer_reverts(address _p) public { vm.assume(_p != PROPOSER); + Types.SuperRootProof memory arbitrarySuperRootProof = _arbitraryRootClaim(); uint256 bondAmount = disputeGameFactory.initBonds(GAME_TYPE); vm.deal(_p, bondAmount); vm.prank(_p, _p); vm.expectRevert(BadAuth.selector); - disputeGameFactory.create{ value: bondAmount }(GAME_TYPE, arbitaryRootClaim, abi.encode(validL2BlockNumber)); + disputeGameFactory.create{ value: bondAmount }( + GAME_TYPE, + Claim.wrap(Hashing.hashSuperRootProof(arbitrarySuperRootProof)), + Encoding.encodeSuperRootProof(arbitrarySuperRootProof) + ); } /// @notice Tests that the challenger can participate in a permissioned dispute game. @@ -293,7 +328,7 @@ contract SuperPermissionedDisputeGame_Initialize_Test is SuperPermissionedDisput // bytecode. // [0 bytes, 31 bytes] u [33 bytes, 23.5 KB] _extraDataLen = bound(_extraDataLen, 0, 23_500); - if (_extraDataLen == 32) { + if (_extraDataLen > 9 && (_extraDataLen - 9) % 64 == 0) { _extraDataLen++; } bytes memory _extraData = new bytes(_extraDataLen); diff --git a/packages/contracts-bedrock/test/invariants/SuperFaultDisputeGame.t.sol b/packages/contracts-bedrock/test/invariants/SuperFaultDisputeGame.t.sol index 4e33e83b78f..cc0816d67e7 100644 --- a/packages/contracts-bedrock/test/invariants/SuperFaultDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/invariants/SuperFaultDisputeGame.t.sol @@ -9,9 +9,11 @@ import { RandomClaimActor } from "test/invariants/FaultDisputeGame.t.sol"; // Libraries import "src/dispute/lib/Types.sol"; import "src/dispute/lib/Errors.sol"; +import { Types } from "src/libraries/Types.sol"; +import { Hashing } from "src/libraries/Hashing.sol"; contract SuperFaultDisputeGame_Solvency_Invariant is BaseSuperFaultDisputeGame_TestInit { - Claim internal constant ROOT_CLAIM = Claim.wrap(bytes32(uint256(10))); + Claim internal ROOT_CLAIM; Claim internal constant ABSOLUTE_PRESTATE = Claim.wrap(bytes32((uint256(3) << 248) | uint256(0))); RandomClaimActor internal actor; @@ -19,7 +21,17 @@ contract SuperFaultDisputeGame_Solvency_Invariant is BaseSuperFaultDisputeGame_T function setUp() public override { super.setUp(); - super.init({ _rootClaim: ROOT_CLAIM, _absolutePrestate: ABSOLUTE_PRESTATE, _l2SequenceNumber: 0x10 }); + + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](2); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: 5, root: keccak256(abi.encode(5)) }); + outputRoots[1] = Types.OutputRootWithChainId({ chainId: 6, root: keccak256(abi.encode(6)) }); + Types.SuperRootProof memory superRootProof; + superRootProof.version = bytes1(uint8(1)); + superRootProof.timestamp = uint64(0x10); + superRootProof.outputRoots = outputRoots; + ROOT_CLAIM = Claim.wrap(Hashing.hashSuperRootProof(superRootProof)); + + super.init({ _rootClaim: ROOT_CLAIM, _absolutePrestate: ABSOLUTE_PRESTATE, _super: superRootProof }); actor = new RandomClaimActor(IFaultDisputeGame(address(gameProxy)), vm); diff --git a/packages/contracts-bedrock/test/libraries/Encoding.t.sol b/packages/contracts-bedrock/test/libraries/Encoding.t.sol index 0402e54ebbd..260844cefba 100644 --- a/packages/contracts-bedrock/test/libraries/Encoding.t.sol +++ b/packages/contracts-bedrock/test/libraries/Encoding.t.sol @@ -28,6 +28,10 @@ contract Encoding_Harness { function encodeSuperRootProof(Types.SuperRootProof memory proof) external pure returns (bytes memory) { return Encoding.encodeSuperRootProof(proof); } + + function decodeSuperRootProof(bytes memory _super) external pure returns (Types.SuperRootProof memory) { + return Encoding.decodeSuperRootProof(_super); + } } /// @title Encoding_TestInit @@ -303,6 +307,255 @@ contract Encoding_EncodeSuperRootProof_Test is Encoding_TestInit { } } +/// @title Encoding_DecodeSuperRootProof_Test +/// @notice Tests the `decodeSuperRootProof` function of the `Encoding` contract. +contract Encoding_DecodeSuperRootProof_Test is Encoding_TestInit { + /// @notice Tests successful decoding of a valid super root proof + /// @param _timestamp The timestamp of the super root proof + /// @param _length The number of output roots in the super root proof + /// @param _seed The seed used to generate the output roots + function testFuzz_decodeSuperRootProof_succeeds(uint64 _timestamp, uint256 _length, uint256 _seed) external pure { + // Ensure at least 1 element and cap at a reasonable maximum to avoid gas issues + _length = uint256(bound(_length, 1, 50)); + + // Create output roots array + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](_length); + + // Generate deterministic chain IDs and roots based on the seed + for (uint256 i = 0; i < _length; i++) { + // Use different derivations of the seed for each value + uint256 chainId = uint256(keccak256(abi.encode(_seed, "chainId", i))); + bytes32 root = keccak256(abi.encode(_seed, "root", i)); + + outputRoots[i] = Types.OutputRootWithChainId({ chainId: chainId, root: root }); + } + + // Create the super root proof + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: _timestamp, outputRoots: outputRoots }); + + // Encode the proof + bytes memory encoded = Encoding.encodeSuperRootProof(proof); + + // Decode the proof + Types.SuperRootProof memory decoded = Encoding.decodeSuperRootProof(encoded); + + // Verify the decoded values match the original + assertEq(uint8(decoded.version), uint8(proof.version), "Version should match"); + assertEq(decoded.timestamp, proof.timestamp, "Timestamp should match"); + assertEq(decoded.outputRoots.length, proof.outputRoots.length, "Output roots length should match"); + + // Verify each output root + for (uint256 i = 0; i < _length; i++) { + assertEq(decoded.outputRoots[i].chainId, proof.outputRoots[i].chainId, "Chain ID should match"); + assertEq(decoded.outputRoots[i].root, proof.outputRoots[i].root, "Root should match"); + } + } + + /// @notice Tests decoding with a single output root + function test_decodeSuperRootProof_singleOutputRoot_succeeds() external pure { + // Create a single output root + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](1); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: 10, root: bytes32(uint256(0xdeadbeef)) }); + + // Create the super root proof + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: 1234567890, outputRoots: outputRoots }); + + // Encode the proof + bytes memory encoded = Encoding.encodeSuperRootProof(proof); + + // Decode the proof + Types.SuperRootProof memory decoded = Encoding.decodeSuperRootProof(encoded); + + // Verify the decoded values match the original + assertEq(uint8(decoded.version), 0x01, "Version should be 0x01"); + assertEq(decoded.timestamp, 1234567890, "Timestamp should match"); + assertEq(decoded.outputRoots.length, 1, "Should have one output root"); + assertEq(decoded.outputRoots[0].chainId, 10, "Chain ID should match"); + assertEq(decoded.outputRoots[0].root, bytes32(uint256(0xdeadbeef)), "Root should match"); + } + + /// @notice Tests decoding with multiple output roots + function test_decodeSuperRootProof_multipleOutputRoots_succeeds() external pure { + // Create multiple output roots + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](3); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: 10, root: bytes32(uint256(0xdeadbeef)) }); + outputRoots[1] = Types.OutputRootWithChainId({ chainId: 20, root: bytes32(uint256(0xbeefcafe)) }); + outputRoots[2] = Types.OutputRootWithChainId({ chainId: 30, root: bytes32(uint256(0xcafebabe)) }); + + // Create the super root proof + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: 1234567890, outputRoots: outputRoots }); + + // Encode the proof + bytes memory encoded = Encoding.encodeSuperRootProof(proof); + + // Decode the proof + Types.SuperRootProof memory decoded = Encoding.decodeSuperRootProof(encoded); + + // Verify the decoded values match the original + assertEq(uint8(decoded.version), 0x01, "Version should be 0x01"); + assertEq(decoded.timestamp, 1234567890, "Timestamp should match"); + assertEq(decoded.outputRoots.length, 3, "Should have three output roots"); + + // Verify each output root + assertEq(decoded.outputRoots[0].chainId, 10, "Chain ID 0 should match"); + assertEq(decoded.outputRoots[0].root, bytes32(uint256(0xdeadbeef)), "Root 0 should match"); + assertEq(decoded.outputRoots[1].chainId, 20, "Chain ID 1 should match"); + assertEq(decoded.outputRoots[1].root, bytes32(uint256(0xbeefcafe)), "Root 1 should match"); + assertEq(decoded.outputRoots[2].chainId, 30, "Chain ID 2 should match"); + assertEq(decoded.outputRoots[2].root, bytes32(uint256(0xcafebabe)), "Root 2 should match"); + } + + /// @notice Tests decoding with maximum timestamp value + function test_decodeSuperRootProof_maxTimestamp_succeeds() external pure { + // Create a single output root + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](1); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: 1, root: bytes32(uint256(1)) }); + + // Create the super root proof with max timestamp + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: type(uint64).max, outputRoots: outputRoots }); + + // Encode the proof + bytes memory encoded = Encoding.encodeSuperRootProof(proof); + + // Decode the proof + Types.SuperRootProof memory decoded = Encoding.decodeSuperRootProof(encoded); + + // Verify timestamp is preserved correctly + assertEq(decoded.timestamp, type(uint64).max, "Max timestamp should be preserved"); + } + + /// @notice Tests decoding with maximum chain ID values + function test_decodeSuperRootProof_maxChainId_succeeds() external pure { + // Create output roots with max chain ID + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](2); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: type(uint256).max, root: bytes32(uint256(1)) }); + outputRoots[1] = Types.OutputRootWithChainId({ chainId: type(uint256).max - 1, root: bytes32(uint256(2)) }); + + // Create the super root proof + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: 1234567890, outputRoots: outputRoots }); + + // Encode the proof + bytes memory encoded = Encoding.encodeSuperRootProof(proof); + + // Decode the proof + Types.SuperRootProof memory decoded = Encoding.decodeSuperRootProof(encoded); + + // Verify chain IDs are preserved correctly + assertEq(decoded.outputRoots[0].chainId, type(uint256).max, "Max chain ID should be preserved"); + assertEq(decoded.outputRoots[1].chainId, type(uint256).max - 1, "Max-1 chain ID should be preserved"); + } + + /// @notice Tests that decoding fails when version is not 0x01 + /// @param _version The version to use in the encoded bytes + /// @param _timestamp The timestamp to use in the encoded bytes + function testFuzz_decodeSuperRootProof_invalidVersion_reverts(bytes1 _version, uint64 _timestamp) external { + // Ensure version is not 0x01 + if (_version == 0x01) { + _version = 0x02; + } + + // Manually construct encoded bytes with invalid version + // Structure: 1 byte version + 8 bytes timestamp + (32 bytes chainId + 32 bytes root) * n + // Minimum valid structure needs at least one output root + bytes memory encoded = new bytes(73); // 1 + 8 + 64 + encoded[0] = _version; + + // Encode timestamp (8 bytes) + for (uint256 i = 0; i < 8; i++) { + encoded[1 + i] = bytes1(uint8(_timestamp >> (56 - i * 8))); + } + + // Add a dummy output root (chainId = 1, root = bytes32(uint256(1))) + // ChainId (32 bytes starting at byte 9) + encoded[40] = bytes1(uint8(1)); // Last byte of chainId at offset 9+31=40 + // Root (32 bytes starting at byte 41) + encoded[72] = bytes1(uint8(1)); // Last byte of root at offset 41+31=72 + + // Expect revert when decoding + vm.expectRevert(Encoding.Encoding_InvalidSuperRootVersion.selector); + encoding.decodeSuperRootProof(encoded); + } + + /// @notice Tests that decoding fails when version byte is 0x00 + function test_decodeSuperRootProof_versionZero_reverts() external { + // Create encoded bytes with version 0x00 + bytes memory encoded = new bytes(73); // 1 + 8 + 64 + encoded[0] = bytes1(0x00); + + // Add timestamp and dummy output root + encoded[40] = bytes1(uint8(1)); // ChainId last byte + encoded[72] = bytes1(uint8(1)); // Root last byte + + // Expect revert when decoding + vm.expectRevert(Encoding.Encoding_InvalidSuperRootVersion.selector); + encoding.decodeSuperRootProof(encoded); + } + + /// @notice Tests that decoding fails when version byte is 0xFF + function test_decodeSuperRootProof_versionFF_reverts() external { + // Create encoded bytes with version 0xFF + bytes memory encoded = new bytes(73); // 1 + 8 + 64 + encoded[0] = bytes1(0xFF); + + // Add timestamp and dummy output root + encoded[40] = bytes1(uint8(1)); // ChainId last byte + encoded[72] = bytes1(uint8(1)); // Root last byte + + // Expect revert when decoding + vm.expectRevert(Encoding.Encoding_InvalidSuperRootVersion.selector); + encoding.decodeSuperRootProof(encoded); + } + + /// @notice Tests that decoding fails when output roots array is empty + function testFuzz_decodeSuperRootProof_emptyOutputRoots_reverts(uint64 _timestamp) external { + // Manually construct encoded bytes with no output roots + // Structure: 1 byte version + 8 bytes timestamp + bytes memory encoded = new bytes(9); // 1 + 8 + encoded[0] = bytes1(0x01); + + // Encode timestamp (8 bytes) + for (uint256 i = 0; i < 8; i++) { + encoded[1 + i] = bytes1(uint8(_timestamp >> (56 - i * 8))); + } + + // Expect revert when decoding + vm.expectRevert(Encoding.Encoding_EmptySuperRoot.selector); + encoding.decodeSuperRootProof(encoded); + } + + /// @notice Tests that decoding fails when encoded bytes are incomplete + function testFuzz_decodeSuperRootProof_partial_reverts(uint256 _length) external { + _length = uint256(bound(_length, 0, 8)); + bytes memory encoded = new bytes(_length); + vm.expectRevert(Encoding.Encoding_InvalidSuperRootEncoding.selector); + encoding.decodeSuperRootProof(encoded); + } + + /// @notice Tests that decoding fails when output roots array is incomplete + function testFuzz_decodeSuperRootProof_partialOutputRoots_reverts(uint64 _timestamp, uint256 _length) external { + _length = uint256(bound(_length, 10, 72)); + + // Manually construct encoded bytes with no output roots + // Structure: 1 byte version + 8 bytes timestamp + bytes memory encoded = new bytes(_length); + encoded[0] = bytes1(0x01); + + // Encode timestamp (8 bytes) + for (uint256 i = 0; i < 8; i++) { + encoded[1 + i] = bytes1(uint8(_timestamp >> (56 - i * 8))); + } + + // Expect revert when decoding + vm.expectRevert(Encoding.Encoding_InvalidSuperRootEncoding.selector); + encoding.decodeSuperRootProof(encoded); + } +} + /// @title Encoding_Uncategorized_Test /// @notice General tests that are not testing any function directly of the `Encoding` contract or /// are testing multiple functions at once. diff --git a/packages/contracts-bedrock/test/setup/DisputeGames.sol b/packages/contracts-bedrock/test/setup/DisputeGames.sol index cd389c9042c..abda463cd15 100644 --- a/packages/contracts-bedrock/test/setup/DisputeGames.sol +++ b/packages/contracts-bedrock/test/setup/DisputeGames.sol @@ -33,6 +33,20 @@ contract DisputeGames is FeatureFlags { ) internal returns (address) + { + return createGame(_factory, _gameType, _proposer, _claim, abi.encode(bytes32(_l2BlockNumber))); + } + + /// @notice Helper function to create a permissioned game through the factory + function createGame( + IDisputeGameFactory _factory, + GameType _gameType, + address _proposer, + Claim _claim, + bytes memory _extraData + ) + internal + returns (address) { // Check if there's an init bond required for the game type uint256 initBond = _factory.initBonds(_gameType); @@ -46,8 +60,7 @@ contract DisputeGames is FeatureFlags { // We use vm.startPrank to set both msg.sender and tx.origin to the proposer vm.startPrank(_proposer, _proposer); - IDisputeGame gameProxy = - _factory.create{ value: initBond }(_gameType, _claim, abi.encode(bytes32(_l2BlockNumber))); + IDisputeGame gameProxy = _factory.create{ value: initBond }(_gameType, _claim, _extraData); vm.stopPrank(); @@ -59,6 +72,12 @@ contract DisputeGames is FeatureFlags { || _gameType.raw() == GameTypes.SUPER_PERMISSIONED_CANNON.raw(); } + /// @notice Checks if the game type is a super game type + function isSuperGame(GameType _gameType) internal pure returns (bool) { + return _gameType.raw() == GameTypes.SUPER_PERMISSIONED_CANNON.raw() + || _gameType.raw() == GameTypes.SUPER_CANNON.raw() || _gameType.raw() == GameTypes.SUPER_CANNON_KONA.raw(); + } + enum GameArg { PRESTATE, VM, From 5c49267e26ea96ee874a4fd4a590db16091de709 Mon Sep 17 00:00:00 2001 From: Inphi Date: Wed, 17 Dec 2025 23:03:35 -0500 Subject: [PATCH 06/13] op-program: Reduce disk usage of preimage prestate builds in ci (#18641) --- op-program/scripts/build-prestates.sh | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/op-program/scripts/build-prestates.sh b/op-program/scripts/build-prestates.sh index 5d765796ced..882f551d5a8 100755 --- a/op-program/scripts/build-prestates.sh +++ b/op-program/scripts/build-prestates.sh @@ -129,10 +129,17 @@ EOF VERSIONS_JSON="[]" readarray -t VERSIONS < <(git tag --list 'op-program/v*' --sort taggerdate) -for VERSION in "${VERSIONS[@]}"; do +for i in "${!VERSIONS[@]}"; do pushd . - build_op_program_prestate "${VERSION}" + build_op_program_prestate "${VERSIONS[i]}" popd + # after every 10 builds, cleanup docker artifacts to reclaim disk space + if [ "$CIRCLECI" = "true" ]; then + if (( (i + 1) % 10 == 0 )); then + echo "Pruning docker build artifacts after ${i} builds" + docker system prune -f + fi + fi done echo "${VERSIONS_JSON}" > "${VERSIONS_FILE}" @@ -151,10 +158,17 @@ readarray -t KONA_VERSIONS < <(git ls-remote --tags "$KONA_REPO_URL" | grep kona | sed 's|.*refs/tags/||' | sed 's/\^{}//' | sort -u \ | grep -v beta | grep -v alpha | grep -v -F -f excluded.txt) -for VERSION in "${KONA_VERSIONS[@]}"; do +for i in "${!KONA_VERSIONS[@]}"; do pushd . - build_kona_prestate "${VERSION}" + build_kona_prestate "${KONA_VERSIONS[i]}" popd + # after every 10 builds, cleanup docker artifacts to reclaim disk space + if [ "$CIRCLECI" = "true" ]; then + if (( (i + 1) % 10 == 0 )); then + echo "Pruning docker build artifacts after ${i} builds" + docker system prune -f + fi + fi done echo "${VERSIONS_JSON}" > "${VERSIONS_FILE}" From 56a3b9140f5e3fa6a8f4b16766fadcea4df24d8c Mon Sep 17 00:00:00 2001 From: Josh Klopfenstein Date: Thu, 18 Dec 2025 09:19:46 -0600 Subject: [PATCH 07/13] all: upgrade op-geth (#18646) Pull in superchain registry changes and a few other minor changes. No upstream merges. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 791784f4b85..79a8ad9fcc5 100644 --- a/go.mod +++ b/go.mod @@ -310,7 +310,7 @@ require ( lukechampine.com/blake3 v1.3.0 // indirect ) -replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.101604.0-synctest.0.0.20251208094937-ba6bdcfef423 +replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.101603.6-rc.1 // replace github.com/ethereum/go-ethereum => ../op-geth diff --git a/go.sum b/go.sum index d4297507e1c..746881d6621 100644 --- a/go.sum +++ b/go.sum @@ -240,8 +240,8 @@ github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.4-0.20251001155152-4eb15ccedf7e h1:iy1vBIzACYUyOVyoADUwvAiq2eOPC0yVsDUdolPwQjk= github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.4-0.20251001155152-4eb15ccedf7e/go.mod h1:DYj7+vYJ4cIB7zera9mv4LcAynCL5u4YVfoeUu6Wa+w= -github.com/ethereum-optimism/op-geth v1.101604.0-synctest.0.0.20251208094937-ba6bdcfef423 h1:5xVkCCBRWkOt+bzVWL1p3mOwrpZLjxi/+yWUsja0E48= -github.com/ethereum-optimism/op-geth v1.101604.0-synctest.0.0.20251208094937-ba6bdcfef423/go.mod h1:fCNAwDynfAP6EKsmLqwSDUDgi+GtJIir74Ui3fXXMps= +github.com/ethereum-optimism/op-geth v1.101603.6-rc.1 h1:C4MAM29WbeXeNELMLpX1xU6c6OIwBRNposAcl1NHvVk= +github.com/ethereum-optimism/op-geth v1.101603.6-rc.1/go.mod h1:fCNAwDynfAP6EKsmLqwSDUDgi+GtJIir74Ui3fXXMps= github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20251121143344-5ac16e0fbb00 h1:TR5Y7B+5m63V0Dno7MHcFqv/XZByQzx/4THV1T1A7+U= github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20251121143344-5ac16e0fbb00/go.mod h1:NZ816PzLU1TLv1RdAvYAb6KWOj4Zm5aInT0YpDVml2Y= github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= From f0fcc8d5f03f8a3203946bf5107bc7a0c1a1f351 Mon Sep 17 00:00:00 2001 From: George Knee Date: Thu, 18 Dec 2025 15:34:11 +0000 Subject: [PATCH 08/13] refactor+fix(op-node/sequencing): l1 origin selection improvements (#18589) * WIP * wip * WIP * Treat NotFound next L1 origin as chain end * Use recover mode in sequence window expiry test * Invoke fault proof earlier and fix typos Run env.RunFaultProofProgram after computing l2SafeHead (l2SafeHead.Number.Uint64()/2) and replace the FromGenesis call with RunFaultProofProgram. Fix minor comment typos and wrap a long log line. * reduce diff * Use requireL1OriginAt helper in test Replace manual error assertions around FindL1Origin with requireL1OriginAt and remove the now-unused derive import. * Introduce L2Sequencer.ActMaybeL2StartBlock * add TestRecoverModeWhenChainHealthy acceptance test sysgo only * Add SetSequencerRecoverMode and enable debug logs * Adjust L1 block time and sequence window test Set default L1 block time to 12s to match action helper assumptions. Increase sequencer window size in the test to 50. Compute drift from UnsafeL2 headers (use UnsafeL2.L1Origin). Simplify L1 mining to always BatchMineAndSync and remove the extra numL1Blocks > 10 lag guard. * restore stub * WIP * name errs * refactor * fix * add test * Rename error constant and add L1 origin tests Rename ErrInvalidL1OriginChild to ErrNextL1OriginOrphaned and adjust the error text. Add test cases covering L1 reorg and invalid L1 origin, and refactor the test case construction in the unit test. * Use drift check for next L1 origin and update tests Compute driftNext and use it to decide adoption of the next L1 origin instead of comparing absolute timestamps. Bump MaxSequencerDrift to 1800 and add tests covering max-drift and negative-drift scenarios. * Refactor L1 origin selection and error handling Delegate origin decision logic to FindL1OriginOfNextL2Block and handle synchronous fetch when in recover mode. Remove recover-mode fetching from CurrentAndNextOrigin and return cached values instead. Update sequencer error handling to distinguish invalid/orphaned origin (which triggers a reset) from temporary lookup errors. * fixes * fixes * lint * don't use pointers saves some translation code * handle retries without a "temporary error" fixes action tests * use Fjord drift constant * fix origin_selector_test mainly just asserting on sentinel errors, and taking account of small optimization (fewer network calls) * Simplify FindL1Origin * move new pure function into a method on los * Update comment to refer to empty nextL1Origin * Use errors.Is for L1 origin error checks * Return L1 origin on validation errors * Add expectedResult to origin selector tests Set expectedResult for the test cases with l2Head d1000 and e1000 to assert the expected L1 origins (c101 and c100 respectively) * Add assertion message and clarify origin comments Provide a helpful failure message in sequence_window_expiry_test when the safe head doesn't advance after the sequencing window expires. Document that the current L1 origin should always be non-empty and add a panic guard. Clarify the rationale for requiring the next L1 origin, include a source link, and note the effect on unsafe block production. * Store recoverMode and add comment period * Update op-node/rollup/sequencing/origin_selector.go Co-authored-by: almanax-ai[bot] <174396398+almanax-ai[bot]@users.noreply.github.com> * Update op-node/rollup/sequencing/origin_selector.go Co-authored-by: almanax-ai[bot] <174396398+almanax-ai[bot]@users.noreply.github.com> --------- Co-authored-by: almanax-ai[bot] <174396398+almanax-ai[bot]@users.noreply.github.com> --- justfile | 1 - .../tests/sequencer/init_test.go | 17 ++ .../tests/sequencer/recover_mode_test.go | 45 ++++ op-devstack/dsl/l2_cl.go | 4 + op-e2e/actions/helpers/l2_batcher.go | 2 +- op-e2e/actions/helpers/l2_sequencer.go | 26 ++- op-e2e/actions/helpers/utils.go | 2 +- op-e2e/actions/proofs/helpers/env.go | 3 - .../proofs/sequence_window_expiry_test.go | 132 ++++++++++- op-node/rollup/sequencing/origin_selector.go | 159 ++++++++----- .../rollup/sequencing/origin_selector_test.go | 220 ++++++++++++++++-- op-node/rollup/sequencing/sequencer.go | 20 +- 12 files changed, 527 insertions(+), 104 deletions(-) create mode 100644 op-acceptance-tests/tests/sequencer/init_test.go create mode 100644 op-acceptance-tests/tests/sequencer/recover_mode_test.go diff --git a/justfile b/justfile index ccc0c0e8d0a..aac07f68f8b 100644 --- a/justfile +++ b/justfile @@ -45,4 +45,3 @@ update-op-geth ref: go mod edit -replace=github.com/ethereum/go-ethereum=github.com/ethereum-optimism/op-geth@"$ver"; \ go mod tidy; \ echo "Updated op-geth to $ver" - diff --git a/op-acceptance-tests/tests/sequencer/init_test.go b/op-acceptance-tests/tests/sequencer/init_test.go new file mode 100644 index 00000000000..ed9fb2d9555 --- /dev/null +++ b/op-acceptance-tests/tests/sequencer/init_test.go @@ -0,0 +1,17 @@ +package sequencer + +import ( + "log/slog" + "testing" + + "github.com/ethereum-optimism/optimism/op-devstack/compat" + "github.com/ethereum-optimism/optimism/op-devstack/presets" +) + +// TestMain creates the test-setups against the shared backend +func TestMain(m *testing.M) { + presets.DoMain(m, presets.WithMinimal(), + presets.WithCompatibleTypes(compat.SysGo), + presets.WithLogLevel(slog.LevelDebug), + ) +} diff --git a/op-acceptance-tests/tests/sequencer/recover_mode_test.go b/op-acceptance-tests/tests/sequencer/recover_mode_test.go new file mode 100644 index 00000000000..a0f3382e181 --- /dev/null +++ b/op-acceptance-tests/tests/sequencer/recover_mode_test.go @@ -0,0 +1,45 @@ +package sequencer + +import ( + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/stretchr/testify/require" +) + +// TestRecoverModeWhenChainHealthy checks that the chain +// can progress as normal when recover mode is activated. +// Recover mode is designed to recover from a sequencing +// window expiry when there are ample L1 blocks to eagerly +// progress the l1 origin to. But when the l1 origin is +// close to the tip of the l1 chain, the eagerness would cause +// a delay in unsafe block production while the sequencer waits +// for the next l1 origin to become available. Recover mode +// has since been patched, and the sequencer will not demand the +// next l1 origin until it is actually available. This tests +// protects against a regeression in that behavior. +func TestRecoverModeWhenChainHealthy(gt *testing.T) { + t := devtest.ParallelT(gt) + sys := presets.NewMinimal(t) + tracer := t.Tracer() + ctx := t.Ctx() + + err := sys.L2CL.SetSequencerRecoverMode(true) + require.NoError(t, err) + blockTime := sys.L2Chain.Escape().RollupConfig().BlockTime + numL2Blocks := uint64(20) + waitTime := time.Duration(blockTime*numL2Blocks+5) * time.Second + + num := sys.L2CL.SyncStatus().UnsafeL2.Number + new_num := num + require.Eventually(t, func() bool { + ctx, span := tracer.Start(ctx, "check head") + defer span.End() + + new_num, num = sys.L2CL.SyncStatus().UnsafeL2.Number, new_num + t.Logger().InfoContext(ctx, "unsafe head", "number", new_num, "safe head", sys.L2CL.SyncStatus().SafeL2.Number) + return new_num >= numL2Blocks + }, waitTime, time.Duration(blockTime)*time.Second) +} diff --git a/op-devstack/dsl/l2_cl.go b/op-devstack/dsl/l2_cl.go index f478410f17f..80ee03638a6 100644 --- a/op-devstack/dsl/l2_cl.go +++ b/op-devstack/dsl/l2_cl.go @@ -91,6 +91,10 @@ func (cl *L2CLNode) StopSequencer() common.Hash { return unsafeHead } +func (cl *L2CLNode) SetSequencerRecoverMode(b bool) error { + return cl.inner.RollupAPI().SetRecoverMode(cl.ctx, b) +} + func (cl *L2CLNode) SyncStatus() *eth.SyncStatus { ctx, cancel := context.WithTimeout(cl.ctx, DefaultTimeout) defer cancel() diff --git a/op-e2e/actions/helpers/l2_batcher.go b/op-e2e/actions/helpers/l2_batcher.go index e0d8eac5a28..2e1a941721d 100644 --- a/op-e2e/actions/helpers/l2_batcher.go +++ b/op-e2e/actions/helpers/l2_batcher.go @@ -270,7 +270,7 @@ func (s *L2Batcher) Buffer(t Testing, bufferOpts ...BufferOption) error { } } - s.ActCreateChannel(t, s.rollupCfg.IsDelta(block.Time()), options.channelModifiers...) + s.ActCreateChannel(t, s.rollupCfg.IsDelta(block.Time()) && !s.l2BatcherCfg.ForceSubmitSingularBatch, options.channelModifiers...) if _, err := s.L2ChannelOut.AddBlock(s.rollupCfg, block); err != nil { return err diff --git a/op-e2e/actions/helpers/l2_sequencer.go b/op-e2e/actions/helpers/l2_sequencer.go index 97d8100b1a5..ef3d4e29dfb 100644 --- a/op-e2e/actions/helpers/l2_sequencer.go +++ b/op-e2e/actions/helpers/l2_sequencer.go @@ -40,8 +40,8 @@ func (m *MockL1OriginSelector) FindL1Origin(ctx context.Context, l2Head eth.L2Bl return m.actual.FindL1Origin(ctx, l2Head) } -func (m *MockL1OriginSelector) SetRecoverMode(bool) { - // noop +func (m *MockL1OriginSelector) SetRecoverMode(b bool) { + m.actual.SetRecoverMode(b) } // L2Sequencer is an actor that functions like a rollup node, @@ -98,20 +98,28 @@ func NewL2Sequencer(t Testing, log log.Logger, l1 derive.L1Fetcher, blobSrc deri // ActL2StartBlock starts building of a new L2 block on top of the head func (s *L2Sequencer) ActL2StartBlock(t Testing) { + err := s.ActMaybeL2StartBlock(t) + require.NoError(t, err, "failed to start block building") +} + +// ActMaybeL2StartBlock tries to start building a new L2 block on top of the head +func (s *L2Sequencer) ActMaybeL2StartBlock(t Testing) error { require.NoError(t, s.drainer.Drain()) // can't build when other work is still blocking if !s.L2PipelineIdle { t.InvalidAction("cannot start L2 build when derivation is not idle") - return + return nil } if s.l2Building { t.InvalidAction("already started building L2 block") - return + return nil } s.synchronousEvents.Emit(t.Ctx(), sequencing.SequencerActionEvent{}) - require.NoError(t, s.drainer.DrainUntil(event.Is[engine.BuildStartedEvent], false), - "failed to start block building") - + err := s.drainer.DrainUntil(event.Is[engine.BuildStartedEvent], false) + if err != nil { + return err + } s.l2Building = true + return nil } // ActL2EndBlock completes a new L2 block and applies it to the L2 chain as new canonical unsafe head @@ -272,3 +280,7 @@ func (s *L2Sequencer) ActBuildL2ToInterop(t Testing) { s.ActL2EmptyBlock(t) } } + +func (s *L2Sequencer) ActSetRecoverMode(t Testing, b bool) { + s.sequencer.SetRecoverMode(b) +} diff --git a/op-e2e/actions/helpers/utils.go b/op-e2e/actions/helpers/utils.go index 9fa5d043874..e8dc404ea16 100644 --- a/op-e2e/actions/helpers/utils.go +++ b/op-e2e/actions/helpers/utils.go @@ -15,7 +15,7 @@ func DefaultRollupTestParams() *e2eutils.TestParams { MaxSequencerDrift: 40, SequencerWindowSize: 120, ChannelTimeout: 120, - L1BlockTime: 15, + L1BlockTime: 12, // Many of the action helpers assume a 12s L1 block time AllocType: config.DefaultAllocType, } } diff --git a/op-e2e/actions/proofs/helpers/env.go b/op-e2e/actions/proofs/helpers/env.go index 6872c1d0e1e..d700763d7f7 100644 --- a/op-e2e/actions/proofs/helpers/env.go +++ b/op-e2e/actions/proofs/helpers/env.go @@ -243,15 +243,12 @@ func (env *L2FaultProofEnv) BatchAndMine(t helpers.Testing) { // Returns the L2 Safe Block Reference func (env *L2FaultProofEnv) BatchMineAndSync(t helpers.Testing) eth.L2BlockRef { t.Helper() - id := env.Miner.UnsafeID() env.BatchAndMine(t) env.Sequencer.ActL1HeadSignal(t) env.Sequencer.ActL2PipelineFull(t) // Assertions - syncStatus := env.Sequencer.SyncStatus() - require.Equal(t, syncStatus.UnsafeL2.L1Origin, id, "UnsafeL2.L1Origin should equal L1 Unsafe ID before batch submitted") require.Equal(t, syncStatus.UnsafeL2, syncStatus.SafeL2, "UnsafeL2 should equal SafeL2") return syncStatus.SafeL2 diff --git a/op-e2e/actions/proofs/sequence_window_expiry_test.go b/op-e2e/actions/proofs/sequence_window_expiry_test.go index cb702fe8eb6..cd614afb97a 100644 --- a/op-e2e/actions/proofs/sequence_window_expiry_test.go +++ b/op-e2e/actions/proofs/sequence_window_expiry_test.go @@ -5,18 +5,39 @@ import ( actionsHelpers "github.com/ethereum-optimism/optimism/op-e2e/actions/helpers" "github.com/ethereum-optimism/optimism/op-e2e/actions/proofs/helpers" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils" "github.com/ethereum-optimism/optimism/op-program/client/claim" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" ) -// Run a test that proves a deposit-only block generated due to sequence window expiry. +// Run a test that proves a deposit-only block generated due to sequence window expiry, +// and then recovers the chain using sequencer recover mode. func runSequenceWindowExpireTest(gt *testing.T, testCfg *helpers.TestCfg[any]) { t := actionsHelpers.NewDefaultTesting(gt) - tp := helpers.NewTestParams() - env := helpers.NewL2FaultProofEnv(t, testCfg, tp, helpers.NewBatcherCfg()) + const SEQUENCER_WINDOW_SIZE = 50 // (short, to keep test fast) + tp := helpers.NewTestParams(func(p *e2eutils.TestParams) { + p.SequencerWindowSize = SEQUENCER_WINDOW_SIZE + p.MaxSequencerDrift = 1800 // use 1800 seconds (30 minutes), which is the protocol constant since Fjord + }) + + // It seems more difficult (almost impossible) to recover from sequencing window expiry with span batches, + // since the singular batches within are invalidated _atomically_. + // That is to say, if the oldest batch in the span batch fails the sequencing window check + // (l1 origin + seq window < l1 inclusion) + // All following batches are invalidated / dropped as well. + // https://github.com/ethereum-optimism/optimism/blob/73339162d78a1ebf2daadab01736382eed6f4527/op-node/rollup/derive/batches.go#L96-L100 + // + // If the same blocks were batched with singular batches, the validation rules are different + // https://github.com/ethereum-optimism/optimism/blob/73339162d78a1ebf2daadab01736382eed6f4527/op-node/rollup/derive/batches.go#L83-L86 + // In the case of recover mode, the noTxPool=true condition means autoderviation actually fills + // the gap with identical blocks anyway, meaning the following batches are actually still valid. + bc := helpers.NewBatcherCfg() + bc.ForceSubmitSingularBatch = true - // Mine an empty block for gas estimation purposes. + env := helpers.NewL2FaultProofEnv(t, testCfg, tp, bc) + + // Mine an empty L1 block for gas estimation purposes. env.Miner.ActEmptyBlock(t) // Expire the sequence window by building `SequenceWindow + 1` empty blocks on L1. @@ -24,7 +45,7 @@ func runSequenceWindowExpireTest(gt *testing.T, testCfg *helpers.TestCfg[any]) { env.Alice.L1.ActResetTxOpts(t) env.Alice.ActDeposit(t) - env.Miner.ActL1StartBlock(12)(t) + env.Miner.ActL1StartBlock(tp.L1BlockTime)(t) env.Miner.ActL1IncludeTx(env.Alice.Address())(t) env.Miner.ActL1EndBlock(t) @@ -42,10 +63,107 @@ func runSequenceWindowExpireTest(gt *testing.T, testCfg *helpers.TestCfg[any]) { // Ensure the safe head advanced forcefully. l2SafeHead = env.Engine.L2Chain().CurrentSafeBlock() - require.Greater(t, l2SafeHead.Number.Uint64(), uint64(0)) + require.Greater(t, l2SafeHead.Number.Uint64(), uint64(0), + "The safe head failed to progress after the sequencing window expired (expected deposit-only blocks to be derived).") - // Run the FPP on one of the auto-derived blocks. env.RunFaultProofProgram(t, l2SafeHead.Number.Uint64()/2, testCfg.CheckResult, testCfg.InputParams...) + + // Set recover mode on the sequencer: + env.Sequencer.ActSetRecoverMode(t, true) + // Since recover mode only affects the L2 CL (op-node), + // it won't stop the test environment injecting transactions + // directly into the engine. So we will force the engine + // to ignore such injections if recover mode is enabled. + env.Engine.EngineApi.SetForceEmpty(true) + + // Define "lag" as the difference between the current L1 block number and the safe L2 block's L1 origin number. + computeLag := func() int { + ss := env.Sequencer.SyncStatus() + return int(ss.CurrentL1.Number - ss.SafeL2.L1Origin.Number) + } + + // Define "drift" as the difference between the current L2 block's timestamp and the unsafe L2 block's L1 origin's timestamp. + computeDrift := func() int { + ss := env.Sequencer.SyncStatus() + l2header, err := env.Engine.EthClient().HeaderByHash(t.Ctx(), ss.UnsafeL2.Hash) + require.NoError(t, err) + l1header, err := env.Miner.EthClient().HeaderByHash(t.Ctx(), ss.UnsafeL2.L1Origin.Hash) + require.NoError(t, err) + t.Log("l2header.Time", l2header.Time) + t.Log("l1header.Time", l1header.Time) + return int(l2header.Time) - int(l1header.Time) + } + + // Build both chains and assert the L1 origin catches back up with the tip of the L1 chain. + lag := computeLag() + t.Log("lag", lag) + drift := computeDrift() + t.Log("drift", drift) + require.GreaterOrEqual(t, uint64(lag), tp.SequencerWindowSize, "Lag is less than sequencing window size") + numL1Blocks := 0 + timeout := tp.SequencerWindowSize * 50 + + for numL1Blocks < int(timeout) { + for range 100 * tp.L1BlockTime / env.Sd.RollupCfg.BlockTime { // go at 100x real time + err := env.Sequencer.ActMaybeL2StartBlock(t) + if err != nil { + break + } + env.Bob.L2.ActResetTxOpts(t) + env.Bob.L2.ActMakeTx(t) + env.Engine.ActL2IncludeTx(env.Bob.Address())(t) + // RecoverMode (enabled above) should prevent this + // transaction from being included in the block, which + // is critical for recover mode to work. + env.Sequencer.ActL2EndBlock(t) + drift = computeDrift() + t.Log("drift", drift) + } + env.BatchMineAndSync(t) // Mines 1 block on L1 + numL1Blocks++ + lag = computeLag() + t.Log("lag", lag) + drift = computeDrift() + t.Log("drift", drift) + if lag == 1 { // A lag of 1 is the minimum possible. + break + } + } + + if uint64(numL1Blocks) >= timeout { + t.Fatal("L1 Origin did not catch up to tip within %d L1 blocks (lag is %d)", numL1Blocks, lag) + } else { + t.Logf("L1 Origin caught up to within %d blocks of the tip within %d L1 blocks (sequencing window size %d)", + lag, numL1Blocks, tp.SequencerWindowSize) + } + + switch { + case drift == 0: + t.Fatal("drift is zero, this implies the unsafe l2 head is pinned to the l1 head") + case drift > int(tp.MaxSequencerDrift): + t.Fatal("drift is too high") + default: + t.Log("drift", drift) + } + + // Disable recover mode so we can get some user transactions in again. + env.Sequencer.ActSetRecoverMode(t, false) + env.Engine.EngineApi.SetForceEmpty(false) + l2SafeBefore := env.Sequencer.L2Safe() + env.Sequencer.ActL2StartBlock(t) + env.Bob.L2.ActResetTxOpts(t) + env.Bob.L2.ActMakeTx(t) + env.Engine.ActL2IncludeTx(env.Bob.Address())(t) + env.Sequencer.ActL2EndBlock(t) + env.BatchMineAndSync(t) + l2Safe := env.Sequencer.L2Safe() + require.Equal(t, l2Safe.Number, l2SafeBefore.Number+1, "safe chain did not progress with user transactions") + l2SafeBlock, err := env.Engine.EthClient().BlockByHash(t.Ctx(), l2Safe.Hash) + require.NoError(t, err) + // Assert safe block has at least two transactions + require.GreaterOrEqual(t, len(l2SafeBlock.Transactions()), 2, "safe block did not have at least two transactions") + + env.RunFaultProofProgram(t, l2Safe.Number, testCfg.CheckResult, testCfg.InputParams...) } // Runs a that proves a block in a chain where the batcher opens a channel, the sequence window expires, and then the diff --git a/op-node/rollup/sequencing/origin_selector.go b/op-node/rollup/sequencing/origin_selector.go index c5f23407d76..368226db34e 100644 --- a/op-node/rollup/sequencing/origin_selector.go +++ b/op-node/rollup/sequencing/origin_selector.go @@ -70,80 +70,51 @@ func (los *L1OriginSelector) OnEvent(ctx context.Context, ev event.Event) bool { return true } -// FindL1Origin determines what the next L1 Origin should be. -// The L1 Origin is either the L2 Head's Origin, or the following L1 block -// if the next L2 block's time is greater than or equal to the L2 Head's Origin. -// The origin selection relies purely on block numbers and it is the caller's -// responsibility to detect and handle L1 reorgs. +// FindL1Origin determines what the L1 Origin for the next L2 Block should be. +// It wraps the FindL1OriginOfNextL2Block function and handles caching and network requests. func (los *L1OriginSelector) FindL1Origin(ctx context.Context, l2Head eth.L2BlockRef) (eth.L1BlockRef, error) { + recoverMode := los.recoverMode.Load() + // Get cached values for currentOrigin and nextOrigin currentOrigin, nextOrigin, err := los.CurrentAndNextOrigin(ctx, l2Head) if err != nil { return eth.L1BlockRef{}, err } - - // If the next L2 block time is greater than the next origin block's time, we can choose to - // start building on top of the next origin. Sequencer implementation has some leeway here and - // could decide to continue to build on top of the previous origin until the Sequencer runs out - // of slack. For simplicity, we implement our Sequencer to always start building on the latest - // L1 block when we can. - if nextOrigin != (eth.L1BlockRef{}) && l2Head.Time+los.cfg.BlockTime >= nextOrigin.Time { - return nextOrigin, nil - } - - msd := los.spec.MaxSequencerDrift(currentOrigin.Time) - log := los.log.New("current", currentOrigin, "current_time", currentOrigin.Time, - "l2_head", l2Head, "l2_head_time", l2Head.Time, "max_seq_drift", msd) - - pastSeqDrift := l2Head.Time+los.cfg.BlockTime-currentOrigin.Time > msd - - // If we are not past the max sequencer drift, we can just return the current origin. - if !pastSeqDrift { - return currentOrigin, nil - } - - // Otherwise, we need to find the next L1 origin block in order to continue producing blocks. - log.Warn("Next L2 block time is past the sequencer drift + current origin time") - - if nextOrigin == (eth.L1BlockRef{}) { - fetchCtx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - - // If the next origin is not set, we need to fetch it now. - nextOrigin, err = los.fetch(fetchCtx, currentOrigin.Number+1) - if err != nil { - return eth.L1BlockRef{}, fmt.Errorf("cannot build next L2 block past current L1 origin %s by more than sequencer time drift, and failed to find next L1 origin: %w", currentOrigin, err) + // Try to find the L1 origin given the current data in cache + o, err := los.findL1OriginOfNextL2Block( + l2Head, + currentOrigin, + nextOrigin, + recoverMode) + + // If the cache doesn't have the next origin, but we now + // know we definitely need it, fetch it and try again. + if errors.Is(err, ErrNextL1OriginRequired) { + nextOrigin, err = los.fetch(ctx, currentOrigin.Number+1) + if err == nil || (recoverMode && errors.Is(err, ethereum.NotFound)) { + // If we got the origin, or we are in recover mode and the origin is not found + // (because we recovered the l1 origin up to the l1 tip) + // try again with matchAutoDerivation = false. + return los.findL1OriginOfNextL2Block( + l2Head, + currentOrigin, + nextOrigin, + false) + } else { + return eth.L1BlockRef{}, ErrNextL1OriginRequired } } - - // If the next origin is ahead of the L2 head, we must return the current origin. - if l2Head.Time+los.cfg.BlockTime < nextOrigin.Time { - return currentOrigin, nil - } - - return nextOrigin, nil + return o, err } +// CurrentAndNextOrigin returns the current cached values for the current L1 origin for the supplied l2Head, and its successor. +// It only performs a fetch to L1 if the cache is invalid. +// The cache can be updated asynchronously by other methods on L1OriginSelector. +// The returned currentOrigin should _always_ be non-empty, because it is populated from l2Head whose +// l1Origin is first specified in the rollup.Config.Genesis.L1 and progressed to non-empty values thereafter. func (los *L1OriginSelector) CurrentAndNextOrigin(ctx context.Context, l2Head eth.L2BlockRef) (eth.L1BlockRef, eth.L1BlockRef, error) { los.mu.Lock() defer los.mu.Unlock() - if los.recoverMode.Load() { - currentOrigin, err := los.l1.L1BlockRefByHash(ctx, l2Head.L1Origin.Hash) - if err != nil { - return eth.L1BlockRef{}, eth.L1BlockRef{}, - derive.NewTemporaryError(fmt.Errorf("failed to fetch current L1 origin: %w", err)) - } - los.currentOrigin = currentOrigin - nextOrigin, err := los.l1.L1BlockRefByNumber(ctx, currentOrigin.Number+1) - if err != nil { - return eth.L1BlockRef{}, eth.L1BlockRef{}, - derive.NewTemporaryError(fmt.Errorf("failed to fetch next L1 origin: %w", err)) - } - los.nextOrigin = nextOrigin - los.log.Info("origin selector in recover mode", "current_origin", los.currentOrigin, "next_origin", los.nextOrigin, "l2_head", l2Head) - return los.currentOrigin, los.nextOrigin, nil - } - if l2Head.L1Origin == los.currentOrigin.ID() { // Most likely outcome: the L2 head is still on the current origin. } else if l2Head.L1Origin == los.nextOrigin.ID() { @@ -238,3 +209,69 @@ func (los *L1OriginSelector) reset() { los.currentOrigin = eth.L1BlockRef{} los.nextOrigin = eth.L1BlockRef{} } + +var ( + ErrInvalidL1Origin = fmt.Errorf("origin-selector: currentL1Origin.Hash != l2Head.L1Origin.Hash") + ErrNextL1OriginOrphaned = fmt.Errorf("origin-selector: nextL1Origin.ParentHash != currentL1Origin.Hash") + ErrNextL1OriginRequired = fmt.Errorf("origin-selector: nextL1Origin not supplied but required to satisfy constraints") +) + +// FindL1OriginOfNextL2Block finds the L1 origin of the next L2 block. +// It returns an error if there is no way to build a block satisfying +// derivation constraints with the supplied data. +// You can pass an empty nextL1Origin if it is not yet available +// removing the need for block building to wait on the result of network calls. +// This method is designed to be pure (it only reads the cfg property of the receiver) +// and should not have any side effects. +func (los *L1OriginSelector) findL1OriginOfNextL2Block( + l2Head eth.L2BlockRef, + currentL1Origin eth.L1BlockRef, nextL1Origin eth.L1BlockRef, + matchAutoDerivation bool) (eth.L1BlockRef, error) { + + if (currentL1Origin == eth.L1BlockRef{}) { + // This would indicate a programming error, since the currentL1Origin + // should _always_ be available. + // The first value (for block 1) is specified in rollup.Config.Genesis.L1 + // and it is then only updated to non-empty values. + panic("origin-selector: currentL1Origin is empty") + } + if l2Head.L1Origin.Hash != currentL1Origin.Hash { + return currentL1Origin, ErrInvalidL1Origin + } + if (nextL1Origin != eth.L1BlockRef{} && nextL1Origin.ParentHash != currentL1Origin.Hash) { + return nextL1Origin, ErrNextL1OriginOrphaned + } + + l2BlockTime := los.cfg.BlockTime + maxDrift := rollup.NewChainSpec(los.cfg).MaxSequencerDrift(currentL1Origin.Time) + nextL2BlockTime := l2Head.Time + l2BlockTime + driftCurrent := int64(nextL2BlockTime) - int64(currentL1Origin.Time) + + if (nextL1Origin == eth.L1BlockRef{}) { + if matchAutoDerivation { + // See https://github.com/ethereum-optimism/optimism/blob/ce9fa62d0c0325304fc37d91d87aa2e16a7f8356/op-node/rollup/derive/base_batch_stage.go#L186-L205 + // We need the next L1 origin to decide whether we can eagerly adopt it. + // NOTE: This can cause unsafe block production to slow to the rate of L1 block production, if the L1 origin is caught up to the L1 Head. + // Code higher up the call stack should ensure that matchAutoDerivation is false under such conditions. + return eth.L1BlockRef{}, ErrNextL1OriginRequired + } else { + // If we don't yet have the nextL1Origin, stick with the current L1 origin unless doing so would exceed the maximum drift. + if driftCurrent > int64(maxDrift) { + // Return an error so the caller knows it needs to fetch the next l1 origin now. + return eth.L1BlockRef{}, fmt.Errorf("%w: drift of next L2 block would exceed maximum %d unless nextl1Origin is adopted", ErrNextL1OriginRequired, maxDrift) + } + return currentL1Origin, nil + } + } + + driftNext := int64(nextL2BlockTime) - int64(nextL1Origin.Time) + + // Progress to l1OriginChild if doing so would respect the requirement + // that L2 blocks cannot point to a future L1 block (negative drift). + if driftNext >= 0 { + return nextL1Origin, nil + } else { + // If we cannot adopt the l1OriginChild, use the current l1 origin. + return currentL1Origin, nil + } +} diff --git a/op-node/rollup/sequencing/origin_selector_test.go b/op-node/rollup/sequencing/origin_selector_test.go index be8a0dc7625..5ab947df69f 100644 --- a/op-node/rollup/sequencing/origin_selector_test.go +++ b/op-node/rollup/sequencing/origin_selector_test.go @@ -7,7 +7,6 @@ import ( "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup/confdepth" - "github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-node/rollup/engine" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/testlog" @@ -208,18 +207,14 @@ func TestOriginSelectorAdvances(t *testing.T) { } if recoverMode { - // In recovery mode (only) we make two RPC calls. + // In recovery mode (only) we make an RPC call to find the next origin. // First, cover the case where the nextOrigin // is not ready yet by simulating a NotFound error. - l1.ExpectL1BlockRefByHash(c.Hash, c, nil) l1.ExpectL1BlockRefByNumber(d.Number, eth.BlockRef{}, ethereum.NotFound) - _, err := s.FindL1Origin(ctx, l2Head) - require.ErrorIs(t, err, derive.ErrTemporary) - require.ErrorIs(t, err, ethereum.NotFound) + requireL1OriginAt(l2Head, c) // Now, simulate the block being ready, and ensure // that the origin advances to the next block. - l1.ExpectL1BlockRefByHash(c.Hash, c, nil) l1.ExpectL1BlockRefByNumber(d.Number, d, nil) requireL1OriginAt(l2Head, d) } else { @@ -344,7 +339,7 @@ func TestOriginSelectorFetchesNextOrigin(t *testing.T) { // // There are 3 blocks [a, b, c]. After advancing to b, a reorg is simulated // where b is reorged and replaced by providing a `c` next that has a different parent hash. -// The origin should still provide c as the next origin so upstream services can detect the reorg. +// A sentinel error should be returned. func TestOriginSelectorHandlesReorg(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -384,6 +379,11 @@ func TestOriginSelectorHandlesReorg(t *testing.T) { require.Equal(t, l1ref, next) } + requireFindL1OriginError := func(e error) { + _, err := s.FindL1Origin(ctx, l2Head) + require.ErrorIs(t, err, e) + } + requireFindl1OriginEqual(a) // Selection is stable until the next origin is fetched @@ -413,9 +413,8 @@ func TestOriginSelectorHandlesReorg(t *testing.T) { handled = s.OnEvent(context.Background(), engine.ForkchoiceUpdateEvent{UnsafeL2Head: l2Head}) require.True(t, handled) - // The next origin should be `c` now, otherwise an upstream service cannot detect the reorg - // and the origin will be stuck at `b` - requireFindl1OriginEqual(c) + // We shuold get a sentinel error + requireFindL1OriginError(ErrNextL1OriginOrphaned) } // TestOriginSelectorRespectsOriginTiming ensures that the origin selector @@ -593,7 +592,7 @@ func TestOriginSelectorStrictConfDepth(t *testing.T) { s := NewL1OriginSelector(ctx, log, cfg, confDepthL1) _, err := s.FindL1Origin(ctx, l2Head) - require.ErrorContains(t, err, "sequencer time drift") + require.ErrorIs(t, err, ErrNextL1OriginRequired) } func u64ptr(n uint64) *uint64 { @@ -779,11 +778,11 @@ func TestOriginSelectorHandlesLateL1Blocks(t *testing.T) { s := NewL1OriginSelector(ctx, log, cfg, confDepthL1) _, err := s.FindL1Origin(ctx, l2Head) - require.ErrorContains(t, err, "sequencer time drift") + require.ErrorIs(t, err, ErrNextL1OriginRequired) l1Head = c _, err = s.FindL1Origin(ctx, l2Head) - require.ErrorContains(t, err, "sequencer time drift") + require.ErrorIs(t, err, ErrNextL1OriginRequired) l1Head = d next, err := s.FindL1Origin(ctx, l2Head) @@ -811,3 +810,196 @@ func TestOriginSelectorMiscEvent(t *testing.T) { handled := s.OnEvent(context.Background(), rollup.L1TemporaryErrorEvent{}) require.False(t, handled) } + +func TestFindL1OriginOfNextL2Block(t *testing.T) { + cfg := &rollup.Config{ + MaxSequencerDrift: 1800, // Use Fjord constant value + BlockTime: 2, + } + + los := NewL1OriginSelector(context.Background(), testlog.Logger(t, log.LevelDebug), cfg, &testutils.MockL1Source{}) + + require.Panics(t, func() { + _, _ = los.findL1OriginOfNextL2Block( + eth.L2BlockRef{}, + eth.L1BlockRef{}, + eth.L1BlockRef{}, + false) + }) + + type testCase struct { + name string + l2Head eth.L2BlockRef + currentL1Origin eth.L1BlockRef + nextL1Origin eth.L1BlockRef + matchAutoderivation bool + expectedResult eth.L1BlockRef + expectedError error + } + + tcs := []testCase{} + + // Scenarios with valid data, no drift concerns + // but availability of next l1 origin is modulated. + // + // L1 chain: a100(1200) <- [ a101(1212) ] + // /\ + // L2 chain \_ b1000(1220) + a100 := eth.L1BlockRef{ + Number: 100, + Hash: common.Hash{'a', '1', '0', '0'}, + Time: 1200, + } + a101 := eth.L1BlockRef{ + Number: 101, + ParentHash: a100.Hash, + Hash: common.Hash{'a', '0', '0'}, + Time: 1212, + } + b1000 := eth.L2BlockRef{ + Number: 1000, + Hash: common.Hash{'b', '1', '0', '0', '0'}, + L1Origin: a100.ID(), + Time: 1220, + } + + tcs = append(tcs, + testCase{ + name: "normal operation, progress because we can", + l2Head: b1000, + currentL1Origin: a100, + nextL1Origin: a101, + expectedResult: a101, + }, + testCase{ + name: "recover mode, progress because we can", + l2Head: b1000, + currentL1Origin: a100, + nextL1Origin: a101, + expectedResult: a101, + matchAutoderivation: true, + }, + testCase{ + name: "normal operation, don't need to progress", + l2Head: b1000, + currentL1Origin: a100, + expectedResult: a100, + }, + testCase{ + name: "recover mode, need to progress but can't", + l2Head: b1000, + currentL1Origin: a100, + matchAutoderivation: true, + expectedError: ErrNextL1OriginRequired, + }, + ) + + // Bad input data / reorg scenarios + // L1 chain: c100(1200) <-[x]- c101(1212) + // /\ + // L2 chain \_[x]- d/e1000(1220) + c100 := eth.L1BlockRef{ + Number: 100, + Hash: common.Hash{'a', '1', '0', '0'}, + Time: 1200, + } + c101 := eth.L1BlockRef{ + Number: 101, + ParentHash: common.Hash{}, // does not point to c100 + Hash: common.Hash{'a', '0', '0'}, + Time: 1212, + } + d1000 := eth.L2BlockRef{ + Number: 1000, + L1Origin: c100.ID(), + Hash: common.Hash{'d', '1', '0', '0', '0'}, + Time: 1220, + } + e1000 := eth.L2BlockRef{ + Number: 1000, + L1Origin: eth.BlockID{}, // does not point to c100 + Hash: common.Hash{'d', '1', '0', '0', '0'}, + Time: 1220, + } + tcs = append(tcs, + testCase{ + name: "L1 reorg", + currentL1Origin: c100, + nextL1Origin: c101, + l2Head: d1000, + expectedResult: c101, + expectedError: ErrNextL1OriginOrphaned, + }, + testCase{ + name: "Invalid l1 origin", + currentL1Origin: c100, + nextL1Origin: c101, + l2Head: e1000, + expectedResult: c100, + expectedError: ErrInvalidL1Origin, + }) + + // Drift at maximum, + // L1 chain: a100(1200) <- [ a101(1212) ] + // /\ + // L2 chain \_ f1000(3000) + f1000 := eth.L2BlockRef{ + Number: 1000, + L1Origin: a100.ID(), + Hash: common.Hash{'f', '1', '0', '0', '0'}, + Time: 3000, + } + tcs = append(tcs, + testCase{ + name: "Drift at maximum, nextL1Origin available", + currentL1Origin: a100, + nextL1Origin: a101, + l2Head: f1000, + expectedResult: a101, + }, + testCase{ + name: "Drift at maximum, nextL1Origin unavailable", + currentL1Origin: a100, + l2Head: f1000, + expectedError: ErrNextL1OriginRequired, + }) + + // Negative drift, + // L1 chain: a100(1200) <- a101(1212) + // /\ + // L2 chain \_ g1000(1200) + // Current drift is 0 + // adopting the nextLOrigin would make it negative (add 2 subtract 12) + g1000 := eth.L2BlockRef{ + Number: 1000, + L1Origin: a100.ID(), + Hash: common.Hash{'g', '1', '0', '0', '0'}, + Time: 1200, + } + tcs = append(tcs, + testCase{ + name: "Negative drift", + currentL1Origin: a100, + nextL1Origin: a101, + l2Head: g1000, + expectedResult: a100, + }) + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + result, err := los.findL1OriginOfNextL2Block( + tc.l2Head, + tc.currentL1Origin, + tc.nextL1Origin, + tc.matchAutoderivation) + if tc.expectedError != nil { + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + if result != tc.expectedResult { + t.Errorf("expected result %v, got %v", tc.expectedResult, result) + } + }) + } +} diff --git a/op-node/rollup/sequencing/sequencer.go b/op-node/rollup/sequencing/sequencer.go index 9e2b348c93b..76e743af1dd 100644 --- a/op-node/rollup/sequencing/sequencer.go +++ b/op-node/rollup/sequencing/sequencer.go @@ -503,21 +503,23 @@ func (d *Sequencer) startBuildingBlock() { // Figure out which L1 origin block we're going to be building on top of. l1Origin, err := d.l1OriginSelector.FindL1Origin(ctx, l2Head) - if err != nil { - d.nextAction = d.timeNow().Add(time.Second) - d.nextActionOK = d.active.Load() - d.log.Error("Error finding next L1 Origin", "err", err) - d.emitter.Emit(d.ctx, rollup.L1TemporaryErrorEvent{Err: err}) - return - } - - if !(l2Head.L1Origin.Hash == l1Origin.ParentHash || l2Head.L1Origin.Hash == l1Origin.Hash) { + switch { + case err == nil: + case errors.Is(err, ErrInvalidL1Origin), errors.Is(err, ErrNextL1OriginOrphaned): d.metrics.RecordSequencerInconsistentL1Origin(l2Head.L1Origin, l1Origin.ID()) d.emitter.Emit(d.ctx, rollup.ResetEvent{ Err: fmt.Errorf("cannot build new L2 block with L1 origin %s (parent L1 %s) on current L2 head %s with L1 origin %s", l1Origin, l1Origin.ParentHash, l2Head, l2Head.L1Origin), }) return + case errors.Is(err, ErrNextL1OriginRequired): + fallthrough + default: + d.nextAction = d.timeNow().Add(time.Second) + d.nextActionOK = d.active.Load() + d.log.Error("Error finding next L1 Origin", "err", err) + d.emitter.Emit(d.ctx, rollup.L1TemporaryErrorEvent{Err: err}) + return } d.log.Info("Started sequencing new block", "parent", l2Head, "l1Origin", l1Origin) From 388eea1534ef7a4368383ed2a980be0dce1ec988 Mon Sep 17 00:00:00 2001 From: niha <205694301+0xniha@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:09:46 -0300 Subject: [PATCH 09/13] feat: add ReadSuperchainDeployment support for opcm v2 (#18520) * feat: add readsuperchaindeployment for opcm v2 * fix: argument in PopulateSuperchainState & add docs (#734) * fix: set superchainConfig to zero for populateSuperchainState with opcmV1 * docs: add clarification of opcmv1 deprecation * fix: remove unused opcmV2Enabled var * test: opcm2 add read sup chain tests (#754) * chore: expand comment on ReadSuperchainDeployment.s.sol and init.go * test: add additional test cases for PopulateSuperchainState and InitLiveStrategy * fix: adds the right number of hex digits to OPCMV2DevFlag in devfeatures.go (#755) * fix: remove branching in init.go & link TODO issue (#760) * docs: link todo issue * refactor: remove branching in init.go WIP * fix: remove unused opcmv2 feature flag * fix: remove console logs * fix: set original timeout, fix isOpcmV2 version and add populateSuperchainState tests * fix: set original timeout * fix: isOpcmV2 version * feat: add test cases for populateSuperchainState and unify tests * fix: ocpm version comments --------- Co-authored-by: Flux <175354924+0xiamflux@users.noreply.github.com> Co-authored-by: OneTony Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> --- .../opcm/read_superchain_deployment.go | 20 +- op-deployer/pkg/deployer/pipeline/init.go | 35 +- .../pkg/deployer/pipeline/init_test.go | 610 +++++++++++++++++- .../deploy/ReadSuperchainDeployment.s.sol | 84 ++- 4 files changed, 690 insertions(+), 59 deletions(-) diff --git a/op-deployer/pkg/deployer/opcm/read_superchain_deployment.go b/op-deployer/pkg/deployer/opcm/read_superchain_deployment.go index add97bfe4ad..e5f60ab0eb3 100644 --- a/op-deployer/pkg/deployer/opcm/read_superchain_deployment.go +++ b/op-deployer/pkg/deployer/opcm/read_superchain_deployment.go @@ -6,21 +6,23 @@ import ( ) type ReadSuperchainDeploymentInput struct { - OPCMAddress common.Address `abi:"opcmAddress"` + OPCMAddress common.Address `abi:"opcmAddress"` // TODO(#18612): Remove OPCMAddress field when OPCMv1 gets deprecated + SuperchainConfigProxy common.Address `abi:"superchainConfigProxy"` } type ReadSuperchainDeploymentOutput struct { - ProtocolVersionsImpl common.Address - ProtocolVersionsProxy common.Address - SuperchainConfigImpl common.Address - SuperchainConfigProxy common.Address - SuperchainProxyAdmin common.Address - - Guardian common.Address + // TODO(#18612): Remove ProtocolVersions fields when OPCMv1 gets deprecated + ProtocolVersionsImpl common.Address + ProtocolVersionsProxy common.Address ProtocolVersionsOwner common.Address - SuperchainProxyAdminOwner common.Address RecommendedProtocolVersion [32]byte RequiredProtocolVersion [32]byte + + SuperchainConfigImpl common.Address + SuperchainConfigProxy common.Address + SuperchainProxyAdmin common.Address + Guardian common.Address + SuperchainProxyAdminOwner common.Address } type ReadSuperchainDeploymentScript script.DeployScriptWithOutput[ReadSuperchainDeploymentInput, ReadSuperchainDeploymentOutput] diff --git a/op-deployer/pkg/deployer/pipeline/init.go b/op-deployer/pkg/deployer/pipeline/init.go index f17fe61c00c..00e26709df3 100644 --- a/op-deployer/pkg/deployer/pipeline/init.go +++ b/op-deployer/pkg/deployer/pipeline/init.go @@ -27,25 +27,37 @@ func InitLiveStrategy(ctx context.Context, env *Env, intent *state.Intent, st *s } hasPredeployedOPCM := intent.OPCMAddress != nil + hasSuperchainConfigProxy := intent.SuperchainConfigProxy != nil - if hasPredeployedOPCM { - if intent.SuperchainConfigProxy != nil { - return fmt.Errorf("cannot set superchain config proxy for predeployed OPCM") + if hasPredeployedOPCM || hasSuperchainConfigProxy { + if intent.SuperchainRoles != nil { + return fmt.Errorf("cannot set superchain roles when using predeployed OPCM or SuperchainConfig") } - if intent.SuperchainRoles != nil { - return fmt.Errorf("cannot set superchain roles for predeployed OPCM") + opcmAddr := common.Address{} + if hasPredeployedOPCM { + opcmAddr = *intent.OPCMAddress } - superDeployment, superRoles, err := PopulateSuperchainState(env.L1ScriptHost, *intent.OPCMAddress) + superchainConfigAddr := common.Address{} + if hasSuperchainConfigProxy { + superchainConfigAddr = *intent.SuperchainConfigProxy + } + + // The ReadSuperchainDeployment script (packages/contracts-bedrock/scripts/deploy/ReadSuperchainDeployment.s.sol) + // uses the OPCM's semver version (>= 7.0.0 indicates v2) to determine how to populate the superchain state: + // - OPCMv1 (< 7.0.0): Queries the OPCM contract to get SuperchainConfig and ProtocolVersions + // - OPCMv2 (>= 7.0.0): Uses the provided SuperchainConfigProxy address; ProtocolVersions is deprecated + superDeployment, superRoles, err := PopulateSuperchainState(env.L1ScriptHost, opcmAddr, superchainConfigAddr) if err != nil { return fmt.Errorf("error populating superchain state: %w", err) } st.SuperchainDeployment = superDeployment st.SuperchainRoles = superRoles - if st.ImplementationsDeployment == nil { + + if hasPredeployedOPCM && st.ImplementationsDeployment == nil { st.ImplementationsDeployment = &addresses.ImplementationsContracts{ - OpcmImpl: *intent.OPCMAddress, + OpcmImpl: opcmAddr, } } } @@ -125,14 +137,17 @@ func immutableErr(field string, was, is any) error { return fmt.Errorf("%s is immutable: was %v, is %v", field, was, is) } -func PopulateSuperchainState(host *script.Host, opcmAddr common.Address) (*addresses.SuperchainContracts, *addresses.SuperchainRoles, error) { +// TODO(#18612): Remove OPCMAddress field when OPCMv1 gets deprecated +// TODO(#18612): Remove ProtocolVersions fields when OPCMv1 gets deprecated +func PopulateSuperchainState(host *script.Host, opcmAddr common.Address, superchainConfigProxy common.Address) (*addresses.SuperchainContracts, *addresses.SuperchainRoles, error) { readScript, err := opcm.NewReadSuperchainDeploymentScript(host) if err != nil { return nil, nil, fmt.Errorf("error generating read superchain deployment script: %w", err) } out, err := readScript.Run(opcm.ReadSuperchainDeploymentInput{ - OPCMAddress: opcmAddr, + OPCMAddress: opcmAddr, + SuperchainConfigProxy: superchainConfigProxy, }) if err != nil { return nil, nil, fmt.Errorf("error reading superchain deployment: %w", err) diff --git a/op-deployer/pkg/deployer/pipeline/init_test.go b/op-deployer/pkg/deployer/pipeline/init_test.go index 0a3df1b37b9..c08b4c57a7e 100644 --- a/op-deployer/pkg/deployer/pipeline/init_test.go +++ b/op-deployer/pkg/deployer/pipeline/init_test.go @@ -236,18 +236,600 @@ func TestPopulateSuperchainState(t *testing.T) { superchain, err := standard.SuperchainFor(11155111) require.NoError(t, err) opcmAddr := l1Versions["op-contracts/v2.0.0-rc.1"].OPContractsManager.Address - dep, roles, err := PopulateSuperchainState(host, common.Address(*opcmAddr)) - require.NoError(t, err) - require.Equal(t, addresses.SuperchainContracts{ - SuperchainProxyAdminImpl: common.HexToAddress("0x189aBAAaa82DfC015A588A7dbaD6F13b1D3485Bc"), - SuperchainConfigProxy: superchain.SuperchainConfigAddr, - SuperchainConfigImpl: common.HexToAddress("0x4da82a327773965b8d4D85Fa3dB8249b387458E7"), - ProtocolVersionsProxy: superchain.ProtocolVersionsAddr, - ProtocolVersionsImpl: common.HexToAddress("0x37E15e4d6DFFa9e5E320Ee1eC036922E563CB76C"), - }, *dep) - require.Equal(t, addresses.SuperchainRoles{ - SuperchainProxyAdminOwner: common.HexToAddress("0x1Eb2fFc903729a0F03966B917003800b145F56E2"), - ProtocolVersionsOwner: common.HexToAddress("0xfd1D2e729aE8eEe2E146c033bf4400fE75284301"), - SuperchainGuardian: common.HexToAddress("0x7a50f00e8D05b95F98fE38d8BeE366a7324dCf7E"), - }, *roles) + + t.Run("valid OPCM address only", func(t *testing.T) { + dep, roles, err := PopulateSuperchainState(host, common.Address(*opcmAddr), common.Address{}) + require.NoError(t, err) + require.Equal(t, addresses.SuperchainContracts{ + SuperchainProxyAdminImpl: common.HexToAddress("0x189aBAAaa82DfC015A588A7dbaD6F13b1D3485Bc"), + SuperchainConfigProxy: superchain.SuperchainConfigAddr, + SuperchainConfigImpl: common.HexToAddress("0x4da82a327773965b8d4D85Fa3dB8249b387458E7"), + ProtocolVersionsProxy: superchain.ProtocolVersionsAddr, + ProtocolVersionsImpl: common.HexToAddress("0x37E15e4d6DFFa9e5E320Ee1eC036922E563CB76C"), + }, *dep) + require.Equal(t, addresses.SuperchainRoles{ + SuperchainProxyAdminOwner: common.HexToAddress("0x1Eb2fFc903729a0F03966B917003800b145F56E2"), + ProtocolVersionsOwner: common.HexToAddress("0xfd1D2e729aE8eEe2E146c033bf4400fE75284301"), + SuperchainGuardian: common.HexToAddress("0x7a50f00e8D05b95F98fE38d8BeE366a7324dCf7E"), + }, *roles) + }) + + t.Run("OPCM address with SuperchainConfigProxy", func(t *testing.T) { + // When both are provided and OPCM version < 7.0.0, the script uses v1 flow + // The SuperchainConfigProxy parameter is ignored in v1 flow + dep, roles, err := PopulateSuperchainState(host, common.Address(*opcmAddr), superchain.SuperchainConfigAddr) + require.NoError(t, err) + require.NotNil(t, dep) + require.NotNil(t, roles) + + // For OPCMv1, ProtocolVersions should be populated (read from OPCM) + require.NotEqual(t, common.Address{}, dep.ProtocolVersionsProxy, "ProtocolVersionsProxy should be populated for v1") + require.NotEqual(t, common.Address{}, dep.ProtocolVersionsImpl, "ProtocolVersionsImpl should be populated for v1") + require.NotEqual(t, common.Address{}, roles.ProtocolVersionsOwner, "ProtocolVersionsOwner should be populated for v1") + + // Verify that values match what OPCM returns (not the SuperchainConfigProxy parameter) + require.Equal(t, superchain.SuperchainConfigAddr, dep.SuperchainConfigProxy) + require.Equal(t, superchain.ProtocolVersionsAddr, dep.ProtocolVersionsProxy) + }) + + t.Run("invalid OPCM address", func(t *testing.T) { + // Use an invalid address (non-existent contract) + invalidOpcmAddr := common.HexToAddress("0x1234567890123456789012345678901234567890") + dep, roles, err := PopulateSuperchainState(host, invalidOpcmAddr, common.Address{}) + require.Error(t, err) + require.Nil(t, dep) + require.Nil(t, roles) + require.Contains(t, err.Error(), "error reading superchain deployment") + }) + + t.Run("output mapping validation", func(t *testing.T) { + dep, roles, err := PopulateSuperchainState(host, common.Address(*opcmAddr), common.Address{}) + require.NoError(t, err) + require.NotNil(t, dep) + require.NotNil(t, roles) + + // Verify all SuperchainContracts fields are populated correctly + require.NotEqual(t, common.Address{}, dep.SuperchainProxyAdminImpl, "SuperchainProxyAdminImpl should be populated") + require.NotEqual(t, common.Address{}, dep.SuperchainConfigProxy, "SuperchainConfigProxy should be populated") + require.NotEqual(t, common.Address{}, dep.SuperchainConfigImpl, "SuperchainConfigImpl should be populated") + require.NotEqual(t, common.Address{}, dep.ProtocolVersionsProxy, "ProtocolVersionsProxy should be populated for v1") + require.NotEqual(t, common.Address{}, dep.ProtocolVersionsImpl, "ProtocolVersionsImpl should be populated for v1") + + // Verify implementations are different from proxies + require.NotEqual(t, dep.SuperchainConfigImpl, dep.SuperchainConfigProxy, "SuperchainConfigImpl should differ from proxy") + require.NotEqual(t, dep.ProtocolVersionsImpl, dep.ProtocolVersionsProxy, "ProtocolVersionsImpl should differ from proxy") + + // Verify all SuperchainRoles fields are populated correctly + require.NotEqual(t, common.Address{}, roles.SuperchainProxyAdminOwner, "SuperchainProxyAdminOwner should be populated") + require.NotEqual(t, common.Address{}, roles.ProtocolVersionsOwner, "ProtocolVersionsOwner should be populated for v1") + require.NotEqual(t, common.Address{}, roles.SuperchainGuardian, "SuperchainGuardian should be populated") + + // Verify expected values match + require.Equal(t, superchain.SuperchainConfigAddr, dep.SuperchainConfigProxy) + require.Equal(t, superchain.ProtocolVersionsAddr, dep.ProtocolVersionsProxy) + }) +} + +// TestPopulateSuperchainState_OPCMV2 validates that PopulateSuperchainState handles the OPCM v2 flow, where only a SuperchainConfigProxy +// is provided. This test uses a forked script host configured to a pinned Sepolia block to guarantee deterministic results. +// It asserts that returned roles and addresses are correct for the superchain config under OPCM v2, and that ProtocolVersions +// contract fields—which are not present in OPCM v2—are zeroed out as expected. +func TestPopulateSuperchainState_OPCMV2(t *testing.T) { + t.Parallel() + + rpcURL := os.Getenv("SEPOLIA_RPC_URL") + require.NotEmpty(t, rpcURL, "SEPOLIA_RPC_URL must be set") + + lgr := testlog.Logger(t, slog.LevelInfo) + retryProxy := devnet.NewRetryProxy(lgr, rpcURL) + require.NoError(t, retryProxy.Start()) + t.Cleanup(func() { + require.NoError(t, retryProxy.Stop()) + }) + + rpcClient, err := rpc.Dial(retryProxy.Endpoint()) + require.NoError(t, err) + + _, afacts := testutil.LocalArtifacts(t) + host, err := env.ForkedScriptHost( + broadcaster.NoopBroadcaster(), + testlog.Logger(t, log.LevelInfo), + common.Address{'D'}, + afacts, + rpcClient, + // corresponds to the latest block on sepolia as of 04/30/2025. used to prevent config drift on sepolia + // from failing this test + big.NewInt(8227159), + ) + require.NoError(t, err) + + superchain, err := standard.SuperchainFor(11155111) + require.NoError(t, err) + + t.Run("SuperchainConfigProxy only", func(t *testing.T) { + // opcmAddr is set to 0, all config is provided in the superchainConfigProxy + dep, roles, err := PopulateSuperchainState(host, common.Address{}, superchain.SuperchainConfigAddr) + require.NoError(t, err) + + require.Equal(t, addresses.SuperchainContracts{ + SuperchainProxyAdminImpl: common.HexToAddress("0x189aBAAaa82DfC015A588A7dbaD6F13b1D3485Bc"), + SuperchainConfigProxy: superchain.SuperchainConfigAddr, + SuperchainConfigImpl: common.HexToAddress("0x4da82a327773965b8d4D85Fa3dB8249b387458E7"), + // TODO(#18612): Remove ProtocolVersions fields when OPCMv1 gets deprecated + ProtocolVersionsProxy: common.Address{}, + ProtocolVersionsImpl: common.Address{}, + }, *dep) + require.Equal(t, addresses.SuperchainRoles{ + SuperchainProxyAdminOwner: common.HexToAddress("0x1Eb2fFc903729a0F03966B917003800b145F56E2"), + // TODO(#18612): Remove ProtocolVersions fields when OPCMv1 gets deprecated + ProtocolVersionsOwner: common.Address{}, + SuperchainGuardian: common.HexToAddress("0x7a50f00e8D05b95F98fE38d8BeE366a7324dCf7E"), + }, *roles) + }) + + t.Run("both addresses zero", func(t *testing.T) { + // When both are zero, the script detects OPCMv2 flow (because opcmAddr == 0) + // but then requires SuperchainConfigProxy to be set, so it should error + dep, roles, err := PopulateSuperchainState(host, common.Address{}, common.Address{}) + require.Error(t, err) + require.Nil(t, dep) + require.Nil(t, roles) + require.Contains(t, err.Error(), "superchainConfigProxy required for OPCM v2") + }) + + t.Run("invalid SuperchainConfigProxy", func(t *testing.T) { + // Use an invalid address (non-existent contract) + invalidSuperchainConfigProxy := common.HexToAddress("0x1234567890123456789012345678901234567890") + dep, roles, err := PopulateSuperchainState(host, common.Address{}, invalidSuperchainConfigProxy) + require.Error(t, err) + require.Nil(t, dep) + require.Nil(t, roles) + require.Contains(t, err.Error(), "error reading superchain deployment") + }) + + t.Run("output mapping validation", func(t *testing.T) { + dep, roles, err := PopulateSuperchainState(host, common.Address{}, superchain.SuperchainConfigAddr) + require.NoError(t, err) + require.NotNil(t, dep) + require.NotNil(t, roles) + + // Verify SuperchainConfig fields are populated + require.NotEqual(t, common.Address{}, dep.SuperchainProxyAdminImpl, "SuperchainProxyAdminImpl should be populated") + require.NotEqual(t, common.Address{}, dep.SuperchainConfigProxy, "SuperchainConfigProxy should be populated") + require.NotEqual(t, common.Address{}, dep.SuperchainConfigImpl, "SuperchainConfigImpl should be populated") + require.NotEqual(t, dep.SuperchainConfigImpl, dep.SuperchainConfigProxy, "SuperchainConfigImpl should differ from proxy") + + // Verify ProtocolVersions fields are zeroed for v2 + require.Equal(t, common.Address{}, dep.ProtocolVersionsProxy, "ProtocolVersionsProxy should be zero for v2") + require.Equal(t, common.Address{}, dep.ProtocolVersionsImpl, "ProtocolVersionsImpl should be zero for v2") + + // Verify SuperchainRoles fields are populated correctly + require.NotEqual(t, common.Address{}, roles.SuperchainProxyAdminOwner, "SuperchainProxyAdminOwner should be populated") + require.NotEqual(t, common.Address{}, roles.SuperchainGuardian, "SuperchainGuardian should be populated") + + // Verify ProtocolVersionsOwner is zeroed for v2 + require.Equal(t, common.Address{}, roles.ProtocolVersionsOwner, "ProtocolVersionsOwner should be zero for v2") + + // Verify expected values match + require.Equal(t, superchain.SuperchainConfigAddr, dep.SuperchainConfigProxy) + }) +} + +// Validates the OPCM v2 flow in InitLiveStrategy +// when SuperchainConfigProxy is provided and opcmV2Enabled is true. +func TestInitLiveStrategy_OPCMV2WithSuperchainConfigProxy(t *testing.T) { + t.Parallel() + + rpcURL := os.Getenv("SEPOLIA_RPC_URL") + require.NotEmpty(t, rpcURL, "SEPOLIA_RPC_URL must be set") + + lgr := testlog.Logger(t, slog.LevelInfo) + retryProxy := devnet.NewRetryProxy(lgr, rpcURL) + require.NoError(t, retryProxy.Start()) + t.Cleanup(func() { + require.NoError(t, retryProxy.Stop()) + }) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + rpcClient, err := rpc.Dial(retryProxy.Endpoint()) + require.NoError(t, err) + client := ethclient.NewClient(rpcClient) + + l1ChainID := uint64(11155111) + superchain, err := standard.SuperchainFor(l1ChainID) + require.NoError(t, err) + + _, afacts := testutil.LocalArtifacts(t) + host, err := env.DefaultForkedScriptHost( + ctx, + broadcaster.NoopBroadcaster(), + testlog.Logger(t, log.LevelInfo), + common.Address{'D'}, + afacts, + rpcClient, + ) + require.NoError(t, err) + + // Set opcmV2Enabled flag via devFeatureBitmap + opcmV2Flag := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000010000") + intent := &state.Intent{ + ConfigType: state.IntentTypeStandard, + L1ChainID: l1ChainID, + L1ContractsLocator: artifacts.EmbeddedLocator, + L2ContractsLocator: artifacts.EmbeddedLocator, + SuperchainConfigProxy: &superchain.SuperchainConfigAddr, + GlobalDeployOverrides: map[string]any{ + "devFeatureBitmap": opcmV2Flag, + }, + } + + st := &state.State{ + Version: 1, + } + + err = InitLiveStrategy( + ctx, + &Env{ + L1Client: client, + Logger: lgr, + L1ScriptHost: host, + }, + intent, + st, + ) + require.NoError(t, err) + + // Verify state was populated + require.NotNil(t, st.SuperchainDeployment, "SuperchainDeployment should be populated") + require.NotNil(t, st.SuperchainRoles, "SuperchainRoles should be populated") + + // Verify ProtocolVersions fields are zeroed for v2 + require.Equal(t, common.Address{}, st.SuperchainDeployment.ProtocolVersionsProxy, "ProtocolVersionsProxy should be zero for v2") + require.Equal(t, common.Address{}, st.SuperchainDeployment.ProtocolVersionsImpl, "ProtocolVersionsImpl should be zero for v2") + require.Equal(t, common.Address{}, st.SuperchainRoles.ProtocolVersionsOwner, "ProtocolVersionsOwner should be zero for v2") + + // Verify SuperchainConfig fields are populated + require.Equal(t, superchain.SuperchainConfigAddr, st.SuperchainDeployment.SuperchainConfigProxy) + require.NotEqual(t, common.Address{}, st.SuperchainDeployment.SuperchainConfigImpl) + require.NotEqual(t, common.Address{}, st.SuperchainDeployment.SuperchainProxyAdminImpl) +} + +// Validates that providing both +// SuperchainConfigProxy and SuperchainRoles with opcmV2Enabled returns an error. +func TestInitLiveStrategy_OPCMV2WithSuperchainConfigProxyAndRoles_reverts(t *testing.T) { + t.Parallel() + + rpcURL := os.Getenv("SEPOLIA_RPC_URL") + require.NotEmpty(t, rpcURL, "SEPOLIA_RPC_URL must be set") + + lgr := testlog.Logger(t, slog.LevelInfo) + retryProxy := devnet.NewRetryProxy(lgr, rpcURL) + require.NoError(t, retryProxy.Start()) + t.Cleanup(func() { + require.NoError(t, retryProxy.Stop()) + }) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + rpcClient, err := rpc.Dial(retryProxy.Endpoint()) + require.NoError(t, err) + client := ethclient.NewClient(rpcClient) + + l1ChainID := uint64(11155111) + superchain, err := standard.SuperchainFor(l1ChainID) + require.NoError(t, err) + + // Set opcmV2Enabled flag via devFeatureBitmap + opcmV2Flag := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000010000") + intent := &state.Intent{ + ConfigType: state.IntentTypeStandard, + L1ChainID: l1ChainID, + L1ContractsLocator: artifacts.EmbeddedLocator, + L2ContractsLocator: artifacts.EmbeddedLocator, + SuperchainConfigProxy: &superchain.SuperchainConfigAddr, + SuperchainRoles: &addresses.SuperchainRoles{ + SuperchainGuardian: common.Address{0: 99}, + }, + GlobalDeployOverrides: map[string]any{ + "devFeatureBitmap": opcmV2Flag, + }, + } + + st := &state.State{ + Version: 1, + } + + err = InitLiveStrategy( + ctx, + &Env{ + L1Client: client, + Logger: lgr, + }, + intent, + st, + ) + require.Error(t, err) + require.Contains(t, err.Error(), "cannot set superchain roles when using predeployed OPCM or SuperchainConfig") +} + +// Validates that providing both OPCMAddress and SuperchainConfigProxy works correctly +// The script will use the OPCM's semver to determine the version +func TestInitLiveStrategy_OPCMV1WithSuperchainConfigProxy(t *testing.T) { + t.Parallel() + + rpcURL := os.Getenv("SEPOLIA_RPC_URL") + require.NotEmpty(t, rpcURL, "SEPOLIA_RPC_URL must be set") + + lgr := testlog.Logger(t, slog.LevelInfo) + retryProxy := devnet.NewRetryProxy(lgr, rpcURL) + require.NoError(t, retryProxy.Start()) + t.Cleanup(func() { + require.NoError(t, retryProxy.Stop()) + }) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + rpcClient, err := rpc.Dial(retryProxy.Endpoint()) + require.NoError(t, err) + client := ethclient.NewClient(rpcClient) + + l1ChainID := uint64(11155111) + superchain, err := standard.SuperchainFor(l1ChainID) + require.NoError(t, err) + + opcmAddr, err := standard.OPCMImplAddressFor(l1ChainID, standard.CurrentTag) + require.NoError(t, err) + + _, afacts := testutil.LocalArtifacts(t) + host, err := env.DefaultForkedScriptHost( + ctx, + broadcaster.NoopBroadcaster(), + testlog.Logger(t, log.LevelInfo), + common.Address{'D'}, + afacts, + rpcClient, + ) + require.NoError(t, err) + + // Provide both OPCM address and SuperchainConfigProxy + // The script will check the OPCM version and handle accordingly + intent := &state.Intent{ + ConfigType: state.IntentTypeStandard, + L1ChainID: l1ChainID, + L1ContractsLocator: artifacts.EmbeddedLocator, + L2ContractsLocator: artifacts.EmbeddedLocator, + OPCMAddress: &opcmAddr, + SuperchainConfigProxy: &superchain.SuperchainConfigAddr, + } + + st := &state.State{ + Version: 1, + } + + err = InitLiveStrategy( + ctx, + &Env{ + L1Client: client, + Logger: lgr, + L1ScriptHost: host, + }, + intent, + st, + ) + // Should succeed - the script handles version detection + require.NoError(t, err) + + // For OPCMv1, ProtocolVersions should be populated + require.NotNil(t, st.SuperchainDeployment) + require.NotEqual(t, common.Address{}, st.SuperchainDeployment.ProtocolVersionsProxy) + require.NotEqual(t, common.Address{}, st.SuperchainDeployment.ProtocolVersionsImpl) +} + +// Validates that providing both +// OPCMAddress and SuperchainRoles with opcmV2Enabled=false returns an error. +func TestInitLiveStrategy_OPCMV1WithSuperchainRoles_reverts(t *testing.T) { + t.Parallel() + + rpcURL := os.Getenv("SEPOLIA_RPC_URL") + require.NotEmpty(t, rpcURL, "SEPOLIA_RPC_URL must be set") + + lgr := testlog.Logger(t, slog.LevelInfo) + retryProxy := devnet.NewRetryProxy(lgr, rpcURL) + require.NoError(t, retryProxy.Start()) + t.Cleanup(func() { + require.NoError(t, retryProxy.Stop()) + }) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + rpcClient, err := rpc.Dial(retryProxy.Endpoint()) + require.NoError(t, err) + client := ethclient.NewClient(rpcClient) + + l1ChainID := uint64(11155111) + opcmAddr, err := standard.OPCMImplAddressFor(l1ChainID, standard.CurrentTag) + require.NoError(t, err) + + // Don't set opcmV2Enabled flag (defaults to false) + intent := &state.Intent{ + ConfigType: state.IntentTypeStandard, + L1ChainID: l1ChainID, + L1ContractsLocator: artifacts.EmbeddedLocator, + L2ContractsLocator: artifacts.EmbeddedLocator, + OPCMAddress: &opcmAddr, + SuperchainRoles: &addresses.SuperchainRoles{ + SuperchainGuardian: common.Address{0: 99}, + }, + } + + st := &state.State{ + Version: 1, + } + + err = InitLiveStrategy( + ctx, + &Env{ + L1Client: client, + Logger: lgr, + }, + intent, + st, + ) + require.Error(t, err) + require.Contains(t, err.Error(), "cannot set superchain roles when using predeployed OPCM or SuperchainConfig") +} + +// Validates that the correct flow is chosen when +// hasPredeployedOPCM && !opcmV2Enabled, and that PopulateSuperchainState is called with correct parameters. +func TestInitLiveStrategy_FlowSelection_OPCMV1(t *testing.T) { + t.Parallel() + + rpcURL := os.Getenv("SEPOLIA_RPC_URL") + require.NotEmpty(t, rpcURL, "SEPOLIA_RPC_URL must be set") + + lgr := testlog.Logger(t, slog.LevelInfo) + retryProxy := devnet.NewRetryProxy(lgr, rpcURL) + require.NoError(t, retryProxy.Start()) + t.Cleanup(func() { + require.NoError(t, retryProxy.Stop()) + }) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + rpcClient, err := rpc.Dial(retryProxy.Endpoint()) + require.NoError(t, err) + client := ethclient.NewClient(rpcClient) + + l1ChainID := uint64(11155111) + opcmAddr, err := standard.OPCMImplAddressFor(l1ChainID, standard.CurrentTag) + require.NoError(t, err) + + _, afacts := testutil.LocalArtifacts(t) + host, err := env.DefaultForkedScriptHost( + ctx, + broadcaster.NoopBroadcaster(), + testlog.Logger(t, log.LevelInfo), + common.Address{'D'}, + afacts, + rpcClient, + ) + require.NoError(t, err) + + // Don't set opcmV2Enabled flag (defaults to false) + intent := &state.Intent{ + ConfigType: state.IntentTypeStandard, + L1ChainID: l1ChainID, + L1ContractsLocator: artifacts.EmbeddedLocator, + L2ContractsLocator: artifacts.EmbeddedLocator, + OPCMAddress: &opcmAddr, + } + + st := &state.State{ + Version: 1, + } + + err = InitLiveStrategy( + ctx, + &Env{ + L1Client: client, + Logger: lgr, + L1ScriptHost: host, + }, + intent, + st, + ) + require.NoError(t, err) + + // Verify OPCM v1 flow was used - ProtocolVersions should be populated + require.NotNil(t, st.SuperchainDeployment) + require.NotEqual(t, common.Address{}, st.SuperchainDeployment.ProtocolVersionsProxy, "ProtocolVersionsProxy should be populated for v1") + require.NotEqual(t, common.Address{}, st.SuperchainDeployment.ProtocolVersionsImpl, "ProtocolVersionsImpl should be populated for v1") + require.NotEqual(t, common.Address{}, st.SuperchainRoles.ProtocolVersionsOwner, "ProtocolVersionsOwner should be populated for v1") + + // Verify ImplementationsDeployment was set + require.NotNil(t, st.ImplementationsDeployment) + require.Equal(t, opcmAddr, st.ImplementationsDeployment.OpcmImpl) +} + +// Validates that the correct flow is chosen when +// hasSuperchainConfigProxy && opcmV2Enabled, and that PopulateSuperchainState is called with correct parameters. +func TestInitLiveStrategy_FlowSelection_OPCMV2(t *testing.T) { + t.Parallel() + + rpcURL := os.Getenv("SEPOLIA_RPC_URL") + require.NotEmpty(t, rpcURL, "SEPOLIA_RPC_URL must be set") + + lgr := testlog.Logger(t, slog.LevelInfo) + retryProxy := devnet.NewRetryProxy(lgr, rpcURL) + require.NoError(t, retryProxy.Start()) + t.Cleanup(func() { + require.NoError(t, retryProxy.Stop()) + }) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + rpcClient, err := rpc.Dial(retryProxy.Endpoint()) + require.NoError(t, err) + client := ethclient.NewClient(rpcClient) + + l1ChainID := uint64(11155111) + superchain, err := standard.SuperchainFor(l1ChainID) + require.NoError(t, err) + + _, afacts := testutil.LocalArtifacts(t) + host, err := env.DefaultForkedScriptHost( + ctx, + broadcaster.NoopBroadcaster(), + testlog.Logger(t, log.LevelInfo), + common.Address{'D'}, + afacts, + rpcClient, + ) + require.NoError(t, err) + + // Set opcmV2Enabled flag via devFeatureBitmap + opcmV2Flag := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000010000") + intent := &state.Intent{ + ConfigType: state.IntentTypeStandard, + L1ChainID: l1ChainID, + L1ContractsLocator: artifacts.EmbeddedLocator, + L2ContractsLocator: artifacts.EmbeddedLocator, + SuperchainConfigProxy: &superchain.SuperchainConfigAddr, + GlobalDeployOverrides: map[string]any{ + "devFeatureBitmap": opcmV2Flag, + }, + } + + st := &state.State{ + Version: 1, + } + + err = InitLiveStrategy( + ctx, + &Env{ + L1Client: client, + Logger: lgr, + L1ScriptHost: host, + }, + intent, + st, + ) + require.NoError(t, err) + + // Verify OPCM v2 flow was used - ProtocolVersions should be zeroed + require.NotNil(t, st.SuperchainDeployment) + require.Equal(t, common.Address{}, st.SuperchainDeployment.ProtocolVersionsProxy, "ProtocolVersionsProxy should be zero for v2") + require.Equal(t, common.Address{}, st.SuperchainDeployment.ProtocolVersionsImpl, "ProtocolVersionsImpl should be zero for v2") + require.Equal(t, common.Address{}, st.SuperchainRoles.ProtocolVersionsOwner, "ProtocolVersionsOwner should be zero for v2") + + // Verify SuperchainConfig is populated + require.Equal(t, superchain.SuperchainConfigAddr, st.SuperchainDeployment.SuperchainConfigProxy) + require.NotEqual(t, common.Address{}, st.SuperchainDeployment.SuperchainConfigImpl) } diff --git a/packages/contracts-bedrock/scripts/deploy/ReadSuperchainDeployment.s.sol b/packages/contracts-bedrock/scripts/deploy/ReadSuperchainDeployment.s.sol index 6c7b60a714f..dc2422d819c 100644 --- a/packages/contracts-bedrock/scripts/deploy/ReadSuperchainDeployment.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/ReadSuperchainDeployment.s.sol @@ -9,49 +9,81 @@ import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; +import { SemverComp } from "src/libraries/SemverComp.sol"; contract ReadSuperchainDeployment is Script { struct Input { - IOPContractsManager opcmAddress; + IOPContractsManager opcmAddress; // TODO(#18612): Remove OPCMAddress field when OPCMv1 gets deprecated + ISuperchainConfig superchainConfigProxy; } struct Output { + // TODO(#18612): Remove ProtocolVersions fields when OPCMv1 gets deprecated IProtocolVersions protocolVersionsImpl; IProtocolVersions protocolVersionsProxy; + address protocolVersionsOwner; + bytes32 recommendedProtocolVersion; + bytes32 requiredProtocolVersion; + // Superchain config ISuperchainConfig superchainConfigImpl; ISuperchainConfig superchainConfigProxy; IProxyAdmin superchainProxyAdmin; address guardian; - address protocolVersionsOwner; address superchainProxyAdminOwner; - bytes32 recommendedProtocolVersion; - bytes32 requiredProtocolVersion; } function run(Input memory _input) public returns (Output memory output_) { - require(address(_input.opcmAddress) != address(0), "ReadSuperchainDeployment: opcmAddress not set"); - + // Determine OPCM version by checking the semver or if the OPCM address is set. OPCM v2 starts at version 7.0.0. IOPContractsManager opcm = IOPContractsManager(_input.opcmAddress); + bool isOPCMV2; + if (address(opcm) == address(0)) { + isOPCMV2 = true; + } else { + require(address(opcm).code.length > 0, "ReadSuperchainDeployment: OPCM address has no code"); + isOPCMV2 = SemverComp.gte(opcm.version(), "7.0.0"); + } + + if (isOPCMV2) { + require( + address(_input.superchainConfigProxy) != address(0), + "ReadSuperchainDeployment: superchainConfigProxy required for OPCM v2" + ); + + // For OPCM v2, ProtocolVersions is being removed. Therefore, the ProtocolVersions-related fields + // (protocolVersionsImpl, protocolVersionsProxy, protocolVersionsOwner, recommendedProtocolVersion, + // requiredProtocolVersion) are intentionally left uninitialized. + output_.superchainConfigProxy = _input.superchainConfigProxy; + output_.superchainProxyAdmin = IProxyAdmin(EIP1967Helper.getAdmin(address(output_.superchainConfigProxy))); + + IProxy superchainConfigProxy = IProxy(payable(address(output_.superchainConfigProxy))); + + vm.startPrank(address(0)); + output_.superchainConfigImpl = ISuperchainConfig(superchainConfigProxy.implementation()); + vm.stopPrank(); + + output_.guardian = output_.superchainConfigProxy.guardian(); + output_.superchainProxyAdminOwner = output_.superchainProxyAdmin.owner(); + } else { + // When running on OPCM v1, the OPCM address is used to read the ProtocolVersions contract and + // SuperchainConfig. + output_.protocolVersionsProxy = IProtocolVersions(opcm.protocolVersions()); + output_.superchainConfigProxy = ISuperchainConfig(opcm.superchainConfig()); + output_.superchainProxyAdmin = IProxyAdmin(EIP1967Helper.getAdmin(address(output_.superchainConfigProxy))); + + IProxy protocolVersionsProxy = IProxy(payable(address(output_.protocolVersionsProxy))); + IProxy superchainConfigProxy = IProxy(payable(address(output_.superchainConfigProxy))); + + vm.startPrank(address(0)); + output_.protocolVersionsImpl = IProtocolVersions(protocolVersionsProxy.implementation()); + output_.superchainConfigImpl = ISuperchainConfig(superchainConfigProxy.implementation()); + vm.stopPrank(); - output_.protocolVersionsProxy = IProtocolVersions(opcm.protocolVersions()); - output_.superchainConfigProxy = ISuperchainConfig(opcm.superchainConfig()); - output_.superchainProxyAdmin = IProxyAdmin(EIP1967Helper.getAdmin(address(output_.superchainConfigProxy))); - - IProxy protocolVersionsProxy = IProxy(payable(address(output_.protocolVersionsProxy))); - IProxy superchainConfigProxy = IProxy(payable(address(output_.superchainConfigProxy))); - - vm.startPrank(address(0)); - output_.protocolVersionsImpl = IProtocolVersions(address(protocolVersionsProxy.implementation())); - output_.superchainConfigImpl = ISuperchainConfig(address(superchainConfigProxy.implementation())); - output_.protocolVersionsImpl = IProtocolVersions(protocolVersionsProxy.implementation()); - output_.superchainConfigImpl = ISuperchainConfig(superchainConfigProxy.implementation()); - vm.stopPrank(); - - output_.guardian = output_.superchainConfigProxy.guardian(); - output_.protocolVersionsOwner = output_.protocolVersionsProxy.owner(); - output_.superchainProxyAdminOwner = output_.superchainProxyAdmin.owner(); - output_.recommendedProtocolVersion = - bytes32(ProtocolVersion.unwrap(output_.protocolVersionsProxy.recommended())); - output_.requiredProtocolVersion = bytes32(ProtocolVersion.unwrap(output_.protocolVersionsProxy.required())); + output_.guardian = output_.superchainConfigProxy.guardian(); + output_.protocolVersionsOwner = output_.protocolVersionsProxy.owner(); + output_.superchainProxyAdminOwner = output_.superchainProxyAdmin.owner(); + output_.recommendedProtocolVersion = + bytes32(ProtocolVersion.unwrap(output_.protocolVersionsProxy.recommended())); + output_.requiredProtocolVersion = bytes32(ProtocolVersion.unwrap(output_.protocolVersionsProxy.required())); + } } } From 8fcf57a9b41b56b2ecb4064a11cf4ccfea77b549 Mon Sep 17 00:00:00 2001 From: smartcontracts <14298799+smartcontracts@users.noreply.github.com> Date: Thu, 18 Dec 2025 13:54:23 -0500 Subject: [PATCH 10/13] test: use a custom Test contract for makeAddr (#18509) * test: use a custom Test contract for makeAddr This PR introduces a light wrapper around the forge Test contract specifically for the purpose of making sure people are using a version of makeAddr that will always be reproducible. Without this makeAddr uses a private key which means that fork tests can be unreliable if people mess with these addresses on mainnet. * fix: compilation failures * fix: linting issues, version issues --- .circleci/config.yml | 3 +-- .semgrep/rules/sol-rules.yaml | 11 ++++++++++ ....sol-style-ban-forge-std-test-import.t.sol | 20 +++++++++++++++++++ packages/contracts-bedrock/justfile | 4 ++-- .../snapshots/semver-lock.json | 4 ++-- .../src/L1/OptimismPortalInterop.sol | 4 ++-- .../test/L1/L1StandardBridge.t.sol | 2 +- .../test/L1/OPContractsManager.t.sol | 3 ++- .../test/L1/ResourceMetering.t.sol | 2 +- .../L1/opcm/OPContractsManagerContainer.t.sol | 2 +- .../L1/opcm/OPContractsManagerUtils.t.sol | 2 +- .../test/L2/CrossDomainOwnable.t.sol | 4 ++-- .../test/L2/CrossL2Inbox.t.sol | 4 ++-- .../test/L2/FeeSplitterVaults.t.sol | 8 ++++---- .../test/L2/GasPriceOracle.t.sol | 4 ++-- .../contracts-bedrock/test/L2/L1Block.t.sol | 2 +- .../test/L2/L2StandardBridge.t.sol | 2 +- .../test/L2/L2ToL2CrossDomainMessenger.t.sol | 4 ++-- .../test/L2/LiquidityController.t.sol | 4 ++-- .../test/L2/OptimismSuperchainERC20.t.sol | 4 ++-- .../test/L2/SuperchainERC20.t.sol | 4 ++-- .../test/L2/SuperchainTokenBridge.t.sol | 4 ++-- .../test/cannon/MIPS64.t.sol | 8 +++++++- .../test/cannon/PreimageOracle.t.sol | 4 +++- .../test/dispute/lib/LibClock.t.sol | 5 ++++- .../test/dispute/lib/LibGameArgs.t.sol | 5 ++++- .../test/dispute/lib/LibGameId.t.sol | 4 +++- .../test/dispute/lib/LibPosition.t.sol | 5 ++++- .../test/integration/EventLogger.t.sol | 13 +++++++----- .../test/invariants/CustomGasToken.t.sol | 2 +- .../test/invariants/ETHLiquidity.t.sol | 2 +- .../test/invariants/InvariantTest.sol | 5 ++++- .../test/invariants/OptimismPortal2.t.sol | 2 +- .../OptimismSuperchainERC20.t.sol | 4 ++-- .../test/invariants/SystemConfig.t.sol | 13 +++++++++--- .../test/legacy/DeployerWhitelist.t.sol | 4 ++-- .../test/legacy/L1BlockNumber.t.sol | 2 +- .../test/legacy/L1ChugSplashProxy.t.sol | 4 ++-- .../test/legacy/ResolvedDelegateProxy.t.sol | 4 ++-- .../test/libraries/Blueprint.t.sol | 5 ++++- .../test/libraries/Bytes.t.sol | 6 +++--- .../test/libraries/Constants.t.sol | 7 ++++++- .../test/libraries/DeployUtils.t.sol | 4 ++-- .../test/libraries/DevFeatures.t.sol | 6 +++--- .../test/libraries/EOA.t.sol | 4 ++-- .../test/libraries/GasPayingToken.t.sol | 6 ++++-- .../test/libraries/SafeCall.t.sol | 10 +++++----- .../test/libraries/SemverComp.t.sol | 4 ++-- .../test/libraries/StaticConfig.t.sol | 6 +++--- .../test/libraries/Storage.t.sol | 6 ++++-- .../test/libraries/TransientContext.t.sol | 6 +++--- .../test/libraries/rlp/RLPReader.t.sol | 7 +++++-- .../test/libraries/rlp/RLPWriter.t.sol | 5 ++++- .../test/libraries/trie/MerkleTrie.t.sol | 7 +++++-- .../test/opcm/DeployAlphabetVM.t.sol | 8 +++++--- .../test/opcm/DeployAltDA.t.sol | 8 ++++++-- .../test/opcm/DeployAsterisc.t.sol | 7 ++++--- .../test/opcm/DeployDisputeGame.t.sol | 9 ++++++++- .../test/opcm/DeployFeesDepositor.t.sol | 16 +++++++++------ .../test/opcm/DeployImplementations.t.sol | 3 ++- .../test/opcm/DeployMIPS2.t.sol | 14 ++++++++----- .../test/opcm/DeployOPChain.t.sol | 9 +++++++-- .../test/opcm/DeploySuperchain.t.sol | 10 ++++++++-- .../test/opcm/InteropMigration.t.sol | 10 ++++++++-- .../test/opcm/SetDisputeGameImpl.t.sol | 17 ++++++++++++---- .../test/opcm/UpgradeOPChain.t.sol | 15 ++++++++++---- .../test/opcm/UpgradeSuperchainConfig.t.sol | 9 ++++++--- .../test/periphery/AssetReceiver.t.sol | 6 ++++-- .../test/periphery/Transactor.t.sol | 6 ++++-- .../test/periphery/TransferOnion.t.sol | 8 ++++---- .../test/periphery/drippie/Drippie.t.sol | 11 +++++++--- .../drippie/dripchecks/CheckBalanceLow.t.sol | 5 ++++- .../drippie/dripchecks/CheckSecrets.t.sol | 5 ++++- .../drippie/dripchecks/CheckTrue.t.sol | 5 ++++- .../test/periphery/faucet/Faucet.t.sol | 7 +++++-- .../authmodules/AdminFaucetAuthModule.t.sol | 7 +++++-- .../test/safe-tools/SafeTestTools.sol | 18 ++++++++++++----- .../test/safe/LivenessGuard.t.sol | 18 +++++++++-------- .../test/safe/LivenessModule.t.sol | 10 +++++++--- .../test/safe/LivenessModule2.t.sol | 14 ++++++++----- .../test/safe/SafeSigners.t.sol | 7 +++++-- .../test/safe/TimelockGuard.t.sol | 15 ++++++++++---- .../test/scripts/DeployOwnership.t.sol | 18 ++++++++++++++--- .../test/scripts/FetchChainInfo.t.sol | 7 ++++++- .../test/scripts/L2Genesis.t.sol | 11 ++++++++-- .../test/setup/CommonTest.sol | 4 +--- .../contracts-bedrock/test/setup/Test.sol | 19 ++++++++++++++++++ .../test/universal/CrossDomainMessenger.t.sol | 4 ++-- .../test/universal/Proxy.t.sol | 11 ++++++++-- .../test/universal/ProxyAdmin.t.sol | 7 ++++--- .../test/universal/ReinitializableBase.t.sol | 2 +- .../test/universal/WETH98.t.sol | 8 +++++--- .../test/vendor/AddressAliasHelper.t.sol | 5 ++++- .../test/vendor/InitializableOZv5.t.sol | 17 ++++++++++++---- 94 files changed, 462 insertions(+), 204 deletions(-) create mode 100644 .semgrep/tests/sol-rules.sol-style-ban-forge-std-test-import.t.sol create mode 100644 packages/contracts-bedrock/test/setup/Test.sol diff --git a/.circleci/config.yml b/.circleci/config.yml index 5f3707c0ada..278fad92f0b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2780,10 +2780,9 @@ workflows: parameters: features: &features_matrix - main + - CUSTOM_GAS_TOKEN - OPTIMISM_PORTAL_INTEROP - - CANNON_KONA,DEPLOY_V2_DISPUTE_GAMES - OPCM_V2 - - CUSTOM_GAS_TOKEN - OPCM_V2,CUSTOM_GAS_TOKEN - OPCM_V2,OPTIMISM_PORTAL_INTEROP context: diff --git a/.semgrep/rules/sol-rules.yaml b/.semgrep/rules/sol-rules.yaml index ed1bb708a3d..f260b778a02 100644 --- a/.semgrep/rules/sol-rules.yaml +++ b/.semgrep/rules/sol-rules.yaml @@ -398,3 +398,14 @@ rules: paths: include: - packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol + + - id: sol-style-ban-forge-std-test-import + languages: [solidity] + severity: ERROR + message: Import Test from test/setup/Test.sol, not forge-std/Test.sol. Import other forge-std components (stdStorage, StdStorage, stdError, StdUtils, Vm, console2, etc.) from their specific files (forge-std/StdStorage.sol, forge-std/StdError.sol, forge-std/StdUtils.sol, forge-std/Vm.sol, forge-std/console2.sol, etc.) + pattern-regex: import\s+(\{[^}]*\}\s+from\s+)?"forge-std/Test\.sol"\s*; + paths: + include: + - packages/contracts-bedrock/test + exclude: + - packages/contracts-bedrock/test/setup/Test.sol diff --git a/.semgrep/tests/sol-rules.sol-style-ban-forge-std-test-import.t.sol b/.semgrep/tests/sol-rules.sol-style-ban-forge-std-test-import.t.sol new file mode 100644 index 00000000000..58f6f19357e --- /dev/null +++ b/.semgrep/tests/sol-rules.sol-style-ban-forge-std-test-import.t.sol @@ -0,0 +1,20 @@ +// ruleid: sol-style-ban-forge-std-test-import +import { Test } from "forge-std/Test.sol"; + +// ruleid: sol-style-ban-forge-std-test-import +import { Test as ForgeTest } from "forge-std/Test.sol"; + +// ruleid: sol-style-ban-forge-std-test-import +import { Test, Vm } from "forge-std/Test.sol"; + +// ok: sol-style-ban-forge-std-test-import +import { Test } from "test/setup/Test.sol"; + +// ok: sol-style-ban-forge-std-test-import +import { Test as BaseTest } from "test/setup/Test.sol"; + +// ok: sol-style-ban-forge-std-test-import +import { Vm } from "forge-std/Vm.sol"; + +// ok: sol-style-ban-forge-std-test-import +import { StdUtils } from "forge-std/StdUtils.sol"; diff --git a/packages/contracts-bedrock/justfile b/packages/contracts-bedrock/justfile index 94aab76a70b..efeed6c7c0b 100644 --- a/packages/contracts-bedrock/justfile +++ b/packages/contracts-bedrock/justfile @@ -86,8 +86,8 @@ test-dev *ARGS: build-go-ffi # Default block number for the forked upgrade path. -export sepoliaBlockNumber := "9366100" -export mainnetBlockNumber := "23530400" +export sepoliaBlockNumber := "9770063" +export mainnetBlockNumber := "23942395" export pinnedBlockNumber := if env_var_or_default("FORK_BASE_CHAIN", "") == "mainnet" { mainnetBlockNumber diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index eb0bc8f4b61..627b2b0cbbd 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -36,8 +36,8 @@ "sourceCodeHash": "0x16fb96f4d29a10d03b3b9c70edf56df51e97c2a1a3f0ba36aae79469b446ad5c" }, "src/L1/OptimismPortalInterop.sol:OptimismPortalInterop": { - "initCodeHash": "0x087281cd2a48e882648c09fa90bfcca7487d222e16300f9372deba6b2b8ccfad", - "sourceCodeHash": "0x1cc641a4272aea85e13cbf42d9032d1b91ef858eafe3be6b5649cc8504c9cf69" + "initCodeHash": "0xd361ed3b8d56dcc1f3c068ef3af9c83f3da1165bcdab097250ad4772f350c52e", + "sourceCodeHash": "0xd7d2166d29a22f3a051bc832cbce05f9ca06f1ac1bfb0790f29579f12bb95b8f" }, "src/L1/ProtocolVersions.sol:ProtocolVersions": { "initCodeHash": "0xcb59ad9a5ec2a0831b7f4daa74bdacba82ffa03035dafb499a732c641e017f4e", diff --git a/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol b/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol index 54db98d889f..dec588d5cc1 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol @@ -229,9 +229,9 @@ contract OptimismPortalInterop is Initializable, ResourceMetering, Reinitializab error OptimismPortal_MigratingToSameRegistry(); /// @notice Semantic version. - /// @custom:semver 5.1.0+interop + /// @custom:semver 5.2.0+interop function version() public pure virtual returns (string memory) { - return "5.1.0+interop"; + return "5.2.0+interop"; } /// @param _proofMaturityDelaySeconds The proof maturity delay in seconds. diff --git a/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol b/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol index abec06bf78b..294545b1561 100644 --- a/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol +++ b/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; // Testing -import { stdStorage, StdStorage } from "forge-std/Test.sol"; +import { stdStorage, StdStorage } from "forge-std/StdStorage.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; import { ForgeArtifacts, StorageSlot } from "scripts/libraries/ForgeArtifacts.sol"; diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index 5dd8a9e9dc4..8cb35a2209c 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -2,7 +2,8 @@ pragma solidity 0.8.15; // Testing -import { Test, stdStorage, StdStorage } from "forge-std/Test.sol"; +import { Test } from "test/setup/Test.sol"; +import { stdStorage, StdStorage } from "forge-std/StdStorage.sol"; import { VmSafe } from "forge-std/Vm.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; import { FeatureFlags } from "test/setup/FeatureFlags.sol"; diff --git a/packages/contracts-bedrock/test/L1/ResourceMetering.t.sol b/packages/contracts-bedrock/test/L1/ResourceMetering.t.sol index 8e64d609e9e..eca3d477053 100644 --- a/packages/contracts-bedrock/test/L1/ResourceMetering.t.sol +++ b/packages/contracts-bedrock/test/L1/ResourceMetering.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; // Testing -import { Test } from "forge-std/Test.sol"; +import { Test } from "test/setup/Test.sol"; // Contracts import { ResourceMetering } from "src/L1/ResourceMetering.sol"; diff --git a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerContainer.t.sol b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerContainer.t.sol index 78eafbdf763..fbb579449e9 100644 --- a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerContainer.t.sol +++ b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerContainer.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; // Testing -import { Test } from "forge-std/Test.sol"; +import { Test } from "test/setup/Test.sol"; // Contracts import { OPContractsManagerContainer } from "src/L1/opcm/OPContractsManagerContainer.sol"; diff --git a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerUtils.t.sol b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerUtils.t.sol index 5ca424905f4..ce79986542f 100644 --- a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerUtils.t.sol +++ b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerUtils.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; // Testing -import { Test } from "forge-std/Test.sol"; +import { Test } from "test/setup/Test.sol"; // Contracts import { OPContractsManagerUtils } from "src/L1/opcm/OPContractsManagerUtils.sol"; diff --git a/packages/contracts-bedrock/test/L2/CrossDomainOwnable.t.sol b/packages/contracts-bedrock/test/L2/CrossDomainOwnable.t.sol index 56c3f74dda1..99898df6f4d 100644 --- a/packages/contracts-bedrock/test/L2/CrossDomainOwnable.t.sol +++ b/packages/contracts-bedrock/test/L2/CrossDomainOwnable.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities +// Testing +import { Test } from "test/setup/Test.sol"; import { VmSafe } from "forge-std/Vm.sol"; -import { Test } from "forge-std/Test.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; // Libraries diff --git a/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol b/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol index 323ac2c5b06..7d1b2460099 100644 --- a/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol +++ b/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; import { VmSafe } from "forge-std/Vm.sol"; diff --git a/packages/contracts-bedrock/test/L2/FeeSplitterVaults.t.sol b/packages/contracts-bedrock/test/L2/FeeSplitterVaults.t.sol index fbb475373cd..c316e23af8a 100644 --- a/packages/contracts-bedrock/test/L2/FeeSplitterVaults.t.sol +++ b/packages/contracts-bedrock/test/L2/FeeSplitterVaults.t.sol @@ -1,13 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -// Libraries -import { Predeploys } from "src/libraries/Predeploys.sol"; - // Testing -import { Test } from "forge-std/Test.sol"; +import { Test } from "test/setup/Test.sol"; import { FeeSplitterForTest } from "test/mocks/FeeSplitterForTest.sol"; +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; + // Interfaces import { IFeeSplitter } from "interfaces/L2/IFeeSplitter.sol"; diff --git a/packages/contracts-bedrock/test/L2/GasPriceOracle.t.sol b/packages/contracts-bedrock/test/L2/GasPriceOracle.t.sol index e0f959333c4..72b74f0c36b 100644 --- a/packages/contracts-bedrock/test/L2/GasPriceOracle.t.sol +++ b/packages/contracts-bedrock/test/L2/GasPriceOracle.t.sol @@ -1,13 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities +// Testing import { CommonTest } from "test/setup/CommonTest.sol"; import { Fork } from "scripts/libraries/Config.sol"; +import { stdError } from "forge-std/StdError.sol"; // Libraries import { Encoding } from "src/libraries/Encoding.sol"; -import { stdError } from "forge-std/Test.sol"; contract GasPriceOracle_Test is CommonTest { address depositor; diff --git a/packages/contracts-bedrock/test/L2/L1Block.t.sol b/packages/contracts-bedrock/test/L2/L1Block.t.sol index 5d36bb88631..8b31d1e2ac3 100644 --- a/packages/contracts-bedrock/test/L2/L1Block.t.sol +++ b/packages/contracts-bedrock/test/L2/L1Block.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.15; // Testing import { CommonTest } from "test/setup/CommonTest.sol"; -import { stdStorage, StdStorage } from "forge-std/Test.sol"; +import { stdStorage, StdStorage } from "forge-std/StdStorage.sol"; // Libraries import { Encoding } from "src/libraries/Encoding.sol"; diff --git a/packages/contracts-bedrock/test/L2/L2StandardBridge.t.sol b/packages/contracts-bedrock/test/L2/L2StandardBridge.t.sol index a0f1c0aec8c..8ea1e57ad20 100644 --- a/packages/contracts-bedrock/test/L2/L2StandardBridge.t.sol +++ b/packages/contracts-bedrock/test/L2/L2StandardBridge.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; // Testing -import { stdStorage, StdStorage } from "forge-std/Test.sol"; +import { stdStorage, StdStorage } from "forge-std/StdStorage.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; diff --git a/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol b/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol index 9b2d8642360..508aac46590 100644 --- a/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol +++ b/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import { Vm } from "forge-std/Vm.sol"; // Libraries diff --git a/packages/contracts-bedrock/test/L2/LiquidityController.t.sol b/packages/contracts-bedrock/test/L2/LiquidityController.t.sol index 82208200fcc..9b77c45ea75 100644 --- a/packages/contracts-bedrock/test/L2/LiquidityController.t.sol +++ b/packages/contracts-bedrock/test/L2/LiquidityController.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities +// Testing import { CommonTest } from "test/setup/CommonTest.sol"; -import { stdStorage, StdStorage } from "forge-std/Test.sol"; +import { stdStorage, StdStorage } from "forge-std/StdStorage.sol"; // Libraries import { Features } from "src/libraries/Features.sol"; diff --git a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol index 3479ec0330d..3950de5901a 100644 --- a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; // Libraries diff --git a/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol index b409c2c42bd..7835c11542b 100644 --- a/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; diff --git a/packages/contracts-bedrock/test/L2/SuperchainTokenBridge.t.sol b/packages/contracts-bedrock/test/L2/SuperchainTokenBridge.t.sol index 059d66cc9c1..daf0f1546ee 100644 --- a/packages/contracts-bedrock/test/L2/SuperchainTokenBridge.t.sol +++ b/packages/contracts-bedrock/test/L2/SuperchainTokenBridge.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; diff --git a/packages/contracts-bedrock/test/cannon/MIPS64.t.sol b/packages/contracts-bedrock/test/cannon/MIPS64.t.sol index 6e3caaa1695..774324a22d4 100644 --- a/packages/contracts-bedrock/test/cannon/MIPS64.t.sol +++ b/packages/contracts-bedrock/test/cannon/MIPS64.t.sol @@ -1,10 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +// Scripts import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; + +// Libraries import { UnsupportedStateVersion } from "src/cannon/libraries/CannonErrors.sol"; + +// Interfaces import { IPreimageOracle } from "interfaces/cannon/IPreimageOracle.sol"; import { IMIPS64 } from "interfaces/cannon/IMIPS64.sol"; diff --git a/packages/contracts-bedrock/test/cannon/PreimageOracle.t.sol b/packages/contracts-bedrock/test/cannon/PreimageOracle.t.sol index 59cef2f3653..ce423dae10d 100644 --- a/packages/contracts-bedrock/test/cannon/PreimageOracle.t.sol +++ b/packages/contracts-bedrock/test/cannon/PreimageOracle.t.sol @@ -2,7 +2,9 @@ pragma solidity 0.8.15; // Testing -import { Test, Vm, console2 as console } from "forge-std/Test.sol"; +import { Test } from "test/setup/Test.sol"; +import { Vm } from "forge-std/Vm.sol"; +import { console2 as console } from "forge-std/console2.sol"; // Scripts import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; diff --git a/packages/contracts-bedrock/test/dispute/lib/LibClock.t.sol b/packages/contracts-bedrock/test/dispute/lib/LibClock.t.sol index 4bf90c1dfc1..8ec76658b9b 100644 --- a/packages/contracts-bedrock/test/dispute/lib/LibClock.t.sol +++ b/packages/contracts-bedrock/test/dispute/lib/LibClock.t.sol @@ -1,7 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Libraries import { LibClock } from "src/dispute/lib/LibUDT.sol"; import "src/dispute/lib/Types.sol"; diff --git a/packages/contracts-bedrock/test/dispute/lib/LibGameArgs.t.sol b/packages/contracts-bedrock/test/dispute/lib/LibGameArgs.t.sol index 6fd4c21e22a..1eee52105da 100644 --- a/packages/contracts-bedrock/test/dispute/lib/LibGameArgs.t.sol +++ b/packages/contracts-bedrock/test/dispute/lib/LibGameArgs.t.sol @@ -1,7 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Libraries import { LibGameArgs } from "src/dispute/lib/LibGameArgs.sol"; import { InvalidGameArgsLength } from "src/dispute/lib/Errors.sol"; diff --git a/packages/contracts-bedrock/test/dispute/lib/LibGameId.t.sol b/packages/contracts-bedrock/test/dispute/lib/LibGameId.t.sol index b1878178c1a..d8ea4729d6c 100644 --- a/packages/contracts-bedrock/test/dispute/lib/LibGameId.t.sol +++ b/packages/contracts-bedrock/test/dispute/lib/LibGameId.t.sol @@ -1,8 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +// Libraries import "src/dispute/lib/Types.sol"; /// @title LibGameId_Pack_Test diff --git a/packages/contracts-bedrock/test/dispute/lib/LibPosition.t.sol b/packages/contracts-bedrock/test/dispute/lib/LibPosition.t.sol index 8d392fa49c5..d38223ff0d0 100644 --- a/packages/contracts-bedrock/test/dispute/lib/LibPosition.t.sol +++ b/packages/contracts-bedrock/test/dispute/lib/LibPosition.t.sol @@ -1,7 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Libraries import { LibPosition } from "src/dispute/lib/LibPosition.sol"; import "src/dispute/lib/Types.sol"; diff --git a/packages/contracts-bedrock/test/integration/EventLogger.t.sol b/packages/contracts-bedrock/test/integration/EventLogger.t.sol index 51ecccc1a4e..46265da67b6 100644 --- a/packages/contracts-bedrock/test/integration/EventLogger.t.sol +++ b/packages/contracts-bedrock/test/integration/EventLogger.t.sol @@ -1,17 +1,20 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { Test } from "forge-std/Test.sol"; - -import { Identifier as IfaceIdentifier } from "interfaces/L2/ICrossL2Inbox.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +import { VmSafe } from "forge-std/Vm.sol"; +// Contracts import { EventLogger } from "../../src/integration/EventLogger.sol"; +import { CrossL2Inbox } from "src/L2/CrossL2Inbox.sol"; +// Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; +// Interfaces +import { Identifier as IfaceIdentifier } from "interfaces/L2/ICrossL2Inbox.sol"; import { ICrossL2Inbox, Identifier as ImplIdentifier } from "interfaces/L2/ICrossL2Inbox.sol"; -import { VmSafe } from "forge-std/Vm.sol"; -import { CrossL2Inbox } from "src/L2/CrossL2Inbox.sol"; /// @title EventLogger_TestInit /// @notice Reusable test initialization for `EventLogger` tests. diff --git a/packages/contracts-bedrock/test/invariants/CustomGasToken.t.sol b/packages/contracts-bedrock/test/invariants/CustomGasToken.t.sol index 764a3a0236c..334d8d44a56 100644 --- a/packages/contracts-bedrock/test/invariants/CustomGasToken.t.sol +++ b/packages/contracts-bedrock/test/invariants/CustomGasToken.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; // Testing -import { StdUtils } from "forge-std/Test.sol"; +import { StdUtils } from "forge-std/StdUtils.sol"; import { Vm } from "forge-std/Vm.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; diff --git a/packages/contracts-bedrock/test/invariants/ETHLiquidity.t.sol b/packages/contracts-bedrock/test/invariants/ETHLiquidity.t.sol index ec046d1ebb5..02a4e63e316 100644 --- a/packages/contracts-bedrock/test/invariants/ETHLiquidity.t.sol +++ b/packages/contracts-bedrock/test/invariants/ETHLiquidity.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; // Testing -import { StdUtils } from "forge-std/Test.sol"; +import { StdUtils } from "forge-std/StdUtils.sol"; import { Vm } from "forge-std/Vm.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; diff --git a/packages/contracts-bedrock/test/invariants/InvariantTest.sol b/packages/contracts-bedrock/test/invariants/InvariantTest.sol index eea6c158b35..c4720ec15d1 100644 --- a/packages/contracts-bedrock/test/invariants/InvariantTest.sol +++ b/packages/contracts-bedrock/test/invariants/InvariantTest.sol @@ -1,9 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; +// Testing +import { Test } from "test/setup/Test.sol"; import { FFIInterface } from "test/setup/FFIInterface.sol"; + +// Scripts import { Deploy } from "scripts/deploy/Deploy.s.sol"; -import { Test } from "forge-std/Test.sol"; /// @title InvariantTest /// @dev An extension to `Test` that sets up excluded contracts for invariant testing. diff --git a/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol index f826e026291..297f9a0e47a 100644 --- a/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; // Testing -import { StdUtils } from "forge-std/Test.sol"; +import { StdUtils } from "forge-std/StdUtils.sol"; import { Vm } from "forge-std/Vm.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; diff --git a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/OptimismSuperchainERC20.t.sol index d53d2fd29f9..0d01a97a483 100644 --- a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/OptimismSuperchainERC20.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; diff --git a/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol b/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol index 8dcd0da95a5..b10a10471ba 100644 --- a/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol +++ b/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol @@ -1,11 +1,18 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Scripts +import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; + +// Libraries +import { Constants } from "src/libraries/Constants.sol"; + +// Interfaces import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; -import { Constants } from "src/libraries/Constants.sol"; -import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; contract SystemConfig_GasLimitBoundaries_Invariant is Test { diff --git a/packages/contracts-bedrock/test/legacy/DeployerWhitelist.t.sol b/packages/contracts-bedrock/test/legacy/DeployerWhitelist.t.sol index 764e9ef8897..b148bcf3e8c 100644 --- a/packages/contracts-bedrock/test/legacy/DeployerWhitelist.t.sol +++ b/packages/contracts-bedrock/test/legacy/DeployerWhitelist.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; // Target contract import { IDeployerWhitelist } from "interfaces/legacy/IDeployerWhitelist.sol"; diff --git a/packages/contracts-bedrock/test/legacy/L1BlockNumber.t.sol b/packages/contracts-bedrock/test/legacy/L1BlockNumber.t.sol index d536ad006e1..2d215dd284c 100644 --- a/packages/contracts-bedrock/test/legacy/L1BlockNumber.t.sol +++ b/packages/contracts-bedrock/test/legacy/L1BlockNumber.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; // Testing -import { Test } from "forge-std/Test.sol"; +import { Test } from "test/setup/Test.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; diff --git a/packages/contracts-bedrock/test/legacy/L1ChugSplashProxy.t.sol b/packages/contracts-bedrock/test/legacy/L1ChugSplashProxy.t.sol index aebc7e30725..c81a0c888b3 100644 --- a/packages/contracts-bedrock/test/legacy/L1ChugSplashProxy.t.sol +++ b/packages/contracts-bedrock/test/legacy/L1ChugSplashProxy.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.15; -// Forge -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import { VmSafe } from "forge-std/Vm.sol"; // Scripts diff --git a/packages/contracts-bedrock/test/legacy/ResolvedDelegateProxy.t.sol b/packages/contracts-bedrock/test/legacy/ResolvedDelegateProxy.t.sol index 9df666980ae..9c4a7bd6a67 100644 --- a/packages/contracts-bedrock/test/legacy/ResolvedDelegateProxy.t.sol +++ b/packages/contracts-bedrock/test/legacy/ResolvedDelegateProxy.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; // Target contract dependencies import { IAddressManager } from "interfaces/legacy/IAddressManager.sol"; diff --git a/packages/contracts-bedrock/test/libraries/Blueprint.t.sol b/packages/contracts-bedrock/test/libraries/Blueprint.t.sol index 433271f4cf3..ae03959dcb3 100644 --- a/packages/contracts-bedrock/test/libraries/Blueprint.t.sol +++ b/packages/contracts-bedrock/test/libraries/Blueprint.t.sol @@ -1,7 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Libraries import { Blueprint } from "src/libraries/Blueprint.sol"; /// @dev Used to test that constructor args are appended properly when deploying from a blueprint. diff --git a/packages/contracts-bedrock/test/libraries/Bytes.t.sol b/packages/contracts-bedrock/test/libraries/Bytes.t.sol index aa919f19a0b..3cd5b0e3551 100644 --- a/packages/contracts-bedrock/test/libraries/Bytes.t.sol +++ b/packages/contracts-bedrock/test/libraries/Bytes.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; -// Target contract +// Libraries import { Bytes } from "src/libraries/Bytes.sol"; contract Bytes_Harness { diff --git a/packages/contracts-bedrock/test/libraries/Constants.t.sol b/packages/contracts-bedrock/test/libraries/Constants.t.sol index b83029e8226..60bb52bd878 100644 --- a/packages/contracts-bedrock/test/libraries/Constants.t.sol +++ b/packages/contracts-bedrock/test/libraries/Constants.t.sol @@ -1,8 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Libraries import { Constants } from "src/libraries/Constants.sol"; + +// Interfaces import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; /// @title Constants_Test diff --git a/packages/contracts-bedrock/test/libraries/DeployUtils.t.sol b/packages/contracts-bedrock/test/libraries/DeployUtils.t.sol index 1b1af20cf53..4bee5fe1908 100644 --- a/packages/contracts-bedrock/test/libraries/DeployUtils.t.sol +++ b/packages/contracts-bedrock/test/libraries/DeployUtils.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Forge -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; // Libraries import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; diff --git a/packages/contracts-bedrock/test/libraries/DevFeatures.t.sol b/packages/contracts-bedrock/test/libraries/DevFeatures.t.sol index f4851b4afeb..31cf7d6a62a 100644 --- a/packages/contracts-bedrock/test/libraries/DevFeatures.t.sol +++ b/packages/contracts-bedrock/test/libraries/DevFeatures.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; -// Target contract +// Libraries import { DevFeatures } from "src/libraries/DevFeatures.sol"; contract DevFeatures_isDevFeatureEnabled_Test is Test { diff --git a/packages/contracts-bedrock/test/libraries/EOA.t.sol b/packages/contracts-bedrock/test/libraries/EOA.t.sol index 69a894c90a6..cc6712eb28e 100644 --- a/packages/contracts-bedrock/test/libraries/EOA.t.sol +++ b/packages/contracts-bedrock/test/libraries/EOA.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Forge -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; // Libraries import { EOA } from "src/libraries/EOA.sol"; diff --git a/packages/contracts-bedrock/test/libraries/GasPayingToken.t.sol b/packages/contracts-bedrock/test/libraries/GasPayingToken.t.sol index c20ade9a9d6..66f4d8adea0 100644 --- a/packages/contracts-bedrock/test/libraries/GasPayingToken.t.sol +++ b/packages/contracts-bedrock/test/libraries/GasPayingToken.t.sol @@ -1,10 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Target contract +// Testing +import { Test } from "test/setup/Test.sol"; + +// Libraries import { GasPayingToken } from "src/libraries/GasPayingToken.sol"; import { Constants } from "src/libraries/Constants.sol"; -import { Test } from "forge-std/Test.sol"; import { LibString } from "@solady/utils/LibString.sol"; contract GasPayingToken_Harness { diff --git a/packages/contracts-bedrock/test/libraries/SafeCall.t.sol b/packages/contracts-bedrock/test/libraries/SafeCall.t.sol index 084c601cbcb..3f6a3bdf255 100644 --- a/packages/contracts-bedrock/test/libraries/SafeCall.t.sol +++ b/packages/contracts-bedrock/test/libraries/SafeCall.t.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Scripts -import { Config } from "scripts/libraries/Config.sol"; - -// Forge -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import { VmSafe } from "forge-std/Vm.sol"; import { StdCheatsSafe } from "forge-std/StdCheats.sol"; +// Scripts +import { Config } from "scripts/libraries/Config.sol"; + // Libraries import { LibString } from "@solady/utils/LibString.sol"; import { SafeCall } from "src/libraries/SafeCall.sol"; diff --git a/packages/contracts-bedrock/test/libraries/SemverComp.t.sol b/packages/contracts-bedrock/test/libraries/SemverComp.t.sol index b182af378ff..45f2ace041c 100644 --- a/packages/contracts-bedrock/test/libraries/SemverComp.t.sol +++ b/packages/contracts-bedrock/test/libraries/SemverComp.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Forge -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; // Libraries import { JSONParserLib } from "solady/src/utils/JSONParserLib.sol"; diff --git a/packages/contracts-bedrock/test/libraries/StaticConfig.t.sol b/packages/contracts-bedrock/test/libraries/StaticConfig.t.sol index 00c8fe20957..47f51380b47 100644 --- a/packages/contracts-bedrock/test/libraries/StaticConfig.t.sol +++ b/packages/contracts-bedrock/test/libraries/StaticConfig.t.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import { FFIInterface } from "test/setup/FFIInterface.sol"; -// Target contract +// Libraries import { StaticConfig } from "src/libraries/StaticConfig.sol"; /// @title StaticConfig_TestInit diff --git a/packages/contracts-bedrock/test/libraries/Storage.t.sol b/packages/contracts-bedrock/test/libraries/Storage.t.sol index 5a801c24afc..7ead6532477 100644 --- a/packages/contracts-bedrock/test/libraries/Storage.t.sol +++ b/packages/contracts-bedrock/test/libraries/Storage.t.sol @@ -1,9 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Target contract +// Testing +import { Test } from "test/setup/Test.sol"; + +// Contracts import { StorageSetter } from "src/universal/StorageSetter.sol"; -import { Test } from "forge-std/Test.sol"; /// @title Storage_TestInit /// @notice Reusable test initialization for `Storage` tests. diff --git a/packages/contracts-bedrock/test/libraries/TransientContext.t.sol b/packages/contracts-bedrock/test/libraries/TransientContext.t.sol index 99d6264e2ba..9d3511cf023 100644 --- a/packages/contracts-bedrock/test/libraries/TransientContext.t.sol +++ b/packages/contracts-bedrock/test/libraries/TransientContext.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; -// Target contractS +// Target contracts import { TransientContext } from "src/libraries/TransientContext.sol"; import { TransientReentrancyAware } from "src/libraries/TransientContext.sol"; diff --git a/packages/contracts-bedrock/test/libraries/rlp/RLPReader.t.sol b/packages/contracts-bedrock/test/libraries/rlp/RLPReader.t.sol index e4ba42414a9..7e6f752950c 100644 --- a/packages/contracts-bedrock/test/libraries/rlp/RLPReader.t.sol +++ b/packages/contracts-bedrock/test/libraries/rlp/RLPReader.t.sol @@ -1,8 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { stdError } from "forge-std/Test.sol"; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +import { stdError } from "forge-std/StdError.sol"; + +// Libraries import { RLPReader } from "src/libraries/rlp/RLPReader.sol"; import "src/libraries/rlp/RLPErrors.sol"; diff --git a/packages/contracts-bedrock/test/libraries/rlp/RLPWriter.t.sol b/packages/contracts-bedrock/test/libraries/rlp/RLPWriter.t.sol index 7ff838845be..a143d4e68d4 100644 --- a/packages/contracts-bedrock/test/libraries/rlp/RLPWriter.t.sol +++ b/packages/contracts-bedrock/test/libraries/rlp/RLPWriter.t.sol @@ -1,8 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Libraries import { RLPWriter } from "src/libraries/rlp/RLPWriter.sol"; -import { Test } from "forge-std/Test.sol"; /// @title RLPWriter_writeString_Test /// @notice Tests the `writeString` function of the `RLPWriter` library. diff --git a/packages/contracts-bedrock/test/libraries/trie/MerkleTrie.t.sol b/packages/contracts-bedrock/test/libraries/trie/MerkleTrie.t.sol index d746933990d..51e0cb1ae9a 100644 --- a/packages/contracts-bedrock/test/libraries/trie/MerkleTrie.t.sol +++ b/packages/contracts-bedrock/test/libraries/trie/MerkleTrie.t.sol @@ -1,10 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +import { FFIInterface } from "test/setup/FFIInterface.sol"; + +// Libraries import { MerkleTrie } from "src/libraries/trie/MerkleTrie.sol"; import { RLPReader } from "src/libraries/rlp/RLPReader.sol"; -import { FFIInterface } from "test/setup/FFIInterface.sol"; import "src/libraries/rlp/RLPErrors.sol"; contract MerkleTrie_Harness { diff --git a/packages/contracts-bedrock/test/opcm/DeployAlphabetVM.t.sol b/packages/contracts-bedrock/test/opcm/DeployAlphabetVM.t.sol index 64f9f380dc4..bc87618e0ee 100644 --- a/packages/contracts-bedrock/test/opcm/DeployAlphabetVM.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployAlphabetVM.t.sol @@ -1,13 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Scripts +import { DeployAlphabetVM } from "scripts/deploy/DeployAlphabetVM.s.sol"; // Interfaces import { IPreimageOracle } from "interfaces/cannon/IPreimageOracle.sol"; -import { DeployAlphabetVM } from "scripts/deploy/DeployAlphabetVM.s.sol"; - contract DeployAlphabetVM2_Test is Test { DeployAlphabetVM deployAlphanetVM; diff --git a/packages/contracts-bedrock/test/opcm/DeployAltDA.t.sol b/packages/contracts-bedrock/test/opcm/DeployAltDA.t.sol index 312e82d57e1..39828f6691d 100644 --- a/packages/contracts-bedrock/test/opcm/DeployAltDA.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployAltDA.t.sol @@ -1,13 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +// Scripts import { DeployAltDA } from "scripts/deploy/DeployAltDA.s.sol"; +import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; + +// Interfaces import { IDataAvailabilityChallenge } from "interfaces/L1/IDataAvailabilityChallenge.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; -import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; contract DeployAltDA_Test is Test { DeployAltDA deployAltDA; diff --git a/packages/contracts-bedrock/test/opcm/DeployAsterisc.t.sol b/packages/contracts-bedrock/test/opcm/DeployAsterisc.t.sol index f2bd971e646..4abe6967de7 100644 --- a/packages/contracts-bedrock/test/opcm/DeployAsterisc.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployAsterisc.t.sol @@ -1,15 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +// Scripts import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; +import { DeployAsterisc } from "scripts/deploy/DeployAsterisc.s.sol"; // Interfaces import { IPreimageOracle } from "interfaces/cannon/IPreimageOracle.sol"; -import { DeployAsterisc } from "scripts/deploy/DeployAsterisc.s.sol"; - contract DeployAsterisc_Test is Test { DeployAsterisc deployAsterisc; diff --git a/packages/contracts-bedrock/test/opcm/DeployDisputeGame.t.sol b/packages/contracts-bedrock/test/opcm/DeployDisputeGame.t.sol index 6931a6e13c2..f71c30d88dd 100644 --- a/packages/contracts-bedrock/test/opcm/DeployDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployDisputeGame.t.sol @@ -1,7 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Libraries +import { LibPosition } from "src/dispute/lib/LibPosition.sol"; +import { GameType } from "src/dispute/lib/Types.sol"; +import { LibString } from "@solady/utils/LibString.sol"; // Interfaces import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; @@ -14,6 +20,7 @@ import { LibPosition } from "src/dispute/lib/LibPosition.sol"; import { GameType } from "src/dispute/lib/Types.sol"; import { LibString } from "@solady/utils/LibString.sol"; +// Contracts import { PreimageOracle } from "src/cannon/PreimageOracle.sol"; import { DeployDisputeGame } from "scripts/deploy/DeployDisputeGame.s.sol"; diff --git a/packages/contracts-bedrock/test/opcm/DeployFeesDepositor.t.sol b/packages/contracts-bedrock/test/opcm/DeployFeesDepositor.t.sol index e8d934427be..d0a277673fa 100644 --- a/packages/contracts-bedrock/test/opcm/DeployFeesDepositor.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployFeesDepositor.t.sol @@ -1,18 +1,22 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; + +// Scripts +import { DeployFeesDepositor } from "scripts/deploy/DeployFeesDepositor.s.sol"; + +// Contracts +import { FeesDepositor } from "src/L1/FeesDepositor.sol"; +import { Proxy } from "src/universal/Proxy.sol"; // Interfaces import { IFeesDepositor } from "interfaces/L1/IFeesDepositor.sol"; import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; -import { DeployFeesDepositor } from "scripts/deploy/DeployFeesDepositor.s.sol"; -import { FeesDepositor } from "src/L1/FeesDepositor.sol"; -import { Proxy } from "src/universal/Proxy.sol"; -import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; - /// @title DeployFeesDepositor_Test /// @notice This test is used to test the DeployFeesDepositor script. contract DeployFeesDepositor_Test is Test { diff --git a/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol b/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol index 3136aaeb0c2..465026596ba 100644 --- a/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol @@ -2,7 +2,8 @@ pragma solidity 0.8.15; // Testing -import { Test, stdStorage, StdStorage } from "forge-std/Test.sol"; +import { Test } from "test/setup/Test.sol"; +import { stdStorage, StdStorage } from "forge-std/StdStorage.sol"; import "../setup/FeatureFlags.sol"; // Libraries diff --git a/packages/contracts-bedrock/test/opcm/DeployMIPS2.t.sol b/packages/contracts-bedrock/test/opcm/DeployMIPS2.t.sol index 71f6de0f2dd..e1a61fd2b2a 100644 --- a/packages/contracts-bedrock/test/opcm/DeployMIPS2.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployMIPS2.t.sol @@ -1,15 +1,19 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; - -// Interfaces -import { IPreimageOracle } from "interfaces/cannon/IPreimageOracle.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +// Scripts import { DeployMIPS2 } from "scripts/deploy/DeployMIPS2.s.sol"; -import { MIPS64 } from "src/cannon/MIPS64.sol"; import { StandardConstants } from "scripts/deploy/StandardConstants.sol"; +// Contracts +import { MIPS64 } from "src/cannon/MIPS64.sol"; + +// Interfaces +import { IPreimageOracle } from "interfaces/cannon/IPreimageOracle.sol"; + contract DeployMIPS2_Test is Test { DeployMIPS2 deployMIPS; diff --git a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol index 53defc933f1..f54a08e230d 100644 --- a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol @@ -1,16 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import { FeatureFlags } from "test/setup/FeatureFlags.sol"; -import { Features } from "src/libraries/Features.sol"; +// Scripts import { DeploySuperchain } from "scripts/deploy/DeploySuperchain.s.sol"; import { DeployImplementations } from "scripts/deploy/DeployImplementations.s.sol"; import { DeployOPChain } from "scripts/deploy/DeployOPChain.s.sol"; import { StandardConstants } from "scripts/deploy/StandardConstants.sol"; import { Types } from "scripts/libraries/Types.sol"; +// Libraries +import { Features } from "src/libraries/Features.sol"; + +// Interfaces import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; import { Claim, Duration, GameType, GameTypes } from "src/dispute/lib/Types.sol"; import { IPermissionedDisputeGame } from "interfaces/dispute/IPermissionedDisputeGame.sol"; diff --git a/packages/contracts-bedrock/test/opcm/DeploySuperchain.t.sol b/packages/contracts-bedrock/test/opcm/DeploySuperchain.t.sol index ae52362446b..3c8983c7b63 100644 --- a/packages/contracts-bedrock/test/opcm/DeploySuperchain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeploySuperchain.t.sol @@ -1,11 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +// Scripts +import { DeploySuperchain } from "scripts/deploy/DeploySuperchain.s.sol"; + +// Contracts import { Proxy } from "src/universal/Proxy.sol"; + +// Interfaces import { ProtocolVersion } from "interfaces/L1/IProtocolVersions.sol"; -import { DeploySuperchain } from "scripts/deploy/DeploySuperchain.s.sol"; contract DeploySuperchain_Test is Test { DeploySuperchain deploySuperchain; diff --git a/packages/contracts-bedrock/test/opcm/InteropMigration.t.sol b/packages/contracts-bedrock/test/opcm/InteropMigration.t.sol index 27c71d4329c..a98f8731ed6 100644 --- a/packages/contracts-bedrock/test/opcm/InteropMigration.t.sol +++ b/packages/contracts-bedrock/test/opcm/InteropMigration.t.sol @@ -1,13 +1,19 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +// Scripts import { InteropMigrationInput, InteropMigration, InteropMigrationOutput } from "scripts/deploy/InteropMigration.s.sol"; + +// Libraries +import { Claim } from "src/dispute/lib/Types.sol"; + +// Interfaces import { IOPContractsManagerInteropMigrator, IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; -import { Claim } from "src/dispute/lib/Types.sol"; contract InteropMigrationInput_Test is Test { InteropMigrationInput input; diff --git a/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol b/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol index ad266dccd79..d0a86a4c6a1 100644 --- a/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol +++ b/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol @@ -1,16 +1,25 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { Test } from "forge-std/Test.sol"; -import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; -import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; -import { GameType, Proposal, Hash } from "src/dispute/lib/Types.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Scripts import { SetDisputeGameImpl, SetDisputeGameImplInput } from "scripts/deploy/SetDisputeGameImpl.s.sol"; + +// Contracts import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol"; import { Proxy } from "src/universal/Proxy.sol"; import { SuperchainConfig } from "src/L1/SuperchainConfig.sol"; import { AnchorStateRegistry } from "src/dispute/AnchorStateRegistry.sol"; import { SystemConfig } from "src/L1/SystemConfig.sol"; + +// Libraries +import { GameType, Proposal, Hash } from "src/dispute/lib/Types.sol"; + +// Interfaces +import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; +import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; diff --git a/packages/contracts-bedrock/test/opcm/UpgradeOPChain.t.sol b/packages/contracts-bedrock/test/opcm/UpgradeOPChain.t.sol index 6a662631580..d713e4d8268 100644 --- a/packages/contracts-bedrock/test/opcm/UpgradeOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/UpgradeOPChain.t.sol @@ -1,13 +1,20 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; -import { Claim } from "src/dispute/lib/Types.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; -import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; +// Scripts +import { UpgradeOPChain, UpgradeOPChainInput } from "scripts/deploy/UpgradeOPChain.s.sol"; +// Contracts import { OPContractsManager } from "src/L1/OPContractsManager.sol"; -import { UpgradeOPChain, UpgradeOPChainInput } from "scripts/deploy/UpgradeOPChain.s.sol"; + +// Libraries +import { Claim } from "src/dispute/lib/Types.sol"; + +// Interfaces +import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; contract UpgradeOPChainInput_Test is Test { UpgradeOPChainInput input; diff --git a/packages/contracts-bedrock/test/opcm/UpgradeSuperchainConfig.t.sol b/packages/contracts-bedrock/test/opcm/UpgradeSuperchainConfig.t.sol index da09590493a..a822997b2e1 100644 --- a/packages/contracts-bedrock/test/opcm/UpgradeSuperchainConfig.t.sol +++ b/packages/contracts-bedrock/test/opcm/UpgradeSuperchainConfig.t.sol @@ -1,11 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; - -import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +// Scripts import { UpgradeSuperchainConfig } from "scripts/deploy/UpgradeSuperchainConfig.s.sol"; + +// Interfaces +import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; /// @title MockOPCM diff --git a/packages/contracts-bedrock/test/periphery/AssetReceiver.t.sol b/packages/contracts-bedrock/test/periphery/AssetReceiver.t.sol index 3e493984df1..45e7aa87dc5 100644 --- a/packages/contracts-bedrock/test/periphery/AssetReceiver.t.sol +++ b/packages/contracts-bedrock/test/periphery/AssetReceiver.t.sol @@ -1,10 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import { TestERC20 } from "test/mocks/TestERC20.sol"; import { TestERC721 } from "test/mocks/TestERC721.sol"; + +// Contracts import { AssetReceiver } from "src/periphery/AssetReceiver.sol"; /// @title AssetReceiver_TestInit diff --git a/packages/contracts-bedrock/test/periphery/Transactor.t.sol b/packages/contracts-bedrock/test/periphery/Transactor.t.sol index e62a484e648..a05bc2f6a53 100644 --- a/packages/contracts-bedrock/test/periphery/Transactor.t.sol +++ b/packages/contracts-bedrock/test/periphery/Transactor.t.sol @@ -1,8 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Contracts import { Transactor } from "src/periphery/Transactor.sol"; /// @title Transactor_TestInit diff --git a/packages/contracts-bedrock/test/periphery/TransferOnion.t.sol b/packages/contracts-bedrock/test/periphery/TransferOnion.t.sol index 2d4261cc93d..fcc1b9406b3 100644 --- a/packages/contracts-bedrock/test/periphery/TransferOnion.t.sol +++ b/packages/contracts-bedrock/test/periphery/TransferOnion.t.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities -import { Test } from "forge-std/Test.sol"; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; -// Target contract +// Contracts +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { TransferOnion } from "src/periphery/TransferOnion.sol"; /// @title TransferOnion_TestInit diff --git a/packages/contracts-bedrock/test/periphery/drippie/Drippie.t.sol b/packages/contracts-bedrock/test/periphery/drippie/Drippie.t.sol index e230f705902..e1869ef176d 100644 --- a/packages/contracts-bedrock/test/periphery/drippie/Drippie.t.sol +++ b/packages/contracts-bedrock/test/periphery/drippie/Drippie.t.sol @@ -1,11 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +import { SimpleStorage } from "test/mocks/SimpleStorage.sol"; + +// Contracts import { Drippie } from "src/periphery/drippie/Drippie.sol"; -import { IDripCheck } from "src/periphery/drippie/IDripCheck.sol"; import { CheckTrue } from "src/periphery/drippie/dripchecks/CheckTrue.sol"; -import { SimpleStorage } from "test/mocks/SimpleStorage.sol"; + +// Interfaces +import { IDripCheck } from "src/periphery/drippie/IDripCheck.sol"; /// @title TestDrippie /// @notice This is a wrapper contract around Drippie used for testing. Returning an entire diff --git a/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckBalanceLow.t.sol b/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckBalanceLow.t.sol index bfc051c9eac..980c46dc044 100644 --- a/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckBalanceLow.t.sol +++ b/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckBalanceLow.t.sol @@ -1,7 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Contracts import { CheckBalanceLow } from "src/periphery/drippie/dripchecks/CheckBalanceLow.sol"; /// @title CheckBalanceLow_TestInit diff --git a/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckSecrets.t.sol b/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckSecrets.t.sol index 9da99bfcd97..891c6d874d5 100644 --- a/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckSecrets.t.sol +++ b/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckSecrets.t.sol @@ -1,7 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Contracts import { CheckSecrets } from "src/periphery/drippie/dripchecks/CheckSecrets.sol"; /// @title CheckSecrets_TestInit diff --git a/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckTrue.t.sol b/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckTrue.t.sol index 4fc04862b87..ef6dccdfb5a 100644 --- a/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckTrue.t.sol +++ b/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckTrue.t.sol @@ -1,7 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Contracts import { CheckTrue } from "src/periphery/drippie/dripchecks/CheckTrue.sol"; /// @title CheckTrue_TestInit diff --git a/packages/contracts-bedrock/test/periphery/faucet/Faucet.t.sol b/packages/contracts-bedrock/test/periphery/faucet/Faucet.t.sol index 98c97df6556..7d663af9c1c 100644 --- a/packages/contracts-bedrock/test/periphery/faucet/Faucet.t.sol +++ b/packages/contracts-bedrock/test/periphery/faucet/Faucet.t.sol @@ -1,10 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +import { FaucetHelper } from "test/mocks/FaucetHelper.sol"; + +// Contracts import { Faucet } from "src/periphery/faucet/Faucet.sol"; import { AdminFaucetAuthModule } from "src/periphery/faucet/authmodules/AdminFaucetAuthModule.sol"; -import { FaucetHelper } from "test/mocks/FaucetHelper.sol"; /// @title Faucet_TestInit /// @notice Reusable test initialization for `Faucet` tests. diff --git a/packages/contracts-bedrock/test/periphery/faucet/authmodules/AdminFaucetAuthModule.t.sol b/packages/contracts-bedrock/test/periphery/faucet/authmodules/AdminFaucetAuthModule.t.sol index 2bc5e7b4d56..57ca1937a72 100644 --- a/packages/contracts-bedrock/test/periphery/faucet/authmodules/AdminFaucetAuthModule.t.sol +++ b/packages/contracts-bedrock/test/periphery/faucet/authmodules/AdminFaucetAuthModule.t.sol @@ -1,10 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +import { FaucetHelper } from "test/mocks/FaucetHelper.sol"; + +// Contracts import { AdminFaucetAuthModule } from "src/periphery/faucet/authmodules/AdminFaucetAuthModule.sol"; import { Faucet } from "src/periphery/faucet/Faucet.sol"; -import { FaucetHelper } from "test/mocks/FaucetHelper.sol"; /// @title AdminFaucetAuthModule_TestInit /// @notice Reusable test initialization for `AdminFaucetAuthModule` tests. diff --git a/packages/contracts-bedrock/test/safe-tools/SafeTestTools.sol b/packages/contracts-bedrock/test/safe-tools/SafeTestTools.sol index 15457e8aefc..35b0ef9ee8e 100644 --- a/packages/contracts-bedrock/test/safe-tools/SafeTestTools.sol +++ b/packages/contracts-bedrock/test/safe-tools/SafeTestTools.sol @@ -1,16 +1,24 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.7.0 <0.9.0; -import "forge-std/Test.sol"; -import { LibSort } from "@solady/utils/LibSort.sol"; +// Forge +import { console2 as console } from "forge-std/console2.sol"; +import { Vm } from "forge-std/Vm.sol"; + +// Testing +import "./CompatibilityFallbackHandler_1_3_0.sol"; + +// Contracts import { Safe as GnosisSafe } from "safe-contracts/Safe.sol"; +import { SafeProxyFactory as GnosisSafeProxyFactory } from "safe-contracts/proxies/SafeProxyFactory.sol"; +import { SignMessageLib } from "safe-contracts/libraries/SignMessageLib.sol"; + +// Libraries +import { LibSort } from "@solady/utils/LibSort.sol"; import { OwnerManager } from "safe-contracts/base/OwnerManager.sol"; import { ModuleManager } from "safe-contracts/base/ModuleManager.sol"; import { GuardManager } from "safe-contracts/base/GuardManager.sol"; -import { SafeProxyFactory as GnosisSafeProxyFactory } from "safe-contracts/proxies/SafeProxyFactory.sol"; import { Enum } from "safe-contracts/common/Enum.sol"; -import { SignMessageLib } from "safe-contracts/libraries/SignMessageLib.sol"; -import "./CompatibilityFallbackHandler_1_3_0.sol"; // Tools to simplify testing Safe contracts // Author: Colin Nielsen (https://github.com/colinnielsen/safe-tools) diff --git a/packages/contracts-bedrock/test/safe/LivenessGuard.t.sol b/packages/contracts-bedrock/test/safe/LivenessGuard.t.sol index 11891092201..17733a9bda3 100644 --- a/packages/contracts-bedrock/test/safe/LivenessGuard.t.sol +++ b/packages/contracts-bedrock/test/safe/LivenessGuard.t.sol @@ -1,17 +1,19 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; -import { StdUtils } from "forge-std/StdUtils.sol"; -import { StdCheats } from "forge-std/StdCheats.sol"; -import { Safe } from "safe-contracts/Safe.sol"; -import { OwnerManager } from "safe-contracts/base/OwnerManager.sol"; -import { Enum } from "safe-contracts/common/Enum.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import "test/safe-tools/SafeTestTools.sol"; -import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +// Contracts +import { Safe } from "safe-contracts/Safe.sol"; +import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import { LivenessGuard } from "src/safe/LivenessGuard.sol"; +// Libraries +import { OwnerManager } from "safe-contracts/base/OwnerManager.sol"; +import { Enum } from "safe-contracts/common/Enum.sol"; + /// @notice A wrapper contract exposing the length of the ownersBefore set in the LivenessGuard. contract LivenessGuard_WrappedGuard_Harness is LivenessGuard { using EnumerableSet for EnumerableSet.AddressSet; @@ -165,7 +167,7 @@ contract LivenessGuard_ShowLiveness_Test is LivenessGuard_TestInit { /// @title LivenessGuard_Uncategorized_Test /// @notice General tests that are not testing any function directly of the `LivenessGuard` /// contract or are testing multiple functions at once. -contract LivenessGuard_Uncategorized_Test is StdCheats, StdUtils, LivenessGuard_TestInit { +contract LivenessGuard_Uncategorized_Test is LivenessGuard_TestInit { using SafeTestLib for SafeInstance; /// @notice Enumerates the possible owner management operations diff --git a/packages/contracts-bedrock/test/safe/LivenessModule.t.sol b/packages/contracts-bedrock/test/safe/LivenessModule.t.sol index ee69895946b..12b15d5ebfa 100644 --- a/packages/contracts-bedrock/test/safe/LivenessModule.t.sol +++ b/packages/contracts-bedrock/test/safe/LivenessModule.t.sol @@ -1,14 +1,18 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; -import { Safe } from "safe-contracts/Safe.sol"; -import { OwnerManager } from "safe-contracts/base/OwnerManager.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import "test/safe-tools/SafeTestTools.sol"; +// Contracts +import { Safe } from "safe-contracts/Safe.sol"; import { LivenessModule } from "src/safe/LivenessModule.sol"; import { LivenessGuard } from "src/safe/LivenessGuard.sol"; +// Libraries +import { OwnerManager } from "safe-contracts/base/OwnerManager.sol"; + /// @title LivenessModule_TestInit /// @notice Reusable test initialization for `LivenessModule` tests. abstract contract LivenessModule_TestInit is Test, SafeTestTools { diff --git a/packages/contracts-bedrock/test/safe/LivenessModule2.t.sol b/packages/contracts-bedrock/test/safe/LivenessModule2.t.sol index e64b0b27e2d..1059c0703ec 100644 --- a/packages/contracts-bedrock/test/safe/LivenessModule2.t.sol +++ b/packages/contracts-bedrock/test/safe/LivenessModule2.t.sol @@ -1,17 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; -import { Enum } from "safe-contracts/common/Enum.sol"; -import { Safe } from "safe-contracts/Safe.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import "test/safe-tools/SafeTestTools.sol"; -import { Constants } from "src/libraries/Constants.sol"; +import { DummyGuard } from "test/mocks/DummyGuard.sol"; +// Contracts +import { Safe } from "safe-contracts/Safe.sol"; import { LivenessModule2 } from "src/safe/LivenessModule2.sol"; import { SaferSafes } from "src/safe/SaferSafes.sol"; + +// Libraries +import { Enum } from "safe-contracts/common/Enum.sol"; +import { Constants } from "src/libraries/Constants.sol"; import { ModuleManager } from "safe-contracts/base/ModuleManager.sol"; import { GuardManager } from "safe-contracts/base/GuardManager.sol"; -import { DummyGuard } from "test/mocks/DummyGuard.sol"; /// @title LivenessModule2_TestUtils /// @notice Reusable helper methods for LivenessModule2 tests. diff --git a/packages/contracts-bedrock/test/safe/SafeSigners.t.sol b/packages/contracts-bedrock/test/safe/SafeSigners.t.sol index f59ad036064..b2aec90c5a1 100644 --- a/packages/contracts-bedrock/test/safe/SafeSigners.t.sol +++ b/packages/contracts-bedrock/test/safe/SafeSigners.t.sol @@ -1,10 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; -import { SafeSigners } from "src/safe/SafeSigners.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import "test/safe-tools/SafeTestTools.sol"; +// Contracts +import { SafeSigners } from "src/safe/SafeSigners.sol"; + /// @title SafeSigners_TestInit /// @notice Reusable test initialization for `SafeSigners` tests. abstract contract SafeSigners_TestInit is Test { diff --git a/packages/contracts-bedrock/test/safe/TimelockGuard.t.sol b/packages/contracts-bedrock/test/safe/TimelockGuard.t.sol index 2333c23208d..0bb2e4b84c0 100644 --- a/packages/contracts-bedrock/test/safe/TimelockGuard.t.sol +++ b/packages/contracts-bedrock/test/safe/TimelockGuard.t.sol @@ -1,15 +1,22 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; -import { Safe } from "safe-contracts/Safe.sol"; -import { GuardManager } from "safe-contracts/base/GuardManager.sol"; -import { ITransactionGuard } from "interfaces/safe/ITransactionGuard.sol"; +// Testing import "test/safe-tools/SafeTestTools.sol"; +import { Test } from "test/setup/Test.sol"; +import { stdStorage, StdStorage } from "forge-std/StdStorage.sol"; +// Contracts +import { Safe } from "safe-contracts/Safe.sol"; import { TimelockGuard } from "src/safe/TimelockGuard.sol"; import { SaferSafes } from "src/safe/SaferSafes.sol"; +// Libraries +import { GuardManager } from "safe-contracts/base/GuardManager.sol"; + +// Interfaces +import { ITransactionGuard } from "interfaces/safe/ITransactionGuard.sol"; + using TransactionBuilder for TransactionBuilder.Transaction; /// @title TransactionBuilder diff --git a/packages/contracts-bedrock/test/scripts/DeployOwnership.t.sol b/packages/contracts-bedrock/test/scripts/DeployOwnership.t.sol index 7f9df4cd260..eec100fa85d 100644 --- a/packages/contracts-bedrock/test/scripts/DeployOwnership.t.sol +++ b/packages/contracts-bedrock/test/scripts/DeployOwnership.t.sol @@ -1,19 +1,27 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; +// Forge +import { StdCheatsSafe } from "forge-std/StdCheats.sol"; + +// Testing +import { Test } from "test/setup/Test.sol"; + +// Scripts import { DeployOwnership, SafeConfig, SecurityCouncilConfig, LivenessModuleConfig } from "scripts/deploy/DeployOwnership.s.sol"; -import { Test } from "forge-std/Test.sol"; +// Contracts import { Safe } from "safe-contracts/Safe.sol"; -import { ModuleManager } from "safe-contracts/base/ModuleManager.sol"; - import { LivenessModule2 } from "src/safe/LivenessModule2.sol"; +// Libraries +import { ModuleManager } from "safe-contracts/base/ModuleManager.sol"; + contract DeployOwnershipTest is Test, DeployOwnership { address internal constant SENTINEL_MODULES = address(0x1); @@ -22,6 +30,10 @@ contract DeployOwnershipTest is Test, DeployOwnership { run(); } + function makeAddr(string memory _name) internal override(Test, StdCheatsSafe) returns (address) { + return Test.makeAddr(_name); + } + /// @dev Helper function to make assertions on basic Safe config properties. function _checkSafeConfig(SafeConfig memory _safeConfig, Safe _safe) internal view { assertEq(_safe.getThreshold(), _safeConfig.threshold); diff --git a/packages/contracts-bedrock/test/scripts/FetchChainInfo.t.sol b/packages/contracts-bedrock/test/scripts/FetchChainInfo.t.sol index 95a93038751..906c47d37ff 100644 --- a/packages/contracts-bedrock/test/scripts/FetchChainInfo.t.sol +++ b/packages/contracts-bedrock/test/scripts/FetchChainInfo.t.sol @@ -1,8 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Scripts import { FetchChainInfo, FetchChainInfoInput, FetchChainInfoOutput } from "scripts/FetchChainInfo.s.sol"; + +// Libraries import { GameTypes, GameType } from "src/dispute/lib/Types.sol"; import { LibGameType } from "src/dispute/lib/LibUDT.sol"; diff --git a/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol b/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol index f4943f82ea8..e157c1cff99 100644 --- a/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol +++ b/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol @@ -1,11 +1,18 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; + +// Scripts import { L2Genesis } from "scripts/L2Genesis.s.sol"; -import { Predeploys } from "src/libraries/Predeploys.sol"; import { LATEST_FORK } from "scripts/libraries/Config.sol"; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; + +// Interfaces import { ISuperchainRevSharesCalculator } from "interfaces/L2/ISuperchainRevSharesCalculator.sol"; import { ISequencerFeeVault } from "interfaces/L2/ISequencerFeeVault.sol"; import { IBaseFeeVault } from "interfaces/L2/IBaseFeeVault.sol"; diff --git a/packages/contracts-bedrock/test/setup/CommonTest.sol b/packages/contracts-bedrock/test/setup/CommonTest.sol index 529534b7ceb..5185f52d8ca 100644 --- a/packages/contracts-bedrock/test/setup/CommonTest.sol +++ b/packages/contracts-bedrock/test/setup/CommonTest.sol @@ -1,10 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -// Forge -import { Test } from "forge-std/Test.sol"; - // Testing +import { Test } from "test/setup/Test.sol"; import { Setup } from "test/setup/Setup.sol"; import { Events } from "test/setup/Events.sol"; import { FFIInterface } from "test/setup/FFIInterface.sol"; diff --git a/packages/contracts-bedrock/test/setup/Test.sol b/packages/contracts-bedrock/test/setup/Test.sol new file mode 100644 index 00000000000..5ce331b9109 --- /dev/null +++ b/packages/contracts-bedrock/test/setup/Test.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Forge +import { Test as ForgeTest } from "forge-std/Test.sol"; + +/// @title Test +/// @notice Test is a minimal extension of the Test contract with op-specific tweaks. +abstract contract Test is ForgeTest { + /// @notice Makes an address without a private key, labels it, and cleans it. + /// @param _name The name of the address. + /// @return The address. + function makeAddr(string memory _name) internal virtual override returns (address) { + address addr = address(uint160(uint256(keccak256(abi.encode(_name))))); + destroyAccount(addr, address(0)); + vm.label(addr, _name); + return addr; + } +} diff --git a/packages/contracts-bedrock/test/universal/CrossDomainMessenger.t.sol b/packages/contracts-bedrock/test/universal/CrossDomainMessenger.t.sol index dd9cf4eb1e3..3d11320d33a 100644 --- a/packages/contracts-bedrock/test/universal/CrossDomainMessenger.t.sol +++ b/packages/contracts-bedrock/test/universal/CrossDomainMessenger.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; // Libraries diff --git a/packages/contracts-bedrock/test/universal/Proxy.t.sol b/packages/contracts-bedrock/test/universal/Proxy.t.sol index a19cb26d8af..b3b7b52342e 100644 --- a/packages/contracts-bedrock/test/universal/Proxy.t.sol +++ b/packages/contracts-bedrock/test/universal/Proxy.t.sol @@ -1,10 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Scripts +import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; + +// Libraries import { Bytes32AddressLib } from "@rari-capital/solmate/src/utils/Bytes32AddressLib.sol"; + +// Interfaces import { IProxy } from "interfaces/universal/IProxy.sol"; -import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; contract Proxy_SimpleStorage_Harness { mapping(uint256 => uint256) internal store; diff --git a/packages/contracts-bedrock/test/universal/ProxyAdmin.t.sol b/packages/contracts-bedrock/test/universal/ProxyAdmin.t.sol index b86196e60bb..f81ec40007e 100644 --- a/packages/contracts-bedrock/test/universal/ProxyAdmin.t.sol +++ b/packages/contracts-bedrock/test/universal/ProxyAdmin.t.sol @@ -2,9 +2,12 @@ pragma solidity 0.8.15; // Testing -import { Test } from "forge-std/Test.sol"; +import { Test } from "test/setup/Test.sol"; import { Proxy_SimpleStorage_Harness } from "test/universal/Proxy.t.sol"; +// Scripts +import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; + // Interfaces import { IAddressManager } from "interfaces/legacy/IAddressManager.sol"; import { IL1ChugSplashProxy } from "interfaces/legacy/IL1ChugSplashProxy.sol"; @@ -12,8 +15,6 @@ import { IResolvedDelegateProxy } from "interfaces/legacy/IResolvedDelegateProxy import { IProxy } from "interfaces/universal/IProxy.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; -import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; - /// @title ProxyAdmin_TestInit /// @notice Reusable test initialization for `ProxyAdmin` tests. abstract contract ProxyAdmin_TestInit is Test { diff --git a/packages/contracts-bedrock/test/universal/ReinitializableBase.t.sol b/packages/contracts-bedrock/test/universal/ReinitializableBase.t.sol index 805da5b5fab..a6d61ec6a32 100644 --- a/packages/contracts-bedrock/test/universal/ReinitializableBase.t.sol +++ b/packages/contracts-bedrock/test/universal/ReinitializableBase.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; // Testing -import { Test } from "forge-std/Test.sol"; +import { Test } from "test/setup/Test.sol"; // Contracts import { ReinitializableBase } from "src/universal/ReinitializableBase.sol"; diff --git a/packages/contracts-bedrock/test/universal/WETH98.t.sol b/packages/contracts-bedrock/test/universal/WETH98.t.sol index 96065c8c890..8173dec67d3 100644 --- a/packages/contracts-bedrock/test/universal/WETH98.t.sol +++ b/packages/contracts-bedrock/test/universal/WETH98.t.sol @@ -2,12 +2,14 @@ pragma solidity 0.8.15; // Testing -import { Test } from "forge-std/Test.sol"; +import { Test } from "test/setup/Test.sol"; -// Contracts -import { IWETH98 } from "interfaces/universal/IWETH98.sol"; +// Scripts import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; +// Interfaces +import { IWETH98 } from "interfaces/universal/IWETH98.sol"; + /// @title WETH98_TestInit /// @notice Reusable test initialization for `WETH98` tests. abstract contract WETH98_TestInit is Test { diff --git a/packages/contracts-bedrock/test/vendor/AddressAliasHelper.t.sol b/packages/contracts-bedrock/test/vendor/AddressAliasHelper.t.sol index b803abba9fa..68d838f6921 100644 --- a/packages/contracts-bedrock/test/vendor/AddressAliasHelper.t.sol +++ b/packages/contracts-bedrock/test/vendor/AddressAliasHelper.t.sol @@ -1,7 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Libraries import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol"; /// @title AddressAliasHelper_ApplyL1ToL2Alias_Test diff --git a/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol b/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol index b12bade2a9f..c8077d79b04 100644 --- a/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol +++ b/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol @@ -1,13 +1,22 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import { Test } from "forge-std/Test.sol"; -import { IOptimismSuperchainERC20 } from "interfaces/L2/IOptimismSuperchainERC20.sol"; -import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Scripts import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; -import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; + +// Contracts +import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol"; + +// Libraries import { Types } from "src/libraries/Types.sol"; +// Interfaces +import { IOptimismSuperchainERC20 } from "interfaces/L2/IOptimismSuperchainERC20.sol"; +import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; + /// @title InitializerOZv5_Test /// @dev Ensures that the `initialize()` function on contracts cannot be called more than /// once. Tests the contracts inheriting from `Initializable` from OpenZeppelin Contracts v5. From b9dc5808f61c160de048fb7db39a04fe4fba4574 Mon Sep 17 00:00:00 2001 From: steven <12021290+stevennevins@users.noreply.github.com> Date: Thu, 18 Dec 2025 16:44:58 -0500 Subject: [PATCH 11/13] test: update succinct tests to follow our conventions (#18452) * test: update succinct tests to follow our conventions * fix: test naming and remove unused imports * chore: add AccessManager snapshots Contract changed from abstract to concrete, now needs snapshot files. * fix: specify GameOver revert in expectRevert * fix: snapshot for access manager * fix: create interface for contract * fix: snapshot semver bump * fix: bump semver * chore: bump semver * fix: semver lock mismatch * fix: enforce strict pragma for deterministic init code hashes * chore: fix pragma * fix: lock pragma versions * fix: semver lock passing * chore: cleanup whitespace * chore: bump semver * chore: reduce diff * fix: strict pragma * feat: fix for ci and compiler profile * chore: forge fmt * fix: regenerate semver-lock with clean build The semver-lock.json had incorrect hashes due to stale build artifacts. Clean build restores hashes to match develop. * fix: semgrep for proxy * fix: workaround for potential cache issue * chore: clean build semver bump * fix: remove troubleshooting * chore: bump semver * fix: compiler setting issues * chore: remove helper for vm.getCode * refactor: reuse DisputeGameFactory_TestInit in OPSuccinct tests Remove duplicated setup logic from OPSuccinctFaultDisputeGame tests by inheriting from DisputeGameFactory_TestInit instead of Test. - Add setupOPSuccinctFaultDisputeGame() helper to DisputeGameFactory_TestInit - Add OPSuccinctGameParams struct for configurable game parameters - Remove ~100 lines of duplicated contract deployment code - Update variable references to use inherited contracts * refactor: rename OPSuccinctFaultDisputeGame to OptimisticZkGame Align contract naming with op-challenger's OptimisticZKGameType. Renames: - IOPSuccinctFaultDisputeGame -> IOptimisticZkGame - OPSuccinctFaultDisputeGame -> OptimisticZkGame - Test contracts and helper functions updated accordingly * chore: remove unused import * chore: bump snapshots * fix: update abi_loader.go embed path after rename * fix: update snapshots * fix: circle ci cache fix * fix: ci cache * chore: add back placeholder for opsuccinet * chore: remove cache-lite * fix: make OptimisticZkGame tests compatible with fork mode Replace hardcoded block numbers and game indices with dynamically calculated values based on anchor state. This allows tests to pass in both regular mode (empty factory) and fork mode (mainnet fork with existing games). * fix: skip OptimisticZkGame tests on fork Tests create games with specific state assumptions that conflict with existing state on forked networks. * fix: use same pattern as fault dispute game * chore: check CI skpping for tests for op zk * fix: remove OPSuccinctFaultDisputeGame from compiler restrictions * fix: move OptimisticZk type into GameTypes and update usage * fix: bump semver --- .circleci/config.yml | 2 +- packages/contracts-bedrock/foundry.toml | 2 - ...tDisputeGame.sol => IOptimisticZkGame.sol} | 6 +- .../snapshots/abi/AccessManager.json | 285 +++++++ ...DisputeGame.json => OptimisticZkGame.json} | 6 +- .../contracts-bedrock/snapshots/abi_loader.go | 2 +- .../snapshots/semver-lock.json | 6 +- .../storageLayout/AccessManager.json | 23 + ...DisputeGame.json => OptimisticZkGame.json} | 2 +- .../src/dispute/lib/Errors.sol | 2 +- .../src/dispute/lib/Types.sol | 6 +- .../src/dispute/zk/AccessManager.sol | 9 +- ...ltDisputeGame.sol => OptimisticZkGame.sol} | 16 +- .../test/dispute/DisputeGameFactory.t.sol | 59 ++ .../test/dispute/zk/OptimisticZkGame.t.sol | 732 ++++++++++++++++++ .../test/dispute/zk/mocks/SP1MockVerifier.sol | 12 + .../test/vendor/Initializable.t.sol | 2 +- 17 files changed, 1140 insertions(+), 32 deletions(-) rename packages/contracts-bedrock/interfaces/dispute/zk/{IOPSuccinctFaultDisputeGame.sol => IOptimisticZkGame.sol} (96%) create mode 100644 packages/contracts-bedrock/snapshots/abi/AccessManager.json rename packages/contracts-bedrock/snapshots/abi/{OPSuccinctFaultDisputeGame.json => OptimisticZkGame.json} (98%) create mode 100644 packages/contracts-bedrock/snapshots/storageLayout/AccessManager.json rename packages/contracts-bedrock/snapshots/storageLayout/{OPSuccinctFaultDisputeGame.json => OptimisticZkGame.json} (95%) rename packages/contracts-bedrock/src/dispute/zk/{OPSuccinctFaultDisputeGame.sol => OptimisticZkGame.sol} (98%) create mode 100644 packages/contracts-bedrock/test/dispute/zk/OptimisticZkGame.t.sol create mode 100644 packages/contracts-bedrock/test/dispute/zk/mocks/SP1MockVerifier.sol diff --git a/.circleci/config.yml b/.circleci/config.yml index 278fad92f0b..ded47d01b96 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1373,7 +1373,7 @@ jobs: command: forge --version - run: name: Pull cached artifacts - command: bash scripts/ops/pull-artifacts.sh --fallback-to-latest + command: bash scripts/ops/pull-artifacts.sh working_directory: packages/contracts-bedrock - run: name: Run checks diff --git a/packages/contracts-bedrock/foundry.toml b/packages/contracts-bedrock/foundry.toml index f03a6bac0c9..ad3b0e163ea 100644 --- a/packages/contracts-bedrock/foundry.toml +++ b/packages/contracts-bedrock/foundry.toml @@ -25,7 +25,6 @@ compilation_restrictions = [ { paths = "src/dispute/v2/FaultDisputeGameV2.sol", optimizer_runs = 5000 }, { paths = "src/dispute/PermissionedDisputeGame.sol", optimizer_runs = 5000 }, { paths = "src/dispute/v2/PermissionedDisputeGameV2.sol", optimizer_runs = 5000 }, - { paths = "src/dispute/zk/OPSuccinctFaultDisputeGame.sol", optimizer_runs = 5000 }, { paths = "src/dispute/SuperFaultDisputeGame.sol", optimizer_runs = 5000 }, { paths = "src/dispute/SuperPermissionedDisputeGame.sol", optimizer_runs = 5000 }, { paths = "src/L1/OPContractsManager.sol", optimizer_runs = 5000 }, @@ -157,7 +156,6 @@ compilation_restrictions = [ { paths = "src/dispute/v2/FaultDisputeGameV2.sol", optimizer_runs = 0 }, { paths = "src/dispute/PermissionedDisputeGame.sol", optimizer_runs = 0 }, { paths = "src/dispute/v2/PermissionedDisputeGameV2.sol", optimizer_runs = 0 }, - { paths = "src/dispute/zk/OPSuccinctFaultDisputeGame.sol", optimizer_runs = 0 }, { paths = "src/dispute/SuperFaultDisputeGame.sol", optimizer_runs = 0 }, { paths = "src/dispute/SuperPermissionedDisputeGame.sol", optimizer_runs = 0 }, { paths = "src/L1/OPContractsManager.sol", optimizer_runs = 0 }, diff --git a/packages/contracts-bedrock/interfaces/dispute/zk/IOPSuccinctFaultDisputeGame.sol b/packages/contracts-bedrock/interfaces/dispute/zk/IOptimisticZkGame.sol similarity index 96% rename from packages/contracts-bedrock/interfaces/dispute/zk/IOPSuccinctFaultDisputeGame.sol rename to packages/contracts-bedrock/interfaces/dispute/zk/IOptimisticZkGame.sol index 9d852b5f76f..748ccb06642 100644 --- a/packages/contracts-bedrock/interfaces/dispute/zk/IOPSuccinctFaultDisputeGame.sol +++ b/packages/contracts-bedrock/interfaces/dispute/zk/IOptimisticZkGame.sol @@ -18,9 +18,9 @@ import { ISP1Verifier } from "src/dispute/zk/ISP1Verifier.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; import { AccessManager } from "src/dispute/zk/AccessManager.sol"; -/// @title IOPSuccinctFaultDisputeGame -/// @notice Interface for the OPSuccinctFaultDisputeGame contract. -interface IOPSuccinctFaultDisputeGame is IDisputeGame, ISemver { +/// @title IOptimisticZkGame +/// @notice Interface for the OptimisticZkGame contract. +interface IOptimisticZkGame is IDisputeGame, ISemver { enum ProposalStatus { Unchallenged, Challenged, diff --git a/packages/contracts-bedrock/snapshots/abi/AccessManager.json b/packages/contracts-bedrock/snapshots/abi/AccessManager.json new file mode 100644 index 00000000000..adcdb13fd52 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/AccessManager.json @@ -0,0 +1,285 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "_fallbackTimeout", + "type": "uint256" + }, + { + "internalType": "contract IDisputeGameFactory", + "name": "_disputeGameFactory", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "DEPLOYMENT_TIMESTAMP", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "DISPUTE_GAME_FACTORY", + "outputs": [ + { + "internalType": "contract IDisputeGameFactory", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "FALLBACK_TIMEOUT", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "challengers", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getLastProposalTimestamp", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_challenger", + "type": "address" + } + ], + "name": "isAllowedChallenger", + "outputs": [ + { + "internalType": "bool", + "name": "allowed_", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_proposer", + "type": "address" + } + ], + "name": "isAllowedProposer", + "outputs": [ + { + "internalType": "bool", + "name": "allowed_", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isProposalPermissionlessMode", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "proposers", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_challenger", + "type": "address" + }, + { + "internalType": "bool", + "name": "_allowed", + "type": "bool" + } + ], + "name": "setChallenger", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_proposer", + "type": "address" + }, + { + "internalType": "bool", + "name": "_allowed", + "type": "bool" + } + ], + "name": "setProposer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "challenger", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "allowed", + "type": "bool" + } + ], + "name": "ChallengerPermissionUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "proposer", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "allowed", + "type": "bool" + } + ], + "name": "ProposerPermissionUpdated", + "type": "event" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OPSuccinctFaultDisputeGame.json b/packages/contracts-bedrock/snapshots/abi/OptimisticZkGame.json similarity index 98% rename from packages/contracts-bedrock/snapshots/abi/OPSuccinctFaultDisputeGame.json rename to packages/contracts-bedrock/snapshots/abi/OptimisticZkGame.json index 9e60e00347a..0109d574c03 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPSuccinctFaultDisputeGame.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimisticZkGame.json @@ -112,7 +112,7 @@ "name": "challenge", "outputs": [ { - "internalType": "enum OPSuccinctFaultDisputeGame.ProposalStatus", + "internalType": "enum OptimisticZkGame.ProposalStatus", "name": "", "type": "uint8" } @@ -171,7 +171,7 @@ "type": "bytes32" }, { - "internalType": "enum OPSuccinctFaultDisputeGame.ProposalStatus", + "internalType": "enum OptimisticZkGame.ProposalStatus", "name": "status", "type": "uint8" }, @@ -413,7 +413,7 @@ "name": "prove", "outputs": [ { - "internalType": "enum OPSuccinctFaultDisputeGame.ProposalStatus", + "internalType": "enum OptimisticZkGame.ProposalStatus", "name": "", "type": "uint8" } diff --git a/packages/contracts-bedrock/snapshots/abi_loader.go b/packages/contracts-bedrock/snapshots/abi_loader.go index 8a69f9321a9..83119016ae9 100644 --- a/packages/contracts-bedrock/snapshots/abi_loader.go +++ b/packages/contracts-bedrock/snapshots/abi_loader.go @@ -16,7 +16,7 @@ var superFaultDisputeGame []byte //go:embed abi/FaultDisputeGame.json var faultDisputeGame []byte -//go:embed abi/OPSuccinctFaultDisputeGame.json +//go:embed abi/OptimisticZkGame.json var zkDisputeGame []byte //go:embed abi/PreimageOracle.json diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 627b2b0cbbd..610fe404992 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -219,9 +219,9 @@ "initCodeHash": "0x9896fd04e9a3f9fe4f1d6e93eb298b37a6bfa33424aa705e68cc58d0ba7f3f90", "sourceCodeHash": "0xc0ff6e93b6e2b9111c11e81b5df8948ab71d02b9d2c4dfda982fcb615519f1f7" }, - "src/dispute/zk/OPSuccinctFaultDisputeGame.sol:OPSuccinctFaultDisputeGame": { - "initCodeHash": "0xb9d0d9ca4df242f188b2d5be7d692459a12409a67a6504ef44ef589c6ca2c1a9", - "sourceCodeHash": "0x85f80adb845f59e9137d462e219c0cdba27058be77d855075e286aa316735aa0" + "src/dispute/zk/OptimisticZkGame.sol:OptimisticZkGame": { + "initCodeHash": "0x0e905fc81f45b1e9aa786e66bfe70b3ec4abd8d550be5d4c8f43cdad6f2618b2", + "sourceCodeHash": "0x162408c90e8d9d8afd982e9825db093eba5c387cbe1145c1a9b86e2361547138" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x2e0ef4c341367eb59cc6c25190c64eff441d3fe130189da91d4d126f6bdbc9b5", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/AccessManager.json b/packages/contracts-bedrock/snapshots/storageLayout/AccessManager.json new file mode 100644 index 00000000000..dbeafc3e604 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/AccessManager.json @@ -0,0 +1,23 @@ +[ + { + "bytes": "20", + "label": "_owner", + "offset": 0, + "slot": "0", + "type": "address" + }, + { + "bytes": "32", + "label": "proposers", + "offset": 0, + "slot": "1", + "type": "mapping(address => bool)" + }, + { + "bytes": "32", + "label": "challengers", + "offset": 0, + "slot": "2", + "type": "mapping(address => bool)" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OPSuccinctFaultDisputeGame.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimisticZkGame.json similarity index 95% rename from packages/contracts-bedrock/snapshots/storageLayout/OPSuccinctFaultDisputeGame.json rename to packages/contracts-bedrock/snapshots/storageLayout/OptimisticZkGame.json index d50456001fd..34ce5c1cfc3 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OPSuccinctFaultDisputeGame.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimisticZkGame.json @@ -32,7 +32,7 @@ "label": "claimData", "offset": 0, "slot": "1", - "type": "struct OPSuccinctFaultDisputeGame.ClaimData" + "type": "struct OptimisticZkGame.ClaimData" }, { "bytes": "32", diff --git a/packages/contracts-bedrock/src/dispute/lib/Errors.sol b/packages/contracts-bedrock/src/dispute/lib/Errors.sol index fa23f8d7d86..5779fc3f92e 100644 --- a/packages/contracts-bedrock/src/dispute/lib/Errors.sol +++ b/packages/contracts-bedrock/src/dispute/lib/Errors.sol @@ -151,7 +151,7 @@ error GamePaused(); error InvalidGameArgsLength(); //////////////////////////////////////////////////////////////// -// `OPSuccinctFaultDisputeGame` Errors // +// `OptimisticZkGame` Errors // //////////////////////////////////////////////////////////////// /// @notice Thrown when the claim has already been challenged. diff --git a/packages/contracts-bedrock/src/dispute/lib/Types.sol b/packages/contracts-bedrock/src/dispute/lib/Types.sol index 54f795db549..eb9438e2b5b 100644 --- a/packages/contracts-bedrock/src/dispute/lib/Types.sol +++ b/packages/contracts-bedrock/src/dispute/lib/Types.sol @@ -89,6 +89,8 @@ library GameTypes { /// @notice A dispute game type that uses RISC Zero's Kailua GameType internal constant KAILUA = GameType.wrap(1337); + + GameType internal constant OPTIMISTIC_ZK_GAME_TYPE = GameType.wrap(10); } /// @title VMStatuses @@ -127,11 +129,9 @@ library LocalPreimageKey { } //////////////////////////////////////////////////////////////// -// `OPSuccinctFaultDisputeGame` Types // +// `OptimisticZkGame` Types // //////////////////////////////////////////////////////////////// -uint32 constant OP_SUCCINCT_FAULT_DISPUTE_GAME_TYPE = 42; - /// @notice The public values committed to for an OP Succinct aggregation program. struct AggregationOutputs { bytes32 l1Head; diff --git a/packages/contracts-bedrock/src/dispute/zk/AccessManager.sol b/packages/contracts-bedrock/src/dispute/zk/AccessManager.sol index fe2d5fdb0ad..b03b034a1a3 100644 --- a/packages/contracts-bedrock/src/dispute/zk/AccessManager.sol +++ b/packages/contracts-bedrock/src/dispute/zk/AccessManager.sol @@ -1,15 +1,14 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; +pragma solidity 0.8.15; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; -import { GameType } from "src/dispute/lib/Types.sol"; +import { GameType, GameTypes } from "src/dispute/lib/Types.sol"; import { Timestamp } from "src/dispute/lib/LibUDT.sol"; -import { OP_SUCCINCT_FAULT_DISPUTE_GAME_TYPE } from "src/dispute/lib/Types.sol"; /// @title AccessManager /// @notice Manages permissions for dispute game proposers and challengers. -abstract contract AccessManager is Ownable { +contract AccessManager is Ownable { //////////////////////////////////////////////////////////////// // Events // //////////////////////////////////////////////////////////////// @@ -79,7 +78,7 @@ abstract contract AccessManager is Ownable { /// @return The last proposal timestamp. function getLastProposalTimestamp() public view returns (uint256) { // Get the latest game to check its timestamp. - GameType gameType = GameType.wrap(OP_SUCCINCT_FAULT_DISPUTE_GAME_TYPE); + GameType gameType = GameTypes.OPTIMISTIC_ZK_GAME_TYPE; uint256 numGames = DISPUTE_GAME_FACTORY.gameCount(); // Early return if no games exist. diff --git a/packages/contracts-bedrock/src/dispute/zk/OPSuccinctFaultDisputeGame.sol b/packages/contracts-bedrock/src/dispute/zk/OptimisticZkGame.sol similarity index 98% rename from packages/contracts-bedrock/src/dispute/zk/OPSuccinctFaultDisputeGame.sol rename to packages/contracts-bedrock/src/dispute/zk/OptimisticZkGame.sol index f7c1e6e7639..ee9ab2589b1 100644 --- a/packages/contracts-bedrock/src/dispute/zk/OPSuccinctFaultDisputeGame.sol +++ b/packages/contracts-bedrock/src/dispute/zk/OptimisticZkGame.sol @@ -13,7 +13,7 @@ import { Timestamp, Proposal } from "src/dispute/lib/Types.sol"; -import { AggregationOutputs, OP_SUCCINCT_FAULT_DISPUTE_GAME_TYPE } from "src/dispute/lib/Types.sol"; +import { AggregationOutputs, GameTypes } from "src/dispute/lib/Types.sol"; import { AlreadyInitialized, BadAuth, @@ -43,10 +43,10 @@ import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.so // Contracts import { AccessManager } from "src/dispute/zk/AccessManager.sol"; -/// @title OPSuccinctFaultDisputeGame +/// @title OptimisticZkGame /// @notice An implementation of the `IFaultDisputeGame` interface. /// @dev Derived from https://github.com/succinctlabs/op-succinct (at commit c13844a9bbc330cca69eef2538d8f8ec123e1653) -contract OPSuccinctFaultDisputeGame is Clone, ISemver, IDisputeGame { +contract OptimisticZkGame is Clone, ISemver, IDisputeGame { //////////////////////////////////////////////////////////////// // Enums // //////////////////////////////////////////////////////////////// @@ -146,8 +146,8 @@ contract OPSuccinctFaultDisputeGame is Clone, ISemver, IDisputeGame { AccessManager internal immutable ACCESS_MANAGER; /// @notice Semantic version. - /// @custom:semver 0.0.0 - string public constant version = "0.0.0"; + /// @custom:semver 0.0.1 + string public constant version = "0.0.1"; /// @notice The starting timestamp of the game. Timestamp public createdAt; @@ -202,7 +202,7 @@ contract OPSuccinctFaultDisputeGame is Clone, ISemver, IDisputeGame { AccessManager _accessManager ) { // Set up initial game state. - GAME_TYPE = GameType.wrap(OP_SUCCINCT_FAULT_DISPUTE_GAME_TYPE); + GAME_TYPE = GameTypes.OPTIMISTIC_ZK_GAME_TYPE; MAX_CHALLENGE_DURATION = _maxChallengeDuration; MAX_PROVE_DURATION = _maxProveDuration; DISPUTE_GAME_FACTORY = _disputeGameFactory; @@ -279,8 +279,8 @@ contract OPSuccinctFaultDisputeGame is Clone, ISemver, IDisputeGame { } startingProposal = Proposal({ - l2SequenceNumber: OPSuccinctFaultDisputeGame(address(proxy)).l2SequenceNumber(), - root: Hash.wrap(OPSuccinctFaultDisputeGame(address(proxy)).rootClaim().raw()) + l2SequenceNumber: OptimisticZkGame(address(proxy)).l2SequenceNumber(), + root: Hash.wrap(OptimisticZkGame(address(proxy)).rootClaim().raw()) }); // INVARIANT: The parent game must be a valid game. diff --git a/packages/contracts-bedrock/test/dispute/DisputeGameFactory.t.sol b/packages/contracts-bedrock/test/dispute/DisputeGameFactory.t.sol index e17fde3c0a6..f628b17cf7b 100644 --- a/packages/contracts-bedrock/test/dispute/DisputeGameFactory.t.sol +++ b/packages/contracts-bedrock/test/dispute/DisputeGameFactory.t.sol @@ -25,6 +25,12 @@ import { IPermissionedDisputeGameV2 } from "interfaces/dispute/v2/IPermissionedD import { ISuperPermissionedDisputeGame } from "interfaces/dispute/ISuperPermissionedDisputeGame.sol"; // Mocks import { AlphabetVM } from "test/mocks/AlphabetVM.sol"; +import { SP1MockVerifier } from "test/dispute/zk/mocks/SP1MockVerifier.sol"; + +// OptimisticZk +import { OptimisticZkGame } from "src/dispute/zk/OptimisticZkGame.sol"; +import { AccessManager } from "src/dispute/zk/AccessManager.sol"; +import { ISP1Verifier } from "src/dispute/zk/ISP1Verifier.sol"; /// @notice A fake clone used for testing the `DisputeGameFactory` contract's `create` function. contract DisputeGameFactory_FakeClone_Harness { @@ -388,6 +394,59 @@ abstract contract DisputeGameFactory_TestInit is CommonTest { }); _setGame(gameImpl_, GameTypes.SUPER_PERMISSIONED_CANNON, _implArgs); } + + /// @notice Parameters for OptimisticZk game setup + struct OptimisticZkGameParams { + Duration maxChallengeDuration; + Duration maxProveDuration; + address proposer; + address challenger; + bytes32 rollupConfigHash; + bytes32 aggregationVkey; + bytes32 rangeVkeyCommitment; + uint256 challengerBond; + } + + /// @notice Sets up an OptimisticZk game implementation + function setupOptimisticZkGame(OptimisticZkGameParams memory _params) + internal + returns (address gameImpl_, AccessManager accessManager_, ISP1Verifier sp1Verifier_) + { + // Deploy mock verifier + sp1Verifier_ = ISP1Verifier(address(new SP1MockVerifier())); + + // Deploy access manager + accessManager_ = new AccessManager(2 weeks, disputeGameFactory); + accessManager_.setProposer(_params.proposer, true); + accessManager_.setChallenger(_params.challenger, true); + + // Deploy game implementation + gameImpl_ = address( + new OptimisticZkGame( + _params.maxChallengeDuration, + _params.maxProveDuration, + disputeGameFactory, + sp1Verifier_, + _params.rollupConfigHash, + _params.aggregationVkey, + _params.rangeVkeyCommitment, + _params.challengerBond, + anchorStateRegistry, + accessManager_ + ) + ); + + // Set respected game type for OptimisticZk + GameType gameType = GameTypes.OPTIMISTIC_ZK_GAME_TYPE; + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.setRespectedGameType(gameType); + + // Register with factory + vm.startPrank(disputeGameFactory.owner()); + disputeGameFactory.setImplementation(gameType, IDisputeGame(gameImpl_)); + disputeGameFactory.setInitBond(gameType, _params.challengerBond); + vm.stopPrank(); + } } /// @title DisputeGameFactory_Initialize_Test diff --git a/packages/contracts-bedrock/test/dispute/zk/OptimisticZkGame.t.sol b/packages/contracts-bedrock/test/dispute/zk/OptimisticZkGame.t.sol new file mode 100644 index 00000000000..fff896b9872 --- /dev/null +++ b/packages/contracts-bedrock/test/dispute/zk/OptimisticZkGame.t.sol @@ -0,0 +1,732 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing +import { DisputeGameFactory_TestInit } from "test/dispute/DisputeGameFactory.t.sol"; + +// Libraries +import { Claim, Duration, GameStatus, GameType, Timestamp } from "src/dispute/lib/Types.sol"; +import { + BadAuth, + IncorrectBondAmount, + UnexpectedRootClaim, + NoCreditToClaim, + GameNotFinalized, + ParentGameNotResolved, + InvalidParentGame, + ClaimAlreadyChallenged, + GameOver, + GameNotOver, + IncorrectDisputeGameFactory +} from "src/dispute/lib/Errors.sol"; +import { GameTypes } from "src/dispute/lib/Types.sol"; + +// Contracts +import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol"; +import { OptimisticZkGame } from "src/dispute/zk/OptimisticZkGame.sol"; +import { AccessManager } from "src/dispute/zk/AccessManager.sol"; + +// Interfaces +import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; +import { Proxy } from "src/universal/Proxy.sol"; + +/// @title OptimisticZkGame_TestInit +/// @notice Base test contract with shared setup for OptimisticZkGame tests. +abstract contract OptimisticZkGame_TestInit is DisputeGameFactory_TestInit { + // Events + event Challenged(address indexed challenger); + event Proved(address indexed prover); + event Resolved(GameStatus indexed status); + + OptimisticZkGame gameImpl; + OptimisticZkGame parentGame; + OptimisticZkGame game; + AccessManager accessManager; + + address proposer = address(0x123); + address challenger = address(0x456); + address prover = address(0x789); + + // Fixed parameters. + GameType gameType = GameTypes.OPTIMISTIC_ZK_GAME_TYPE; + Duration maxChallengeDuration = Duration.wrap(12 hours); + Duration maxProveDuration = Duration.wrap(3 days); + Claim rootClaim = Claim.wrap(keccak256("rootClaim")); + + // Sequence number offsets from anchor state (for parent and child games). + uint256 parentSequenceOffset = 1000; + uint256 childSequenceOffset = 2000; + + // Game indices are set dynamically in setUp (on fork, existing games already exist) + uint32 parentGameIndex; + uint32 childGameIndex; + + // Offsets from child sequence number for grandchild games. + uint256 grandchildOffset1 = 1000; + uint256 grandchildOffset2 = 2000; + uint256 grandchildOffset3 = 3000; + uint256 grandchildOffset4 = 8000; + + // Actual sequence numbers (set in setUp based on anchor state) + uint256 anchorL2SequenceNumber; + uint256 parentL2SequenceNumber; + uint256 childL2SequenceNumber; + + // For a new parent game that we manipulate separately in some tests. + OptimisticZkGame separateParentGame; + + function setUp() public virtual override { + super.setUp(); + skipIfForkTest("Skip not supported yet"); + + // Get anchor state to calculate valid sequence numbers + (, anchorL2SequenceNumber) = anchorStateRegistry.getAnchorRoot(); + parentL2SequenceNumber = anchorL2SequenceNumber + parentSequenceOffset; + childL2SequenceNumber = anchorL2SequenceNumber + childSequenceOffset; + + // Setup game implementation using shared helper + address impl; + (impl, accessManager,) = setupOptimisticZkGame( + OptimisticZkGameParams({ + maxChallengeDuration: maxChallengeDuration, + maxProveDuration: maxProveDuration, + proposer: proposer, + challenger: challenger, + rollupConfigHash: bytes32(0), + aggregationVkey: bytes32(0), + rangeVkeyCommitment: bytes32(0), + challengerBond: 1 ether + }) + ); + gameImpl = OptimisticZkGame(impl); + + // Create the first (parent) game – it uses uint32.max as parent index. + vm.startPrank(proposer); + vm.deal(proposer, 2 ether); + + // Warp time forward to ensure the parent game is created after the respectedGameTypeUpdatedAt timestamp. + vm.warp(block.timestamp + 1000); + + // Create parent game (uses uint32.max to indicate first game in chain). + parentGame = OptimisticZkGame( + address( + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("genesis")), + abi.encodePacked(parentL2SequenceNumber, type(uint32).max) + ) + ) + ); + + // Record actual index of parent game (on fork, existing games already occupy indices 0, 1, ...) + parentGameIndex = uint32(disputeGameFactory.gameCount() - 1); + + // We want the parent game to finalize. We'll skip its challenge period. + (,,,,, Timestamp parentGameDeadline) = parentGame.claimData(); + vm.warp(parentGameDeadline.raw() + 1 seconds); + parentGame.resolve(); + + uint256 finalityDelay = anchorStateRegistry.disputeGameFinalityDelaySeconds(); + vm.warp(parentGame.resolvedAt().raw() + finalityDelay + 1 seconds); + parentGame.claimCredit(proposer); + + // Create the child game referencing actual parent game index. + game = OptimisticZkGame( + address( + disputeGameFactory.create{ value: 1 ether }( + gameType, rootClaim, abi.encodePacked(childL2SequenceNumber, parentGameIndex) + ) + ) + ); + + // Record actual index of child game. + childGameIndex = uint32(disputeGameFactory.gameCount() - 1); + + vm.stopPrank(); + } +} + +/// @title OptimisticZkGame_Initialize_Test +/// @notice Tests for initialization of OptimisticZkGame. +contract OptimisticZkGame_Initialize_Test is OptimisticZkGame_TestInit { + function test_initialize_succeeds() public view { + // Test that the factory is correctly initialized. + assertEq(address(disputeGameFactory.owner()), address(this)); + assertEq(address(disputeGameFactory.gameImpls(gameType)), address(gameImpl)); + // We expect games including parent and child (indices may vary on fork). + assertEq(disputeGameFactory.gameCount(), childGameIndex + 1); + + // Check that our child game matches the game at childGameIndex. + (,, IDisputeGame proxy_) = disputeGameFactory.gameAtIndex(childGameIndex); + assertEq(address(game), address(proxy_)); + + // Check the child game fields. + assertEq(game.gameType().raw(), gameType.raw()); + assertEq(game.rootClaim().raw(), rootClaim.raw()); + assertEq(game.maxChallengeDuration().raw(), maxChallengeDuration.raw()); + assertEq(game.maxProveDuration().raw(), maxProveDuration.raw()); + assertEq(address(game.disputeGameFactory()), address(disputeGameFactory)); + assertEq(game.l2SequenceNumber(), childL2SequenceNumber); + + // The parent's sequence number (startingBlockNumber() returns l2SequenceNumber). + assertEq(game.startingBlockNumber(), parentL2SequenceNumber); + + // The parent's root was keccak256("genesis"). + assertEq(game.startingRootHash().raw(), keccak256("genesis")); + + assertEq(address(game).balance, 1 ether); + + // Check the claimData. + ( + uint32 parentIndex_, + address counteredBy_, + address prover_, + Claim claim_, + OptimisticZkGame.ProposalStatus status_, + Timestamp deadline_ + ) = game.claimData(); + + assertEq(parentIndex_, parentGameIndex); + assertEq(counteredBy_, address(0)); + assertEq(game.gameCreator(), proposer); + assertEq(prover_, address(0)); + assertEq(claim_.raw(), rootClaim.raw()); + + // Initially, the status is Unchallenged. + assertEq(uint8(status_), uint8(OptimisticZkGame.ProposalStatus.Unchallenged)); + + // The child's initial deadline is block.timestamp + maxChallengeDuration. + uint256 currentTime = block.timestamp; + uint256 expectedDeadline = currentTime + maxChallengeDuration.raw(); + assertEq(deadline_.raw(), expectedDeadline); + } + + function test_initialize_blockNumberTooSmall_reverts() public { + // Try to create a child game that references a block number smaller than parent's. + vm.startPrank(proposer); + vm.deal(proposer, 1 ether); + + // We expect revert because l2BlockNumber (1) < parent's block number + vm.expectRevert( + abi.encodeWithSelector( + UnexpectedRootClaim.selector, + Claim.wrap(keccak256("rootClaim")) // The rootClaim we pass. + ) + ); + + disputeGameFactory.create{ value: 1 ether }( + gameType, + rootClaim, + abi.encodePacked(uint256(1), parentGameIndex) // L2 block is smaller than parent's block. + ); + vm.stopPrank(); + } + + function test_initialize_parentBlacklisted_reverts() public { + // Blacklist the game on the anchor state registry (which is what's actually used for validation). + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.blacklistDisputeGame(IDisputeGame(address(game))); + + vm.startPrank(proposer); + vm.deal(proposer, 1 ether); + vm.expectRevert(InvalidParentGame.selector); + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("blacklisted-parent-game")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset1, childGameIndex) + ); + vm.stopPrank(); + } + + function test_initialize_parentNotRespected_reverts() public { + // Create a new game which will be the parent. + vm.startPrank(proposer); + vm.deal(proposer, 1 ether); + OptimisticZkGame parentNotRespected = OptimisticZkGame( + address( + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("not-respected-parent-game")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset1, childGameIndex) + ) + ) + ); + uint32 parentNotRespectedIndex = uint32(disputeGameFactory.gameCount() - 1); + vm.stopPrank(); + + // Blacklist the parent game to make it invalid. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.blacklistDisputeGame(IDisputeGame(address(parentNotRespected))); + + // Try to create a game with a parent game that is not valid. + vm.startPrank(proposer); + vm.deal(proposer, 1 ether); + vm.expectRevert(InvalidParentGame.selector); + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("child-with-not-respected-parent")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset2, parentNotRespectedIndex) + ); + vm.stopPrank(); + } + + function test_initialize_noPermission_reverts() public { + address maliciousProposer = address(0x1234); + + vm.startPrank(maliciousProposer); + vm.deal(maliciousProposer, 1 ether); + + vm.expectRevert(BadAuth.selector); + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("new-claim")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset1, childGameIndex) + ); + + vm.stopPrank(); + } + + function test_initialize_wrongFactory_reverts() public { + // Deploy the implementation contract for new DisputeGameFactory. + DisputeGameFactory newFactoryImpl = new DisputeGameFactory(); + + // Deploy a proxy pointing to the new factory implementation. + Proxy newFactoryProxyContract = new Proxy(address(this)); + newFactoryProxyContract.upgradeTo(address(newFactoryImpl)); + + // Cast the proxy to the DisputeGameFactory interface and initialize it. + DisputeGameFactory newFactory = DisputeGameFactory(address(newFactoryProxyContract)); + newFactory.initialize(address(this)); + + // Set the implementation with the same implementation as the old disputeGameFactory. + newFactory.setImplementation(gameType, IDisputeGame(address(gameImpl))); + newFactory.setInitBond(gameType, 1 ether); + + vm.startPrank(proposer); + vm.deal(proposer, 1 ether); + + vm.expectRevert(IncorrectDisputeGameFactory.selector); + newFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("new-claim")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset1, childGameIndex) + ); + + vm.stopPrank(); + } +} + +/// @title OptimisticZkGame_Resolve_Test +/// @notice Tests for resolve functionality of OptimisticZkGame. +contract OptimisticZkGame_Resolve_Test is OptimisticZkGame_TestInit { + function test_resolve_unchallenged_succeeds() public { + assertEq(uint8(game.status()), uint8(GameStatus.IN_PROGRESS)); + + // Should revert if we try to resolve before deadline. + vm.expectRevert(GameNotOver.selector); + game.resolve(); + + // Warp forward past the challenge deadline. + (,,,,, Timestamp deadline) = game.claimData(); + vm.warp(deadline.raw() + 1); + + // Expect the Resolved event. + vm.expectEmit(true, false, false, false, address(game)); + emit Resolved(GameStatus.DEFENDER_WINS); + + // Now we can resolve successfully. + game.resolve(); + + // Proposer gets the bond back. + vm.warp(game.resolvedAt().raw() + anchorStateRegistry.disputeGameFinalityDelaySeconds() + 1 seconds); + game.claimCredit(proposer); + + // Check final state + assertEq(uint8(game.status()), uint8(GameStatus.DEFENDER_WINS)); + // The contract should have paid back the proposer. + assertEq(address(game).balance, 0); + // Proposer posted 1 ether, so they get it back. + assertEq(proposer.balance, 2 ether); + assertEq(challenger.balance, 0); + } + + function test_resolve_unchallengedWithProof_succeeds() public { + assertEq(uint8(game.status()), uint8(GameStatus.IN_PROGRESS)); + + // Should revert if we try to resolve before the first challenge deadline. + vm.expectRevert(GameNotOver.selector); + game.resolve(); + + // Prover proves the claim while unchallenged. + vm.startPrank(prover); + game.prove(bytes("")); + vm.stopPrank(); + + // Now the proposal is UnchallengedAndValidProofProvided; we can resolve immediately. + game.resolve(); + + // Prover does not get any credit. + vm.warp(game.resolvedAt().raw() + anchorStateRegistry.disputeGameFinalityDelaySeconds() + 1 seconds); + vm.expectRevert(NoCreditToClaim.selector); + game.claimCredit(prover); + + // Proposer gets the bond back. + game.claimCredit(proposer); + + // Final status: DEFENDER_WINS. + assertEq(uint8(game.status()), uint8(GameStatus.DEFENDER_WINS)); + assertEq(address(game).balance, 0); + + // Proposer gets their 1 ether back. + assertEq(proposer.balance, 2 ether); + // Prover does NOT get the reward because no challenger posted a bond. + assertEq(prover.balance, 0 ether); + assertEq(challenger.balance, 0); + } + + function test_resolve_challengedWithProof_succeeds() public { + assertEq(uint8(game.status()), uint8(GameStatus.IN_PROGRESS)); + assertEq(address(game).balance, 1 ether); + + // Try to resolve too early. + vm.expectRevert(GameNotOver.selector); + game.resolve(); + + // Challenger posts the bond incorrectly. + vm.startPrank(challenger); + vm.deal(challenger, 1 ether); + + // Must pay exactly the required bond. + vm.expectRevert(IncorrectBondAmount.selector); + game.challenge{ value: 0.5 ether }(); + + // Correctly challenge the game. + game.challenge{ value: 1 ether }(); + vm.stopPrank(); + + // Now the contract holds 2 ether total. + assertEq(address(game).balance, 2 ether); + + // Confirm the proposal is in Challenged state. + (, address counteredBy_,,, OptimisticZkGame.ProposalStatus challStatus,) = game.claimData(); + assertEq(counteredBy_, challenger); + assertEq(uint8(challStatus), uint8(OptimisticZkGame.ProposalStatus.Challenged)); + + // Prover proves the claim in time. + vm.startPrank(prover); + game.prove(bytes("")); + vm.stopPrank(); + + // Confirm the proposal is now ChallengedAndValidProofProvided. + (,,,, challStatus,) = game.claimData(); + assertEq(uint8(challStatus), uint8(OptimisticZkGame.ProposalStatus.ChallengedAndValidProofProvided)); + assertEq(uint8(game.status()), uint8(GameStatus.IN_PROGRESS)); + + // Resolve the game. + game.resolve(); + + // Prover gets the proof reward. + vm.warp(game.resolvedAt().raw() + anchorStateRegistry.disputeGameFinalityDelaySeconds() + 1 seconds); + game.claimCredit(prover); + + // Proposer gets the bond back. + game.claimCredit(proposer); + + assertEq(uint8(game.status()), uint8(GameStatus.DEFENDER_WINS)); + assertEq(address(game).balance, 0); + + // Final balances: + // - The proposer recovers their 1 ether stake. + // - The prover gets 1 ether reward. + // - The challenger gets nothing. + assertEq(proposer.balance, 2 ether); + assertEq(prover.balance, 1 ether); + assertEq(challenger.balance, 0); + } + + function test_resolve_challengedNoProof_succeeds() public { + // Challenge the game. + vm.startPrank(challenger); + vm.deal(challenger, 2 ether); + game.challenge{ value: 1 ether }(); + vm.stopPrank(); + + // The contract now has 2 ether total. + assertEq(address(game).balance, 2 ether); + + // We must wait for the prove deadline to pass. + (,,,,, Timestamp deadline) = game.claimData(); + vm.warp(deadline.raw() + 1); + + // Now we can resolve, resulting in CHALLENGER_WINS. + game.resolve(); + + // Challenger gets the bond back and wins proposer's bond. + vm.warp(game.resolvedAt().raw() + anchorStateRegistry.disputeGameFinalityDelaySeconds() + 1 seconds); + game.claimCredit(challenger); + + assertEq(uint8(game.status()), uint8(GameStatus.CHALLENGER_WINS)); + + // The challenger receives the entire 3 ether. + assertEq(challenger.balance, 3 ether); // started with 2, spent 1, got 2 from the game. + + // The proposer loses their 1 ether stake. + assertEq(proposer.balance, 1 ether); // started with 2, lost 1. + // The contract balance is zero. + assertEq(address(game).balance, 0); + } + + function test_resolve_parentGameInProgress_reverts() public { + vm.startPrank(proposer); + + // Create a new game referencing the child game as parent. + OptimisticZkGame childGame = OptimisticZkGame( + address( + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("new-claim")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset1, childGameIndex) + ) + ) + ); + + vm.stopPrank(); + + // The parent game is still in progress, not resolved. + // So, if we try to resolve the childGame, it should revert with ParentGameNotResolved. + vm.expectRevert(ParentGameNotResolved.selector); + childGame.resolve(); + } + + function test_resolve_parentGameInvalid_succeeds() public { + // 1) Now create a child game referencing that losing parent at index 1. + vm.startPrank(proposer); + OptimisticZkGame childGame = OptimisticZkGame( + address( + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("child-of-loser")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset4, childGameIndex) + ) + ) + ); + vm.stopPrank(); + + // 2) Challenge the parent game so that it ends up CHALLENGER_WINS when proof is not provided within the prove + // deadline. + vm.startPrank(challenger); + vm.deal(challenger, 2 ether); + game.challenge{ value: 1 ether }(); + vm.stopPrank(); + + // 3) Warp past the prove deadline. + (,,,,, Timestamp gameDeadline) = game.claimData(); + vm.warp(gameDeadline.raw() + 1); + + // 4) The game resolves as CHALLENGER_WINS. + game.resolve(); + + // Challenger gets the bond back and wins proposer's bond. + vm.warp(game.resolvedAt().raw() + anchorStateRegistry.disputeGameFinalityDelaySeconds() + 1 seconds); + game.claimCredit(challenger); + + assertEq(uint8(game.status()), uint8(GameStatus.CHALLENGER_WINS)); + + // 5) If we try to resolve the child game, it should be resolved as CHALLENGER_WINS + // because parent's claim is invalid. + // The child's bond is lost since there is no challenger for the child game. + childGame.resolve(); + + // Challenger hasn't challenged the child game, so it gets nothing. + vm.warp(childGame.resolvedAt().raw() + anchorStateRegistry.disputeGameFinalityDelaySeconds() + 1 seconds); + + vm.expectRevert(NoCreditToClaim.selector); + childGame.claimCredit(challenger); + + assertEq(uint8(childGame.status()), uint8(GameStatus.CHALLENGER_WINS)); + + assertEq(address(childGame).balance, 1 ether); + assertEq(address(challenger).balance, 3 ether); + assertEq(address(proposer).balance, 0 ether); + } +} + +/// @title OptimisticZkGame_Challenge_Test +/// @notice Tests for challenge functionality of OptimisticZkGame. +contract OptimisticZkGame_Challenge_Test is OptimisticZkGame_TestInit { + function test_challenge_alreadyChallenged_reverts() public { + // Initially unchallenged. + (, address counteredBy_,,, OptimisticZkGame.ProposalStatus status_,) = game.claimData(); + assertEq(counteredBy_, address(0)); + assertEq(uint8(status_), uint8(OptimisticZkGame.ProposalStatus.Unchallenged)); + + // The first challenge is valid. + vm.startPrank(challenger); + vm.deal(challenger, 2 ether); + game.challenge{ value: 1 ether }(); + + // A second challenge from any party should revert because the proposal is no longer "Unchallenged". + vm.expectRevert(ClaimAlreadyChallenged.selector); + game.challenge{ value: 1 ether }(); + vm.stopPrank(); + } + + function test_challenge_noPermission_reverts() public { + address maliciousChallenger = address(0x1234); + + vm.startPrank(maliciousChallenger); + vm.deal(maliciousChallenger, 1 ether); + + vm.expectRevert(BadAuth.selector); + game.challenge{ value: 1 ether }(); + + vm.stopPrank(); + } +} + +/// @title OptimisticZkGame_Prove_Test +/// @notice Tests for prove functionality of OptimisticZkGame. +contract OptimisticZkGame_Prove_Test is OptimisticZkGame_TestInit { + function test_prove_afterDeadline_reverts() public { + // Challenge first. + vm.startPrank(challenger); + vm.deal(challenger, 1 ether); + game.challenge{ value: 1 ether }(); + vm.stopPrank(); + + // Move time forward beyond the prove period. + (,,,,, Timestamp deadline) = game.claimData(); + vm.warp(deadline.raw() + 1); + + vm.startPrank(prover); + // Attempting to prove after the deadline is exceeded. + vm.expectRevert(GameOver.selector); + game.prove(bytes("")); + vm.stopPrank(); + } + + function test_prove_alreadyProved_reverts() public { + vm.startPrank(prover); + game.prove(bytes("")); + vm.expectRevert(GameOver.selector); + game.prove(bytes("")); + vm.stopPrank(); + } +} + +/// @title OptimisticZkGame_ClaimCredit_Test +/// @notice Tests for claimCredit functionality of OptimisticZkGame. +contract OptimisticZkGame_ClaimCredit_Test is OptimisticZkGame_TestInit { + function test_claimCredit_notFinalized_reverts() public { + (,,,,, Timestamp deadline) = game.claimData(); + vm.warp(deadline.raw() + 1); + game.resolve(); + + vm.expectRevert(GameNotFinalized.selector); + game.claimCredit(proposer); + } +} + +/// @title OptimisticZkGame_CloseGame_Test +/// @notice Tests for closeGame functionality of OptimisticZkGame. +contract OptimisticZkGame_CloseGame_Test is OptimisticZkGame_TestInit { + function test_closeGame_notResolved_reverts() public { + vm.expectRevert(GameNotFinalized.selector); + game.closeGame(); + } + + function test_closeGame_updatesAnchorGame_succeeds() public { + (,,,,, Timestamp deadline) = game.claimData(); + vm.warp(deadline.raw() + 1); + game.resolve(); + + vm.warp(game.resolvedAt().raw() + anchorStateRegistry.disputeGameFinalityDelaySeconds() + 1 seconds); + game.closeGame(); + + assertEq(address(anchorStateRegistry.anchorGame()), address(game)); + } +} + +/// @title OptimisticZkGame_AccessManager_Test +/// @notice Tests for AccessManager permissionless fallback functionality. +contract OptimisticZkGame_AccessManager_Test is OptimisticZkGame_TestInit { + function test_accessManager_permissionlessAfterTimeout_succeeds() public { + // Initially, unauthorized user should not be allowed + address unauthorizedUser = address(0x9999); + + // Try to create a game as unauthorized user - should fail + vm.prank(unauthorizedUser); + vm.deal(unauthorizedUser, 1 ether); + vm.expectRevert(BadAuth.selector); + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("new-claim-1")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset1, childGameIndex) + ); + + vm.prank(proposer); + vm.deal(proposer, 1 ether); + disputeGameFactory.create{ value: 1 ether }( + gameType, Claim.wrap(keccak256("new-claim-2")), abi.encodePacked(childL2SequenceNumber, parentGameIndex) + ); + + // Warp time forward past the timeout + vm.warp(block.timestamp + 2 weeks + 1); + + // Now unauthorized user should be allowed due to timeout + vm.prank(unauthorizedUser); + vm.deal(unauthorizedUser, 1 ether); + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("new-claim-3")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset2, childGameIndex) + ); + + // After the new game, timeout resets - unauthorized user should not be allowed immediately + vm.prank(unauthorizedUser); + vm.deal(unauthorizedUser, 1 ether); + vm.expectRevert(BadAuth.selector); + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("new-claim-4")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset3, childGameIndex) + ); + } + + function test_accessManager_permissionlessNoGamesAfterTimeout_succeeds() public { + // Initially, unauthorized user should not be allowed + address unauthorizedUser = address(0x9999); + + // Try to create a game as unauthorized user - should fail + vm.prank(unauthorizedUser); + vm.deal(unauthorizedUser, 1 ether); + vm.expectRevert(BadAuth.selector); + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("new-claim-1")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset1, childGameIndex) + ); + + // Warp time forward past the timeout + vm.warp(block.timestamp + 2 weeks + 1 hours); + + // Now unauthorized user should be allowed due to timeout + vm.prank(unauthorizedUser); + vm.deal(unauthorizedUser, 1 ether); + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("new-claim-3")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset2, childGameIndex) + ); + + // After the new game, timeout resets - unauthorized user should not be allowed immediately + vm.prank(unauthorizedUser); + vm.deal(unauthorizedUser, 1 ether); + vm.expectRevert(BadAuth.selector); + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("new-claim-4")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset3, childGameIndex) + ); + } +} diff --git a/packages/contracts-bedrock/test/dispute/zk/mocks/SP1MockVerifier.sol b/packages/contracts-bedrock/test/dispute/zk/mocks/SP1MockVerifier.sol new file mode 100644 index 00000000000..974aa1c8b31 --- /dev/null +++ b/packages/contracts-bedrock/test/dispute/zk/mocks/SP1MockVerifier.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { ISP1Verifier } from "src/dispute/zk/ISP1Verifier.sol"; + +contract SP1MockVerifier is ISP1Verifier { + /// @notice Verifies a mock proof with given public values and vkey. + /// @param proofBytes The proof of the program execution the SP1 zkVM encoded as bytes. + function verifyProof(bytes32, bytes calldata, bytes calldata proofBytes) external pure { + assert(proofBytes.length == 0); + } +} diff --git a/packages/contracts-bedrock/test/vendor/Initializable.t.sol b/packages/contracts-bedrock/test/vendor/Initializable.t.sol index 6bde0b7ccbb..e74a08deec2 100644 --- a/packages/contracts-bedrock/test/vendor/Initializable.t.sol +++ b/packages/contracts-bedrock/test/vendor/Initializable.t.sol @@ -398,7 +398,7 @@ contract Initializer_Test is CommonTest { excludes[j++] = "src/dispute/SuperFaultDisputeGame.sol"; excludes[j++] = "src/dispute/PermissionedDisputeGame.sol"; excludes[j++] = "src/dispute/SuperPermissionedDisputeGame.sol"; - excludes[j++] = "src/dispute/zk/OPSuccinctFaultDisputeGame.sol"; + excludes[j++] = "src/dispute/zk/OptimisticZkGame.sol"; // TODO: Eventually remove this exclusion. Same reason as above dispute contracts. excludes[j++] = "src/L1/OPContractsManager.sol"; // TODO: Eventually remove this exclusion. Same reason as above dispute contracts. From 1ddd67ee5b458a6c0b375ea3e91205433a8f2e5b Mon Sep 17 00:00:00 2001 From: niha <205694301+0xniha@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:12:31 -0300 Subject: [PATCH 12/13] feat: revert opcm v1 & add opcmv2 op-deployer support (#18399) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: revert opcm v1 on public interface functions (#699) * feat: revert opcm v1 on public interface functions * fix: add dev flag check for OPCM v2 * feat: OPCM v2 support on for op-deployer (#701) * feat: add feature flag initial * feat: add feature flags to deploy chains using OPCM v2 * chore: remove unused function, update helper function names * chore: remove unused DeployOPChainV2 * chore: add todo for obtaining a superchain config per deployment * fix: remove unnecessary arg and replace function visibility --------- Co-authored-by: niha <205694301+0xniha@users.noreply.github.com> * fix: add superchainConfig input & fix tests in deployopchain (#705) * fix: add superchainConfig input & fix tests in deployopchain * docs: add TODOs * feat: add opcmv2 flag to deployImplementations and apply opcmv2 deployment test * feat: add opcmv2 flag to deployImplementations and add apply opcmv2 deployment test * fix: hardcode flag to avoid import cycles & refactor createOPCMContractV2 * fix: remove todo comment * fix: deploy implementations test branching * refactor: add deployOPCMStandardValidatorV2 * fix: add test and script checks * fix: remove cgt & fix tests (#736) * fix: remove cgt * fix: query chainIdToBatchInboxAddress in chain assertions script * fix: add skip deploy ocpmv1 when v2 enabled * refactor: remove unnecesary function * fix: opcm semver * chore: add todo comment with issue number (#737) --------- Co-authored-by: IamFlux <175354924+0xiamflux@users.noreply.github.com> * refactor: match assert pattern on OPCM v1 (#749) * refactor: adheres OPCM v1 revert function to assert pattern * chore: enable deny_warnings on foundry.toml * chore: pre-pr ready * fix: add implementations for StandardValidator when using OPCMv2 * chore: pre-pr ready (#758) * fix: remove cgt checks for opcmv2 (#765) * chore: opcm revert develop sync (#769) * feat(ci): mise; cache & retries. (#18572) * op-node: Light CL: Always Follow Source using CL (#18571) * wiring for l2.follow.source * Follow Safe without handling external safe > local unsafe * safe follow * safe follow consider unsafe gap EL Sync * follow finalized * Comments * cleanup * drop unused interface methods * sanity check external finalized * Adjust follow source log level * op-devstack: Follow Source Support * op-acceptance-tests: Follow Source * op-devstack: Follow Source Support * simplify labeling * l1 reorg protection: reset * add reorg tc * typo * follow source: check unsafe * convention * Add unsafe only reorg test * devstack/acceptance test : rename FollowSource to FollowL2 * follow upstream source enabled when derivation disabled, reorg detection * fix unsafe only reorg sync test comments * rm unused interface method * dsl * devstack support for ext sync test + follow l2 * op-acceptance-tests: Follow L2 using Ext + SyncTester * use blockref * fix log msg err * Fix composite interface naming * op-node: Follow Source: EL / CL support * devstack: Allow EL or CL to be follow source * Inject CurrentL1 * logs for injecting CurrentL1 * refactor to less rpc calls * acceptance-tests: Check CurrentL1 is advancing * linter * fix error args * sanity check external values using L1 * op-node: Follow Source CL and drop unsafeOnly * op-acceptance-tests: drop EL follow source / Unsafe Only tests * op-devstack|acceptance-tests: Follow Source CL * op-node: Follow Client: only allow CL source * drop unused args * rm redundant unsafe head reorg logic * rm unused dsl * Do not use functions * chore: Add SafeRename ioutil utility (#18610) * chore: Add SafeRename ioutil utility * fix: Lint * proofs: Update pinned kona client version (#18611) * feat: have OPCMv2 check upgrade ordering version (#18583) * feat: have OPCMv2 check upgrade ordering version This PR introduces a check in OPCM that validates that the version difference in the previous OPCM and the current OPCM is acceptable. This PR explicitly disallows skipping OPCM versions which was previously an implicit requirement. * fix: properly set version testing flag * fix: semver lock * fix: move valid instructions allowance * fix: semver lock * fix: better mechanism for checking permitted upgrades * fix: properly run permitted upgrade sequence tests * chore: bump opcmv2 to v7 (#18631) * chore: bump opcmv2 to v7 * fix: correct version in upgrade sequence * fix: broken tests * op-chain-ops: Add command to calculate an output root from an EL endpoint. (#18626) * verify opcm v2 (#18590) * verify opcm v2 * fixes * fixes * some fixes * some fixes * some fixes * fix verifyOPCM v1 tests * fix verifyOPCM v1 tests * fix ci checks * better approach to tests, less diff * improve test coverage * fix typo * fix semver * Update packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> --------- Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> * codeowners(op-deployer): add platforms team as codeowners (#18635) * ci: Fix contracts test-upgrade test junit output (#18637) * ci: Fix contracts test-upgrade test junit output * fix test-upgrade xml output * proofs: Add provable output roots to super DGs (#18605) * proofs: Add provable output roots to super DGs This patch updates the SuperFaultDisputeGame to explicitly prove output roots contained in the super root. This is accomplished by requiring proposers to provide the preimage of the super root when creating a SuperFaultDisputeGame. The Super DG is then extended with a rootClaim(uint256 _l2ChainId) method that returns specific output roots contained in the proposal. This allows withdrawals to be proven using only an output root. * review comments * fix nits; update interfaces * update snapshots * fix panic in non-l2oo proposals * op-program: Reduce disk usage of preimage prestate builds in ci (#18641) * all: upgrade op-geth (#18646) Pull in superchain registry changes and a few other minor changes. No upstream merges. * refactor+fix(op-node/sequencing): l1 origin selection improvements (#18589) * WIP * wip * WIP * Treat NotFound next L1 origin as chain end * Use recover mode in sequence window expiry test * Invoke fault proof earlier and fix typos Run env.RunFaultProofProgram after computing l2SafeHead (l2SafeHead.Number.Uint64()/2) and replace the FromGenesis call with RunFaultProofProgram. Fix minor comment typos and wrap a long log line. * reduce diff * Use requireL1OriginAt helper in test Replace manual error assertions around FindL1Origin with requireL1OriginAt and remove the now-unused derive import. * Introduce L2Sequencer.ActMaybeL2StartBlock * add TestRecoverModeWhenChainHealthy acceptance test sysgo only * Add SetSequencerRecoverMode and enable debug logs * Adjust L1 block time and sequence window test Set default L1 block time to 12s to match action helper assumptions. Increase sequencer window size in the test to 50. Compute drift from UnsafeL2 headers (use UnsafeL2.L1Origin). Simplify L1 mining to always BatchMineAndSync and remove the extra numL1Blocks > 10 lag guard. * restore stub * WIP * name errs * refactor * fix * add test * Rename error constant and add L1 origin tests Rename ErrInvalidL1OriginChild to ErrNextL1OriginOrphaned and adjust the error text. Add test cases covering L1 reorg and invalid L1 origin, and refactor the test case construction in the unit test. * Use drift check for next L1 origin and update tests Compute driftNext and use it to decide adoption of the next L1 origin instead of comparing absolute timestamps. Bump MaxSequencerDrift to 1800 and add tests covering max-drift and negative-drift scenarios. * Refactor L1 origin selection and error handling Delegate origin decision logic to FindL1OriginOfNextL2Block and handle synchronous fetch when in recover mode. Remove recover-mode fetching from CurrentAndNextOrigin and return cached values instead. Update sequencer error handling to distinguish invalid/orphaned origin (which triggers a reset) from temporary lookup errors. * fixes * fixes * lint * don't use pointers saves some translation code * handle retries without a "temporary error" fixes action tests * use Fjord drift constant * fix origin_selector_test mainly just asserting on sentinel errors, and taking account of small optimization (fewer network calls) * Simplify FindL1Origin * move new pure function into a method on los * Update comment to refer to empty nextL1Origin * Use errors.Is for L1 origin error checks * Return L1 origin on validation errors * Add expectedResult to origin selector tests Set expectedResult for the test cases with l2Head d1000 and e1000 to assert the expected L1 origins (c101 and c100 respectively) * Add assertion message and clarify origin comments Provide a helpful failure message in sequence_window_expiry_test when the safe head doesn't advance after the sequencing window expires. Document that the current L1 origin should always be non-empty and add a panic guard. Clarify the rationale for requiring the next L1 origin, include a source link, and note the effect on unsafe block production. * Store recoverMode and add comment period * Update op-node/rollup/sequencing/origin_selector.go Co-authored-by: almanax-ai[bot] <174396398+almanax-ai[bot]@users.noreply.github.com> * Update op-node/rollup/sequencing/origin_selector.go Co-authored-by: almanax-ai[bot] <174396398+almanax-ai[bot]@users.noreply.github.com> --------- Co-authored-by: almanax-ai[bot] <174396398+almanax-ai[bot]@users.noreply.github.com> * feat: add ReadSuperchainDeployment support for opcm v2 (#18520) * feat: add readsuperchaindeployment for opcm v2 * fix: argument in PopulateSuperchainState & add docs (#734) * fix: set superchainConfig to zero for populateSuperchainState with opcmV1 * docs: add clarification of opcmv1 deprecation * fix: remove unused opcmV2Enabled var * test: opcm2 add read sup chain tests (#754) * chore: expand comment on ReadSuperchainDeployment.s.sol and init.go * test: add additional test cases for PopulateSuperchainState and InitLiveStrategy * fix: adds the right number of hex digits to OPCMV2DevFlag in devfeatures.go (#755) * fix: remove branching in init.go & link TODO issue (#760) * docs: link todo issue * refactor: remove branching in init.go WIP * fix: remove unused opcmv2 feature flag * fix: remove console logs * fix: set original timeout, fix isOpcmV2 version and add populateSuperchainState tests * fix: set original timeout * fix: isOpcmV2 version * feat: add test cases for populateSuperchainState and unify tests * fix: ocpm version comments --------- Co-authored-by: Flux <175354924+0xiamflux@users.noreply.github.com> Co-authored-by: OneTony Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> * fix: opcmv2 view function calls --------- Co-authored-by: Stefano Charissis Co-authored-by: Changwan Park Co-authored-by: Ján Jakub Naništa Co-authored-by: Inphi Co-authored-by: smartcontracts <14298799+smartcontracts@users.noreply.github.com> Co-authored-by: Adrian Sutton Co-authored-by: Michael Amadi Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Co-authored-by: serpixel <5087962+serpixel@users.noreply.github.com> Co-authored-by: Josh Klopfenstein Co-authored-by: George Knee Co-authored-by: almanax-ai[bot] <174396398+almanax-ai[bot]@users.noreply.github.com> Co-authored-by: Flux <175354924+0xiamflux@users.noreply.github.com> Co-authored-by: OneTony * Revert "chore: opcm revert develop sync (#769)" This reverts commit d84f0b9dc1fb6592f0a9462f8440acfbb406c1b0. * fix: sync commit --------- Co-authored-by: IamFlux <175354924+0xiamflux@users.noreply.github.com> Co-authored-by: OneTony Co-authored-by: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Co-authored-by: Stefano Charissis Co-authored-by: Changwan Park Co-authored-by: Ján Jakub Naništa Co-authored-by: Inphi Co-authored-by: smartcontracts <14298799+smartcontracts@users.noreply.github.com> Co-authored-by: Adrian Sutton Co-authored-by: Michael Amadi Co-authored-by: serpixel <5087962+serpixel@users.noreply.github.com> Co-authored-by: Josh Klopfenstein Co-authored-by: George Knee Co-authored-by: almanax-ai[bot] <174396398+almanax-ai[bot]@users.noreply.github.com> --- op-chain-ops/interopgen/deploy.go | 1 + op-deployer/pkg/deployer/devfeatures.go | 3 + .../deployer/integration_test/apply_test.go | 56 ++++ op-deployer/pkg/deployer/opcm/opchain.go | 1 + .../pkg/deployer/pipeline/implementations.go | 2 + op-deployer/pkg/deployer/pipeline/opchain.go | 23 +- .../interfaces/L1/IOPContractsManager.sol | 2 + .../scripts/deploy/ChainAssertions.sol | 27 +- .../scripts/deploy/Deploy.s.sol | 10 +- .../deploy/DeployImplementations.s.sol | 188 +++++++++++--- .../scripts/deploy/DeployOPChain.s.sol | 245 +++++++++++++++--- .../deploy/ReadImplementationAddresses.s.sol | 66 +++-- .../scripts/libraries/Types.sol | 3 + .../snapshots/abi/OPContractsManager.json | 5 + .../snapshots/semver-lock.json | 4 +- .../src/L1/OPContractsManager.sol | 26 +- .../test/L1/OPContractsManager.t.sol | 15 +- .../test/opcm/DeployImplementations.t.sol | 51 +++- .../test/opcm/DeployOPChain.t.sol | 77 ++++-- 19 files changed, 661 insertions(+), 144 deletions(-) diff --git a/op-chain-ops/interopgen/deploy.go b/op-chain-ops/interopgen/deploy.go index aaaeb32456f..9937c5dbf4f 100644 --- a/op-chain-ops/interopgen/deploy.go +++ b/op-chain-ops/interopgen/deploy.go @@ -254,6 +254,7 @@ func DeployL2ToL1(l1Host *script.Host, superCfg *SuperchainConfig, superDeployme AllowCustomDisputeParameters: true, OperatorFeeScalar: cfg.GasPriceOracleOperatorFeeScalar, OperatorFeeConstant: cfg.GasPriceOracleOperatorFeeConstant, + SuperchainConfig: superDeployment.SuperchainConfigProxy, UseCustomGasToken: cfg.UseCustomGasToken, }) if err != nil { diff --git a/op-deployer/pkg/deployer/devfeatures.go b/op-deployer/pkg/deployer/devfeatures.go index fefd390e06b..88993d916ef 100644 --- a/op-deployer/pkg/deployer/devfeatures.go +++ b/op-deployer/pkg/deployer/devfeatures.go @@ -17,6 +17,9 @@ var ( // DeployV2DisputeGamesDevFlag enables deployment of V2 dispute game contracts. DeployV2DisputeGamesDevFlag = common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000100") + + // OPCMV2DevFlag enables the OPContractsManagerV2 contract. + OPCMV2DevFlag = common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000010000") ) // IsDevFeatureEnabled checks if a specific development feature is enabled in a feature bitmap. diff --git a/op-deployer/pkg/deployer/integration_test/apply_test.go b/op-deployer/pkg/deployer/integration_test/apply_test.go index b2ab4c51450..ec0bc620d7d 100644 --- a/op-deployer/pkg/deployer/integration_test/apply_test.go +++ b/op-deployer/pkg/deployer/integration_test/apply_test.go @@ -346,6 +346,62 @@ func TestEndToEndApply(t *testing.T) { require.True(t, exists, "Native asset liquidity predeploy should exist in L2 genesis") require.Equal(t, amount, account.Balance, "Native asset liquidity predeploy should have the configured balance") }) + + t.Run("OPCMV2 deployment", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + lgr := testlog.Logger(t, slog.LevelDebug) + l1RPC, l1Client := devnet.DefaultAnvilRPC(t, lgr) + _, pk, dk := shared.DefaultPrivkey(t) + l1ChainID := new(big.Int).SetUint64(devnet.DefaultChainID) + l2ChainID := uint256.NewInt(1) + loc, _ := testutil.LocalArtifacts(t) + testCacheDir := testutils.IsolatedTestDirWithAutoCleanup(t) + + intent, st := shared.NewIntent(t, l1ChainID, dk, l2ChainID, loc, loc, testCustomGasLimit) + + // Enable OPCMV2 dev flag + intent.GlobalDeployOverrides = map[string]any{ + "devFeatureBitmap": deployer.OPCMV2DevFlag, + } + + require.NoError(t, deployer.ApplyPipeline( + ctx, + deployer.ApplyPipelineOpts{ + DeploymentTarget: deployer.DeploymentTargetLive, + L1RPCUrl: l1RPC, + DeployerPrivateKey: pk, + Intent: intent, + State: st, + Logger: lgr, + StateWriter: pipeline.NoopStateWriter(), + CacheDir: testCacheDir, + }, + )) + + // Verify that OPCMV2 was deployed in implementations + require.NotEmpty(t, st.ImplementationsDeployment.OpcmV2Impl, "OPCMV2 implementation should be deployed") + require.NotEmpty(t, st.ImplementationsDeployment.OpcmContainerImpl, "OPCM container implementation should be deployed") + require.NotEmpty(t, st.ImplementationsDeployment.OpcmStandardValidatorImpl, "OPCM standard validator implementation should be deployed") + + // Verify that implementations are deployed on L1 + cg := ethClientCodeGetter(ctx, l1Client) + + opcmV2Code := cg(t, st.ImplementationsDeployment.OpcmV2Impl) + require.NotEmpty(t, opcmV2Code, "OPCMV2 should have code deployed") + + // Verify that the dev feature bitmap is set to OPCMV2 + require.Equal(t, deployer.OPCMV2DevFlag, intent.GlobalDeployOverrides["devFeatureBitmap"]) + + // Assert that the OPCM V1 addresses are zero + require.Equal(t, common.Address{}, st.ImplementationsDeployment.OpcmImpl, "OPCM V1 implementation should be zero") + require.Equal(t, common.Address{}, st.ImplementationsDeployment.OpcmContractsContainerImpl, "OPCM container implementation should be zero") + require.Equal(t, common.Address{}, st.ImplementationsDeployment.OpcmGameTypeAdderImpl, "OPCM game type adder implementation should be zero") + require.Equal(t, common.Address{}, st.ImplementationsDeployment.OpcmDeployerImpl, "OPCM deployer implementation should be zero") + require.Equal(t, common.Address{}, st.ImplementationsDeployment.OpcmUpgraderImpl, "OPCM upgrader implementation should be zero") + require.Equal(t, common.Address{}, st.ImplementationsDeployment.OpcmInteropMigratorImpl, "OPCM interop migrator implementation should be zero") + }) } func TestGlobalOverrides(t *testing.T) { diff --git a/op-deployer/pkg/deployer/opcm/opchain.go b/op-deployer/pkg/deployer/opcm/opchain.go index 115bb14f9af..a0298c71b66 100644 --- a/op-deployer/pkg/deployer/opcm/opchain.go +++ b/op-deployer/pkg/deployer/opcm/opchain.go @@ -43,6 +43,7 @@ type DeployOPChainInput struct { OperatorFeeScalar uint32 OperatorFeeConstant uint64 + SuperchainConfig common.Address UseCustomGasToken bool } diff --git a/op-deployer/pkg/deployer/pipeline/implementations.go b/op-deployer/pkg/deployer/pipeline/implementations.go index 138186f905a..e3a01d14dfb 100644 --- a/op-deployer/pkg/deployer/pipeline/implementations.go +++ b/op-deployer/pkg/deployer/pipeline/implementations.go @@ -74,6 +74,8 @@ func DeployImplementations(env *Env, intent *state.Intent, st *state.State) erro OpcmUpgraderImpl: dio.OpcmUpgrader, OpcmInteropMigratorImpl: dio.OpcmInteropMigrator, OpcmStandardValidatorImpl: dio.OpcmStandardValidator, + OpcmV2Impl: dio.OpcmV2, + OpcmContainerImpl: dio.OpcmContainer, DelayedWethImpl: dio.DelayedWETHImpl, OptimismPortalImpl: dio.OptimismPortalImpl, OptimismPortalInteropImpl: dio.OptimismPortalInteropImpl, diff --git a/op-deployer/pkg/deployer/pipeline/opchain.go b/op-deployer/pkg/deployer/pipeline/opchain.go index 2ef41d73d60..5fad788d25e 100644 --- a/op-deployer/pkg/deployer/pipeline/opchain.go +++ b/op-deployer/pkg/deployer/pipeline/opchain.go @@ -104,6 +104,15 @@ func makeDCI(intent *state.Intent, thisIntent *state.ChainIntent, chainID common return opcm.DeployOPChainInput{}, fmt.Errorf("error merging proof params from overrides: %w", err) } + // Select which OPCM to use based on dev feature flag + opcmAddr := st.ImplementationsDeployment.OpcmImpl + if devFeatureBitmap, ok := intent.GlobalDeployOverrides["devFeatureBitmap"].(common.Hash); ok { + opcmV2Flag := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000010000") + if isDevFeatureEnabled(devFeatureBitmap, opcmV2Flag) && st.ImplementationsDeployment.OpcmV2Impl != (common.Address{}) { + opcmAddr = st.ImplementationsDeployment.OpcmV2Impl + } + } + return opcm.DeployOPChainInput{ OpChainProxyAdminOwner: thisIntent.Roles.L1ProxyAdminOwner, SystemConfigOwner: thisIntent.Roles.SystemConfigOwner, @@ -114,7 +123,7 @@ func makeDCI(intent *state.Intent, thisIntent *state.ChainIntent, chainID common BasefeeScalar: standard.BasefeeScalar, BlobBaseFeeScalar: standard.BlobBaseFeeScalar, L2ChainId: chainID.Big(), - Opcm: st.ImplementationsDeployment.OpcmImpl, + Opcm: opcmAddr, SaltMixer: st.Create2Salt.String(), // passing through salt generated at state initialization GasLimit: thisIntent.GasLimit, DisputeGameType: proofParams.DisputeGameType, @@ -126,6 +135,7 @@ func makeDCI(intent *state.Intent, thisIntent *state.ChainIntent, chainID common AllowCustomDisputeParameters: proofParams.DangerouslyAllowCustomDisputeParameters, OperatorFeeScalar: thisIntent.OperatorFeeScalar, OperatorFeeConstant: thisIntent.OperatorFeeConstant, + SuperchainConfig: st.SuperchainDeployment.SuperchainConfigProxy, UseCustomGasToken: thisIntent.IsCustomGasTokenEnabled(), }, nil } @@ -170,3 +180,14 @@ func shouldDeployOPChain(st *state.State, chainID common.Hash) bool { return true } + +// isDevFeatureEnabled checks if a specific development feature is enabled in a feature bitmap. +// This mirrors the function in devfeatures.go to avoid import cycles. +func isDevFeatureEnabled(bitmap, flag common.Hash) bool { + b := new(big.Int).SetBytes(bitmap[:]) + f := new(big.Int).SetBytes(flag[:]) + + featuresIsNonZero := f.Cmp(big.NewInt(0)) != 0 + bitmapContainsFeatures := new(big.Int).And(b, f).Cmp(f) == 0 + return featuresIsNonZero && bitmapContainsFeatures +} diff --git a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol index 48c7ab9c6b5..c3a799ecb0e 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol @@ -300,6 +300,8 @@ interface IOPContractsManager { error InvalidDevFeatureAccess(bytes32 devFeature); + error OPContractsManager_V2Enabled(); + // -------- Methods -------- function __constructor__( diff --git a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol index 0ac35465ff8..ec618a04bd1 100644 --- a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol +++ b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol @@ -16,6 +16,7 @@ import { Types } from "scripts/libraries/Types.sol"; import { Blueprint } from "src/libraries/Blueprint.sol"; import { GameTypes } from "src/dispute/lib/Types.sol"; import { Hash } from "src/dispute/lib/Types.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; // Interfaces import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; @@ -35,6 +36,8 @@ import { IMIPS64 } from "interfaces/cannon/IMIPS64.sol"; import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; +import { IOPContractsManagerV2 } from "interfaces/L1/opcm/IOPContractsManagerV2.sol"; +import { IOPContractsManagerUtils } from "interfaces/L1/opcm/IOPContractsManagerUtils.sol"; library ChainAssertions { Vm internal constant vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); @@ -109,10 +112,19 @@ library ChainAssertions { require(config.scalar() >> 248 == 1, "CHECK-SCFG-70"); // Depends on start block being set to 0 in `initialize` require(config.startBlock() == block.number, "CHECK-SCFG-140"); - require( - config.batchInbox() == IOPContractsManager(_doi.opcm).chainIdToBatchInboxAddress(_doi.l2ChainId), - "CHECK-SCFG-150" - ); + if (IOPContractsManager(_doi.opcm).isDevFeatureEnabled(DevFeatures.OPCM_V2)) { + require( + config.batchInbox() + == IOPContractsManagerUtils(IOPContractsManagerV2(address(_doi.opcm)).opcmUtils()) + .chainIdToBatchInboxAddress(_doi.l2ChainId), + "CHECK-SCFG-150" + ); + } else { + require( + config.batchInbox() == IOPContractsManager(_doi.opcm).chainIdToBatchInboxAddress(_doi.l2ChainId), + "CHECK-SCFG-150" + ); + } // Check _addresses require(config.l1CrossDomainMessenger() == _contracts.L1CrossDomainMessenger, "CHECK-SCFG-160"); require(config.l1ERC721Bridge() == _contracts.L1ERC721Bridge, "CHECK-SCFG-170"); @@ -385,9 +397,10 @@ library ChainAssertions { require(address(_opcm) != address(0), "CHECK-OPCM-10"); require(bytes(_opcm.version()).length > 0, "CHECK-OPCM-15"); - require(address(_opcm.protocolVersions()) == _proxies.ProtocolVersions, "CHECK-OPCM-17"); - require(address(_opcm.superchainConfig()) == _proxies.SuperchainConfig, "CHECK-OPCM-19"); - + if (!_opcm.isDevFeatureEnabled(DevFeatures.OPCM_V2)) { + require(address(_opcm.protocolVersions()) == _proxies.ProtocolVersions, "CHECK-OPCM-17"); + require(address(_opcm.superchainConfig()) == _proxies.SuperchainConfig, "CHECK-OPCM-19"); + } // Ensure that the OPCM impls are correctly saved IOPContractsManager.Implementations memory impls = _opcm.implementations(); require(impls.l1ERC721BridgeImpl == _impls.L1ERC721Bridge, "CHECK-OPCM-50"); diff --git a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol index 3467c24dfff..99cf62c9d09 100644 --- a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol @@ -301,7 +301,7 @@ contract Deploy is Deployer { // Save the implementation addresses which are needed outside of this function or script. // When called in a fork test, this will overwrite the existing implementations. artifacts.save("MipsSingleton", address(dio.mipsSingleton)); - if (DevFeatures.isDevFeatureEnabled(dio.opcm.devFeatureBitmap(), DevFeatures.OPCM_V2)) { + if (DevFeatures.isDevFeatureEnabled(cfg.devFeatureBitmap(), DevFeatures.OPCM_V2)) { artifacts.save("OPContractsManagerV2", address(dio.opcmV2)); } else { artifacts.save("OPContractsManager", address(dio.opcm)); @@ -336,10 +336,16 @@ contract Deploy is Deployer { _mips: IMIPS64(address(dio.mipsSingleton)), _oracle: IPreimageOracle(address(dio.preimageOracleSingleton)) }); + IOPContractsManager _opcm; + if (DevFeatures.isDevFeatureEnabled(cfg.devFeatureBitmap(), DevFeatures.OPCM_V2)) { + _opcm = IOPContractsManager(address(dio.opcmV2)); + } else { + _opcm = IOPContractsManager(address(dio.opcm)); + } ChainAssertions.checkOPContractsManager({ _impls: impls, _proxies: _proxies(), - _opcm: IOPContractsManager(address(dio.opcm)), + _opcm: _opcm, _mips: IMIPS64(address(dio.mipsSingleton)) }); ChainAssertions.checkSystemConfigImpls(impls); diff --git a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol index d6d660ff64b..5ccbf1e174c 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol @@ -179,7 +179,41 @@ contract DeployImplementations is Script { superPermissionedDisputeGameImpl: address(_output.superPermissionedDisputeGameImpl) }); - IOPContractsManagerContainer.Implementations memory implementationsV2 = IOPContractsManagerContainer + // Deploy OPCM V1 components + deployOPCMBPImplsContainer(_input, _output, _blueprints, implementations); + deployOPCMGameTypeAdder(_output); + deployOPCMDeployer(_input, _output); + deployOPCMUpgrader(_output); + deployOPCMInteropMigrator(_output); + deployOPCMStandardValidator(_input, _output, implementations); + + // Semgrep rule will fail because the arguments are encoded inside of a separate function. + opcm_ = IOPContractsManager( + // nosemgrep: sol-safety-deployutils-args + DeployUtils.createDeterministic({ + _name: "OPContractsManager", + _args: encodeOPCMConstructor(_input, _output), + _salt: _salt + }) + ); + + vm.label(address(opcm_), "OPContractsManager"); + _output.opcm = opcm_; + + // Set OPCM V2 addresses to zero (not deployed) + _output.opcmV2 = IOPContractsManagerV2(address(0)); + _output.opcmContainer = IOPContractsManagerContainer(address(0)); + } + + function createOPCMContractV2( + Input memory _input, + Output memory _output, + IOPContractsManager.Blueprints memory _blueprints + ) + private + returns (IOPContractsManagerV2 opcmV2_) + { + IOPContractsManagerContainer.Implementations memory implementations = IOPContractsManagerContainer .Implementations({ superchainConfigImpl: address(_output.superchainConfigImpl), protocolVersionsImpl: address(_output.protocolVersionsImpl), @@ -203,7 +237,7 @@ contract DeployImplementations is Script { }); // Convert blueprints to V2 blueprints - IOPContractsManagerContainer.Blueprints memory blueprintsV2 = IOPContractsManagerContainer.Blueprints({ + IOPContractsManagerContainer.Blueprints memory blueprints = IOPContractsManagerContainer.Blueprints({ addressManager: _blueprints.addressManager, proxy: _blueprints.proxy, proxyAdmin: _blueprints.proxyAdmin, @@ -211,28 +245,21 @@ contract DeployImplementations is Script { resolvedDelegateProxy: _blueprints.resolvedDelegateProxy }); - deployOPCMBPImplsContainer(_input, _output, _blueprints, implementations); - deployOPCMContainer(_input, _output, blueprintsV2, implementationsV2); - deployOPCMGameTypeAdder(_output); - deployOPCMDeployer(_input, _output); - deployOPCMUpgrader(_output); - deployOPCMInteropMigrator(_output); - deployOPCMStandardValidator(_input, _output, implementations); + // Deploy OPCM V2 components + deployOPCMContainer(_input, _output, blueprints, implementations); + deployOPCMStandardValidatorV2(_input, _output, implementations); deployOPCMUtils(_output); - deployOPCMV2(_output); + opcmV2_ = deployOPCMV2(_output); - // Semgrep rule will fail because the arguments are encoded inside of a separate function. - opcm_ = IOPContractsManager( - // nosemgrep: sol-safety-deployutils-args - DeployUtils.createDeterministic({ - _name: "OPContractsManager", - _args: encodeOPCMConstructor(_input, _output), - _salt: _salt - }) - ); + // Set OPCM V1 addresses to zero (not deployed) + _output.opcm = IOPContractsManager(address(0)); + _output.opcmContractsContainer = IOPContractsManagerContractsContainer(address(0)); + _output.opcmGameTypeAdder = IOPContractsManagerGameTypeAdder(address(0)); + _output.opcmDeployer = IOPContractsManagerDeployer(address(0)); + _output.opcmUpgrader = IOPContractsManagerUpgrader(address(0)); + _output.opcmInteropMigrator = IOPContractsManagerInteropMigrator(address(0)); - vm.label(address(opcm_), "OPContractsManager"); - _output.opcm = opcm_; + return opcmV2_; } /// @notice Encodes the constructor of the OPContractsManager contract. Used to avoid stack too @@ -283,10 +310,18 @@ contract DeployImplementations is Script { // forgefmt: disable-end vm.stopBroadcast(); - IOPContractsManager opcm = createOPCMContract(_input, _output, blueprints); - - vm.label(address(opcm), "OPContractsManager"); - _output.opcm = opcm; + // Check if OPCM V2 should be deployed + bool deployV2 = DevFeatures.isDevFeatureEnabled(_input.devFeatureBitmap, DevFeatures.OPCM_V2); + + if (deployV2) { + IOPContractsManagerV2 opcmV2 = createOPCMContractV2(_input, _output, blueprints); + vm.label(address(opcmV2), "OPContractsManagerV2"); + _output.opcmV2 = opcmV2; + } else { + IOPContractsManager opcm = createOPCMContract(_input, _output, blueprints); + vm.label(address(opcm), "OPContractsManager"); + _output.opcm = opcm; + } } // --- Core Contracts --- @@ -769,10 +804,56 @@ contract DeployImplementations is Script { _output.opcmUtils = impl; } - function deployOPCMV2(Output memory _output) private { - IOPContractsManagerV2 impl = IOPContractsManagerV2( + function deployOPCMStandardValidatorV2( + Input memory _input, + Output memory _output, + IOPContractsManagerContainer.Implementations memory _implementations + ) + private + { + IOPContractsManagerStandardValidator.Implementations memory opcmImplementations; + opcmImplementations.l1ERC721BridgeImpl = _implementations.l1ERC721BridgeImpl; + opcmImplementations.optimismPortalImpl = _implementations.optimismPortalImpl; + opcmImplementations.optimismPortalInteropImpl = _implementations.optimismPortalInteropImpl; + opcmImplementations.ethLockboxImpl = _implementations.ethLockboxImpl; + opcmImplementations.systemConfigImpl = _implementations.systemConfigImpl; + opcmImplementations.optimismMintableERC20FactoryImpl = _implementations.optimismMintableERC20FactoryImpl; + opcmImplementations.l1CrossDomainMessengerImpl = _implementations.l1CrossDomainMessengerImpl; + opcmImplementations.l1StandardBridgeImpl = _implementations.l1StandardBridgeImpl; + opcmImplementations.disputeGameFactoryImpl = _implementations.disputeGameFactoryImpl; + opcmImplementations.anchorStateRegistryImpl = _implementations.anchorStateRegistryImpl; + opcmImplementations.delayedWETHImpl = _implementations.delayedWETHImpl; + opcmImplementations.mipsImpl = _implementations.mipsImpl; + opcmImplementations.faultDisputeGameImpl = _implementations.faultDisputeGameV2Impl; + opcmImplementations.permissionedDisputeGameImpl = _implementations.permissionedDisputeGameV2Impl; + + IOPContractsManagerStandardValidator impl = IOPContractsManagerStandardValidator( + DeployUtils.createDeterministic({ + _name: "OPContractsManagerStandardValidator.sol:OPContractsManagerStandardValidator", + _args: DeployUtils.encodeConstructor( + abi.encodeCall( + IOPContractsManagerStandardValidator.__constructor__, + ( + opcmImplementations, + _input.superchainConfigProxy, + _input.l1ProxyAdminOwner, + _input.challenger, + _input.withdrawalDelaySeconds, + _input.devFeatureBitmap + ) + ) + ), + _salt: _salt + }) + ); + vm.label(address(impl), "OPContractsManagerStandardValidatorImpl"); + _output.opcmStandardValidator = impl; + } + + function deployOPCMV2(Output memory _output) private returns (IOPContractsManagerV2 opcmV2_) { + opcmV2_ = IOPContractsManagerV2( DeployUtils.createDeterministic({ - _name: "OPContractsManagerV2.sol:OPContractsManagerV2", + _name: "OPContractsManagerV2", _args: DeployUtils.encodeConstructor( abi.encodeCall( IOPContractsManagerV2.__constructor__, @@ -782,8 +863,7 @@ contract DeployImplementations is Script { _salt: _salt }) ); - vm.label(address(impl), "OPContractsManagerV2Impl"); - _output.opcmV2 = impl; + vm.label(address(opcmV2_), "OPContractsManagerV2"); } function deployStorageSetterImpl(Output memory _output) private { @@ -851,8 +931,12 @@ contract DeployImplementations is Script { function assertValidOutput(Input memory _input, Output memory _output) private { // With 12 addresses, we'd get a stack too deep error if we tried to do this inline as a // single call to `Solarray.addresses`. So we split it into two calls. + + // Check which OPCM version was deployed + bool deployedV2 = DevFeatures.isDevFeatureEnabled(_input.devFeatureBitmap, DevFeatures.OPCM_V2); + address[] memory addrs1 = Solarray.addresses( - address(_output.opcm), + deployedV2 ? address(_output.opcmV2) : address(_output.opcm), address(_output.optimismPortalImpl), address(_output.delayedWETHImpl), address(_output.preimageOracleSingleton), @@ -883,6 +967,27 @@ contract DeployImplementations is Script { DeployUtils.assertValidContractAddresses(Solarray.extend(addrs1, addrs2)); + // Validate OPCM V2 flag + if (DevFeatures.isDevFeatureEnabled(_input.devFeatureBitmap, DevFeatures.OPCM_V2)) { + require( + address(_output.opcmV2) != address(0), + "DeployImplementations: OPCM V2 flag enabled but OPCM V2 not deployed" + ); + require( + address(_output.opcm) == address(0), + "DeployImplementations: OPCM V2 flag enabled but OPCM V1 was deployed" + ); + } else { + require( + address(_output.opcm) != address(0), + "DeployImplementations: OPCM V2 flag disabled but OPCM V1 not deployed" + ); + require( + address(_output.opcmV2) == address(0), + "DeployImplementations: OPCM V2 flag disabled but OPCM V2 was deployed" + ); + } + if (!DevFeatures.isDevFeatureEnabled(_input.devFeatureBitmap, DevFeatures.OPTIMISM_PORTAL_INTEROP)) { require( address(_output.superFaultDisputeGameImpl) == address(0), @@ -909,15 +1014,18 @@ contract DeployImplementations is Script { ChainAssertions.checkL1StandardBridgeImpl(_output.l1StandardBridgeImpl); ChainAssertions.checkMIPS(_output.mipsSingleton, _output.preimageOracleSingleton); - Types.ContractSet memory proxies; - proxies.SuperchainConfig = address(_input.superchainConfigProxy); - proxies.ProtocolVersions = address(_input.protocolVersionsProxy); - ChainAssertions.checkOPContractsManager({ - _impls: impls, - _proxies: proxies, - _opcm: IOPContractsManager(address(_output.opcm)), - _mips: IMIPS64(address(_output.mipsSingleton)) - }); + // Only check OPCM V1 if it was deployed + if (!DevFeatures.isDevFeatureEnabled(_input.devFeatureBitmap, DevFeatures.OPCM_V2)) { + Types.ContractSet memory proxies; + proxies.SuperchainConfig = address(_input.superchainConfigProxy); + proxies.ProtocolVersions = address(_input.protocolVersionsProxy); + ChainAssertions.checkOPContractsManager({ + _impls: impls, + _proxies: proxies, + _opcm: IOPContractsManager(address(_output.opcm)), + _mips: IMIPS64(address(_output.mipsSingleton)) + }); + } ChainAssertions.checkOptimismMintableERC20FactoryImpl(_output.optimismMintableERC20FactoryImpl); ChainAssertions.checkOptimismPortal2({ diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index 2f5ff24e752..ddecc78a9df 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -3,6 +3,8 @@ pragma solidity 0.8.15; import { Script } from "forge-std/Script.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; +import { Constants } from "src/libraries/Constants.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; import { Solarray } from "scripts/libraries/Solarray.sol"; import { ChainAssertions } from "scripts/deploy/ChainAssertions.sol"; @@ -11,6 +13,7 @@ import { Types } from "scripts/libraries/Types.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; +import { IOPContractsManagerV2 } from "interfaces/L1/opcm/IOPContractsManagerV2.sol"; import { IAddressManager } from "interfaces/legacy/IAddressManager.sol"; import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; @@ -24,7 +27,7 @@ import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; import { IL1StandardBridge } from "interfaces/L1/IL1StandardBridge.sol"; import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMintableERC20Factory.sol"; import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; -import { IOPContractsManager } from "../../interfaces/L1/IOPContractsManager.sol"; +import { GameTypes } from "src/dispute/lib/Types.sol"; contract DeployOPChain is Script { struct Output { @@ -54,8 +57,66 @@ contract DeployOPChain is Script { function run(Types.DeployOPChainInput memory _input) public returns (Output memory output_) { checkInput(_input); - IOPContractsManager opcm = IOPContractsManager(_input.opcm); + // Check if OPCM v2 should be used. + bool useV2 = isDevFeatureOpcmV2Enabled(_input.opcm); + if (useV2) { + IOPContractsManagerV2 opcmV2 = IOPContractsManagerV2(_input.opcm); + IOPContractsManagerV2.FullConfig memory config = toOPCMV2DeployInput(_input); + + vm.broadcast(msg.sender); + IOPContractsManagerV2.ChainContracts memory chainContracts = opcmV2.deploy(config); + output_ = fromOPCMV2OutputToOutput(chainContracts); + } else { + IOPContractsManager opcm = IOPContractsManager(_input.opcm); + IOPContractsManager.DeployInput memory deployInput = toOPCMV1DeployInput(_input); + + vm.broadcast(msg.sender); + IOPContractsManager.DeployOutput memory deployOutput = opcm.deploy(deployInput); + + output_ = fromOPCMV1OutputToOutput(deployOutput); + } + + checkOutput(_input, output_); + + vm.label(address(output_.opChainProxyAdmin), "opChainProxyAdmin"); + vm.label(address(output_.addressManager), "addressManager"); + vm.label(address(output_.l1ERC721BridgeProxy), "l1ERC721BridgeProxy"); + vm.label(address(output_.systemConfigProxy), "systemConfigProxy"); + vm.label(address(output_.optimismMintableERC20FactoryProxy), "optimismMintableERC20FactoryProxy"); + vm.label(address(output_.l1StandardBridgeProxy), "l1StandardBridgeProxy"); + vm.label(address(output_.l1CrossDomainMessengerProxy), "l1CrossDomainMessengerProxy"); + vm.label(address(output_.optimismPortalProxy), "optimismPortalProxy"); + vm.label(address(output_.ethLockboxProxy), "ethLockboxProxy"); + vm.label(address(output_.disputeGameFactoryProxy), "disputeGameFactoryProxy"); + vm.label(address(output_.anchorStateRegistryProxy), "anchorStateRegistryProxy"); + vm.label(address(output_.delayedWETHPermissionedGameProxy), "delayedWETHPermissionedGameProxy"); + // TODO: Eventually switch from Permissioned to Permissionless. + // vm.label(address(output_.faultDisputeGame), "faultDisputeGame"); + // vm.label(address(output_.delayedWETHPermissionlessGameProxy), "delayedWETHPermissionlessGameProxy"); + } + + // -------- Features -------- + + /// @notice Checks if OPCM v2 dev feature flag is enabled from the contract's dev feature bitmap. + function isDevFeatureOpcmV2Enabled(address _opcmAddr) internal view returns (bool) { + // Both v1 and v2 share the same interface for this function. + return IOPContractsManager(_opcmAddr).isDevFeatureEnabled(DevFeatures.OPCM_V2); + } + + function isDevFeatureV2DisputeGamesEnabled(address _opcmAddr) internal view returns (bool) { + IOPContractsManager opcm = IOPContractsManager(_opcmAddr); + return DevFeatures.isDevFeatureEnabled(opcm.devFeatureBitmap(), DevFeatures.DEPLOY_V2_DISPUTE_GAMES); + } + + /// @notice Converts Types.DeployOPChainInput to IOPContractsManager.DeployInput. + /// @param _input The input parameters. + /// @return deployInput_ The deployed input parameters. + function toOPCMV1DeployInput(Types.DeployOPChainInput memory _input) + internal + pure + returns (IOPContractsManager.DeployInput memory deployInput_) + { IOPContractsManager.Roles memory roles = IOPContractsManager.Roles({ opChainProxyAdminOwner: _input.opChainProxyAdminOwner, systemConfigOwner: _input.systemConfigOwner, @@ -64,7 +125,7 @@ contract DeployOPChain is Script { proposer: _input.proposer, challenger: _input.challenger }); - IOPContractsManager.DeployInput memory deployInput = IOPContractsManager.DeployInput({ + deployInput_ = IOPContractsManager.DeployInput({ roles: roles, basefeeScalar: _input.basefeeScalar, blobBasefeeScalar: _input.blobBaseFeeScalar, @@ -80,46 +141,133 @@ contract DeployOPChain is Script { disputeMaxClockDuration: _input.disputeMaxClockDuration, useCustomGasToken: _input.useCustomGasToken }); + } - vm.broadcast(msg.sender); - IOPContractsManager.DeployOutput memory deployOutput = opcm.deploy(deployInput); - - vm.label(address(deployOutput.opChainProxyAdmin), "opChainProxyAdmin"); - vm.label(address(deployOutput.addressManager), "addressManager"); - vm.label(address(deployOutput.l1ERC721BridgeProxy), "l1ERC721BridgeProxy"); - vm.label(address(deployOutput.systemConfigProxy), "systemConfigProxy"); - vm.label(address(deployOutput.optimismMintableERC20FactoryProxy), "optimismMintableERC20FactoryProxy"); - vm.label(address(deployOutput.l1StandardBridgeProxy), "l1StandardBridgeProxy"); - vm.label(address(deployOutput.l1CrossDomainMessengerProxy), "l1CrossDomainMessengerProxy"); - vm.label(address(deployOutput.optimismPortalProxy), "optimismPortalProxy"); - vm.label(address(deployOutput.ethLockboxProxy), "ethLockboxProxy"); - vm.label(address(deployOutput.disputeGameFactoryProxy), "disputeGameFactoryProxy"); - vm.label(address(deployOutput.anchorStateRegistryProxy), "anchorStateRegistryProxy"); - vm.label(address(deployOutput.permissionedDisputeGame), "permissionedDisputeGame"); - vm.label(address(deployOutput.delayedWETHPermissionedGameProxy), "delayedWETHPermissionedGameProxy"); - // TODO: Eventually switch from Permissioned to Permissionless. - // vm.label(address(deployOutput.faultDisputeGame), "faultDisputeGame"); - // vm.label(address(deployOutput.delayedWETHPermissionlessGameProxy), "delayedWETHPermissionlessGameProxy"); + /// @notice Converts Types.DeployOPChainInput to IOPContractsManagerV2.FullConfig. + /// @param _input The input parameters. + /// @return config_ The deployed input parameters. + function toOPCMV2DeployInput(Types.DeployOPChainInput memory _input) + internal + pure + returns (IOPContractsManagerV2.FullConfig memory config_) + { + // Build dispute game configs - OPCMV2 requires exactly 3 configs: CANNON, PERMISSIONED_CANNON, CANNON_KONA + IOPContractsManagerV2.DisputeGameConfig[] memory disputeGameConfigs = + new IOPContractsManagerV2.DisputeGameConfig[](3); + + // Determine which games should be enabled based on the starting respected game type + bool cannonEnabled = _input.disputeGameType.raw() == GameTypes.CANNON.raw(); + bool permissionedCannonEnabled = true; // PERMISSIONED_CANNON must always be enabled + bool cannonKonaEnabled = _input.disputeGameType.raw() == GameTypes.CANNON_KONA.raw(); + + // Config 0: CANNON + IOPContractsManagerV2.FaultDisputeGameConfig memory cannonConfig = + IOPContractsManagerV2.FaultDisputeGameConfig({ absolutePrestate: _input.disputeAbsolutePrestate }); + + disputeGameConfigs[0] = IOPContractsManagerV2.DisputeGameConfig({ + enabled: cannonEnabled, + initBond: cannonEnabled ? 0.08 ether : 0, // Standard init bond if enabled + gameType: GameTypes.CANNON, + gameArgs: abi.encode(cannonConfig) + }); + + // Config 1: PERMISSIONED_CANNON (must be enabled) + IOPContractsManagerV2.PermissionedDisputeGameConfig memory pdgConfig = IOPContractsManagerV2 + .PermissionedDisputeGameConfig({ + absolutePrestate: _input.disputeAbsolutePrestate, + proposer: _input.proposer, + challenger: _input.challenger + }); + disputeGameConfigs[1] = IOPContractsManagerV2.DisputeGameConfig({ + enabled: permissionedCannonEnabled, + initBond: 0.08 ether, // Standard init bond + gameType: GameTypes.PERMISSIONED_CANNON, + gameArgs: abi.encode(pdgConfig) + }); + + // Config 2: CANNON_KONA + IOPContractsManagerV2.FaultDisputeGameConfig memory cannonKonaConfig = + IOPContractsManagerV2.FaultDisputeGameConfig({ absolutePrestate: _input.disputeAbsolutePrestate }); + + disputeGameConfigs[2] = IOPContractsManagerV2.DisputeGameConfig({ + enabled: cannonKonaEnabled, + initBond: cannonKonaEnabled ? 0.08 ether : 0, // Standard init bond if enabled + gameType: GameTypes.CANNON_KONA, + gameArgs: abi.encode(cannonKonaConfig) + }); + + config_ = IOPContractsManagerV2.FullConfig({ + saltMixer: _input.saltMixer, + superchainConfig: _input.superchainConfig, + proxyAdminOwner: _input.opChainProxyAdminOwner, + systemConfigOwner: _input.systemConfigOwner, + unsafeBlockSigner: _input.unsafeBlockSigner, + batcher: _input.batcher, + startingAnchorRoot: ScriptConstants.DEFAULT_OUTPUT_ROOT(), + startingRespectedGameType: _input.disputeGameType, + basefeeScalar: _input.basefeeScalar, + blobBasefeeScalar: _input.blobBaseFeeScalar, + gasLimit: _input.gasLimit, + l2ChainId: _input.l2ChainId, + resourceConfig: Constants.DEFAULT_RESOURCE_CONFIG(), + disputeGameConfigs: disputeGameConfigs, + useCustomGasToken: _input.useCustomGasToken + }); + } + + /// @notice Converts IOPContractsManagerV2.ChainContracts to Output. + /// @param _chainContracts The chain contracts. + /// @return output_ The output parameters. + function fromOPCMV2OutputToOutput(IOPContractsManagerV2.ChainContracts memory _chainContracts) + internal + pure + returns (Output memory output_) + { output_ = Output({ - opChainProxyAdmin: deployOutput.opChainProxyAdmin, - addressManager: deployOutput.addressManager, - l1ERC721BridgeProxy: deployOutput.l1ERC721BridgeProxy, - systemConfigProxy: deployOutput.systemConfigProxy, - optimismMintableERC20FactoryProxy: deployOutput.optimismMintableERC20FactoryProxy, - l1StandardBridgeProxy: deployOutput.l1StandardBridgeProxy, - l1CrossDomainMessengerProxy: deployOutput.l1CrossDomainMessengerProxy, - optimismPortalProxy: deployOutput.optimismPortalProxy, - ethLockboxProxy: deployOutput.ethLockboxProxy, - disputeGameFactoryProxy: deployOutput.disputeGameFactoryProxy, - anchorStateRegistryProxy: deployOutput.anchorStateRegistryProxy, - faultDisputeGame: deployOutput.faultDisputeGame, - permissionedDisputeGame: deployOutput.permissionedDisputeGame, - delayedWETHPermissionedGameProxy: deployOutput.delayedWETHPermissionedGameProxy, - delayedWETHPermissionlessGameProxy: deployOutput.delayedWETHPermissionlessGameProxy + opChainProxyAdmin: _chainContracts.proxyAdmin, + addressManager: _chainContracts.addressManager, + l1ERC721BridgeProxy: _chainContracts.l1ERC721Bridge, + systemConfigProxy: _chainContracts.systemConfig, + optimismMintableERC20FactoryProxy: _chainContracts.optimismMintableERC20Factory, + l1StandardBridgeProxy: _chainContracts.l1StandardBridge, + l1CrossDomainMessengerProxy: _chainContracts.l1CrossDomainMessenger, + optimismPortalProxy: _chainContracts.optimismPortal, + ethLockboxProxy: _chainContracts.ethLockbox, + disputeGameFactoryProxy: _chainContracts.disputeGameFactory, + anchorStateRegistryProxy: _chainContracts.anchorStateRegistry, + faultDisputeGame: IFaultDisputeGame(address(0)), + permissionedDisputeGame: IPermissionedDisputeGame(address(0)), + delayedWETHPermissionedGameProxy: _chainContracts.delayedWETH, + delayedWETHPermissionlessGameProxy: IDelayedWETH(payable(address(0))) }); + } - checkOutput(_input, output_); + /// @notice Converts IOPContractsManager.DeployOutput to Output. + /// @param _deployOutput The deploy output. + /// @return output_ The output parameters. + function fromOPCMV1OutputToOutput(IOPContractsManager.DeployOutput memory _deployOutput) + internal + pure + returns (Output memory output_) + { + output_ = Output({ + opChainProxyAdmin: _deployOutput.opChainProxyAdmin, + addressManager: _deployOutput.addressManager, + l1ERC721BridgeProxy: _deployOutput.l1ERC721BridgeProxy, + systemConfigProxy: _deployOutput.systemConfigProxy, + optimismMintableERC20FactoryProxy: _deployOutput.optimismMintableERC20FactoryProxy, + l1StandardBridgeProxy: _deployOutput.l1StandardBridgeProxy, + l1CrossDomainMessengerProxy: _deployOutput.l1CrossDomainMessengerProxy, + optimismPortalProxy: _deployOutput.optimismPortalProxy, + ethLockboxProxy: _deployOutput.ethLockboxProxy, + disputeGameFactoryProxy: _deployOutput.disputeGameFactoryProxy, + anchorStateRegistryProxy: _deployOutput.anchorStateRegistryProxy, + faultDisputeGame: _deployOutput.faultDisputeGame, + permissionedDisputeGame: _deployOutput.permissionedDisputeGame, + delayedWETHPermissionedGameProxy: _deployOutput.delayedWETHPermissionedGameProxy, + delayedWETHPermissionlessGameProxy: _deployOutput.delayedWETHPermissionlessGameProxy + }); } // -------- Validations -------- @@ -187,12 +335,23 @@ contract DeployOPChain is Script { SystemConfig: address(_o.systemConfigProxy), L1ERC721Bridge: address(_o.l1ERC721BridgeProxy), ProtocolVersions: address(0), - SuperchainConfig: address(0) + SuperchainConfig: address(_i.superchainConfig) }); - // Check dispute games - // With v2 game contracts enabled, we use the predeployed pdg implementation - address expectedPDGImpl = IOPContractsManager(_i.opcm).implementations().permissionedDisputeGameV2Impl; + // Check dispute games and get superchain config + address expectedPDGImpl = address(_o.permissionedDisputeGame); + + if (isDevFeatureOpcmV2Enabled(_i.opcm)) { + // OPCM v2: use implementations from v2 contract + IOPContractsManagerV2 opcmV2 = IOPContractsManagerV2(_i.opcm); + expectedPDGImpl = opcmV2.implementations().permissionedDisputeGameV2Impl; + } else { + // OPCM v1: use implementations from v1 contract + IOPContractsManager opcm = IOPContractsManager(_i.opcm); + // With v2 game contracts enabled, we use the predeployed pdg implementation + expectedPDGImpl = opcm.implementations().permissionedDisputeGameV2Impl; + } + ChainAssertions.checkDisputeGameFactory( _o.disputeGameFactoryProxy, _i.opChainProxyAdminOwner, expectedPDGImpl, true ); @@ -201,7 +360,7 @@ contract DeployOPChain is Script { ChainAssertions.checkL1CrossDomainMessenger(_o.l1CrossDomainMessengerProxy, vm, true); ChainAssertions.checkOptimismPortal2({ _contracts: proxies, - _superchainConfig: IOPContractsManager(_i.opcm).superchainConfig(), + _superchainConfig: _i.superchainConfig, _opChainProxyAdminOwner: _i.opChainProxyAdminOwner, _isProxy: true }); diff --git a/packages/contracts-bedrock/scripts/deploy/ReadImplementationAddresses.s.sol b/packages/contracts-bedrock/scripts/deploy/ReadImplementationAddresses.s.sol index 23deddc11e7..3fb08277fca 100644 --- a/packages/contracts-bedrock/scripts/deploy/ReadImplementationAddresses.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/ReadImplementationAddresses.s.sol @@ -5,8 +5,11 @@ import { IProxy } from "interfaces/universal/IProxy.sol"; import { Script } from "forge-std/Script.sol"; import { IMIPS64 } from "interfaces/cannon/IMIPS64.sol"; import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; +import { IOPContractsManagerV2 } from "interfaces/L1/opcm/IOPContractsManagerV2.sol"; +import { IOPContractsManagerContainer } from "interfaces/L1/opcm/IOPContractsManagerContainer.sol"; import { IAddressManager } from "interfaces/legacy/IAddressManager.sol"; import { IStaticL1ChugSplashProxy } from "interfaces/legacy/IL1ChugSplashProxy.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; contract ReadImplementationAddresses is Script { struct Input { @@ -59,24 +62,53 @@ contract ReadImplementationAddresses is Script { vm.prank(address(0)); output_.l1StandardBridge = IStaticL1ChugSplashProxy(_input.l1StandardBridgeProxy).getImplementation(); - // Get implementations from OPCM - IOPContractsManager opcm = IOPContractsManager(_input.opcm); - output_.opcmGameTypeAdder = address(opcm.opcmGameTypeAdder()); - output_.opcmDeployer = address(opcm.opcmDeployer()); - output_.opcmUpgrader = address(opcm.opcmUpgrader()); - output_.opcmInteropMigrator = address(opcm.opcmInteropMigrator()); - output_.opcmStandardValidator = address(opcm.opcmStandardValidator()); + // Check if OPCM v2 is being used + bool useV2 = IOPContractsManager(_input.opcm).isDevFeatureEnabled(DevFeatures.OPCM_V2); - IOPContractsManager.Implementations memory impls = opcm.implementations(); - output_.mipsSingleton = impls.mipsImpl; - output_.delayedWETH = impls.delayedWETHImpl; - output_.ethLockbox = impls.ethLockboxImpl; - output_.anchorStateRegistry = impls.anchorStateRegistryImpl; - output_.optimismPortalInterop = impls.optimismPortalInteropImpl; - output_.faultDisputeGameV2 = impls.faultDisputeGameV2Impl; - output_.permissionedDisputeGameV2 = impls.permissionedDisputeGameV2Impl; - output_.superFaultDisputeGame = impls.superFaultDisputeGameImpl; - output_.superPermissionedDisputeGame = impls.superPermissionedDisputeGameImpl; + if (useV2) { + // Get implementations from OPCM V2 + IOPContractsManagerV2 opcmV2 = IOPContractsManagerV2(_input.opcm); + + // OPCMV2 doesn't expose these addresses directly, so we set them to zero + // These are internal to the OPCM container and not meant to be accessed externally + output_.opcmGameTypeAdder = address(0); + output_.opcmDeployer = address(0); + output_.opcmUpgrader = address(0); + output_.opcmInteropMigrator = address(0); + + // StandardValidator is accessible via the standardValidator() method + output_.opcmStandardValidator = address(opcmV2.opcmStandardValidator()); + + IOPContractsManagerContainer.Implementations memory impls = opcmV2.implementations(); + output_.mipsSingleton = impls.mipsImpl; + output_.delayedWETH = impls.delayedWETHImpl; + output_.ethLockbox = impls.ethLockboxImpl; + output_.anchorStateRegistry = impls.anchorStateRegistryImpl; + output_.optimismPortalInterop = impls.optimismPortalInteropImpl; + output_.faultDisputeGameV2 = impls.faultDisputeGameV2Impl; + output_.permissionedDisputeGameV2 = impls.permissionedDisputeGameV2Impl; + output_.superFaultDisputeGame = impls.superFaultDisputeGameImpl; + output_.superPermissionedDisputeGame = impls.superPermissionedDisputeGameImpl; + } else { + // Get implementations from OPCM V1 + IOPContractsManager opcm = IOPContractsManager(_input.opcm); + output_.opcmGameTypeAdder = address(opcm.opcmGameTypeAdder()); + output_.opcmDeployer = address(opcm.opcmDeployer()); + output_.opcmUpgrader = address(opcm.opcmUpgrader()); + output_.opcmInteropMigrator = address(opcm.opcmInteropMigrator()); + output_.opcmStandardValidator = address(opcm.opcmStandardValidator()); + + IOPContractsManager.Implementations memory impls = opcm.implementations(); + output_.mipsSingleton = impls.mipsImpl; + output_.delayedWETH = impls.delayedWETHImpl; + output_.ethLockbox = impls.ethLockboxImpl; + output_.anchorStateRegistry = impls.anchorStateRegistryImpl; + output_.optimismPortalInterop = impls.optimismPortalInteropImpl; + output_.faultDisputeGameV2 = impls.faultDisputeGameV2Impl; + output_.permissionedDisputeGameV2 = impls.permissionedDisputeGameV2Impl; + output_.superFaultDisputeGame = impls.superFaultDisputeGameImpl; + output_.superPermissionedDisputeGame = impls.superPermissionedDisputeGameImpl; + } // Get L1CrossDomainMessenger from AddressManager IAddressManager am = IAddressManager(_input.addressManager); diff --git a/packages/contracts-bedrock/scripts/libraries/Types.sol b/packages/contracts-bedrock/scripts/libraries/Types.sol index 7b90b7204e9..bec2f022e91 100644 --- a/packages/contracts-bedrock/scripts/libraries/Types.sol +++ b/packages/contracts-bedrock/scripts/libraries/Types.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; import { Claim, Duration, GameType } from "src/dispute/lib/Types.sol"; +import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; library Types { /// @notice Represents a set of L1 contracts. Used to represent a set of proxies. @@ -49,6 +50,8 @@ library Types { // Fee params uint32 operatorFeeScalar; uint64 operatorFeeConstant; + // Superchain contracts + ISuperchainConfig superchainConfig; // Whether to use the custom gas token. bool useCustomGasToken; } diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json index b5871765022..07ce75d6ff8 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json @@ -1104,6 +1104,11 @@ "name": "LatestReleaseNotSet", "type": "error" }, + { + "inputs": [], + "name": "OPContractsManager_V2Enabled", + "type": "error" + }, { "inputs": [], "name": "OnlyDelegatecall", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 610fe404992..c369f89b473 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -24,8 +24,8 @@ "sourceCodeHash": "0xfca613b5d055ffc4c3cbccb0773ddb9030abedc1aa6508c9e2e7727cc0cd617b" }, "src/L1/OPContractsManager.sol:OPContractsManager": { - "initCodeHash": "0x51bb4fa1d01503ec16e8611ac1e2f042ea51280310f1cca2a15a4826acfc2db5", - "sourceCodeHash": "0xacc5a0e75797686ad9545dcae82c89b2ca847ba42988eb63466ef03f4e1c739e" + "initCodeHash": "0xdbf23ba71f865d1c3086a10b48f8faaa21ed0d689fdd14ec9ad988f8f013b5c3", + "sourceCodeHash": "0x048f592543a93c05085919f6a1670600ead00991e8370ae83fea1665ca09a5b4" }, "src/L1/OPContractsManagerStandardValidator.sol:OPContractsManagerStandardValidator": { "initCodeHash": "0xdec828fdb9f9bb7a35ca03d851b041fcd088681957642e949b5d320358d9b9a1", diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index b35ee3d98a4..85332af9532 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -1977,9 +1977,9 @@ contract OPContractsManager is ISemver { // -------- Constants and Variables -------- - /// @custom:semver 6.0.0 + /// @custom:semver 6.0.1 function version() public pure virtual returns (string memory) { - return "6.0.0"; + return "6.0.1"; } OPContractsManagerGameTypeAdder public immutable opcmGameTypeAdder; @@ -2046,6 +2046,9 @@ contract OPContractsManager is ISemver { /// @notice Thrown if logic gated by a dev feature flag is incorrectly accessed. error InvalidDevFeatureAccess(bytes32 devFeature); + /// @notice Thrown when OPCM v2 is enabled via dev feature flag. + error OPContractsManager_V2Enabled(); + // -------- Methods -------- constructor( @@ -2130,6 +2133,8 @@ contract OPContractsManager is ISemver { /// @param _input The deploy input parameters for the deployment. /// @return The deploy output values of the deployment. function deploy(DeployInput calldata _input) external virtual returns (DeployOutput memory) { + _assertV2NotEnabled(); + return opcmDeployer.deploy(_input, superchainConfig, msg.sender); } @@ -2139,6 +2144,8 @@ contract OPContractsManager is ISemver { /// `_opChainConfigs`'s ProxyAdmin. /// @dev This function requires that each chain's superchainConfig is already upgraded. function upgrade(OpChainConfig[] memory _opChainConfigs) external virtual { + _assertV2NotEnabled(); + if (address(this) == address(thisOPCM)) revert OnlyDelegatecall(); bytes memory data = abi.encodeCall(OPContractsManagerUpgrader.upgrade, (_opChainConfigs)); @@ -2150,6 +2157,8 @@ contract OPContractsManager is ISemver { /// @dev This function is intended to be DELEGATECALLed by the superchainConfig's ProxyAdminOwner. /// @dev This function will revert if the SuperchainConfig is already at or above the target version. function upgradeSuperchainConfig(ISuperchainConfig _superchainConfig) external { + _assertV2NotEnabled(); + if (address(this) == address(thisOPCM)) revert OnlyDelegatecall(); bytes memory data = abi.encodeCall(OPContractsManagerUpgrader.upgradeSuperchainConfig, (_superchainConfig)); @@ -2159,6 +2168,8 @@ contract OPContractsManager is ISemver { /// @notice addGameType deploys a new dispute game and links it to the DisputeGameFactory. The inputted _gameConfigs /// must be added in ascending GameType order. function addGameType(AddGameInput[] memory _gameConfigs) public virtual returns (AddGameOutput[] memory) { + _assertV2NotEnabled(); + if (address(this) == address(thisOPCM)) revert OnlyDelegatecall(); bytes memory data = abi.encodeCall(OPContractsManagerGameTypeAdder.addGameType, (_gameConfigs)); @@ -2170,6 +2181,8 @@ contract OPContractsManager is ISemver { /// @notice Updates the prestate hash for dispute games while keeping all other parameters the same /// @param _prestateUpdateInputs The new prestate hashes to use function updatePrestate(UpdatePrestateInput[] memory _prestateUpdateInputs) public { + _assertV2NotEnabled(); + if (address(this) == address(thisOPCM)) revert OnlyDelegatecall(); bytes memory data = abi.encodeCall(OPContractsManagerGameTypeAdder.updatePrestate, (_prestateUpdateInputs)); @@ -2180,6 +2193,8 @@ contract OPContractsManager is ISemver { /// @notice Migrates the Optimism contracts to the latest version. /// @param _input Input parameters for the migration. function migrate(OPContractsManagerInteropMigrator.MigrateInput calldata _input) external virtual { + _assertV2NotEnabled(); + if (address(this) == address(thisOPCM)) revert OnlyDelegatecall(); bytes memory data = abi.encodeCall(OPContractsManagerInteropMigrator.migrate, (_input)); @@ -2220,6 +2235,13 @@ contract OPContractsManager is ISemver { return opcmDeployer.isDevFeatureEnabled(_feature); } + /// @notice Reverts if the dev feature flag for OPCM v2 is enabled. + function _assertV2NotEnabled() internal view { + if (isDevFeatureEnabled(DevFeatures.OPCM_V2)) { + revert OPContractsManager_V2Enabled(); + } + } + /// @notice Helper function to perform a delegatecall to a target contract /// @param _target The target contract address /// @param _data The calldata to send to the target diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index 8cb35a2209c..7feffe72daa 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -2242,6 +2242,11 @@ contract OPContractsManager_Migrate_Test is OPContractsManager_TestInit { contract OPContractsManager_Deploy_Test is DeployOPChain_TestBase, DisputeGames { using stdStorage for StdStorage; + function setUp() public override { + super.setUp(); + skipIfDevFeatureEnabled(DevFeatures.OPCM_V2); + } + // This helper function is used to convert the input struct type defined in DeployOPChain.s.sol // to the input struct type defined in OPContractsManager.sol. function toOPCMDeployInput(Types.DeployOPChainInput memory _doi) @@ -2279,7 +2284,7 @@ contract OPContractsManager_Deploy_Test is DeployOPChain_TestBase, DisputeGames input.l2ChainId = 0; vm.expectRevert(IOPContractsManager.InvalidChainId.selector); - opcm.deploy(input); + IOPContractsManager(opcmAddr).deploy(input); } function test_deploy_l2ChainIdEqualsCurrentChainId_reverts() public { @@ -2287,19 +2292,19 @@ contract OPContractsManager_Deploy_Test is DeployOPChain_TestBase, DisputeGames input.l2ChainId = block.chainid; vm.expectRevert(IOPContractsManager.InvalidChainId.selector); - opcm.deploy(input); + IOPContractsManager(opcmAddr).deploy(input); } function test_deploy_succeeds() public { vm.expectEmit(true, true, true, false); // TODO precompute the expected `deployOutput`. emit Deployed(deployOPChainInput.l2ChainId, address(this), bytes("")); - opcm.deploy(toOPCMDeployInput(deployOPChainInput)); + IOPContractsManager(opcmAddr).deploy(toOPCMDeployInput(deployOPChainInput)); } /// @notice Test that deploy sets the permissioned dispute game implementation function test_deployPermissioned_succeeds() public { // Sanity-check setup is consistent with devFeatures flag - IOPContractsManager.Implementations memory impls = opcm.implementations(); + IOPContractsManager.Implementations memory impls = IOPContractsManager(opcmAddr).implementations(); address pdgImpl = address(impls.permissionedDisputeGameV2Impl); address fdgImpl = address(impls.faultDisputeGameV2Impl); assertFalse(pdgImpl == address(0), "PDG implementation address should be non-zero"); @@ -2307,7 +2312,7 @@ contract OPContractsManager_Deploy_Test is DeployOPChain_TestBase, DisputeGames // Run OPCM.deploy IOPContractsManager.DeployInput memory opcmInput = toOPCMDeployInput(deployOPChainInput); - IOPContractsManager.DeployOutput memory opcmOutput = opcm.deploy(opcmInput); + IOPContractsManager.DeployOutput memory opcmOutput = IOPContractsManager(opcmAddr).deploy(opcmInput); // Verify that the DisputeGameFactory has registered an implementation for the PERMISSIONED_CANNON game type address actualPDGAddress = address(opcmOutput.disputeGameFactoryProxy.gameImpls(GameTypes.PERMISSIONED_CANNON)); diff --git a/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol b/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol index 465026596ba..4b58a66ede8 100644 --- a/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol @@ -246,6 +246,9 @@ contract DeployImplementations_Test is Test, FeatureFlags { DeployImplementations.Output memory output = deployImplementations.run(input); + // Check which OPCM version is deployed + bool opcmV2Enabled = DevFeatures.isDevFeatureEnabled(_devFeatureBitmap, DevFeatures.OPCM_V2); + // Basic assertions assertNotEq(address(output.anchorStateRegistryImpl), address(0), "100"); assertNotEq(address(output.delayedWETHImpl), address(0), "200"); @@ -255,10 +258,26 @@ contract DeployImplementations_Test is Test, FeatureFlags { assertNotEq(address(output.l1ERC721BridgeImpl), address(0), "500"); assertNotEq(address(output.l1StandardBridgeImpl), address(0), "600"); assertNotEq(address(output.mipsSingleton), address(0), "700"); - assertNotEq(address(output.opcm), address(0), "800"); - assertNotEq(address(output.opcmContractsContainer), address(0), "900"); - assertNotEq(address(output.opcmDeployer), address(0), "1000"); - assertNotEq(address(output.opcmGameTypeAdder), address(0), "1100"); + + // OPCM version-specific assertions + if (opcmV2Enabled) { + assertNotEq(address(output.opcmV2), address(0), "800"); + assertNotEq(address(output.opcmContainer), address(0), "900"); + assertNotEq(address(output.opcmStandardValidator), address(0), "1000"); + // V1 contracts should be null when V2 is enabled + assertEq(address(output.opcm), address(0), "800-v1"); + assertEq(address(output.opcmContractsContainer), address(0), "900-v1"); + assertEq(address(output.opcmDeployer), address(0), "1000-v1"); + assertEq(address(output.opcmGameTypeAdder), address(0), "1100-v1"); + } else { + assertNotEq(address(output.opcm), address(0), "800"); + assertNotEq(address(output.opcmContractsContainer), address(0), "900"); + assertNotEq(address(output.opcmDeployer), address(0), "1000"); + assertNotEq(address(output.opcmGameTypeAdder), address(0), "1100"); + // V2 contracts should be null when V1 is enabled + assertEq(address(output.opcmV2), address(0), "800-v2"); + assertEq(address(output.opcmContainer), address(0), "900-v2"); + } assertNotEq(address(output.faultDisputeGameV2Impl), address(0), "V2 should be deployed when enabled"); assertNotEq(address(output.permissionedDisputeGameV2Impl), address(0), "V2 should be deployed when enabled"); @@ -352,10 +371,26 @@ contract DeployImplementations_Test is Test, FeatureFlags { assertNotEq(address(output.l1ERC721BridgeImpl).code, empty, "1700"); assertNotEq(address(output.l1StandardBridgeImpl).code, empty, "1800"); assertNotEq(address(output.mipsSingleton).code, empty, "1900"); - assertNotEq(address(output.opcm).code, empty, "2000"); - assertNotEq(address(output.opcmContractsContainer).code, empty, "2100"); - assertNotEq(address(output.opcmDeployer).code, empty, "2200"); - assertNotEq(address(output.opcmGameTypeAdder).code, empty, "2300"); + + // OPCM version-specific code assertions + if (opcmV2Enabled) { + assertNotEq(address(output.opcmV2).code, empty, "2000"); + assertNotEq(address(output.opcmContainer).code, empty, "2100"); + assertNotEq(address(output.opcmStandardValidator).code, empty, "2200"); + // V1 contracts should be empty when V2 is enabled + assertEq(address(output.opcm).code, empty, "2000-v1"); + assertEq(address(output.opcmContractsContainer).code, empty, "2100-v1"); + assertEq(address(output.opcmDeployer).code, empty, "2200-v1"); + assertEq(address(output.opcmGameTypeAdder).code, empty, "2300-v1"); + } else { + assertNotEq(address(output.opcm).code, empty, "2000"); + assertNotEq(address(output.opcmContractsContainer).code, empty, "2100"); + assertNotEq(address(output.opcmDeployer).code, empty, "2200"); + assertNotEq(address(output.opcmGameTypeAdder).code, empty, "2300"); + // V2 contracts should be empty when V1 is enabled + assertEq(address(output.opcmV2).code, empty, "2000-v2"); + assertEq(address(output.opcmContainer).code, empty, "2100-v2"); + } assertNotEq(address(output.faultDisputeGameV2Impl).code, empty, "V2 FDG should have code when enabled"); assertNotEq(address(output.permissionedDisputeGameV2Impl).code, empty, "V2 PDG should have code when enabled"); diff --git a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol index f54a08e230d..d5a3b3d7507 100644 --- a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.15; // Testing import { Test } from "test/setup/Test.sol"; import { FeatureFlags } from "test/setup/FeatureFlags.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; // Scripts import { DeploySuperchain } from "scripts/deploy/DeploySuperchain.s.sol"; @@ -19,6 +20,7 @@ import { Features } from "src/libraries/Features.sol"; import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; import { Claim, Duration, GameType, GameTypes } from "src/dispute/lib/Types.sol"; import { IPermissionedDisputeGame } from "interfaces/dispute/IPermissionedDisputeGame.sol"; +import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; contract DeployOPChain_TestBase is Test, FeatureFlags { DeploySuperchain deploySuperchain; @@ -62,7 +64,8 @@ contract DeployOPChain_TestBase is Test, FeatureFlags { uint256 disputeSplitDepth = 30; Duration disputeClockExtension = Duration.wrap(3 hours); Duration disputeMaxClockDuration = Duration.wrap(3.5 days); - IOPContractsManager opcm; + address opcmAddr; + ISuperchainConfig superchainConfig; bool useCustomGasToken = false; event Deployed(uint256 indexed l2ChainId, address indexed deployer, bytes deployOutput); @@ -106,8 +109,13 @@ contract DeployOPChain_TestBase is Test, FeatureFlags { devFeatureBitmap: devFeatureBitmap }) ); - opcm = dio.opcm; - vm.label(address(opcm), "opcm"); + // Select OPCM v1 or v2 based on feature flag + opcmAddr = isDevFeatureEnabled(DevFeatures.OPCM_V2) ? address(dio.opcmV2) : address(dio.opcm); + vm.label(address(dio.opcm), "opcm"); + vm.label(address(dio.opcmV2), "opcmV2"); + + // Set superchainConfig from deployment + superchainConfig = dso.superchainConfigProxy; // 3) Build DeployOPChainInput struct deployOPChainInput = Types.DeployOPChainInput({ @@ -120,7 +128,7 @@ contract DeployOPChain_TestBase is Test, FeatureFlags { basefeeScalar: basefeeScalar, blobBaseFeeScalar: blobBaseFeeScalar, l2ChainId: l2ChainId, - opcm: address(opcm), + opcm: opcmAddr, saltMixer: saltMixer, gasLimit: gasLimit, disputeGameType: disputeGameType, @@ -132,6 +140,7 @@ contract DeployOPChain_TestBase is Test, FeatureFlags { allowCustomDisputeParameters: false, operatorFeeScalar: 0, operatorFeeConstant: 0, + superchainConfig: superchainConfig, useCustomGasToken: useCustomGasToken }); } @@ -167,6 +176,34 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { useCustomGasToken, "SystemConfig CUSTOM_GAS_TOKEN feature" ); + + // Verify superchainConfig is set correctly + assertEq( + address(doo.systemConfigProxy.superchainConfig()), + address(deployOPChainInput.superchainConfig), + "superchainConfig mismatch" + ); + + // OPCM v2 specific assertions + if (isDevFeatureEnabled(DevFeatures.OPCM_V2)) { + // PERMISSIONED_CANNON must always be enabled with 0.08 ether init bond + assertEq(doo.disputeGameFactoryProxy.initBonds(GameTypes.PERMISSIONED_CANNON), 0.08 ether); + assertNotEq(address(doo.disputeGameFactoryProxy.gameImpls(GameTypes.PERMISSIONED_CANNON)), address(0)); + + // CANNON is only enabled if it's the starting game type + bool cannonEnabled = deployOPChainInput.disputeGameType.raw() == GameTypes.CANNON.raw(); + assertEq(doo.disputeGameFactoryProxy.initBonds(GameTypes.CANNON), cannonEnabled ? 0.08 ether : 0); + if (cannonEnabled) { + assertNotEq(address(doo.disputeGameFactoryProxy.gameImpls(GameTypes.CANNON)), address(0)); + } + + // CANNON_KONA is only enabled if it's the starting game type + bool cannonKonaEnabled = deployOPChainInput.disputeGameType.raw() == GameTypes.CANNON_KONA.raw(); + assertEq(doo.disputeGameFactoryProxy.initBonds(GameTypes.CANNON_KONA), cannonKonaEnabled ? 0.08 ether : 0); + if (cannonKonaEnabled) { + assertNotEq(address(doo.disputeGameFactoryProxy.gameImpls(GameTypes.CANNON_KONA)), address(0)); + } + } } function testFuzz_run_memory_succeeds(bytes32 _seed) public { @@ -183,27 +220,33 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { DeployOPChain.Output memory doo = deployOPChain.run(deployOPChainInput); - // Verify that the initial bonds are zero. - assertEq(doo.disputeGameFactoryProxy.initBonds(GameTypes.CANNON), 0, "2700"); - assertEq(doo.disputeGameFactoryProxy.initBonds(GameTypes.PERMISSIONED_CANNON), 0, "2800"); + // Skip init bond checks for OPCM v2 (bonds are set during deployment, not zero) + if (!isDevFeatureEnabled(DevFeatures.OPCM_V2)) { + // Verify that the initial bonds are zero for OPCM v1. + assertEq(doo.disputeGameFactoryProxy.initBonds(GameTypes.CANNON), 0, "2700"); + assertEq(doo.disputeGameFactoryProxy.initBonds(GameTypes.PERMISSIONED_CANNON), 0, "2800"); + } // Check dispute game deployments // Validate permissionedDisputeGame (PDG) address - IOPContractsManager.Implementations memory impls = opcm.implementations(); + IOPContractsManager.Implementations memory impls = IOPContractsManager(opcmAddr).implementations(); address expectedPDGAddress = impls.permissionedDisputeGameV2Impl; address actualPDGAddress = address(doo.disputeGameFactoryProxy.gameImpls(GameTypes.PERMISSIONED_CANNON)); assertNotEq(actualPDGAddress, address(0), "PDG address should be non-zero"); assertEq(actualPDGAddress, expectedPDGAddress, "PDG address should match expected address"); - // Check PDG getters - IPermissionedDisputeGame pdg = IPermissionedDisputeGame(actualPDGAddress); - bytes32 expectedPrestate = bytes32(0); - assertEq(pdg.l2BlockNumber(), 0, "3000"); - assertEq(Claim.unwrap(pdg.absolutePrestate()), expectedPrestate, "3100"); - assertEq(Duration.unwrap(pdg.clockExtension()), 10800, "3200"); - assertEq(Duration.unwrap(pdg.maxClockDuration()), 302400, "3300"); - assertEq(pdg.splitDepth(), 30, "3400"); - assertEq(pdg.maxGameDepth(), 73, "3500"); + // Skip PDG getter checks for OPCM v2 (game args are passed at creation time) + if (!isDevFeatureEnabled(DevFeatures.OPCM_V2)) { + // Check PDG getters + IPermissionedDisputeGame pdg = IPermissionedDisputeGame(actualPDGAddress); + bytes32 expectedPrestate = bytes32(0); + assertEq(pdg.l2BlockNumber(), 0, "3000"); + assertEq(Claim.unwrap(pdg.absolutePrestate()), expectedPrestate, "3100"); + assertEq(Duration.unwrap(pdg.clockExtension()), 10800, "3200"); + assertEq(Duration.unwrap(pdg.maxClockDuration()), 302400, "3300"); + assertEq(pdg.splitDepth(), 30, "3400"); + assertEq(pdg.maxGameDepth(), 73, "3500"); + } // Verify custom gas token feature is set as seeded assertEq( From 8418c977ea4c6f2ee699e2c5730a78c38af39d94 Mon Sep 17 00:00:00 2001 From: Disco <131301107+0xDiscotech@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:14:55 -0300 Subject: [PATCH 13/13] chore: improve comments accuracy (#741) (#18622) * chore: improve comments accuracy (#741) * fix: semver (#772) --------- Co-authored-by: Chiin <77933451+0xChin@users.noreply.github.com> --- packages/contracts-bedrock/snapshots/semver-lock.json | 4 ++-- packages/contracts-bedrock/src/L2/L1Withdrawer.sol | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index c369f89b473..dc38b2a0205 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -88,8 +88,8 @@ "sourceCodeHash": "0x34186bcab29963237b4e0d7575b0a1cff7caf42ccdb55d4b2b2c767db3279189" }, "src/L2/L1Withdrawer.sol:L1Withdrawer": { - "initCodeHash": "0x91e0be0d49636212678191c06b9b6840c399f08ad946bc7b52f24231691be28b", - "sourceCodeHash": "0x25422bdaf51d611c1688a835737368c0ff2ab639dac852af8a20ebb4e16fc103" + "initCodeHash": "0x6efb9055142e90b408c6312074243769df0d365f6f984e226e0320bec55a45b8", + "sourceCodeHash": "0x6a12e541b47b79f19d1061ff7b64ffdcffa1e8d06225cca6798daca53fd96890" }, "src/L2/L2CrossDomainMessenger.sol:L2CrossDomainMessenger": { "initCodeHash": "0xe160be403df12709c371c33195d1b9c3b5e9499e902e86bdabc8eed749c3fd61", diff --git a/packages/contracts-bedrock/src/L2/L1Withdrawer.sol b/packages/contracts-bedrock/src/L2/L1Withdrawer.sol index bbaf18e4638..8b55fdea67e 100644 --- a/packages/contracts-bedrock/src/L2/L1Withdrawer.sol +++ b/packages/contracts-bedrock/src/L2/L1Withdrawer.sol @@ -51,14 +51,14 @@ contract L1Withdrawer is ISemver { event WithdrawalGasLimitUpdated(uint32 oldWithdrawalGasLimit, uint32 newWithdrawalGasLimit); /// @notice Semantic version. - /// @custom:semver 1.0.0 - string public constant version = "1.0.0"; + /// @custom:semver 1.0.1 + string public constant version = "1.0.1"; /// @notice Constructs the L1Withdrawer contract. /// @param _minWithdrawalAmount The minimum amount of ETH required to trigger a withdrawal. /// @param _recipient The L1 address that will receive withdrawals. /// @param _withdrawalGasLimit The gas limit for the L1 withdrawal transaction. - /// @dev If target on L1 is `FeesDepositor`, the gas limit should be above 800k gas. + /// @dev If target on L1 is `FeesDepositor`, the gas limit should be at or above 800k gas. constructor(uint256 _minWithdrawalAmount, address _recipient, uint32 _withdrawalGasLimit) { minWithdrawalAmount = _minWithdrawalAmount; recipient = _recipient; @@ -105,7 +105,7 @@ contract L1Withdrawer is ISemver { /// @notice Updates the withdrawal gas limit. Only callable by the ProxyAdmin owner. /// @param _newWithdrawalGasLimit The new withdrawal gas limit. - /// @dev If target on L1 is `FeesDepositor`, the gas limit should be above 800k gas. + /// @dev If target on L1 is `FeesDepositor`, the gas limit should be at or above 800k gas. function setWithdrawalGasLimit(uint32 _newWithdrawalGasLimit) external { if (msg.sender != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()) { revert L1Withdrawer_OnlyProxyAdminOwner();