Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,11 @@ func checkTestMethodName(artifact *solc.ForgeArtifact, contractName string, func
return nil
}
}
// Check for feature test pattern (e.g., FeatBatchUpgrade)
if strings.HasPrefix(functionName, "Feat") {
// Pattern: <ContractName>_Feat*_Test
return nil
}
// Pattern: <ContractName>_<FunctionName>_Test - validate function exists
if !checkFunctionExists(artifact, functionName) {
// Convert to camelCase for error message
Expand Down
119 changes: 119 additions & 0 deletions packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { VmSafe } from "forge-std/Vm.sol";
import { CommonTest } from "test/setup/CommonTest.sol";
import { DisputeGames } from "test/setup/DisputeGames.sol";
import { PastUpgrades } from "test/setup/PastUpgrades.sol";
import { BatchUpgrader } from "test/L1/opcm/helpers/BatchUpgrader.sol";

// Libraries
import { Config } from "scripts/libraries/Config.sol";
Expand Down Expand Up @@ -1473,3 +1474,121 @@ contract OPContractsManagerV2_Migrate_Test is OPContractsManagerV2_TestInit {
);
}
}

/// @title OPContractsManagerV2_FeatBatchUpgrade_Test
/// @notice Tests batch upgrade functionality with freshly deployed chains (non-forked).
contract OPContractsManagerV2_FeatBatchUpgrade_Test is OPContractsManagerV2_TestInit {
/// @notice Tests that multiple upgrade operations (15 chains) can be executed within a single transaction.
/// This enforces the OPCMV2 invariant that approximately 15 upgrade operations should be
/// executable in one transaction.
function test_batchUpgrade_multipleChains_succeeds() public {
skipIfCoverage();

uint256 numberOfChains = 15;

// 1. Deploy BatchUpgrader helper contract.
BatchUpgrader batchUpgrader = new BatchUpgrader(opcmV2);

// 2. Set up base configuration for deploying chains.
IOPContractsManagerV2.FullConfig memory baseConfig;
baseConfig.superchainConfig = superchainConfig;
baseConfig.proxyAdminOwner = address(batchUpgrader);
baseConfig.systemConfigOwner = makeAddr("systemConfigOwner");
baseConfig.batcher = makeAddr("batcher");
baseConfig.unsafeBlockSigner = makeAddr("unsafeBlockSigner");
baseConfig.startingAnchorRoot = Proposal({ root: Hash.wrap(bytes32(hex"1234")), l2SequenceNumber: 123 });
baseConfig.startingRespectedGameType = GameTypes.PERMISSIONED_CANNON;
baseConfig.basefeeScalar = 1368;
baseConfig.blobBasefeeScalar = 801949;
baseConfig.gasLimit = 60_000_000;
baseConfig.resourceConfig = IResourceMetering.ResourceConfig({
maxResourceLimit: 20_000_000,
elasticityMultiplier: 10,
baseFeeMaxChangeDenominator: 8,
minimumBaseFee: 1 gwei,
systemTxMaxGas: 1_000_000,
maximumBaseFee: type(uint128).max
});

// Set up dispute game configs.
address initialChallenger = makeAddr("challenger");
address initialProposer = makeAddr("proposer");
baseConfig.disputeGameConfigs = new IOPContractsManagerUtils.DisputeGameConfig[](3);
baseConfig.disputeGameConfigs[0] = IOPContractsManagerUtils.DisputeGameConfig({
enabled: true,
initBond: DEFAULT_DISPUTE_GAME_INIT_BOND,
gameType: GameTypes.CANNON,
gameArgs: abi.encode(IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: cannonPrestate }))
});
baseConfig.disputeGameConfigs[1] = IOPContractsManagerUtils.DisputeGameConfig({
enabled: true,
initBond: DEFAULT_DISPUTE_GAME_INIT_BOND,
gameType: GameTypes.PERMISSIONED_CANNON,
gameArgs: abi.encode(
IOPContractsManagerUtils.PermissionedDisputeGameConfig({
absolutePrestate: cannonPrestate,
proposer: initialProposer,
challenger: initialChallenger
})
)
});
baseConfig.disputeGameConfigs[2] = IOPContractsManagerUtils.DisputeGameConfig({
enabled: true,
initBond: DEFAULT_DISPUTE_GAME_INIT_BOND,
gameType: GameTypes.CANNON_KONA,
gameArgs: abi.encode(IOPContractsManagerUtils.FaultDisputeGameConfig({ absolutePrestate: cannonKonaPrestate }))
});

// 3. Deploy 15 separate chains using opcmV2.deploy().
IOPContractsManagerV2.ChainContracts[] memory chains =
new IOPContractsManagerV2.ChainContracts[](numberOfChains);
for (uint256 i = 0; i < numberOfChains; i++) {
IOPContractsManagerV2.FullConfig memory config = baseConfig;
config.saltMixer = string.concat("chain-", vm.toString(i));
config.l2ChainId = 1000 + i;
chains[i] = opcmV2.deploy(config);
}

// 4. Prepare upgrade inputs for each chain.
IOPContractsManagerV2.UpgradeInput[] memory upgradeInputs =
new IOPContractsManagerV2.UpgradeInput[](numberOfChains);
for (uint256 i = 0; i < numberOfChains; i++) {
upgradeInputs[i] = IOPContractsManagerV2.UpgradeInput({
systemConfig: chains[i].systemConfig,
disputeGameConfigs: baseConfig.disputeGameConfigs,
extraInstructions: new IOPContractsManagerUtils.ExtraInstruction[](0)
});
}

// 5. Execute batch upgrade - all 15 upgrades in a single transaction.
batchUpgrader.batchUpgrade(upgradeInputs);
VmSafe.Gas memory gas = vm.lastCallGas();

// 6. Verify that the upgrade gas usage is less than the EIP-7825 gas limit.
// See https://eip.tools/eip/eip-7825.md for more details.
// The upgradeGasBuffer amount below is an approximation of the overhead that is required
// to execute a call to Safe.executeTransaction() prior to the call to IOPContractsManagerV2.upgrade().
// The approximate value of 65,000 gas, was taken from a previous upgrade transaction on OP Mainnet:
// https://dashboard.tenderly.co/oplabs/op-mainnet/tx/0x9b9aa2d8e857e1a28e55b124e931eac706b3ae04c1b33ba949f0366359860993/gas-usage?trace=0.1.7
uint256 fusakaLimit = 2 ** 24;
uint256 upgradeGasBuffer = 65_000;
assertLt(gas.gasTotalUsed, fusakaLimit - upgradeGasBuffer, "Upgrade exceeds gas target");

// 7. Verify all chains upgraded successfully.
for (uint256 i = 0; i < numberOfChains; i++) {
ISystemConfig systemConfig = chains[i].systemConfig;

// Verify OPCM release version was updated.
string memory version = systemConfig.lastUsedOPCMVersion();
assertEq(version, opcmV2.version(), string.concat("Chain ", vm.toString(i), " version mismatch"));

// Verify SystemConfig implementation was upgraded.
address impl = EIP1967Helper.getImplementation(address(systemConfig));
assertEq(
impl,
opcmV2.implementations().systemConfigImpl,
string.concat("Chain ", vm.toString(i), " SystemConfig impl not upgraded")
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

import { IOPContractsManagerV2 } from "interfaces/L1/opcm/IOPContractsManagerV2.sol";
import { LibString } from "@solady/utils/LibString.sol";

/// @title BatchUpgrader
/// @notice Helper contract for testing that multiple upgrade operations can be executed
/// within a single transaction. Used to enforce the OPCMV2 invariant that
/// approximately 5 upgrade operations should be executable in one transaction.
contract BatchUpgrader {
/// @notice The OPContractsManagerV2 instance to use for upgrades.
IOPContractsManagerV2 public immutable opcm;

/// @param _opcm The OPContractsManagerV2 contract address.
constructor(IOPContractsManagerV2 _opcm) {
opcm = _opcm;
}

/// @notice Executes multiple upgrade operations sequentially in a single transaction.
/// @param _inputs Array of upgrade inputs, one per chain to upgrade.
function batchUpgrade(IOPContractsManagerV2.UpgradeInput[] memory _inputs) external {
for (uint256 i = 0; i < _inputs.length; i++) {
(bool success, bytes memory returnData) =
address(opcm).delegatecall(abi.encodeCall(IOPContractsManagerV2.upgrade, (_inputs[i])));
require(
success,
string.concat(
"BatchUpgrader: upgrade failed for chain ", LibString.toString(i), ": ", string(returnData)
)
);
}
}
}