From 5e2ab17b3ed5b2e23ec4f857c5cd97241914fb40 Mon Sep 17 00:00:00 2001 From: Jack Chuma Date: Mon, 24 Aug 2026 16:57:35 -0400 Subject: [PATCH 1/6] fix(L1): seed imported schedule protocol version Require imported activations to initialize their minimum packed version atomically so nodes never read a scheduled upgrade with version zero. Co-authored-by: Cursor --- interfaces/L1/IProtocolVersions.sol | 7 +++- scripts/deploy/DeployConfig.s.sol | 8 +++++ scripts/deploy/SystemDeploy.s.sol | 40 ++++++++++++++++++++-- scripts/libraries/Types.sol | 3 ++ scripts/multiproof/DeployDevBase.s.sol | 5 ++- scripts/multiproof/README.md | 1 + snapshots/abi/ProtocolVersions.json | 5 +++ snapshots/semver-lock.json | 4 +-- src/L1/ProtocolVersions.sol | 25 +++++++++++--- test/L1/ProtocolVersions.t.sol | 47 ++++++++++++++++++++++---- test/L1/proofs/BaseTest.t.sol | 4 +-- test/deploy/DeployConfig.t.sol | 28 +++++++++++++-- test/deploy/SystemDeploy.t.sol | 24 ++++++++++--- test/vendor/Initializable.t.sol | 2 +- 14 files changed, 175 insertions(+), 28 deletions(-) diff --git a/interfaces/L1/IProtocolVersions.sol b/interfaces/L1/IProtocolVersions.sol index 60f51f47b..a3ac2bfe3 100644 --- a/interfaces/L1/IProtocolVersions.sol +++ b/interfaces/L1/IProtocolVersions.sol @@ -30,7 +30,12 @@ interface IProtocolVersions is IProxyAdminOwnedBase, ISemver, IReinitializableBa error ProtocolVersions_NotInitialized(); error ProtocolVersions_InsufficientNotice(uint64 timestamp); - function initialize(address _incidentResponder, uint64[] calldata _initialSchedule) external; + function initialize( + address _incidentResponder, + uint64[] calldata _initialSchedule, + uint256 _initialMinimumProtocolVersion + ) + external; function registerUpgrade(uint64 timestamp, uint256 minProtocolVersion) external returns (uint256); function setMinimumProtocolVersion(uint256 protocolVersion) external; function setTimestamp(uint256 id, uint64 timestamp) external; diff --git a/scripts/deploy/DeployConfig.s.sol b/scripts/deploy/DeployConfig.s.sol index 99cff3959..975749864 100644 --- a/scripts/deploy/DeployConfig.s.sol +++ b/scripts/deploy/DeployConfig.s.sol @@ -58,6 +58,7 @@ contract DeployConfig is Script { uint256 public operatorFeeVaultMinimumWithdrawalAmount; uint256 public operatorFeeVaultWithdrawalNetwork; uint256 public proofMaturityDelaySeconds; + uint256 public protocolVersionsInitialMinimumVersion; uint256 public respectedGameType; uint256 public sequencerFeeVaultMinimumWithdrawalAmount; uint256 public sequencerFeeVaultWithdrawalNetwork; @@ -121,6 +122,7 @@ contract DeployConfig is Script { operatorFeeVaultMinimumWithdrawalAmount = _json.readUint("$.operatorFeeVaultMinimumWithdrawalAmount"); operatorFeeVaultWithdrawalNetwork = _json.readUint("$.operatorFeeVaultWithdrawalNetwork"); proofMaturityDelaySeconds = _json.readUintOr("$.proofMaturityDelaySeconds", 0); + _readProtocolVersionsInitialMinimumVersion(_json); respectedGameType = _json.readUintOr("$.respectedGameType", 0); sequencerFeeVaultMinimumWithdrawalAmount = _json.readUint("$.sequencerFeeVaultMinimumWithdrawalAmount"); sequencerFeeVaultWithdrawalNetwork = _json.readUint("$.sequencerFeeVaultWithdrawalNetwork"); @@ -128,6 +130,12 @@ contract DeployConfig is Script { _readProtocolVersionsInitialSchedule(_json); } + function _readProtocolVersionsInitialMinimumVersion(string memory _json) internal { + uint256 minimumVersion = _json.readUintOr("$.protocolVersionsInitialMinimumVersion", 0); + require(minimumVersion <= type(uint128).max, "DeployConfig: initial minimum protocol version exceeds uint128"); + protocolVersionsInitialMinimumVersion = minimumVersion; + } + /// @dev Read separately so a rerun of `read` replaces the previous schedule rather than appending /// to it, and so an out-of-range timestamp fails loudly instead of silently truncating. function _readProtocolVersionsInitialSchedule(string memory _json) internal { diff --git a/scripts/deploy/SystemDeploy.s.sol b/scripts/deploy/SystemDeploy.s.sol index 82ee8a6d7..d8f75b6e5 100644 --- a/scripts/deploy/SystemDeploy.s.sol +++ b/scripts/deploy/SystemDeploy.s.sol @@ -308,7 +308,8 @@ contract SystemDeploy is Script { }), saltMixer: "salt mixer", gasLimit: uint64(cfg.l2GenesisBlockGasLimit()), - initialUpgradeSchedule: cfg.protocolVersionsInitialSchedule() + initialUpgradeSchedule: cfg.protocolVersionsInitialSchedule(), + initialMinimumProtocolVersion: cfg.protocolVersionsInitialMinimumVersion() }); } @@ -645,9 +646,34 @@ contract SystemDeploy is Script { address(_output.protocolVersionsProxy), _impls.protocolVersionsImpl, abi.encodeCall( - IProtocolVersions.initialize, (_input.roles.incidentResponder, _input.initialUpgradeSchedule) + IProtocolVersions.initialize, + (_input.roles.incidentResponder, _input.initialUpgradeSchedule, _input.initialMinimumProtocolVersion) ) ); + _assertProtocolVersionsInitialized( + _output.protocolVersionsProxy, _input.initialUpgradeSchedule, _input.initialMinimumProtocolVersion + ); + } + + function _assertProtocolVersionsInitialized( + IProtocolVersions _protocolVersions, + uint64[] memory _expectedSchedule, + uint256 _expectedMinimumProtocolVersion + ) + internal + view + { + uint64[] memory actualSchedule = _protocolVersions.getSchedule(); + require( + actualSchedule.length == _expectedSchedule.length, "SystemDeploy: ProtocolVersions schedule length mismatch" + ); + for (uint256 i = 0; i < actualSchedule.length; i++) { + require(actualSchedule[i] == _expectedSchedule[i], "SystemDeploy: ProtocolVersions schedule mismatch"); + } + require( + _protocolVersions.minimumProtocolVersion() == _expectedMinimumProtocolVersion, + "SystemDeploy: ProtocolVersions minimum version mismatch" + ); } function _upgradeSuperchainConfigIfNeeded( @@ -1093,6 +1119,16 @@ contract SystemDeploy is Script { if (_input.roles.systemConfigOwner == address(0)) revert InvalidRoleAddress("systemConfigOwner"); if (_input.roles.batcher == address(0)) revert InvalidRoleAddress("batcher"); if (_input.roles.unsafeBlockSigner == address(0)) revert InvalidRoleAddress("unsafeBlockSigner"); + if (_input.initialMinimumProtocolVersion > type(uint128).max) { + revert IProtocolVersions.ProtocolVersions_InvalidProtocolVersion(); + } + if (_input.initialMinimumProtocolVersion == 0) { + for (uint256 i = 0; i < _input.initialUpgradeSchedule.length; i++) { + if (_input.initialUpgradeSchedule[i] != 0) { + revert IProtocolVersions.ProtocolVersions_InvalidProtocolVersion(); + } + } + } if (Hash.unwrap(_input.startingAnchorRoot.root) == bytes32(0)) { revert InvalidStartingAnchorRoot(); } diff --git a/scripts/libraries/Types.sol b/scripts/libraries/Types.sol index 007ecdfea..1f6f398b2 100644 --- a/scripts/libraries/Types.sol +++ b/scripts/libraries/Types.sol @@ -36,6 +36,8 @@ library Types { /// upgrade in the node's fork order, zero for unscheduled ones. Seeds /// `ProtocolVersions` at initialization, which is the only way to enter /// activations that are already in the past. + /// @custom:field initialMinimumProtocolVersion Packed semver required by non-zero timestamps in the initial + /// schedule. struct DeployInput { Roles roles; uint32 basefeeScalar; @@ -45,6 +47,7 @@ library Types { string saltMixer; uint64 gasLimit; uint64[] initialUpgradeSchedule; + uint256 initialMinimumProtocolVersion; } /// @notice The full set of outputs from deploying a new OP Stack chain. diff --git a/scripts/multiproof/DeployDevBase.s.sol b/scripts/multiproof/DeployDevBase.s.sol index d88180df8..cbd6afe26 100644 --- a/scripts/multiproof/DeployDevBase.s.sol +++ b/scripts/multiproof/DeployDevBase.s.sol @@ -110,7 +110,10 @@ abstract contract DeployDevBase is Script { Proxy protocolVersionsProxy = new Proxy(msg.sender); protocolVersionsProxy.upgradeToAndCall( address(new ProtocolVersions()), - abi.encodeCall(IProtocolVersions.initialize, (address(0), cfg.protocolVersionsInitialSchedule())) + abi.encodeCall( + IProtocolVersions.initialize, + (address(0), cfg.protocolVersionsInitialSchedule(), cfg.protocolVersionsInitialMinimumVersion()) + ) ); protocolVersionsProxy.changeAdmin(address(proxyAdmin)); diff --git a/scripts/multiproof/README.md b/scripts/multiproof/README.md index ee8ee7084..708bffe84 100644 --- a/scripts/multiproof/README.md +++ b/scripts/multiproof/README.md @@ -48,6 +48,7 @@ Other relevant fields: | `multiproofGenesisOutputRoot` | Initial anchor output root | | `multiproofGenesisBlockNumber` | Initial anchor L2 block number | | `protocolVersionsInitialSchedule` | Hardfork activation timestamps in the node's fork order, `0` for unscheduled forks. Omit for a chain with no history; past activations cannot be added after deployment | +| `protocolVersionsInitialMinimumVersion` | Packed semver required by every non-zero initial schedule timestamp. Must be non-zero when the initial schedule contains an activation | ### Step 2: Deploy contracts diff --git a/snapshots/abi/ProtocolVersions.json b/snapshots/abi/ProtocolVersions.json index b797d24cb..1d263c24b 100644 --- a/snapshots/abi/ProtocolVersions.json +++ b/snapshots/abi/ProtocolVersions.json @@ -117,6 +117,11 @@ "internalType": "uint64[]", "name": "_initialSchedule", "type": "uint64[]" + }, + { + "internalType": "uint256", + "name": "_initialMinimumProtocolVersion", + "type": "uint256" } ], "name": "initialize", diff --git a/snapshots/semver-lock.json b/snapshots/semver-lock.json index b3c09b855..3af646a8f 100644 --- a/snapshots/semver-lock.json +++ b/snapshots/semver-lock.json @@ -16,8 +16,8 @@ "sourceCodeHash": "0x811596e7486cab9ceeeb61405b9ae510d93fcbbdfa11949201a367506c53194a" }, "src/L1/ProtocolVersions.sol:ProtocolVersions": { - "initCodeHash": "0x909a923012cab53fdfc4ea8f9f54173d64d2e3c250e8d69cb10ea226e21a4adf", - "sourceCodeHash": "0x2b338e2c3cb163445841a1bfca2370e731b4cba4be35018cb07b2d22a3465f67" + "initCodeHash": "0x2b600ab7dfda18f9daeb60714d98b20eef3133bf76988ee79ba9e24ab67222ed", + "sourceCodeHash": "0x101a6c1f8f9c44cbeb60f321ea90f7bd49060ff3a4d7695ce5a4f7e0784bba1a" }, "src/L1/SuperchainConfig.sol:SuperchainConfig": { "initCodeHash": "0x9b1f3555b499709485d51d5d9665002c0eb1e5eb893be1fb978a30749e894858", diff --git a/src/L1/ProtocolVersions.sol b/src/L1/ProtocolVersions.sol index 93988f282..92c359437 100644 --- a/src/L1/ProtocolVersions.sol +++ b/src/L1/ProtocolVersions.sol @@ -92,7 +92,7 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable /// @notice Thrown when an upgrade id is not registered. error ProtocolVersions_UnknownUpgrade(uint256 id); - /// @notice Thrown when a protocol version is zero. + /// @notice Thrown when a protocol version is zero where required or exceeds 128 bits. error ProtocolVersions_InvalidProtocolVersion(); /// @notice Thrown when modifying a timestamp whose activation has already passed. error ProtocolVersions_ActivationAlreadyPassed(uint256 id, uint64 activationTimestamp); @@ -123,27 +123,37 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable } /// @notice Initializes the registry by seeding the hash chain, importing any preexisting upgrade - /// schedule, and appointing the initial incidentResponder. Callable only by the - /// ProxyAdmin or its owner. + /// schedule and its minimum protocol version, and appointing the initial incidentResponder. + /// Callable only by the ProxyAdmin or its owner. + /// /// @dev `_initialSchedule` is the only path that can enter an activation which is not at least /// MIN_NOTICE in the future. It exists so a chain that already has a hardfork history can be /// represented faithfully at deployment, while it is still impossible for any proof game to /// have pinned a commitment from this registry. Every later write goes through /// `registerUpgrade`, `setTimestamp`, or `delayTimestamp`, which together guarantee that an /// activation is never created or moved once L1 is within FREEZE_WINDOW of it. + /// @dev Any non-zero imported timestamp requires a non-zero packed protocol version so nodes can + /// validate the schedule immediately after deployment. + /// /// @param _incidentResponder Initial incidentResponder allowed to delay activations, or address(0) to leave unset. /// @param _initialSchedule Activation timestamps for already-known upgrades, ordered by ascending /// upgrade id, using 0 for an upgrade that is registered but unscheduled. /// Pass an empty array for a chain with no upgrade history. + /// @param _initialMinimumProtocolVersion Packed semver required by an imported activation, or 0 when + /// the initial schedule has no non-zero timestamps. function initialize( address _incidentResponder, - uint64[] calldata _initialSchedule + uint64[] calldata _initialSchedule, + uint256 _initialMinimumProtocolVersion ) external reinitializer(initVersion()) { // Initialization transactions must come from the ProxyAdmin or its owner. _assertOnlyProxyAdminOrProxyAdminOwner(); + if (_initialMinimumProtocolVersion > type(uint128).max) { + revert ProtocolVersions_InvalidProtocolVersion(); + } // Seed the hash chain at index 0. Keeping the seed as the first array element lets // `scheduleId` and `_refreshScheduleId` avoid an empty-registry special case, and makes a @@ -152,6 +162,9 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable for (uint256 id = 0; id < _initialSchedule.length; id++) { uint64 timestamp = _initialSchedule[id]; + if (timestamp != 0 && _initialMinimumProtocolVersion == 0) { + revert ProtocolVersions_InvalidProtocolVersion(); + } // `id` is the index about to be appended, so this reads only the entries already imported. _assertTimestampAfterPrevious(id, timestamp); _timestamps.push(timestamp); @@ -164,6 +177,10 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable // pass. With an empty import this just re-emits the seed as the current commitment. _refreshScheduleId(0); + if (_initialMinimumProtocolVersion != 0) { + _writeMinimumProtocolVersion(_initialMinimumProtocolVersion); + } + incidentResponder = _incidentResponder; emit IncidentResponderUpdated(address(0), _incidentResponder); } diff --git a/test/L1/ProtocolVersions.t.sol b/test/L1/ProtocolVersions.t.sol index 290f19b65..410b61834 100644 --- a/test/L1/ProtocolVersions.t.sol +++ b/test/L1/ProtocolVersions.t.sol @@ -70,6 +70,7 @@ contract ProtocolVersions_Initialize_Test is ProtocolVersions_TestInit { // responder from config and seeds the hash chain (scheduleId == the bytes32(0) seed). assertEq(protocolVersions.proxyAdminOwner(), proxyAdminOwner); assertEq(protocolVersions.incidentResponder(), deploy.cfg().superchainConfigIncidentResponder()); + assertEq(protocolVersions.minimumProtocolVersion(), deploy.cfg().protocolVersionsInitialMinimumVersion()); assertEq(protocolVersions.scheduleId(), bytes32(0)); } @@ -80,7 +81,7 @@ contract ProtocolVersions_Initialize_Test is ProtocolVersions_TestInit { vm.expectEmit(true, true, false, false, address(uninitialized)); emit IncidentResponderUpdated(address(0), _incidentResponder); vm.prank(EIP1967Helper.getAdmin(address(uninitialized))); - uninitialized.initialize(_incidentResponder, new uint64[](0)); + uninitialized.initialize(_incidentResponder, new uint64[](0), 0); assertEq(uninitialized.incidentResponder(), _incidentResponder); } @@ -90,7 +91,7 @@ contract ProtocolVersions_Initialize_Test is ProtocolVersions_TestInit { IProtocolVersions uninitialized = _deployUninitializedProxy(); vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner.selector); vm.prank(_nonOwner); - uninitialized.initialize(_incidentResponder, new uint64[](0)); + uninitialized.initialize(_incidentResponder, new uint64[](0), 0); } /// @notice Tests that the initializer imports a preexisting schedule, building the same hash @@ -103,13 +104,14 @@ contract ProtocolVersions_Initialize_Test is ProtocolVersions_TestInit { IProtocolVersions imported = _deployUninitializedProxy(); vm.prank(EIP1967Helper.getAdmin(address(imported))); - imported.initialize(_incidentResponder, schedule); + imported.initialize(_incidentResponder, schedule, 42); uint64[] memory stored = imported.getSchedule(); assertEq(stored.length, schedule.length); assertEq(stored[0], schedule[0]); assertEq(stored[1], schedule[1]); assertEq(stored[2], schedule[2]); + assertEq(imported.minimumProtocolVersion(), 42); bytes32 link0 = keccak256(abi.encode(bytes32(0), uint256(0), uint64(10))); bytes32 link1 = keccak256(abi.encode(link0, uint256(1), uint64(0))); @@ -135,21 +137,52 @@ contract ProtocolVersions_Initialize_Test is ProtocolVersions_TestInit { ) ); vm.prank(EIP1967Helper.getAdmin(address(imported))); - imported.initialize(address(0), schedule); + imported.initialize(address(0), schedule, 42); + } + + /// @notice Tests that an imported activation cannot omit the minimum protocol version required by nodes. + function test_initialize_importWithoutMinimumProtocolVersion_reverts() external { + uint64[] memory schedule = new uint64[](1); + schedule[0] = 1; + + IProtocolVersions imported = _deployUninitializedProxy(); + vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidProtocolVersion.selector); + vm.prank(EIP1967Helper.getAdmin(address(imported))); + imported.initialize(address(0), schedule, 0); + } + + /// @notice Tests that zero-only imports may leave the minimum protocol version unset. + function test_initialize_zeroOnlyScheduleWithoutMinimumProtocolVersion_succeeds() external { + uint64[] memory schedule = new uint64[](1); + + IProtocolVersions imported = _deployUninitializedProxy(); + vm.prank(EIP1967Helper.getAdmin(address(imported))); + imported.initialize(address(0), schedule, 0); + + assertEq(imported.getSchedule().length, 1); + assertEq(imported.minimumProtocolVersion(), 0); + } + + /// @notice Tests that the initial minimum protocol version must fit in the node's 128-bit packed semver layout. + function test_initialize_minimumProtocolVersionTooLarge_reverts() external { + IProtocolVersions imported = _deployUninitializedProxy(); + vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidProtocolVersion.selector); + vm.prank(EIP1967Helper.getAdmin(address(imported))); + imported.initialize(address(0), new uint64[](0), uint256(type(uint128).max) + 1); } /// @notice Tests that the contract cannot be initialized twice. function test_initialize_alreadyInitialized_reverts() external { vm.expectRevert("Initializable: contract is already initialized"); vm.prank(EIP1967Helper.getAdmin(address(protocolVersions))); - protocolVersions.initialize(address(0), new uint64[](0)); + protocolVersions.initialize(address(0), new uint64[](0), 0); } /// @notice Tests that the implementation itself cannot be initialized (initializers disabled). function test_initialize_implementationDisabled_reverts() external { IProtocolVersions impl = IProtocolVersions(EIP1967Helper.getImplementation(address(protocolVersions))); vm.expectRevert("Initializable: contract is already initialized"); - impl.initialize(address(0), new uint64[](0)); + impl.initialize(address(0), new uint64[](0), 0); } } @@ -1090,7 +1123,7 @@ contract ProtocolVersions_ActivatedScheduleId_Test is ProtocolVersions_TestInit function _importSchedule(uint64[] memory schedule) private returns (IProtocolVersions) { IProtocolVersions imported = _deployUninitializedProxy(); vm.prank(EIP1967Helper.getAdmin(address(imported))); - imported.initialize(address(0), schedule); + imported.initialize(address(0), schedule, 1); return imported; } } diff --git a/test/L1/proofs/BaseTest.t.sol b/test/L1/proofs/BaseTest.t.sol index a45a75f25..3845c0136 100644 --- a/test/L1/proofs/BaseTest.t.sol +++ b/test/L1/proofs/BaseTest.t.sol @@ -112,7 +112,7 @@ contract BaseTest is Test { ); factory.initialize(address(this)); delayedWETH.initialize(systemConfig); - protocolVersions.initialize(address(0), new uint64[](0)); + protocolVersions.initialize(address(0), new uint64[](0), 0); } /// @dev Rebuilds the schedule registry around a preset schedule and rebinds the verifier to it. @@ -121,7 +121,7 @@ contract BaseTest is Test { /// Must be called before any game is created, since games pin the registry they see. function _importProtocolVersionsSchedule(uint64[] memory schedule) internal { protocolVersions = ProtocolVersions(_deployProxy(address(new ProtocolVersions()))); - protocolVersions.initialize(address(0), schedule); + protocolVersions.initialize(address(0), schedule, 1); _deployAndSetAggregateVerifier(); } diff --git a/test/deploy/DeployConfig.t.sol b/test/deploy/DeployConfig.t.sol index e1a65b2c6..fdbdd4201 100644 --- a/test/deploy/DeployConfig.t.sol +++ b/test/deploy/DeployConfig.t.sol @@ -6,6 +6,10 @@ import { Test } from "lib/forge-std/src/Test.sol"; import { DeployConfig } from "scripts/deploy/DeployConfig.s.sol"; contract DeployConfigHarness is DeployConfig { + function readMinimumVersion(string memory _json) public { + _readProtocolVersionsInitialMinimumVersion(_json); + } + function readSchedule(string memory _json) public { _readProtocolVersionsInitialSchedule(_json); } @@ -53,11 +57,29 @@ contract DeployConfig_Test is Test { cfg.readSchedule('{"protocolVersionsInitialSchedule":[18446744073709551616]}'); } - /// @notice The shipped configs describe chains without a recorded history, so they must keep - /// producing an empty registry. - function test_read_localConfig_leavesScheduleEmpty_succeeds() public { + function test_readMinimumVersion_parsesValue_succeeds() public { + cfg.readMinimumVersion('{"protocolVersionsInitialMinimumVersion":42}'); + + assertEq(cfg.protocolVersionsInitialMinimumVersion(), 42); + } + + function test_readMinimumVersion_omitted_defaultsToZero_succeeds() public { + cfg.readMinimumVersion('{"l1ChainId":1}'); + + assertEq(cfg.protocolVersionsInitialMinimumVersion(), 0); + } + + function test_readMinimumVersion_aboveUint128_reverts() public { + vm.expectRevert("DeployConfig: initial minimum protocol version exceeds uint128"); + cfg.readMinimumVersion('{"protocolVersionsInitialMinimumVersion":340282366920938463463374607431768211456}'); + } + + /// @notice The shipped configs describe chains without a recorded history, so they must keep the initial registry + /// state empty. + function test_read_localConfig_leavesProtocolVersionsStateEmpty_succeeds() public { cfg.read("deploy-config/local.json"); assertEq(cfg.protocolVersionsInitialSchedule().length, 0); + assertEq(cfg.protocolVersionsInitialMinimumVersion(), 0); } } diff --git a/test/deploy/SystemDeploy.t.sol b/test/deploy/SystemDeploy.t.sol index a77b154e9..27bda0a5d 100644 --- a/test/deploy/SystemDeploy.t.sol +++ b/test/deploy/SystemDeploy.t.sol @@ -154,10 +154,10 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { assertValidStandardSystem(_expected(output, input)); } - /// @notice A chain that already has a hardfork history has to seed the registry at deploy time. - /// `registerUpgrade` cannot enter activations that are already in the past, and - /// `initialize` runs once, so an empty import here would be permanent. - function test_deploy_seedsProtocolVersionsWithInitialSchedule_succeeds() public { + /// @notice A chain that already has a hardfork history has to seed the registry and minimum protocol version at + /// deploy time. `registerUpgrade` cannot enter activations that are already in the past, and `initialize` runs + /// once, so an empty import here would be permanent. + function test_deploy_seedsProtocolVersionsWithInitialState_succeeds() public { vm.warp(1_800_000_000); uint64[] memory schedule = new uint64[](4); @@ -168,6 +168,7 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { SystemDeploy.DeployInput memory input = _defaultDeployInput(); input.opChainInput.initialUpgradeSchedule = schedule; + input.opChainInput.initialMinimumProtocolVersion = 42; SystemDeploy.DeployOutput memory output = systemDeploy.deploy(input); @@ -176,6 +177,7 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { for (uint256 i = 0; i < schedule.length; i++) { assertEq(imported[i], schedule[i], "schedule entry"); } + assertEq(output.opChain.protocolVersionsProxy.minimumProtocolVersion(), 42, "minimum protocol version"); assertEq( address(AggregateVerifier(address(output.opChain.aggregateVerifier)).PROTOCOL_VERSIONS()), @@ -184,6 +186,17 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { ); } + function test_deploy_initialScheduleWithoutMinimumProtocolVersion_reverts() public { + uint64[] memory schedule = new uint64[](1); + schedule[0] = 1; + + SystemDeploy.DeployInput memory input = _defaultDeployInput(); + input.opChainInput.initialUpgradeSchedule = schedule; + + vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidProtocolVersion.selector); + systemDeploy.deploy(input); + } + function test_deploy_multiproofDisabled_allowsUnsetL2BlockTime_succeeds() public { SystemDeploy.DeployInput memory input = _defaultDeployInput(); input.implementationsInput.multiproofConfigHash = bytes32(0); @@ -401,7 +414,8 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { startingAnchorRoot: Proposal({ root: Hash.wrap(bytes32(uint256(1))), l2SequenceNumber: 0 }), saltMixer: "system-deploy-test", gasLimit: 60_000_000, - initialUpgradeSchedule: new uint64[](0) + initialUpgradeSchedule: new uint64[](0), + initialMinimumProtocolVersion: 0 }); } diff --git a/test/vendor/Initializable.t.sol b/test/vendor/Initializable.t.sol index ea61bbff3..828e20807 100644 --- a/test/vendor/Initializable.t.sol +++ b/test/vendor/Initializable.t.sol @@ -213,7 +213,7 @@ contract Initializer_Test is CommonTest { // ProtocolVersions is deployed by the standard deployment script but is absent on older // forked chains, so only track it when the proxy is present. if (address(protocolVersions) != address(0)) { - initCalldata = abi.encodeCall(protocolVersions.initialize, (address(0), new uint64[](0))); + initCalldata = abi.encodeCall(protocolVersions.initialize, (address(0), new uint64[](0), 0)); contracts.push( InitializeableContract({ name: "ProtocolVersionsImpl", From 10147d8d21b793a9ec7091435fe15e553ebbd093 Mon Sep 17 00:00:00 2001 From: Jack Chuma Date: Mon, 24 Aug 2026 17:52:18 -0400 Subject: [PATCH 2/6] refactor(deploy): remove duplicate ProtocolVersions checks Rely on the atomic initializer for schedule validation and keep deployment-state verification in the integration test. Co-authored-by: Cursor --- scripts/deploy/SystemDeploy.s.sol | 34 ------------------------------- test/deploy/SystemDeploy.t.sol | 11 ---------- 2 files changed, 45 deletions(-) diff --git a/scripts/deploy/SystemDeploy.s.sol b/scripts/deploy/SystemDeploy.s.sol index d8f75b6e5..24ed2be0f 100644 --- a/scripts/deploy/SystemDeploy.s.sol +++ b/scripts/deploy/SystemDeploy.s.sol @@ -650,30 +650,6 @@ contract SystemDeploy is Script { (_input.roles.incidentResponder, _input.initialUpgradeSchedule, _input.initialMinimumProtocolVersion) ) ); - _assertProtocolVersionsInitialized( - _output.protocolVersionsProxy, _input.initialUpgradeSchedule, _input.initialMinimumProtocolVersion - ); - } - - function _assertProtocolVersionsInitialized( - IProtocolVersions _protocolVersions, - uint64[] memory _expectedSchedule, - uint256 _expectedMinimumProtocolVersion - ) - internal - view - { - uint64[] memory actualSchedule = _protocolVersions.getSchedule(); - require( - actualSchedule.length == _expectedSchedule.length, "SystemDeploy: ProtocolVersions schedule length mismatch" - ); - for (uint256 i = 0; i < actualSchedule.length; i++) { - require(actualSchedule[i] == _expectedSchedule[i], "SystemDeploy: ProtocolVersions schedule mismatch"); - } - require( - _protocolVersions.minimumProtocolVersion() == _expectedMinimumProtocolVersion, - "SystemDeploy: ProtocolVersions minimum version mismatch" - ); } function _upgradeSuperchainConfigIfNeeded( @@ -1119,16 +1095,6 @@ contract SystemDeploy is Script { if (_input.roles.systemConfigOwner == address(0)) revert InvalidRoleAddress("systemConfigOwner"); if (_input.roles.batcher == address(0)) revert InvalidRoleAddress("batcher"); if (_input.roles.unsafeBlockSigner == address(0)) revert InvalidRoleAddress("unsafeBlockSigner"); - if (_input.initialMinimumProtocolVersion > type(uint128).max) { - revert IProtocolVersions.ProtocolVersions_InvalidProtocolVersion(); - } - if (_input.initialMinimumProtocolVersion == 0) { - for (uint256 i = 0; i < _input.initialUpgradeSchedule.length; i++) { - if (_input.initialUpgradeSchedule[i] != 0) { - revert IProtocolVersions.ProtocolVersions_InvalidProtocolVersion(); - } - } - } if (Hash.unwrap(_input.startingAnchorRoot.root) == bytes32(0)) { revert InvalidStartingAnchorRoot(); } diff --git a/test/deploy/SystemDeploy.t.sol b/test/deploy/SystemDeploy.t.sol index 27bda0a5d..01fbe792c 100644 --- a/test/deploy/SystemDeploy.t.sol +++ b/test/deploy/SystemDeploy.t.sol @@ -186,17 +186,6 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { ); } - function test_deploy_initialScheduleWithoutMinimumProtocolVersion_reverts() public { - uint64[] memory schedule = new uint64[](1); - schedule[0] = 1; - - SystemDeploy.DeployInput memory input = _defaultDeployInput(); - input.opChainInput.initialUpgradeSchedule = schedule; - - vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidProtocolVersion.selector); - systemDeploy.deploy(input); - } - function test_deploy_multiproofDisabled_allowsUnsetL2BlockTime_succeeds() public { SystemDeploy.DeployInput memory input = _defaultDeployInput(); input.implementationsInput.multiproofConfigHash = bytes32(0); From b0c3066b3412d57c3407ed7da58cb8b1b68471ef Mon Sep 17 00:00:00 2001 From: Jack Chuma Date: Mon, 24 Aug 2026 18:14:53 -0400 Subject: [PATCH 3/6] fix(deploy): preflight imported ProtocolVersions state Validate the imported schedule and minimum version before any broadcast so a bad config cannot strand a partially deployed deterministic system. Co-authored-by: Cursor --- scripts/deploy/SystemDeploy.s.sol | 15 ++++++++++++++- test/deploy/SystemDeploy.t.sol | 23 +++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/scripts/deploy/SystemDeploy.s.sol b/scripts/deploy/SystemDeploy.s.sol index 24ed2be0f..7118844a9 100644 --- a/scripts/deploy/SystemDeploy.s.sol +++ b/scripts/deploy/SystemDeploy.s.sol @@ -314,6 +314,10 @@ contract SystemDeploy is Script { } function deploy(DeployInput memory _input) public returns (DeployOutput memory output_) { + // Validate before any broadcast because a later revert cannot roll back transactions already sent by the + // script. + _assertValidOPChainInput(_input.opChainInput); + output_.superchain = _deployOrLoadSuperchain(_input); if (_implementationsEmpty(_input.implementations)) { output_.impls = _deployImplementations(_input.implementationsInput); @@ -484,7 +488,6 @@ contract SystemDeploy is Script { internal returns (Types.DeployOutput memory output_, Types.Implementations memory impls_) { - _assertValidOPChainInput(_input); impls_ = _impls; output_.opChainProxyAdmin = IProxyAdmin( @@ -1095,6 +1098,16 @@ contract SystemDeploy is Script { if (_input.roles.systemConfigOwner == address(0)) revert InvalidRoleAddress("systemConfigOwner"); if (_input.roles.batcher == address(0)) revert InvalidRoleAddress("batcher"); if (_input.roles.unsafeBlockSigner == address(0)) revert InvalidRoleAddress("unsafeBlockSigner"); + if (_input.initialMinimumProtocolVersion > type(uint128).max) { + revert IProtocolVersions.ProtocolVersions_InvalidProtocolVersion(); + } + if (_input.initialMinimumProtocolVersion == 0) { + for (uint256 i = 0; i < _input.initialUpgradeSchedule.length; i++) { + if (_input.initialUpgradeSchedule[i] != 0) { + revert IProtocolVersions.ProtocolVersions_InvalidProtocolVersion(); + } + } + } if (Hash.unwrap(_input.startingAnchorRoot.root) == bytes32(0)) { revert InvalidStartingAnchorRoot(); } diff --git a/test/deploy/SystemDeploy.t.sol b/test/deploy/SystemDeploy.t.sol index 01fbe792c..f571640a1 100644 --- a/test/deploy/SystemDeploy.t.sol +++ b/test/deploy/SystemDeploy.t.sol @@ -186,6 +186,29 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { ); } + /// @notice Pins ProtocolVersions input validation before superchain deployment can broadcast. + function test_deploy_initialScheduleWithoutMinimumProtocolVersion_reverts() public { + uint64[] memory schedule = new uint64[](1); + schedule[0] = 1; + + SystemDeploy.DeployInput memory input = _defaultDeployInput(); + input.opChainInput.initialUpgradeSchedule = schedule; + input.superchainInput.superchainProxyAdminOwner = address(0); + + vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidProtocolVersion.selector); + systemDeploy.deploy(input); + } + + /// @notice Pins the packed-version bound check before superchain deployment can broadcast. + function test_deploy_initialMinimumProtocolVersionTooLarge_reverts() public { + SystemDeploy.DeployInput memory input = _defaultDeployInput(); + input.opChainInput.initialMinimumProtocolVersion = uint256(type(uint128).max) + 1; + input.superchainInput.superchainProxyAdminOwner = address(0); + + vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidProtocolVersion.selector); + systemDeploy.deploy(input); + } + function test_deploy_multiproofDisabled_allowsUnsetL2BlockTime_succeeds() public { SystemDeploy.DeployInput memory input = _defaultDeployInput(); input.implementationsInput.multiproofConfigHash = bytes32(0); From 8d7c02ed1d6b821c6e16cf1940f93727da285d81 Mon Sep 17 00:00:00 2001 From: Jack Chuma Date: Wed, 26 Aug 2026 07:30:07 -0400 Subject: [PATCH 4/6] fix(deploy): preflight all imported schedule invariants Share ordering, notice, and minimum-version validation across standard and multiproof deployments so invalid configuration cannot strand partial broadcasts. Co-authored-by: Cursor --- scripts/deploy/SystemDeploy.s.sol | 14 ++----- .../libraries/ProtocolVersionsDeployUtils.sol | 36 ++++++++++++++++ scripts/multiproof/DeployDevBase.s.sol | 4 ++ test/deploy/SystemDeploy.t.sol | 41 +++++++++++++++++++ 4 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 scripts/libraries/ProtocolVersionsDeployUtils.sol diff --git a/scripts/deploy/SystemDeploy.s.sol b/scripts/deploy/SystemDeploy.s.sol index 7118844a9..63db8a228 100644 --- a/scripts/deploy/SystemDeploy.s.sol +++ b/scripts/deploy/SystemDeploy.s.sol @@ -9,6 +9,7 @@ import { Artifacts } from "scripts/Artifacts.s.sol"; import { Config } from "scripts/libraries/Config.sol"; import { DeployConfig } from "scripts/deploy/DeployConfig.s.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; +import { ProtocolVersionsDeployUtils } from "scripts/libraries/ProtocolVersionsDeployUtils.sol"; import { StateDiff } from "scripts/libraries/StateDiff.sol"; import { Types } from "scripts/libraries/Types.sol"; @@ -1098,16 +1099,9 @@ contract SystemDeploy is Script { if (_input.roles.systemConfigOwner == address(0)) revert InvalidRoleAddress("systemConfigOwner"); if (_input.roles.batcher == address(0)) revert InvalidRoleAddress("batcher"); if (_input.roles.unsafeBlockSigner == address(0)) revert InvalidRoleAddress("unsafeBlockSigner"); - if (_input.initialMinimumProtocolVersion > type(uint128).max) { - revert IProtocolVersions.ProtocolVersions_InvalidProtocolVersion(); - } - if (_input.initialMinimumProtocolVersion == 0) { - for (uint256 i = 0; i < _input.initialUpgradeSchedule.length; i++) { - if (_input.initialUpgradeSchedule[i] != 0) { - revert IProtocolVersions.ProtocolVersions_InvalidProtocolVersion(); - } - } - } + ProtocolVersionsDeployUtils.assertValidInitialState( + _input.initialUpgradeSchedule, _input.initialMinimumProtocolVersion + ); if (Hash.unwrap(_input.startingAnchorRoot.root) == bytes32(0)) { revert InvalidStartingAnchorRoot(); } diff --git a/scripts/libraries/ProtocolVersionsDeployUtils.sol b/scripts/libraries/ProtocolVersionsDeployUtils.sol new file mode 100644 index 000000000..b7010db30 --- /dev/null +++ b/scripts/libraries/ProtocolVersionsDeployUtils.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; + +library ProtocolVersionsDeployUtils { + /// @dev Must match ProtocolVersions.MIN_NOTICE. + uint64 internal constant MIN_NOTICE = 1 hours; + + /// @notice Validates imported ProtocolVersions state before a deployment script broadcasts any transactions. + function assertValidInitialState(uint64[] memory _schedule, uint256 _minimumProtocolVersion) internal view { + if (_minimumProtocolVersion > type(uint128).max) { + revert IProtocolVersions.ProtocolVersions_InvalidProtocolVersion(); + } + + uint256 previousId; + uint64 previousTimestamp; + for (uint256 id = 0; id < _schedule.length; id++) { + uint64 timestamp = _schedule[id]; + if (timestamp != 0 && _minimumProtocolVersion == 0) { + revert IProtocolVersions.ProtocolVersions_InvalidProtocolVersion(); + } + if (timestamp > uint64(block.timestamp) && timestamp < uint64(block.timestamp) + MIN_NOTICE) { + revert IProtocolVersions.ProtocolVersions_InsufficientNotice(timestamp); + } + if (timestamp == 0) continue; + if (previousTimestamp != 0 && timestamp < previousTimestamp) { + revert IProtocolVersions.ProtocolVersions_TimestampNotAfterPrevious( + id, previousId, previousTimestamp, timestamp + ); + } + previousId = id; + previousTimestamp = timestamp; + } + } +} diff --git a/scripts/multiproof/DeployDevBase.s.sol b/scripts/multiproof/DeployDevBase.s.sol index cbd6afe26..cac45ebe6 100644 --- a/scripts/multiproof/DeployDevBase.s.sol +++ b/scripts/multiproof/DeployDevBase.s.sol @@ -13,6 +13,7 @@ import { GameType, Hash } from "src/libraries/bridge/Types.sol"; import { DeployConfig } from "scripts/deploy/DeployConfig.s.sol"; import { Config } from "scripts/libraries/Config.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; +import { ProtocolVersionsDeployUtils } from "scripts/libraries/ProtocolVersionsDeployUtils.sol"; import { AggregateVerifier } from "src/L1/proofs/AggregateVerifier.sol"; import { IVerifier } from "interfaces/L1/proofs/IVerifier.sol"; @@ -170,6 +171,9 @@ abstract contract DeployDevBase is Script { function _preflight() internal virtual { require(cfg.l2BlockTime() != 0, "l2BlockTime must be set in config"); require(cfg.l2GenesisTimestamp() != 0, "l2GenesisTimestamp must be set in config"); + ProtocolVersionsDeployUtils.assertValidInitialState( + cfg.protocolVersionsInitialSchedule(), cfg.protocolVersionsInitialMinimumVersion() + ); } function _serializeExtra(string memory key) internal virtual { } diff --git a/test/deploy/SystemDeploy.t.sol b/test/deploy/SystemDeploy.t.sol index f571640a1..84e0085f8 100644 --- a/test/deploy/SystemDeploy.t.sol +++ b/test/deploy/SystemDeploy.t.sol @@ -209,6 +209,47 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { systemDeploy.deploy(input); } + /// @notice Pins imported schedule ordering validation before superchain deployment can broadcast. + function test_deploy_unorderedInitialSchedule_reverts() public { + vm.warp(31); + uint64[] memory schedule = new uint64[](3); + schedule[0] = 30; + schedule[2] = 10; + + SystemDeploy.DeployInput memory input = _defaultDeployInput(); + input.opChainInput.initialUpgradeSchedule = schedule; + input.opChainInput.initialMinimumProtocolVersion = 42; + input.superchainInput.superchainProxyAdminOwner = address(0); + + vm.expectRevert( + abi.encodeWithSelector( + IProtocolVersions.ProtocolVersions_TimestampNotAfterPrevious.selector, + uint256(2), + uint256(0), + uint64(30), + uint64(10) + ) + ); + systemDeploy.deploy(input); + } + + /// @notice Pins future activation notice validation before superchain deployment can broadcast. + function test_deploy_initialFutureActivationWithInsufficientNotice_reverts() public { + uint64 activation = uint64(block.timestamp) + 1 hours - 1; + uint64[] memory schedule = new uint64[](1); + schedule[0] = activation; + + SystemDeploy.DeployInput memory input = _defaultDeployInput(); + input.opChainInput.initialUpgradeSchedule = schedule; + input.opChainInput.initialMinimumProtocolVersion = 42; + input.superchainInput.superchainProxyAdminOwner = address(0); + + vm.expectRevert( + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_InsufficientNotice.selector, activation) + ); + systemDeploy.deploy(input); + } + function test_deploy_multiproofDisabled_allowsUnsetL2BlockTime_succeeds() public { SystemDeploy.DeployInput memory input = _defaultDeployInput(); input.implementationsInput.multiproofConfigHash = bytes32(0); From a7775d9b391673d88b924245feb1b7f9fad38387 Mon Sep 17 00:00:00 2001 From: Jack Chuma Date: Wed, 26 Aug 2026 14:49:45 -0400 Subject: [PATCH 5/6] fix(deploy): buffer initial activation notice Reserve one hour for sequential broadcasts so a future activation accepted during preflight remains valid when ProtocolVersions is initialized. Co-authored-by: Cursor --- .../libraries/ProtocolVersionsDeployUtils.sol | 6 +++++- test/deploy/SystemDeploy.t.sol | 21 ++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/scripts/libraries/ProtocolVersionsDeployUtils.sol b/scripts/libraries/ProtocolVersionsDeployUtils.sol index b7010db30..dc934a249 100644 --- a/scripts/libraries/ProtocolVersionsDeployUtils.sol +++ b/scripts/libraries/ProtocolVersionsDeployUtils.sol @@ -6,6 +6,8 @@ import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; library ProtocolVersionsDeployUtils { /// @dev Must match ProtocolVersions.MIN_NOTICE. uint64 internal constant MIN_NOTICE = 1 hours; + /// @dev Reserves a full notice window for sequential deployment transactions to be mined before initialization. + uint64 internal constant DEPLOYMENT_NOTICE_BUFFER = MIN_NOTICE; /// @notice Validates imported ProtocolVersions state before a deployment script broadcasts any transactions. function assertValidInitialState(uint64[] memory _schedule, uint256 _minimumProtocolVersion) internal view { @@ -13,6 +15,8 @@ library ProtocolVersionsDeployUtils { revert IProtocolVersions.ProtocolVersions_InvalidProtocolVersion(); } + uint64 currentTimestamp = uint64(block.timestamp); + uint64 minimumFutureTimestamp = currentTimestamp + MIN_NOTICE + DEPLOYMENT_NOTICE_BUFFER; uint256 previousId; uint64 previousTimestamp; for (uint256 id = 0; id < _schedule.length; id++) { @@ -20,7 +24,7 @@ library ProtocolVersionsDeployUtils { if (timestamp != 0 && _minimumProtocolVersion == 0) { revert IProtocolVersions.ProtocolVersions_InvalidProtocolVersion(); } - if (timestamp > uint64(block.timestamp) && timestamp < uint64(block.timestamp) + MIN_NOTICE) { + if (timestamp > currentTimestamp && timestamp < minimumFutureTimestamp) { revert IProtocolVersions.ProtocolVersions_InsufficientNotice(timestamp); } if (timestamp == 0) continue; diff --git a/test/deploy/SystemDeploy.t.sol b/test/deploy/SystemDeploy.t.sol index 84e0085f8..832813952 100644 --- a/test/deploy/SystemDeploy.t.sol +++ b/test/deploy/SystemDeploy.t.sol @@ -233,9 +233,9 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { systemDeploy.deploy(input); } - /// @notice Pins future activation notice validation before superchain deployment can broadcast. - function test_deploy_initialFutureActivationWithInsufficientNotice_reverts() public { - uint64 activation = uint64(block.timestamp) + 1 hours - 1; + /// @notice Pins a deployment buffer beyond the initializer's minimum notice before any broadcast. + function test_deploy_initialFutureActivationWithoutDeploymentBuffer_reverts() public { + uint64 activation = uint64(block.timestamp) + 1 hours; uint64[] memory schedule = new uint64[](1); schedule[0] = activation; @@ -250,6 +250,21 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { systemDeploy.deploy(input); } + /// @notice Pins the inclusive future activation boundary after the deployment notice buffer. + function test_deploy_initialFutureActivationAtDeploymentBuffer_succeeds() public { + uint64 activation = uint64(block.timestamp) + 2 hours; + uint64[] memory schedule = new uint64[](1); + schedule[0] = activation; + + SystemDeploy.DeployInput memory input = _defaultDeployInput(); + input.opChainInput.initialUpgradeSchedule = schedule; + input.opChainInput.initialMinimumProtocolVersion = 42; + + SystemDeploy.DeployOutput memory output = systemDeploy.deploy(input); + + assertEq(output.opChain.protocolVersionsProxy.getSchedule()[0], activation); + } + function test_deploy_multiproofDisabled_allowsUnsetL2BlockTime_succeeds() public { SystemDeploy.DeployInput memory input = _defaultDeployInput(); input.implementationsInput.multiproofConfigHash = bytes32(0); From 1237b5fdba49f16f6a40129c8b0713e85c943bf2 Mon Sep 17 00:00:00 2001 From: Jack Chuma Date: Wed, 26 Aug 2026 16:03:55 -0400 Subject: [PATCH 6/6] refactor(deploy): fold protocol preflight into DeployUtils Remove the one-off ProtocolVersions deployment library while preserving shared validation across both broadcast paths. Co-authored-by: Cursor --- scripts/deploy/SystemDeploy.s.sol | 3 +- scripts/libraries/DeployUtils.sol | 43 +++++++++++++++++++ .../libraries/ProtocolVersionsDeployUtils.sol | 40 ----------------- scripts/multiproof/DeployDevBase.s.sol | 3 +- 4 files changed, 45 insertions(+), 44 deletions(-) delete mode 100644 scripts/libraries/ProtocolVersionsDeployUtils.sol diff --git a/scripts/deploy/SystemDeploy.s.sol b/scripts/deploy/SystemDeploy.s.sol index 63db8a228..b196fc42f 100644 --- a/scripts/deploy/SystemDeploy.s.sol +++ b/scripts/deploy/SystemDeploy.s.sol @@ -9,7 +9,6 @@ import { Artifacts } from "scripts/Artifacts.s.sol"; import { Config } from "scripts/libraries/Config.sol"; import { DeployConfig } from "scripts/deploy/DeployConfig.s.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; -import { ProtocolVersionsDeployUtils } from "scripts/libraries/ProtocolVersionsDeployUtils.sol"; import { StateDiff } from "scripts/libraries/StateDiff.sol"; import { Types } from "scripts/libraries/Types.sol"; @@ -1099,7 +1098,7 @@ contract SystemDeploy is Script { if (_input.roles.systemConfigOwner == address(0)) revert InvalidRoleAddress("systemConfigOwner"); if (_input.roles.batcher == address(0)) revert InvalidRoleAddress("batcher"); if (_input.roles.unsafeBlockSigner == address(0)) revert InvalidRoleAddress("unsafeBlockSigner"); - ProtocolVersionsDeployUtils.assertValidInitialState( + DeployUtils.assertValidProtocolVersionsInitialState( _input.initialUpgradeSchedule, _input.initialMinimumProtocolVersion ); if (Hash.unwrap(_input.startingAnchorRoot.root) == bytes32(0)) { diff --git a/scripts/libraries/DeployUtils.sol b/scripts/libraries/DeployUtils.sol index 9d3dded7d..66eaa6a85 100644 --- a/scripts/libraries/DeployUtils.sol +++ b/scripts/libraries/DeployUtils.sol @@ -4,6 +4,9 @@ pragma solidity ^0.8.0; // Scripts import { Vm } from "lib/forge-std/src/Vm.sol"; +// Interfaces +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; + // Libraries import { LibString } from "lib/solady/src/utils/LibString.sol"; import { Bytes } from "src/libraries/Bytes.sol"; @@ -12,6 +15,10 @@ library DeployUtils { Vm internal constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); bytes32 internal constant DEFAULT_SALT = keccak256("op-stack-contract-impls-salt-v0"); + /// @dev Must match ProtocolVersions.MIN_NOTICE. + uint64 internal constant PROTOCOL_VERSIONS_MIN_NOTICE = 1 hours; + /// @dev Reserves a full notice window for sequential deployment transactions to be mined before initialization. + uint64 internal constant PROTOCOL_VERSIONS_DEPLOYMENT_NOTICE_BUFFER = PROTOCOL_VERSIONS_MIN_NOTICE; function create1(string memory _name, bytes memory _args) internal returns (address payable addr_) { bytes memory bytecode = abi.encodePacked(vm.getCode(_name), _args); @@ -91,6 +98,42 @@ library DeployUtils { assertUniqueAddresses(_addrs); } + /// @notice Validates imported ProtocolVersions state before a deployment script broadcasts any transactions. + function assertValidProtocolVersionsInitialState( + uint64[] memory _schedule, + uint256 _minimumProtocolVersion + ) + internal + view + { + if (_minimumProtocolVersion > type(uint128).max) { + revert IProtocolVersions.ProtocolVersions_InvalidProtocolVersion(); + } + + uint64 currentTimestamp = uint64(block.timestamp); + uint64 minimumFutureTimestamp = + currentTimestamp + PROTOCOL_VERSIONS_MIN_NOTICE + PROTOCOL_VERSIONS_DEPLOYMENT_NOTICE_BUFFER; + uint256 previousId; + uint64 previousTimestamp; + for (uint256 id = 0; id < _schedule.length; id++) { + uint64 timestamp = _schedule[id]; + if (timestamp != 0 && _minimumProtocolVersion == 0) { + revert IProtocolVersions.ProtocolVersions_InvalidProtocolVersion(); + } + if (timestamp > currentTimestamp && timestamp < minimumFutureTimestamp) { + revert IProtocolVersions.ProtocolVersions_InsufficientNotice(timestamp); + } + if (timestamp == 0) continue; + if (previousTimestamp != 0 && timestamp < previousTimestamp) { + revert IProtocolVersions.ProtocolVersions_TimestampNotAfterPrevious( + id, previousId, previousTimestamp, timestamp + ); + } + previousId = id; + previousTimestamp = timestamp; + } + } + /// @notice Etches a contract, labels it, and allows cheatcodes for it. /// @param _etchTo Address of the contract to etch. /// @param _cname The contract name (also used to label the contract). MUST be the name of both the file and the diff --git a/scripts/libraries/ProtocolVersionsDeployUtils.sol b/scripts/libraries/ProtocolVersionsDeployUtils.sol deleted file mode 100644 index dc934a249..000000000 --- a/scripts/libraries/ProtocolVersionsDeployUtils.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; - -library ProtocolVersionsDeployUtils { - /// @dev Must match ProtocolVersions.MIN_NOTICE. - uint64 internal constant MIN_NOTICE = 1 hours; - /// @dev Reserves a full notice window for sequential deployment transactions to be mined before initialization. - uint64 internal constant DEPLOYMENT_NOTICE_BUFFER = MIN_NOTICE; - - /// @notice Validates imported ProtocolVersions state before a deployment script broadcasts any transactions. - function assertValidInitialState(uint64[] memory _schedule, uint256 _minimumProtocolVersion) internal view { - if (_minimumProtocolVersion > type(uint128).max) { - revert IProtocolVersions.ProtocolVersions_InvalidProtocolVersion(); - } - - uint64 currentTimestamp = uint64(block.timestamp); - uint64 minimumFutureTimestamp = currentTimestamp + MIN_NOTICE + DEPLOYMENT_NOTICE_BUFFER; - uint256 previousId; - uint64 previousTimestamp; - for (uint256 id = 0; id < _schedule.length; id++) { - uint64 timestamp = _schedule[id]; - if (timestamp != 0 && _minimumProtocolVersion == 0) { - revert IProtocolVersions.ProtocolVersions_InvalidProtocolVersion(); - } - if (timestamp > currentTimestamp && timestamp < minimumFutureTimestamp) { - revert IProtocolVersions.ProtocolVersions_InsufficientNotice(timestamp); - } - if (timestamp == 0) continue; - if (previousTimestamp != 0 && timestamp < previousTimestamp) { - revert IProtocolVersions.ProtocolVersions_TimestampNotAfterPrevious( - id, previousId, previousTimestamp, timestamp - ); - } - previousId = id; - previousTimestamp = timestamp; - } - } -} diff --git a/scripts/multiproof/DeployDevBase.s.sol b/scripts/multiproof/DeployDevBase.s.sol index cac45ebe6..5e2f89208 100644 --- a/scripts/multiproof/DeployDevBase.s.sol +++ b/scripts/multiproof/DeployDevBase.s.sol @@ -13,7 +13,6 @@ import { GameType, Hash } from "src/libraries/bridge/Types.sol"; import { DeployConfig } from "scripts/deploy/DeployConfig.s.sol"; import { Config } from "scripts/libraries/Config.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; -import { ProtocolVersionsDeployUtils } from "scripts/libraries/ProtocolVersionsDeployUtils.sol"; import { AggregateVerifier } from "src/L1/proofs/AggregateVerifier.sol"; import { IVerifier } from "interfaces/L1/proofs/IVerifier.sol"; @@ -171,7 +170,7 @@ abstract contract DeployDevBase is Script { function _preflight() internal virtual { require(cfg.l2BlockTime() != 0, "l2BlockTime must be set in config"); require(cfg.l2GenesisTimestamp() != 0, "l2GenesisTimestamp must be set in config"); - ProtocolVersionsDeployUtils.assertValidInitialState( + DeployUtils.assertValidProtocolVersionsInitialState( cfg.protocolVersionsInitialSchedule(), cfg.protocolVersionsInitialMinimumVersion() ); }