diff --git a/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerContainer.sol b/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerContainer.sol index 03d11b5304a7b..fb1351e1ff10a 100644 --- a/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerContainer.sol +++ b/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerContainer.sol @@ -36,7 +36,7 @@ interface IOPContractsManagerContainer { address storageSetterImpl; } - error OPContractsManagerContractsContainer_DevFeatureInProd(); + error OPContractsManagerContainer_DevFeatureInProd(); function blueprints() external view returns (Blueprints memory); function implementations() external view returns (Implementations memory); diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerContainer.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerContainer.json index 29b2d118a3d32..f81f6cda7507c 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerContainer.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerContainer.json @@ -368,7 +368,7 @@ }, { "inputs": [], - "name": "OPContractsManagerContractsContainer_DevFeatureInProd", + "name": "OPContractsManagerContainer_DevFeatureInProd", "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerContainer.sol b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerContainer.sol index fc4d18eb15152..7775cc485ee8a 100644 --- a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerContainer.sol +++ b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerContainer.sol @@ -63,7 +63,7 @@ contract OPContractsManagerContainer { bytes32 public immutable devFeatureBitmap; /// @notice Thrown when a development feature is enabled in production. - error OPContractsManagerContractsContainer_DevFeatureInProd(); + error OPContractsManagerContainer_DevFeatureInProd(); /// @param _blueprints The blueprint contract addresses. /// @param _implementations The implementation contract addresses. @@ -75,7 +75,7 @@ contract OPContractsManagerContainer { // Development features MUST NOT be enabled on Mainnet. if (block.chainid == 1 && !_isTestingEnvironment() && uint256(_devFeatureBitmap) != 0) { - revert OPContractsManagerContractsContainer_DevFeatureInProd(); + revert OPContractsManagerContainer_DevFeatureInProd(); } } diff --git a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerContainer.t.sol b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerContainer.t.sol new file mode 100644 index 0000000000000..8f42a64236047 --- /dev/null +++ b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerContainer.t.sol @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing +import { Test } from "forge-std/Test.sol"; + +// Contracts +import { OPContractsManagerContainer } from "src/L1/opcm/OPContractsManagerContainer.sol"; + +// Libraries +import { Constants } from "src/libraries/Constants.sol"; + +/// @title OPContractsManagerContainer_TestInit +/// @notice Shared setup for OPContractsManagerContainer tests. +contract OPContractsManagerContainer_TestInit is Test { + OPContractsManagerContainer.Blueprints internal blueprints; + OPContractsManagerContainer.Implementations internal implementations; + + function setUp() public virtual { + blueprints = OPContractsManagerContainer.Blueprints({ + addressManager: makeAddr("addressManager"), + proxy: makeAddr("proxy"), + proxyAdmin: makeAddr("proxyAdmin"), + l1ChugSplashProxy: makeAddr("l1ChugSplashProxy"), + resolvedDelegateProxy: makeAddr("resolvedDelegateProxy"), + permissionedDisputeGame1: makeAddr("permissionedDisputeGame1"), + permissionedDisputeGame2: makeAddr("permissionedDisputeGame2"), + permissionlessDisputeGame1: makeAddr("permissionlessDisputeGame1"), + permissionlessDisputeGame2: makeAddr("permissionlessDisputeGame2") + }); + + implementations = OPContractsManagerContainer.Implementations({ + superchainConfigImpl: makeAddr("superchainConfigImpl"), + protocolVersionsImpl: makeAddr("protocolVersionsImpl"), + l1ERC721BridgeImpl: makeAddr("l1ERC721BridgeImpl"), + optimismPortalImpl: makeAddr("optimismPortalImpl"), + optimismPortalInteropImpl: makeAddr("optimismPortalInteropImpl"), + ethLockboxImpl: makeAddr("ethLockboxImpl"), + systemConfigImpl: makeAddr("systemConfigImpl"), + optimismMintableERC20FactoryImpl: makeAddr("optimismMintableERC20FactoryImpl"), + l1CrossDomainMessengerImpl: makeAddr("l1CrossDomainMessengerImpl"), + l1StandardBridgeImpl: makeAddr("l1StandardBridgeImpl"), + disputeGameFactoryImpl: makeAddr("disputeGameFactoryImpl"), + anchorStateRegistryImpl: makeAddr("anchorStateRegistryImpl"), + delayedWETHImpl: makeAddr("delayedWETHImpl"), + mipsImpl: makeAddr("mipsImpl"), + faultDisputeGameV2Impl: makeAddr("faultDisputeGameV2Impl"), + permissionedDisputeGameV2Impl: makeAddr("permissionedDisputeGameV2Impl"), + superFaultDisputeGameImpl: makeAddr("superFaultDisputeGameImpl"), + superPermissionedDisputeGameImpl: makeAddr("superPermissionedDisputeGameImpl"), + storageSetterImpl: makeAddr("storageSetterImpl") + }); + } + + /// @notice Deploys a new OPContractsManagerContainer with the given dev feature bitmap. + /// @param _devFeatureBitmap The dev feature bitmap to use. + /// @return The deployed OPContractsManagerContainer. + function _deploy(bytes32 _devFeatureBitmap) internal returns (OPContractsManagerContainer) { + return new OPContractsManagerContainer(blueprints, implementations, _devFeatureBitmap); + } +} + +/// @title OPContractsManagerContainer_Constructor_Test +/// @notice Tests the constructor of OPContractsManagerContainer. +contract OPContractsManagerContainer_Constructor_Test is OPContractsManagerContainer_TestInit { + /// @notice Tests that the constructor succeeds with any dev bitmap when in a test environment. + /// @param _chainId The chain ID to use. + /// @param _devFeatureBitmap The dev feature bitmap to use. + function testFuzz_constructor_devBitmapInTestEnv_succeeds(uint64 _chainId, bytes32 _devFeatureBitmap) public { + // Etch code into the magic testing address so we're recognized as a test env. + vm.etch(Constants.TESTING_ENVIRONMENT_ADDRESS, hex"01"); + + // Set chain ID. + vm.chainId(_chainId); + + OPContractsManagerContainer container = _deploy(_devFeatureBitmap); + + assertEq(container.devFeatureBitmap(), _devFeatureBitmap); + } + + /// @notice Tests that the constructor reverts when dev features are enabled on mainnet without + /// test env. + /// @param _devFeatureBitmap The dev feature bitmap to use. + function testFuzz_constructor_devBitmapOnMainnet_reverts(bytes32 _devFeatureBitmap) public { + // Ensure at least one dev feature is enabled. + _devFeatureBitmap = bytes32(bound(uint256(_devFeatureBitmap), 1, type(uint256).max)); + + // Clear the magic testing address so we're recognized as production. + vm.etch(Constants.TESTING_ENVIRONMENT_ADDRESS, hex""); + + // Set chain ID to mainnet. + vm.chainId(1); + + vm.expectRevert(OPContractsManagerContainer.OPContractsManagerContainer_DevFeatureInProd.selector); + _deploy(_devFeatureBitmap); + } + + /// @notice Tests that the constructor succeeds on mainnet with a zero dev bitmap. + function test_constructor_zeroBitmapOnMainnet_succeeds() public { + // Clear the magic testing address. + vm.etch(Constants.TESTING_ENVIRONMENT_ADDRESS, hex""); + + // Set chain ID to mainnet. + vm.chainId(1); + + OPContractsManagerContainer container = _deploy(bytes32(0)); + + assertEq(container.devFeatureBitmap(), bytes32(0)); + } +} + +/// @title OPContractsManagerContainer_Blueprints_Test +/// @notice Tests the blueprints() getter. +contract OPContractsManagerContainer_Blueprints_Test is OPContractsManagerContainer_TestInit { + /// @notice Tests that blueprints() returns the struct provided at construction. + function test_blueprints_succeeds() public { + OPContractsManagerContainer container = _deploy(bytes32(0)); + + assertEq(abi.encode(container.blueprints()), abi.encode(blueprints)); + } +} + +/// @title OPContractsManagerContainer_Implementations_Test +/// @notice Tests the implementations() getter. +contract OPContractsManagerContainer_Implementations_Test is OPContractsManagerContainer_TestInit { + /// @notice Tests that implementations() returns the struct provided at construction. + function test_implementations_succeeds() public { + OPContractsManagerContainer container = _deploy(bytes32(0)); + + assertEq(abi.encode(container.implementations()), abi.encode(implementations)); + } +} + +/// @title OPContractsManagerContainer_IsDevFeatureEnabled_Test +/// @notice Tests the isDevFeatureEnabled() function. +contract OPContractsManagerContainer_IsDevFeatureEnabled_Test is OPContractsManagerContainer_TestInit { + /// @notice Tests that isDevFeatureEnabled returns true when the feature bit is set. + /// @param _bitIndex The bit index to test. + function testFuzz_isDevFeatureEnabled_bitSet_succeeds(uint8 _bitIndex) public { + bytes32 bitmap = bytes32(uint256(1) << _bitIndex); + bytes32 feature = bytes32(uint256(1) << _bitIndex); + + OPContractsManagerContainer container = _deploy(bitmap); + + assertTrue(container.isDevFeatureEnabled(feature)); + assertFalse(container.isDevFeatureEnabled(bytes32(0))); + } + + /// @notice Tests that isDevFeatureEnabled returns false when the feature bit is not set. + /// @param _bitIndex The bit index to test. + function testFuzz_isDevFeatureEnabled_bitNotSet_succeeds(uint8 _bitIndex) public { + // Create a bitmap with all bits set except the one we're testing. + bytes32 bitmap = bytes32(type(uint256).max ^ (uint256(1) << _bitIndex)); + bytes32 feature = bytes32(uint256(1) << _bitIndex); + + OPContractsManagerContainer container = _deploy(bitmap); + + assertFalse(container.isDevFeatureEnabled(feature)); + } + + /// @notice Tests that isDevFeatureEnabled returns false when the bitmap is zero. + /// @param _feature The feature to check. + function testFuzz_isDevFeatureEnabled_zeroBitmap_succeeds(bytes32 _feature) public { + OPContractsManagerContainer container = _deploy(bytes32(0)); + + assertFalse(container.isDevFeatureEnabled(_feature)); + } + + /// @notice Tests that isDevFeatureEnabled returns true for multiple features set at once. + function test_isDevFeatureEnabled_multipleBitsSet_succeeds() public { + uint256 numFeatures = vm.randomUint(1, 16); + uint256 bitmap; + uint8[] memory bitIndices = new uint8[](numFeatures); + + // Set random bits in the bitmap. + for (uint256 i = 0; i < numFeatures; i++) { + uint8 bitIndex = uint8(vm.randomUint(0, 255)); + bitIndices[i] = bitIndex; + bitmap |= uint256(1) << bitIndex; + } + + OPContractsManagerContainer container = _deploy(bytes32(bitmap)); + + // Verify each feature is enabled. + for (uint256 i = 0; i < numFeatures; i++) { + bytes32 feature = bytes32(uint256(1) << bitIndices[i]); + assertTrue(container.isDevFeatureEnabled(feature)); + } + } +} + +/// @title OPContractsManagerContainer_DevFeatureBitmap_Test +/// @notice Tests the devFeatureBitmap() getter. +contract OPContractsManagerContainer_DevFeatureBitmap_Test is OPContractsManagerContainer_TestInit { + /// @notice Tests that devFeatureBitmap() returns the value provided at construction. + /// @param _devFeatureBitmap The dev feature bitmap to use. + function testFuzz_devFeatureBitmap_succeeds(bytes32 _devFeatureBitmap) public { + OPContractsManagerContainer container = _deploy(_devFeatureBitmap); + + assertEq(container.devFeatureBitmap(), _devFeatureBitmap); + } +} diff --git a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol index 6f69658758a91..314edd4bebaa0 100644 --- a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol +++ b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol @@ -10,7 +10,7 @@ import { DisputeGames } from "test/setup/DisputeGames.sol"; import { Config } from "scripts/libraries/Config.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { Claim } from "src/dispute/lib/LibUDT.sol"; -import { GameTypes } from "src/dispute/lib/Types.sol"; +import { GameType, GameTypes } from "src/dispute/lib/Types.sol"; import { DevFeatures } from "src/libraries/DevFeatures.sol"; // Interfaces @@ -119,10 +119,12 @@ contract OPContractsManagerV2_Upgrade_TestInit is CommonTest, DisputeGames { /// @param _opcm The OPCM contract to reference for shared components. /// @param _delegateCaller The address of the delegate caller to use for superchain upgrade. /// @param _revertBytes The bytes of the revert to expect. + /// @param _expectedValidatorErrors The StandardValidator errors to expect. function _runOpcmV2UpgradeAndChecks( IOPContractsManagerV2 _opcm, address _delegateCaller, - bytes memory _revertBytes + bytes memory _revertBytes, + string memory _expectedValidatorErrors ) internal { @@ -199,6 +201,19 @@ contract OPContractsManagerV2_Upgrade_TestInit is CommonTest, DisputeGames { // try to apply to this function call instead. IOPContractsManagerStandardValidator validator = _opcm.standardValidator(); + // 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. + if (bytes(_expectedValidatorErrors).length > 0) { + vm.expectRevert( + bytes( + string.concat( + "OPContractsManagerStandardValidator: OVERRIDES-L1PAOMULTISIG,OVERRIDES-CHALLENGER,", + _expectedValidatorErrors + ) + ) + ); + } + // Run the StandardValidator checks. if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { validator.validateWithOverrides( @@ -247,14 +262,28 @@ contract OPContractsManagerV2_Upgrade_TestInit is CommonTest, DisputeGames { /// @notice Executes the current V2 upgrade and checks the results. /// @param _delegateCaller The address of the delegate caller to use for the superchain upgrade. function runCurrentUpgradeV2(address _delegateCaller) public { - _runOpcmV2UpgradeAndChecks(opcmV2, _delegateCaller, bytes("")); + _runOpcmV2UpgradeAndChecks(opcmV2, _delegateCaller, bytes(""), ""); } /// @notice Executes the current V2 upgrade and expects reverts. /// @param _delegateCaller The address of the delegate caller to use for the superchain upgrade. /// @param _revertBytes The bytes of the revert to expect. function runCurrentUpgradeV2(address _delegateCaller, bytes memory _revertBytes) public { - _runOpcmV2UpgradeAndChecks(opcmV2, _delegateCaller, _revertBytes); + _runOpcmV2UpgradeAndChecks(opcmV2, _delegateCaller, _revertBytes, ""); + } + + /// @notice Executes the current V2 upgrade and expects reverts. + /// @param _delegateCaller The address of the delegate caller to use for the superchain upgrade. + /// @param _revertBytes The bytes of the revert to expect. + /// @param _expectedValidatorErrors The StandardValidator errors to expect. + function runCurrentUpgradeV2( + address _delegateCaller, + bytes memory _revertBytes, + string memory _expectedValidatorErrors + ) + public + { + _runOpcmV2UpgradeAndChecks(opcmV2, _delegateCaller, _revertBytes, _expectedValidatorErrors); } } @@ -443,6 +472,102 @@ contract OPContractsManagerV2_Upgrade_Test is OPContractsManagerV2_Upgrade_TestI ) ); } + + /// @notice Tests that repeatedly upgrading can enable a previously disabled game type. + function test_upgrade_enableGameType_succeeds() public { + uint256 originalBond = disputeGameFactory.initBonds(GameTypes.CANNON); + + // First, disable Cannon and clear its bond so the factory entry is removed. + v2UpgradeInput.disputeGameConfigs[0].enabled = false; + v2UpgradeInput.disputeGameConfigs[0].initBond = 0; + runCurrentUpgradeV2(chainPAO, hex"", "PLDG-10"); + assertEq(address(disputeGameFactory.gameImpls(GameTypes.CANNON)), address(0), "game impl not cleared"); + + // Re-enable Cannon and restore its bond so that it is re-installed. + v2UpgradeInput.disputeGameConfigs[0].enabled = true; + v2UpgradeInput.disputeGameConfigs[0].initBond = originalBond; + runCurrentUpgradeV2(chainPAO); + assertEq( + address(disputeGameFactory.gameImpls(GameTypes.CANNON)), + opcmV2.implementations().faultDisputeGameV2Impl, + "game impl not restored" + ); + assertEq(disputeGameFactory.initBonds(GameTypes.CANNON), originalBond, "init bond not restored"); + } + + /// @notice Tests that disabling a game type removes it from the factory. + function test_upgrade_disableGameType_succeeds() public { + // Establish the baseline where Cannon is enabled. + runCurrentUpgradeV2(chainPAO); + assertEq( + address(disputeGameFactory.gameImpls(GameTypes.CANNON)), + opcmV2.implementations().faultDisputeGameV2Impl, + "initial game impl mismatch" + ); + + // Disable Cannon and zero its bond, then ensure it is removed. + v2UpgradeInput.disputeGameConfigs[0].enabled = false; + v2UpgradeInput.disputeGameConfigs[0].initBond = 0; + runCurrentUpgradeV2(chainPAO, hex"", "PLDG-10"); + assertEq(address(disputeGameFactory.gameImpls(GameTypes.CANNON)), address(0), "game impl not cleared"); + assertEq(disputeGameFactory.initBonds(GameTypes.CANNON), 0, "init bond not cleared"); + assertEq(disputeGameFactory.gameArgs(GameTypes.CANNON), bytes(""), "game args not cleared"); + } + + /// @notice Tests that the upgrade flow can update the Cannon and Permissioned prestate. + function test_upgrade_updatePrestate_succeeds() public { + skipIfDevFeatureDisabled(DevFeatures.OPCM_V2); + + // Run baseline upgrade and capture the current prestates. + runCurrentUpgradeV2(chainPAO); + assertEq( + _gameArgsAbsolutePrestate(GameTypes.CANNON), + Claim.unwrap(cannonPrestate), + "baseline cannon prestate mismatch" + ); + assertEq( + _gameArgsAbsolutePrestate(GameTypes.PERMISSIONED_CANNON), + Claim.unwrap(cannonPrestate), + "baseline permissioned prestate mismatch" + ); + + // Prepare new prestates. + Claim newPrestate = Claim.wrap(bytes32(keccak256("new cannon prestate"))); + cannonPrestate = newPrestate; + + // Update the dispute game configs to point at the new prestates. + v2UpgradeInput.disputeGameConfigs[0].gameArgs = + abi.encode(IOPContractsManagerV2.FaultDisputeGameConfig({ absolutePrestate: newPrestate })); + v2UpgradeInput.disputeGameConfigs[1].gameArgs = abi.encode( + IOPContractsManagerV2.PermissionedDisputeGameConfig({ + absolutePrestate: newPrestate, + proposer: permissionedGameProposer(disputeGameFactory), + challenger: permissionedGameChallenger(disputeGameFactory) + }) + ); + + // Run the upgrade again and ensure prestates updated. + runCurrentUpgradeV2(chainPAO); + assertEq(_gameArgsAbsolutePrestate(GameTypes.CANNON), Claim.unwrap(newPrestate), "cannon prestate not updated"); + assertEq( + _gameArgsAbsolutePrestate(GameTypes.PERMISSIONED_CANNON), + Claim.unwrap(newPrestate), + "permissioned prestate not updated" + ); + } + + /// @notice Extracts the absolute prestate embedded in a dispute game config. + /// @param _gameType Game type to inspect. + /// @return prestate_ The absolute prestate stored in the factory's game args. + function _gameArgsAbsolutePrestate(GameType _gameType) internal view returns (bytes32 prestate_) { + bytes memory args = disputeGameFactory.gameArgs(_gameType); + if (args.length == 0) { + return bytes32(0); + } + assembly { + prestate_ := mload(add(args, 0x20)) + } + } } /// @title OPContractsManagerV2_UpgradeSuperchain_Test