From 25d2cf5ca7808a6ab348a1cb52d49e8c8c34e286 Mon Sep 17 00:00:00 2001 From: PelleKrab Date: Thu, 25 Jun 2026 10:18:13 -0700 Subject: [PATCH 01/20] feat(L1): add ProtocolVersions upgrade schedule contract Add the Security Council-controlled upgrade activation schedule contract, its interface, and tests. Maintains an ordered registry of upgrades and their L2 activation timestamps, committed via scheduleId. Generated with Claude Code Co-Authored-By: Claude --- interfaces/L1/IProtocolVersions.sol | 21 ++ src/L1/ProtocolVersions.sol | 257 +++++++++++++++++++++ test/L1/ProtocolVersions.t.sol | 343 ++++++++++++++++++++++++++++ 3 files changed, 621 insertions(+) create mode 100644 interfaces/L1/IProtocolVersions.sol create mode 100644 src/L1/ProtocolVersions.sol create mode 100644 test/L1/ProtocolVersions.t.sol diff --git a/interfaces/L1/IProtocolVersions.sol b/interfaces/L1/IProtocolVersions.sol new file mode 100644 index 000000000..1a21534c6 --- /dev/null +++ b/interfaces/L1/IProtocolVersions.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ISemver } from "interfaces/universal/ISemver.sol"; + +interface IProtocolVersions is ISemver { + function l2ChainId() external view returns (uint256); + function chainTeam() external view returns (address); + function scheduleId() external view returns (bytes32); + function lastUpdatedAtBlock() external view returns (uint256); + function upgradeCount() external view returns (uint256); + function upgradeIdAt(uint256 index) external view returns (bytes32); + function upgradeIds() external view returns (bytes32[] memory); + function getTimestamp(string calldata upgradeId) external view returns (uint256); + function getProtocolVersion(string calldata upgradeId) external view returns (uint256); + + function registerUpgrade(string calldata upgradeId, uint256 protocolVersion) external; + function setTimestamp(string calldata upgradeId, uint64 timestamp) external; + function setChainTeam(address newChainTeam) external; + function delayTimestamp(string calldata upgradeId, uint64 newTimestamp) external; +} diff --git a/src/L1/ProtocolVersions.sol b/src/L1/ProtocolVersions.sol new file mode 100644 index 000000000..b5e3bad30 --- /dev/null +++ b/src/L1/ProtocolVersions.sol @@ -0,0 +1,257 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Contracts +import { Ownable } from "lib/openzeppelin-contracts/contracts/access/Ownable.sol"; + +// Interfaces +import { ISemver } from "interfaces/universal/ISemver.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; + +/// @title ProtocolVersions +/// @notice Security Council-controlled upgrade activation schedule contract. +/// @dev Maintains an ordered registry of upgrades and their L2 activation timestamps. +/// The canonical schedule commitment (`scheduleId`) is a hash over all (key, timestamp) +/// pairs in registration order. Proof journals bind to `scheduleId`, pinning every proof +/// in a dispute game to the schedule that was in effect at the game's L1 origin block. +/// +/// An `upgradeId` is a human-readable string (e.g. "canyon"); each is packed into a +/// bytes32 storage `key`. The registry starts empty; all upgrades are added via +/// `registerUpgrade` owner calls. +contract ProtocolVersions is Ownable, IProtocolVersions { + /// @notice Semantic version. + /// @custom:semver 1.0.0 + string public constant version = "1.0.0"; + + /// @notice The L2 chain ID whose schedule this contract commits to. + uint256 public immutable l2ChainId; + + /// @notice The latest block number in which `scheduleId` changed. + uint256 public lastUpdatedAtBlock; + + bytes32 internal _scheduleId; + + /// @notice Ordered list of registered upgrade keys. Defines the iteration order used to + /// compute `scheduleId`, so it must be reproduced exactly by off-chain consumers. + bytes32[] internal _upgradeKeys; + + /// @notice 1-based registration index of each upgrade key into `_upgradeKeys`. 0 = unregistered. + /// Provides O(1) registered/duplicate checks without iterating `_upgradeKeys`. + mapping(bytes32 => uint256) internal _upgradeIndex; + + mapping(bytes32 => uint64) internal _timestamps; + + /// @notice Protocol version for each upgrade, chosen by the owner and set at registration via + /// `registerUpgrade`. Immutable once registered. + mapping(bytes32 => uint256) internal _protocolVersions; + + /// @notice Address allowed to delay (push out) already-scheduled activation timestamps. + /// @dev Appointed and revocable by the owner. This is a restricted secondary role: it can + /// only move an already-scheduled, not-yet-activated upgrade further into the future via + /// `delaySignal`. It cannot register upgrades, clear timestamps, pull an activation + /// earlier, or schedule a brand-new activation. Unset (zero) by default. + address public chainTeam; + + /// @notice Emitted when a new upgrade is added to the registry. + /// @param key The canonical bytes32 storage key. + /// @param index The 0-based registration index of this upgrade. + /// @param upgradeId The upgrade identifier string. + /// @param protocolVersion The owner-assigned protocol version for this upgrade. + event UpgradeRegistered(bytes32 indexed key, uint256 indexed index, string upgradeId, uint256 protocolVersion); + + /// @notice Emitted when an activation timestamp is set for an upgrade. + event TimestampSet(bytes32 indexed key, uint256 timestamp); + + /// @notice Emitted when the schedule commitment changes. + event ScheduleIdUpdated(bytes32 indexed oldScheduleId, bytes32 indexed newScheduleId, uint256 indexed blockNumber); + + /// @notice Emitted when the chainTeam role is appointed, changed, or cleared. + event ChainTeamUpdated(address indexed previousChainTeam, address indexed newChainTeam); + + /// @notice Thrown when the owner is unset. + error ProtocolVersions_ZeroOwner(); + + /// @notice Thrown when the L2 chain ID is zero. + error ProtocolVersions_InvalidL2ChainId(); + + /// @notice Thrown when an upgradeId does not resolve to a registered upgrade. + error ProtocolVersions_UnknownUpgradeName(string upgradeId); + + /// @notice Thrown when registering an upgrade that is already registered. + error ProtocolVersions_UpgradeAlreadyRegistered(bytes32 key); + + /// @notice Thrown when an upgradeId is malformed (empty, or longer than 32 bytes). + error ProtocolVersions_InvalidUpgradeId(); + + /// @notice Thrown when trying to modify an upgrade whose activation timestamp has already passed. + error ProtocolVersions_ActivationAlreadyPassed(bytes32 key, uint64 activationTimestamp); + + /// @notice Thrown when setting an activation timestamp that is not in the future. + error ProtocolVersions_ActivationTimestampInPast(uint64 timestamp); + + /// @notice Thrown when a chainTeam-only function is called by another account. + error ProtocolVersions_NotChainTeam(); + + /// @notice Thrown when delaying an upgrade that has no scheduled activation timestamp. + error ProtocolVersions_NotScheduled(bytes32 key); + + /// @notice Thrown when a delay does not push the activation strictly later than its current value. + error ProtocolVersions_DelayMustBeLater(uint64 currentTimestamp, uint64 newTimestamp); + + /// @param owner_ Initial Security Council owner. + /// @param l2ChainId_ L2 chain ID whose schedule is being committed. + constructor(address owner_, uint256 l2ChainId_) Ownable() { + if (owner_ == address(0)) revert ProtocolVersions_ZeroOwner(); + if (l2ChainId_ == 0) revert ProtocolVersions_InvalidL2ChainId(); + + _transferOwnership(owner_); + l2ChainId = l2ChainId_; + + _scheduleId = _computeScheduleId(); + lastUpdatedAtBlock = block.number; + emit ScheduleIdUpdated(bytes32(0), _scheduleId, block.number); + } + + /// @notice Restricts a call to the appointed chainTeam role. + modifier onlyChainTeam() { + if (msg.sender != chainTeam) revert ProtocolVersions_NotChainTeam(); + _; + } + + /// @notice Returns the canonical schedule commitment. + function scheduleId() public view returns (bytes32) { + return _scheduleId; + } + + /// @notice Returns the number of registered upgrades. + function upgradeCount() external view returns (uint256) { + return _upgradeKeys.length; + } + + /// @notice Returns the registered upgrade key at `index` (registration order). + function upgradeIdAt(uint256 index) external view returns (bytes32) { + return _upgradeKeys[index]; + } + + /// @notice Returns the full ordered list of registered upgrade keys. + function upgradeIds() external view returns (bytes32[] memory) { + return _upgradeKeys; + } + + /// @notice Returns the activation timestamp for `upgradeId` (0 = not scheduled). + function getTimestamp(string calldata upgradeId) external view returns (uint256) { + return _timestamps[_registeredKey(upgradeId)]; + } + + /// @notice Returns the owner-assigned protocol version for `upgradeId`. Reverts if not registered. + function getProtocolVersion(string calldata upgradeId) external view returns (uint256) { + return _protocolVersions[_registeredKey(upgradeId)]; + } + + /// @notice Registers a new upgrade by upgradeId with its protocol version. Owner only. + /// @dev The upgradeId is packed into a bytes32 key and `protocolVersion` is recorded for it. + /// Adding an upgrade extends the committed schedule, so `scheduleId` changes. The proof + /// image must already understand the new upgrade before its timestamp is activated. + function registerUpgrade(string calldata upgradeId, uint256 protocolVersion) external onlyOwner { + _registerUpgrade(upgradeId, protocolVersion); + _refreshScheduleId(); + } + + /// @notice Sets the activation timestamp for one upgrade by upgradeId. Pass 0 to clear. + /// @dev The activation timestamp must be in the future and the upgrade must not have + /// already activated. Pass 0 to remove a scheduled activation. + function setTimestamp(string calldata upgradeId, uint64 timestamp) external onlyOwner { + _applyTimestamp(_registeredKey(upgradeId), timestamp); + _refreshScheduleId(); + } + + /// @notice Appoints, replaces, or clears (set to zero) the chainTeam role. Owner only. + function setChainTeam(address newChainTeam) external onlyOwner { + emit ChainTeamUpdated(chainTeam, newChainTeam); + chainTeam = newChainTeam; + } + + /// @notice Pushes an already-scheduled upgrade's activation timestamp further into the future. + /// Can only be called by the chainTeam. + /// @dev The upgrade must already have a non-zero activation timestamp that has not yet passed, + /// and `newTimestamp` must be strictly later than the current value. This role can only + /// delay an activation; it cannot pull one earlier, clear it, or schedule a new one — use + /// the owner's `setTimestamp` for those. Because `current` is in the future and `newTimestamp` + /// is later still, the new value is always in the future. + function delayTimestamp(string calldata upgradeId, uint64 newTimestamp) external onlyChainTeam { + bytes32 key = _registeredKey(upgradeId); + uint64 current = _timestamps[key]; + + // The upgrade must already have a scheduled activation to delay. + if (current == 0) revert ProtocolVersions_NotScheduled(key); + // Cannot modify an activation that has already passed. + if (uint64(block.timestamp) >= current) { + revert ProtocolVersions_ActivationAlreadyPassed(key, current); + } + // The role can only push the activation later, never earlier or to the same time. + if (newTimestamp <= current) revert ProtocolVersions_DelayMustBeLater(current, newTimestamp); + + _timestamps[key] = newTimestamp; + emit TimestampSet(key, newTimestamp); + _refreshScheduleId(); + } + + function _applyTimestamp(bytes32 key, uint64 timestamp) internal { + uint64 current = _timestamps[key]; + // Cannot modify a timestamp that has already activated. + if (current != 0 && uint64(block.timestamp) >= current) { + revert ProtocolVersions_ActivationAlreadyPassed(key, current); + } + // Non-zero timestamps must be in the future. + if (timestamp != 0 && uint64(block.timestamp) >= timestamp) { + revert ProtocolVersions_ActivationTimestampInPast(timestamp); + } + if (current != timestamp) { + _timestamps[key] = timestamp; + emit TimestampSet(key, timestamp); + } + } + + function _registerUpgrade(string calldata upgradeId, uint256 protocolVersion) internal { + bytes32 key = _keyFromUpgradeId(upgradeId); + if (_upgradeIndex[key] != 0) revert ProtocolVersions_UpgradeAlreadyRegistered(key); + + uint256 index = _upgradeKeys.length; + _upgradeKeys.push(key); + _upgradeIndex[key] = index + 1; + _protocolVersions[key] = protocolVersion; + emit UpgradeRegistered(key, index, upgradeId, protocolVersion); + } + + function _refreshScheduleId() internal { + bytes32 oldScheduleId = _scheduleId; + bytes32 newScheduleId = _computeScheduleId(); + if (newScheduleId != oldScheduleId) { + _scheduleId = newScheduleId; + lastUpdatedAtBlock = block.number; + emit ScheduleIdUpdated(oldScheduleId, newScheduleId, block.number); + } + } + + function _computeScheduleId() internal view returns (bytes32) { + uint256 count = _upgradeKeys.length; + uint64[] memory timestamps = new uint64[](count); + for (uint256 i = 0; i < count; i++) { + timestamps[i] = _timestamps[_upgradeKeys[i]]; + } + return keccak256(abi.encode(l2ChainId, address(this), _upgradeKeys, timestamps)); + } + + function _registeredKey(string calldata upgradeId) internal view returns (bytes32 key) { + key = _keyFromUpgradeId(upgradeId); + if (_upgradeIndex[key] == 0) revert ProtocolVersions_UnknownUpgradeName(upgradeId); + } + + function _keyFromUpgradeId(string calldata upgradeId) internal pure returns (bytes32 key) { + bytes memory raw = bytes(upgradeId); + if (raw.length == 0 || raw.length > 32) revert ProtocolVersions_InvalidUpgradeId(); + assembly { + key := mload(add(raw, 32)) + } + } +} diff --git a/test/L1/ProtocolVersions.t.sol b/test/L1/ProtocolVersions.t.sol new file mode 100644 index 000000000..828301374 --- /dev/null +++ b/test/L1/ProtocolVersions.t.sol @@ -0,0 +1,343 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { Test } from "lib/forge-std/src/Test.sol"; + +import { ProtocolVersions } from "src/L1/ProtocolVersions.sol"; + +contract ProtocolVersions_Test is Test { + /// @dev Mirror of ProtocolVersions.UpgradeRegistered for use with vm.expectEmit. + event UpgradeRegistered(bytes32 indexed key, uint256 indexed index, string upgradeId, uint256 protocolVersion); + + address internal owner = makeAddr("owner"); + address internal nonOwner = makeAddr("non-owner"); + address internal chainTeam = makeAddr("chain-team"); + ProtocolVersions internal protocolVersions; + + function setUp() public { + protocolVersions = new ProtocolVersions(owner, 8453); + } + + /// @dev Registers `canyon` and schedules it for `block.timestamp + delay`, returning that timestamp. + function _scheduleCanyon(uint64 delay) internal returns (uint64 ts) { + vm.prank(owner); + protocolVersions.registerUpgrade("canyon", 1); + ts = uint64(block.timestamp + delay); + vm.prank(owner); + protocolVersions.setTimestamp("canyon", ts); + } + + function testSetSignalUpdatesTimestampAndScheduleId() public { + bytes32 initialScheduleId = protocolVersions.scheduleId(); + + vm.prank(owner); + protocolVersions.registerUpgrade("canyon", 1); + + vm.roll(block.number + 1); + vm.warp(block.timestamp + 1); + uint64 ts = uint64(block.timestamp + 100); + vm.prank(owner); + protocolVersions.setTimestamp("canyon", ts); + + assertEq(protocolVersions.getTimestamp("canyon"), ts); + assertEq(protocolVersions.lastUpdatedAtBlock(), block.number); + assertNotEq(protocolVersions.scheduleId(), initialScheduleId); + } + + function testSameTimestampDoesNotChangeScheduleId() public { + vm.prank(owner); + protocolVersions.registerUpgrade("canyon", 1); + + vm.roll(block.number + 1); + vm.warp(block.timestamp + 1); + uint64 ts = uint64(block.timestamp + 100); + vm.prank(owner); + protocolVersions.setTimestamp("canyon", ts); + + bytes32 scheduleIdAfterSet = protocolVersions.scheduleId(); + uint256 updateBlockAfterSet = protocolVersions.lastUpdatedAtBlock(); + + vm.roll(block.number + 1); + vm.prank(owner); + protocolVersions.setTimestamp("canyon", ts); + + assertEq(protocolVersions.getTimestamp("canyon"), ts); + assertEq(protocolVersions.scheduleId(), scheduleIdAfterSet); + assertEq(protocolVersions.lastUpdatedAtBlock(), updateBlockAfterSet); + } + + function testClearingSignalResetsTimestamp() public { + vm.prank(owner); + protocolVersions.registerUpgrade("canyon", 1); + bytes32 scheduleIdAfterRegister = protocolVersions.scheduleId(); + + vm.roll(block.number + 1); + vm.warp(block.timestamp + 1); + vm.prank(owner); + protocolVersions.setTimestamp("canyon", uint64(block.timestamp + 100)); + + vm.roll(block.number + 1); + vm.prank(owner); + protocolVersions.setTimestamp("canyon", 0); + + assertEq(protocolVersions.getTimestamp("canyon"), 0); + assertEq(protocolVersions.scheduleId(), scheduleIdAfterRegister); + } + + function testOnlyOwnerCanUpdateSignals() public { + vm.prank(owner); + protocolVersions.registerUpgrade("canyon", 1); + + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(nonOwner); + protocolVersions.setTimestamp("canyon", uint64(block.timestamp + 100)); + } + + function testSetSignalRejectsTimestampInPast() public { + vm.prank(owner); + protocolVersions.registerUpgrade("canyon", 1); + + vm.warp(1000); + vm.expectRevert( + abi.encodeWithSelector(ProtocolVersions.ProtocolVersions_ActivationTimestampInPast.selector, uint64(500)) + ); + vm.prank(owner); + protocolVersions.setTimestamp("canyon", 500); + } + + function testSetSignalRejectsChangeAfterActivation() public { + vm.prank(owner); + protocolVersions.registerUpgrade("canyon", 1); + + vm.warp(100); + vm.prank(owner); + protocolVersions.setTimestamp("canyon", 200); + + vm.warp(300); + bytes32 canyonId = bytes32("canyon"); + vm.expectRevert( + abi.encodeWithSelector( + ProtocolVersions.ProtocolVersions_ActivationAlreadyPassed.selector, canyonId, uint64(200) + ) + ); + vm.prank(owner); + protocolVersions.setTimestamp("canyon", 400); + } + + function testGetTimestampRejectsUnknownUpgradeName() public { + vm.expectRevert( + abi.encodeWithSelector(ProtocolVersions.ProtocolVersions_UnknownUpgradeName.selector, "unknown") + ); + protocolVersions.getTimestamp("unknown"); + } + + function testRegistryStartsEmpty() public view { + assertEq(protocolVersions.upgradeCount(), 0); + assertEq(protocolVersions.upgradeIds().length, 0); + } + + function testRegisterUpgradeExtendsScheduleAndChangesScheduleId() public { + bytes32 initialScheduleId = protocolVersions.scheduleId(); + + assertEq(protocolVersions.upgradeCount(), 0); + + vm.roll(block.number + 1); + vm.prank(owner); + protocolVersions.registerUpgrade("antares", 1); + + assertEq(protocolVersions.upgradeCount(), 1); + assertEq(protocolVersions.upgradeIdAt(0), bytes32("antares")); + assertNotEq(protocolVersions.scheduleId(), initialScheduleId); + assertEq(protocolVersions.lastUpdatedAtBlock(), block.number); + + vm.warp(block.timestamp + 1); + vm.roll(block.number + 1); + vm.prank(owner); + protocolVersions.setTimestamp("antares", uint64(block.timestamp + 100)); + assertEq(protocolVersions.getTimestamp("antares"), block.timestamp + 100); + } + + function testUpgradeRegisteredEventEmitsKeyIndexNameAndVersion() public { + // First registration: index 0, with its key, id string, and owner-chosen version. + vm.expectEmit(true, true, true, true, address(protocolVersions)); + emit UpgradeRegistered(bytes32("canyon"), 0, "canyon", 1); + vm.prank(owner); + protocolVersions.registerUpgrade("canyon", 1); + + // Second registration: index increments to 1. + vm.expectEmit(true, true, true, true, address(protocolVersions)); + emit UpgradeRegistered(bytes32("ecotone"), 1, "ecotone", 2); + vm.prank(owner); + protocolVersions.registerUpgrade("ecotone", 2); + } + + function testRegisterUpgradeRejectsDuplicate() public { + vm.prank(owner); + protocolVersions.registerUpgrade("canyon", 1); + + vm.expectRevert( + abi.encodeWithSelector( + ProtocolVersions.ProtocolVersions_UpgradeAlreadyRegistered.selector, bytes32("canyon") + ) + ); + vm.prank(owner); + protocolVersions.registerUpgrade("canyon", 1); + } + + function testRegisterUpgradeRejectsEmptyName() public { + vm.expectRevert(ProtocolVersions.ProtocolVersions_InvalidUpgradeId.selector); + vm.prank(owner); + protocolVersions.registerUpgrade("", 1); + } + + function testRegisterUpgradeRejectsNameLongerThan32Bytes() public { + vm.expectRevert(ProtocolVersions.ProtocolVersions_InvalidUpgradeId.selector); + vm.prank(owner); + protocolVersions.registerUpgrade("this-upgrade-name-is-definitely-too-long", 1); + } + + function testOnlyOwnerCanRegisterUpgrade() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(nonOwner); + protocolVersions.registerUpgrade("antares", 1); + } + + function testSetSignalRejectsUnregisteredUpgrade() public { + vm.expectRevert( + abi.encodeWithSelector(ProtocolVersions.ProtocolVersions_UnknownUpgradeName.selector, "antares") + ); + vm.prank(owner); + protocolVersions.setTimestamp("antares", uint64(block.timestamp + 100)); + } + + function testRegisterUpgradeStoresOwnerChosenProtocolVersion() public { + // Versions are owner-chosen at registration, not derived from registration order. + vm.prank(owner); + protocolVersions.registerUpgrade("canyon", 5); + vm.prank(owner); + protocolVersions.registerUpgrade("ecotone", 9); + + assertEq(protocolVersions.getProtocolVersion("canyon"), 5); + assertEq(protocolVersions.getProtocolVersion("ecotone"), 9); + } + + function testGetProtocolVersionRejectsUnknownUpgradeName() public { + vm.expectRevert( + abi.encodeWithSelector(ProtocolVersions.ProtocolVersions_UnknownUpgradeName.selector, "unknown") + ); + protocolVersions.getProtocolVersion("unknown"); + } + + function testChainTeamStartsUnset() public view { + assertEq(protocolVersions.chainTeam(), address(0)); + } + + function testOwnerCanSetChainTeam() public { + vm.prank(owner); + protocolVersions.setChainTeam(chainTeam); + assertEq(protocolVersions.chainTeam(), chainTeam); + } + + function testOnlyOwnerCanSetChainTeam() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(nonOwner); + protocolVersions.setChainTeam(chainTeam); + } + + function testDelaySignalPushesTimestampLater() public { + uint64 ts = _scheduleCanyon(100); + vm.prank(owner); + protocolVersions.setChainTeam(chainTeam); + + bytes32 scheduleIdBefore = protocolVersions.scheduleId(); + vm.roll(block.number + 1); + + uint64 later = ts + 50; + vm.prank(chainTeam); + protocolVersions.delayTimestamp("canyon", later); + + assertEq(protocolVersions.getTimestamp("canyon"), later); + assertNotEq(protocolVersions.scheduleId(), scheduleIdBefore); + assertEq(protocolVersions.lastUpdatedAtBlock(), block.number); + } + + function testOnlyChainTeamCanDelaySignal() public { + uint64 ts = _scheduleCanyon(100); + vm.prank(owner); + protocolVersions.setChainTeam(chainTeam); + + // Not even the owner may call delayTimestamp — it is chainTeam-only. + vm.expectRevert(ProtocolVersions.ProtocolVersions_NotChainTeam.selector); + vm.prank(owner); + protocolVersions.delayTimestamp("canyon", ts + 50); + + vm.expectRevert(ProtocolVersions.ProtocolVersions_NotChainTeam.selector); + vm.prank(nonOwner); + protocolVersions.delayTimestamp("canyon", ts + 50); + } + + function testDelaySignalRejectsEarlierTimestamp() public { + uint64 ts = _scheduleCanyon(100); + vm.prank(owner); + protocolVersions.setChainTeam(chainTeam); + + vm.expectRevert( + abi.encodeWithSelector( + ProtocolVersions.ProtocolVersions_DelayMustBeLater.selector, ts, ts - 10 + ) + ); + vm.prank(chainTeam); + protocolVersions.delayTimestamp("canyon", ts - 10); + } + + function testDelaySignalRejectsEqualTimestamp() public { + uint64 ts = _scheduleCanyon(100); + vm.prank(owner); + protocolVersions.setChainTeam(chainTeam); + + vm.expectRevert( + abi.encodeWithSelector(ProtocolVersions.ProtocolVersions_DelayMustBeLater.selector, ts, ts) + ); + vm.prank(chainTeam); + protocolVersions.delayTimestamp("canyon", ts); + } + + function testDelaySignalRejectsUnscheduledUpgrade() public { + vm.prank(owner); + protocolVersions.registerUpgrade("canyon", 1); + vm.prank(owner); + protocolVersions.setChainTeam(chainTeam); + + vm.expectRevert( + abi.encodeWithSelector(ProtocolVersions.ProtocolVersions_NotScheduled.selector, bytes32("canyon")) + ); + vm.prank(chainTeam); + protocolVersions.delayTimestamp("canyon", uint64(block.timestamp + 100)); + } + + function testDelaySignalRejectsAfterActivation() public { + uint64 ts = _scheduleCanyon(100); + vm.prank(owner); + protocolVersions.setChainTeam(chainTeam); + + vm.warp(ts + 1); + vm.expectRevert( + abi.encodeWithSelector( + ProtocolVersions.ProtocolVersions_ActivationAlreadyPassed.selector, bytes32("canyon"), ts + ) + ); + vm.prank(chainTeam); + protocolVersions.delayTimestamp("canyon", ts + 100); + } + + function testDelaySignalRejectsUnregisteredUpgrade() public { + vm.prank(owner); + protocolVersions.setChainTeam(chainTeam); + + vm.expectRevert( + abi.encodeWithSelector(ProtocolVersions.ProtocolVersions_UnknownUpgradeName.selector, "antares") + ); + vm.prank(chainTeam); + protocolVersions.delayTimestamp("antares", uint64(block.timestamp + 100)); + } +} From 1b89555622e3369692a90ce53734c0a60e9e55b1 Mon Sep 17 00:00:00 2001 From: PelleKrab Date: Thu, 25 Jun 2026 15:02:42 -0700 Subject: [PATCH 02/20] fix(L1): refine ProtocolVersions with reproducible scheduleId and repo conventions Replace the history-dependent rolling hash with a per-upgrade cumulative hash chain reproducible from (l2ChainId, address, ordered keys, current timestamps). When a timestamp changes, only the affected suffix is recomputed (O(n-j) bubble-up) rather than hashing the full state on every mutation. - Add MIN_NOTICE (1 hour) guard on setTimestamp; expose it in the interface - Require non-zero protocolVersion; use _protocolVersions as existence sentinel, removing the separate _upgradeIndex mapping - Move all errors and events into IProtocolVersions; add MIN_NOTICE to interface - Replace assembly in _keyFromUpgradeId with bytes32(raw) Solidity cast - Rename constructor params to leading-underscore convention - Restructure test file into grouped contracts with snake_case naming, external visibility, and @notice on each test function per repo style Generated with Claude Code Co-Authored-By: Claude --- interfaces/L1/IProtocolVersions.sol | 20 + src/L1/ProtocolVersions.sol | 208 +++++----- test/L1/ProtocolVersions.t.sol | 576 ++++++++++++++++++---------- 3 files changed, 510 insertions(+), 294 deletions(-) diff --git a/interfaces/L1/IProtocolVersions.sol b/interfaces/L1/IProtocolVersions.sol index 1a21534c6..5bc61c257 100644 --- a/interfaces/L1/IProtocolVersions.sol +++ b/interfaces/L1/IProtocolVersions.sol @@ -3,7 +3,27 @@ pragma solidity ^0.8.0; import { ISemver } from "interfaces/universal/ISemver.sol"; +/// @title IProtocolVersions +/// @notice Interface for the ProtocolVersions upgrade schedule contract. interface IProtocolVersions is ISemver { + error ProtocolVersions_ZeroOwner(); + error ProtocolVersions_InvalidL2ChainId(); + error ProtocolVersions_UnknownUpgradeName(string upgradeId); + error ProtocolVersions_UpgradeAlreadyRegistered(bytes32 key); + error ProtocolVersions_InvalidUpgradeId(); + error ProtocolVersions_InvalidProtocolVersion(); + error ProtocolVersions_ActivationAlreadyPassed(bytes32 key, uint64 activationTimestamp); + error ProtocolVersions_ActivationTimestampInPast(uint64 timestamp); + error ProtocolVersions_NotChainTeam(); + error ProtocolVersions_NotScheduled(bytes32 key); + error ProtocolVersions_DelayMustBeLater(uint64 currentTimestamp, uint64 newTimestamp); + + event UpgradeRegistered(bytes32 indexed key, uint256 indexed index, string upgradeId, uint256 protocolVersion); + event TimestampSet(bytes32 indexed key, uint256 timestamp); + event ScheduleIdUpdated(bytes32 indexed oldScheduleId, bytes32 indexed newScheduleId, uint256 indexed blockNumber); + event ChainTeamUpdated(address indexed previousChainTeam, address indexed newChainTeam); + + function MIN_NOTICE() external view returns (uint64); function l2ChainId() external view returns (uint256); function chainTeam() external view returns (address); function scheduleId() external view returns (bytes32); diff --git a/src/L1/ProtocolVersions.sol b/src/L1/ProtocolVersions.sol index b5e3bad30..9704401fc 100644 --- a/src/L1/ProtocolVersions.sol +++ b/src/L1/ProtocolVersions.sol @@ -5,15 +5,23 @@ pragma solidity 0.8.15; import { Ownable } from "lib/openzeppelin-contracts/contracts/access/Ownable.sol"; // Interfaces -import { ISemver } from "interfaces/universal/ISemver.sol"; import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; /// @title ProtocolVersions /// @notice Security Council-controlled upgrade activation schedule contract. /// @dev Maintains an ordered registry of upgrades and their L2 activation timestamps. -/// The canonical schedule commitment (`scheduleId`) is a hash over all (key, timestamp) -/// pairs in registration order. Proof journals bind to `scheduleId`, pinning every proof -/// in a dispute game to the schedule that was in effect at the game's L1 origin block. +/// The canonical schedule commitment (`scheduleId`) is the tail of a per-upgrade hash chain: +/// +/// seed = keccak256(abi.encode(l2ChainId, address(this))) +/// upgradeScheduleId[0] = keccak256(abi.encode(seed, key_0, timestamp_0)) +/// upgradeScheduleId[i] = keccak256(abi.encode(upgradeScheduleId[i-1], key_i, timestamp_i)) +/// scheduleId = upgradeScheduleId[n-1] (or seed when n == 0) +/// +/// where timestamp_i is the upgrade's current activation timestamp (0 = not yet scheduled). +/// Changing any upgrade's timestamp recomputes its link and all subsequent links, making +/// scheduleId fully reproducible from (l2ChainId, address, ordered keys, current timestamps). +/// Proof journals bind to `scheduleId`, pinning every proof in a dispute game to the schedule +/// that was in effect at the game's L1 origin block. /// /// An `upgradeId` is a human-readable string (e.g. "canyon"); each is packed into a /// bytes32 storage `key`. The registry starts empty; all upgrades are added via @@ -23,91 +31,53 @@ contract ProtocolVersions is Ownable, IProtocolVersions { /// @custom:semver 1.0.0 string public constant version = "1.0.0"; + /// @notice Minimum notice period required when scheduling or modifying an activation timestamp. + uint64 public constant MIN_NOTICE = 1 hours; + /// @notice The L2 chain ID whose schedule this contract commits to. uint256 public immutable l2ChainId; /// @notice The latest block number in which `scheduleId` changed. uint256 public lastUpdatedAtBlock; + /// @notice The current schedule commitment hash, extended on every timestamp change. bytes32 internal _scheduleId; - /// @notice Ordered list of registered upgrade keys. Defines the iteration order used to - /// compute `scheduleId`, so it must be reproduced exactly by off-chain consumers. + /// @notice Ordered list of registered upgrade keys. bytes32[] internal _upgradeKeys; - /// @notice 1-based registration index of each upgrade key into `_upgradeKeys`. 0 = unregistered. - /// Provides O(1) registered/duplicate checks without iterating `_upgradeKeys`. - mapping(bytes32 => uint256) internal _upgradeIndex; + /// @notice Cumulative schedule hash up to and including each registered upgrade. + /// upgradeScheduleId[i] = keccak256(abi.encode(upgradeScheduleId[i-1], key_i, timestamps[key_i])). + /// Stored per-key so that changing upgrade j's timestamp requires recomputing only + /// j..n-1 links rather than the entire chain. + mapping(bytes32 => bytes32) internal _upgradeScheduleId; + /// @notice Activation timestamp for each registered upgrade key (0 = not scheduled). mapping(bytes32 => uint64) internal _timestamps; /// @notice Protocol version for each upgrade, chosen by the owner and set at registration via - /// `registerUpgrade`. Immutable once registered. + /// `registerUpgrade`. Immutable once registered. Non-zero for registered upgrades; + /// zero for unregistered keys (serves as the existence sentinel). mapping(bytes32 => uint256) internal _protocolVersions; /// @notice Address allowed to delay (push out) already-scheduled activation timestamps. /// @dev Appointed and revocable by the owner. This is a restricted secondary role: it can /// only move an already-scheduled, not-yet-activated upgrade further into the future via - /// `delaySignal`. It cannot register upgrades, clear timestamps, pull an activation + /// `delayTimestamp`. It cannot register upgrades, clear timestamps, pull an activation /// earlier, or schedule a brand-new activation. Unset (zero) by default. address public chainTeam; - /// @notice Emitted when a new upgrade is added to the registry. - /// @param key The canonical bytes32 storage key. - /// @param index The 0-based registration index of this upgrade. - /// @param upgradeId The upgrade identifier string. - /// @param protocolVersion The owner-assigned protocol version for this upgrade. - event UpgradeRegistered(bytes32 indexed key, uint256 indexed index, string upgradeId, uint256 protocolVersion); - - /// @notice Emitted when an activation timestamp is set for an upgrade. - event TimestampSet(bytes32 indexed key, uint256 timestamp); - - /// @notice Emitted when the schedule commitment changes. - event ScheduleIdUpdated(bytes32 indexed oldScheduleId, bytes32 indexed newScheduleId, uint256 indexed blockNumber); - - /// @notice Emitted when the chainTeam role is appointed, changed, or cleared. - event ChainTeamUpdated(address indexed previousChainTeam, address indexed newChainTeam); - - /// @notice Thrown when the owner is unset. - error ProtocolVersions_ZeroOwner(); - - /// @notice Thrown when the L2 chain ID is zero. - error ProtocolVersions_InvalidL2ChainId(); - - /// @notice Thrown when an upgradeId does not resolve to a registered upgrade. - error ProtocolVersions_UnknownUpgradeName(string upgradeId); - - /// @notice Thrown when registering an upgrade that is already registered. - error ProtocolVersions_UpgradeAlreadyRegistered(bytes32 key); - - /// @notice Thrown when an upgradeId is malformed (empty, or longer than 32 bytes). - error ProtocolVersions_InvalidUpgradeId(); - - /// @notice Thrown when trying to modify an upgrade whose activation timestamp has already passed. - error ProtocolVersions_ActivationAlreadyPassed(bytes32 key, uint64 activationTimestamp); - - /// @notice Thrown when setting an activation timestamp that is not in the future. - error ProtocolVersions_ActivationTimestampInPast(uint64 timestamp); - - /// @notice Thrown when a chainTeam-only function is called by another account. - error ProtocolVersions_NotChainTeam(); + /// @notice Deploys the contract, sets the owner and chain ID, and emits the initial scheduleId. + /// @param _owner Initial Security Council owner. + /// @param _l2ChainId L2 chain ID whose schedule is being committed. + constructor(address _owner, uint256 _l2ChainId) Ownable() { + if (_owner == address(0)) revert ProtocolVersions_ZeroOwner(); + if (_l2ChainId == 0) revert ProtocolVersions_InvalidL2ChainId(); - /// @notice Thrown when delaying an upgrade that has no scheduled activation timestamp. - error ProtocolVersions_NotScheduled(bytes32 key); + _transferOwnership(_owner); + l2ChainId = _l2ChainId; - /// @notice Thrown when a delay does not push the activation strictly later than its current value. - error ProtocolVersions_DelayMustBeLater(uint64 currentTimestamp, uint64 newTimestamp); - - /// @param owner_ Initial Security Council owner. - /// @param l2ChainId_ L2 chain ID whose schedule is being committed. - constructor(address owner_, uint256 l2ChainId_) Ownable() { - if (owner_ == address(0)) revert ProtocolVersions_ZeroOwner(); - if (l2ChainId_ == 0) revert ProtocolVersions_InvalidL2ChainId(); - - _transferOwnership(owner_); - l2ChainId = l2ChainId_; - - _scheduleId = _computeScheduleId(); + _scheduleId = keccak256(abi.encode(l2ChainId, address(this))); lastUpdatedAtBlock = block.number; emit ScheduleIdUpdated(bytes32(0), _scheduleId, block.number); } @@ -119,53 +89,68 @@ contract ProtocolVersions is Ownable, IProtocolVersions { } /// @notice Returns the canonical schedule commitment. - function scheduleId() public view returns (bytes32) { + /// @return The current scheduleId hash. + function scheduleId() external view returns (bytes32) { return _scheduleId; } /// @notice Returns the number of registered upgrades. + /// @return The length of the upgrade registry. function upgradeCount() external view returns (uint256) { return _upgradeKeys.length; } /// @notice Returns the registered upgrade key at `index` (registration order). + /// @param index 0-based position in the registry. + /// @return The bytes32 key at that position. function upgradeIdAt(uint256 index) external view returns (bytes32) { return _upgradeKeys[index]; } /// @notice Returns the full ordered list of registered upgrade keys. + /// @return Ordered array of bytes32 keys in registration order. function upgradeIds() external view returns (bytes32[] memory) { return _upgradeKeys; } /// @notice Returns the activation timestamp for `upgradeId` (0 = not scheduled). + /// @param upgradeId The human-readable upgrade identifier string. + /// @return The scheduled L2 activation timestamp, or 0 if unscheduled. function getTimestamp(string calldata upgradeId) external view returns (uint256) { return _timestamps[_registeredKey(upgradeId)]; } /// @notice Returns the owner-assigned protocol version for `upgradeId`. Reverts if not registered. + /// @param upgradeId The human-readable upgrade identifier string. + /// @return The protocol version assigned at registration time. function getProtocolVersion(string calldata upgradeId) external view returns (uint256) { return _protocolVersions[_registeredKey(upgradeId)]; } /// @notice Registers a new upgrade by upgradeId with its protocol version. Owner only. /// @dev The upgradeId is packed into a bytes32 key and `protocolVersion` is recorded for it. - /// Adding an upgrade extends the committed schedule, so `scheduleId` changes. The proof - /// image must already understand the new upgrade before its timestamp is activated. + /// Registration extends the scheduleId chain with the new (key, timestamp=0) link. + /// @param upgradeId Human-readable upgrade name (1–32 bytes, e.g. "canyon"). + /// @param protocolVersion Owner-assigned protocol version number for this upgrade (must be non-zero). function registerUpgrade(string calldata upgradeId, uint256 protocolVersion) external onlyOwner { _registerUpgrade(upgradeId, protocolVersion); - _refreshScheduleId(); } /// @notice Sets the activation timestamp for one upgrade by upgradeId. Pass 0 to clear. - /// @dev The activation timestamp must be in the future and the upgrade must not have - /// already activated. Pass 0 to remove a scheduled activation. + /// @dev The activation timestamp must be at least MIN_NOTICE seconds in the future and the + /// upgrade must not have already activated. Pass 0 to remove a not-yet-activated scheduled + /// timestamp; reverts if the upgrade has already passed its activation time. + /// @param upgradeId The upgrade to schedule. + /// @param timestamp Future Unix timestamp for L2 activation (must be >= block.timestamp + MIN_NOTICE), or 0 to clear. function setTimestamp(string calldata upgradeId, uint64 timestamp) external onlyOwner { - _applyTimestamp(_registeredKey(upgradeId), timestamp); - _refreshScheduleId(); + bytes32 key = _registeredKey(upgradeId); + if (_applyTimestamp(key, timestamp)) { + _refreshScheduleId(key); + } } /// @notice Appoints, replaces, or clears (set to zero) the chainTeam role. Owner only. + /// @param newChainTeam New chainTeam address, or address(0) to revoke the role. function setChainTeam(address newChainTeam) external onlyOwner { emit ChainTeamUpdated(chainTeam, newChainTeam); chainTeam = newChainTeam; @@ -178,6 +163,8 @@ contract ProtocolVersions is Ownable, IProtocolVersions { /// delay an activation; it cannot pull one earlier, clear it, or schedule a new one — use /// the owner's `setTimestamp` for those. Because `current` is in the future and `newTimestamp` /// is later still, the new value is always in the future. + /// @param upgradeId The upgrade whose activation to delay. + /// @param newTimestamp New activation timestamp, must be strictly later than the current one. function delayTimestamp(string calldata upgradeId, uint64 newTimestamp) external onlyChainTeam { bytes32 key = _registeredKey(upgradeId); uint64 current = _timestamps[key]; @@ -193,65 +180,84 @@ contract ProtocolVersions is Ownable, IProtocolVersions { _timestamps[key] = newTimestamp; emit TimestampSet(key, newTimestamp); - _refreshScheduleId(); + _refreshScheduleId(key); } - function _applyTimestamp(bytes32 key, uint64 timestamp) internal { + /// @dev Validates and applies a timestamp update for a registered upgrade key. + /// Returns true if the timestamp changed (caller should then call _refreshScheduleId), + /// false if unchanged (no-op, scheduleId must not be extended). + function _applyTimestamp(bytes32 key, uint64 timestamp) internal returns (bool) { uint64 current = _timestamps[key]; // Cannot modify a timestamp that has already activated. if (current != 0 && uint64(block.timestamp) >= current) { revert ProtocolVersions_ActivationAlreadyPassed(key, current); } - // Non-zero timestamps must be in the future. - if (timestamp != 0 && uint64(block.timestamp) >= timestamp) { + // Non-zero timestamps must be at least MIN_NOTICE seconds in the future. + if (timestamp != 0 && timestamp < uint64(block.timestamp) + MIN_NOTICE) { revert ProtocolVersions_ActivationTimestampInPast(timestamp); } - if (current != timestamp) { - _timestamps[key] = timestamp; - emit TimestampSet(key, timestamp); - } + if (current == timestamp) return false; + _timestamps[key] = timestamp; + emit TimestampSet(key, timestamp); + return true; } + /// @dev Validates and appends a new upgrade to the registry, then extends the scheduleId chain. + /// `_protocolVersions[key] != 0` serves as the existence sentinel, so protocolVersion must + /// be non-zero. The new upgrade is always the last entry, so _refreshScheduleId is O(1). function _registerUpgrade(string calldata upgradeId, uint256 protocolVersion) internal { + if (protocolVersion == 0) revert ProtocolVersions_InvalidProtocolVersion(); bytes32 key = _keyFromUpgradeId(upgradeId); - if (_upgradeIndex[key] != 0) revert ProtocolVersions_UpgradeAlreadyRegistered(key); + if (_protocolVersions[key] != 0) revert ProtocolVersions_UpgradeAlreadyRegistered(key); uint256 index = _upgradeKeys.length; _upgradeKeys.push(key); - _upgradeIndex[key] = index + 1; _protocolVersions[key] = protocolVersion; emit UpgradeRegistered(key, index, upgradeId, protocolVersion); + _refreshScheduleId(key); } - function _refreshScheduleId() internal { - bytes32 oldScheduleId = _scheduleId; - bytes32 newScheduleId = _computeScheduleId(); - if (newScheduleId != oldScheduleId) { - _scheduleId = newScheduleId; - lastUpdatedAtBlock = block.number; - emit ScheduleIdUpdated(oldScheduleId, newScheduleId, block.number); + /// @dev Recomputes the per-upgrade cumulative hash chain starting from `changedKey` and bubbles + /// the result through all subsequent registered upgrades. Cost is O(n-j) where j is the + /// 0-based registration index of `changedKey`. + function _refreshScheduleId(bytes32 changedKey) internal { + uint256 n = _upgradeKeys.length; + bytes32 seed = keccak256(abi.encode(l2ChainId, address(this))); + + // Locate changedKey and initialise prev to the hash of the preceding link (or seed). + uint256 startIndex; + for (startIndex = 0; startIndex < n; startIndex++) { + if (_upgradeKeys[startIndex] == changedKey) break; } - } + bytes32 prev = startIndex == 0 ? seed : _upgradeScheduleId[_upgradeKeys[startIndex - 1]]; - function _computeScheduleId() internal view returns (bytes32) { - uint256 count = _upgradeKeys.length; - uint64[] memory timestamps = new uint64[](count); - for (uint256 i = 0; i < count; i++) { - timestamps[i] = _timestamps[_upgradeKeys[i]]; + // Recompute from startIndex onward, storing each link. + for (uint256 i = startIndex; i < n; i++) { + bytes32 k = _upgradeKeys[i]; + prev = keccak256(abi.encode(prev, k, _timestamps[k])); + _upgradeScheduleId[k] = prev; + } + + bytes32 oldScheduleId = _scheduleId; + if (prev != oldScheduleId) { + _scheduleId = prev; + lastUpdatedAtBlock = block.number; + emit ScheduleIdUpdated(oldScheduleId, prev, block.number); } - return keccak256(abi.encode(l2ChainId, address(this), _upgradeKeys, timestamps)); } + /// @dev Resolves an upgradeId string to its storage key, reverting if not registered. function _registeredKey(string calldata upgradeId) internal view returns (bytes32 key) { key = _keyFromUpgradeId(upgradeId); - if (_upgradeIndex[key] == 0) revert ProtocolVersions_UnknownUpgradeName(upgradeId); + if (_protocolVersions[key] == 0) revert ProtocolVersions_UnknownUpgradeName(upgradeId); } + /// @dev Packs a UTF-8 upgradeId string (1–32 bytes) into a bytes32 key. Shorter strings are + /// zero-padded on the right; strings differing only by trailing null bytes map to the same + /// key. Callers are expected to use printable ASCII names only. function _keyFromUpgradeId(string calldata upgradeId) internal pure returns (bytes32 key) { bytes memory raw = bytes(upgradeId); if (raw.length == 0 || raw.length > 32) revert ProtocolVersions_InvalidUpgradeId(); - assembly { - key := mload(add(raw, 32)) - } + key = bytes32(raw); } } diff --git a/test/L1/ProtocolVersions.t.sol b/test/L1/ProtocolVersions.t.sol index 828301374..aaf00dd9f 100644 --- a/test/L1/ProtocolVersions.t.sol +++ b/test/L1/ProtocolVersions.t.sol @@ -1,42 +1,204 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; +// Testing import { Test } from "lib/forge-std/src/Test.sol"; +// Contracts import { ProtocolVersions } from "src/L1/ProtocolVersions.sol"; -contract ProtocolVersions_Test is Test { - /// @dev Mirror of ProtocolVersions.UpgradeRegistered for use with vm.expectEmit. +// Interfaces +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; + +/// @title ProtocolVersions_TestInit +/// @notice Reusable test initialization for ProtocolVersions tests. +abstract contract ProtocolVersions_TestInit is Test { event UpgradeRegistered(bytes32 indexed key, uint256 indexed index, string upgradeId, uint256 protocolVersion); + event ChainTeamUpdated(address indexed previousChainTeam, address indexed newChainTeam); + event TimestampSet(bytes32 indexed key, uint256 timestamp); + event ScheduleIdUpdated(bytes32 indexed oldScheduleId, bytes32 indexed newScheduleId, uint256 indexed blockNumber); - address internal owner = makeAddr("owner"); - address internal nonOwner = makeAddr("non-owner"); - address internal chainTeam = makeAddr("chain-team"); + address internal _owner = makeAddr("owner"); + address internal _nonOwner = makeAddr("non-owner"); + address internal _chainTeam = makeAddr("chain-team"); ProtocolVersions internal protocolVersions; - function setUp() public { - protocolVersions = new ProtocolVersions(owner, 8453); + function setUp() public virtual { + protocolVersions = new ProtocolVersions(_owner, 8453); } - /// @dev Registers `canyon` and schedules it for `block.timestamp + delay`, returning that timestamp. - function _scheduleCanyon(uint64 delay) internal returns (uint64 ts) { - vm.prank(owner); + /// @dev Registers `canyon` and schedules it for block.timestamp + MIN_NOTICE + delay. + function _scheduleCanyon(uint64 _delay) internal returns (uint64 ts_) { + vm.prank(_owner); protocolVersions.registerUpgrade("canyon", 1); - ts = uint64(block.timestamp + delay); - vm.prank(owner); - protocolVersions.setTimestamp("canyon", ts); + ts_ = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + _delay; + vm.prank(_owner); + protocolVersions.setTimestamp("canyon", ts_); } +} - function testSetSignalUpdatesTimestampAndScheduleId() public { - bytes32 initialScheduleId = protocolVersions.scheduleId(); +/// @title ProtocolVersions_Constructor_Test +/// @notice Test contract for the ProtocolVersions constructor. +contract ProtocolVersions_Constructor_Test is ProtocolVersions_TestInit { + /// @notice Tests that the constructor sets the correct initial state. + function test_constructor_setsInitialState_succeeds() external view { + assertEq(protocolVersions.owner(), _owner); + assertEq(protocolVersions.l2ChainId(), 8453); + assertEq(protocolVersions.lastUpdatedAtBlock(), block.number); + assertNotEq(protocolVersions.scheduleId(), bytes32(0)); + } + + /// @notice Tests that the constructor reverts when the owner is address(0). + function test_constructor_zeroOwner_reverts() external { + vm.expectRevert(IProtocolVersions.ProtocolVersions_ZeroOwner.selector); + new ProtocolVersions(address(0), 8453); + } + + /// @notice Tests that the constructor reverts when the chain ID is zero. + function test_constructor_zeroChainId_reverts() external { + vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidL2ChainId.selector); + new ProtocolVersions(_owner, 0); + } +} + +/// @title ProtocolVersions_Version_Test +/// @notice Test contract for the `version` function. +contract ProtocolVersions_Version_Test is ProtocolVersions_TestInit { + /// @notice Tests that the `version` function returns the expected value. + function test_version_succeeds() external view { + assertEq(protocolVersions.version(), "1.0.0"); + } +} + +/// @title ProtocolVersions_RegisterUpgrade_Test +/// @notice Test contract for the `registerUpgrade` function. +contract ProtocolVersions_RegisterUpgrade_Test is ProtocolVersions_TestInit { + /// @notice Tests that registering an upgrade extends the scheduleId chain and updates lastUpdatedAtBlock. + function test_registerUpgrade_changesScheduleId_succeeds() external { + bytes32 idBefore = protocolVersions.scheduleId(); + + vm.roll(block.number + 1); + vm.prank(_owner); + protocolVersions.registerUpgrade("canyon", 1); + + assertNotEq(protocolVersions.scheduleId(), idBefore); + assertEq(protocolVersions.lastUpdatedAtBlock(), block.number); + } + + /// @notice Tests that registering an upgrade adds it to the ordered registry. + function test_registerUpgrade_extendsRegistry_succeeds() external { + assertEq(protocolVersions.upgradeCount(), 0); - vm.prank(owner); + vm.prank(_owner); + protocolVersions.registerUpgrade("antares", 1); + + assertEq(protocolVersions.upgradeCount(), 1); + assertEq(protocolVersions.upgradeIdAt(0), bytes32("antares")); + } + + /// @notice Tests that registering multiple upgrades preserves registration order. + function test_registerUpgrade_preservesOrder_succeeds() external { + vm.prank(_owner); + protocolVersions.registerUpgrade("canyon", 1); + vm.prank(_owner); + protocolVersions.registerUpgrade("ecotone", 2); + vm.prank(_owner); + protocolVersions.registerUpgrade("fjord", 3); + + bytes32[] memory ids = protocolVersions.upgradeIds(); + assertEq(ids.length, 3); + assertEq(ids[0], bytes32("canyon")); + assertEq(ids[1], bytes32("ecotone")); + assertEq(ids[2], bytes32("fjord")); + } + + /// @notice Tests that `registerUpgrade` emits the `UpgradeRegistered` event with correct fields. + function test_registerUpgrade_emitsEvent_succeeds() external { + vm.expectEmit(true, true, true, true, address(protocolVersions)); + emit UpgradeRegistered(bytes32("canyon"), 0, "canyon", 1); + vm.prank(_owner); + protocolVersions.registerUpgrade("canyon", 1); + + vm.expectEmit(true, true, true, true, address(protocolVersions)); + emit UpgradeRegistered(bytes32("ecotone"), 1, "ecotone", 2); + vm.prank(_owner); + protocolVersions.registerUpgrade("ecotone", 2); + } + + /// @notice Tests that registering an upgrade stores the owner-assigned protocol version. + function test_registerUpgrade_storesProtocolVersion_succeeds() external { + vm.prank(_owner); + protocolVersions.registerUpgrade("canyon", 5); + vm.prank(_owner); + protocolVersions.registerUpgrade("ecotone", 9); + + assertEq(protocolVersions.getProtocolVersion("canyon"), 5); + assertEq(protocolVersions.getProtocolVersion("ecotone"), 9); + } + + /// @notice Tests that registering a 32-byte name (the maximum) succeeds. + function test_registerUpgrade_exactly32ByteName_succeeds() external { + string memory name32 = "exactly-32-bytes-upgrade-name!!!"; + vm.prank(_owner); + protocolVersions.registerUpgrade(name32, 7); + assertEq(protocolVersions.upgradeCount(), 1); + assertEq(protocolVersions.getProtocolVersion(name32), 7); + } + + /// @notice Tests that registering the same upgrade twice reverts. + function test_registerUpgrade_duplicate_reverts() external { + vm.prank(_owner); + protocolVersions.registerUpgrade("canyon", 1); + + vm.expectRevert( + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_UpgradeAlreadyRegistered.selector, bytes32("canyon")) + ); + vm.prank(_owner); + protocolVersions.registerUpgrade("canyon", 1); + } + + /// @notice Tests that registering an upgrade with protocolVersion zero reverts. + function test_registerUpgrade_zeroProtocolVersion_reverts() external { + vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidProtocolVersion.selector); + vm.prank(_owner); + protocolVersions.registerUpgrade("canyon", 0); + } + + /// @notice Tests that registering an upgrade with an empty name reverts. + function test_registerUpgrade_emptyName_reverts() external { + vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidUpgradeId.selector); + vm.prank(_owner); + protocolVersions.registerUpgrade("", 1); + } + + /// @notice Tests that registering an upgrade with a name longer than 32 bytes reverts. + function test_registerUpgrade_nameTooLong_reverts() external { + vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidUpgradeId.selector); + vm.prank(_owner); + protocolVersions.registerUpgrade("this-upgrade-name-is-definitely-too-long", 1); + } + + /// @notice Tests that only the owner can call `registerUpgrade`. + function test_registerUpgrade_callerNotOwner_reverts() external { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(_nonOwner); + protocolVersions.registerUpgrade("antares", 1); + } +} + +/// @title ProtocolVersions_SetTimestamp_Test +/// @notice Test contract for the `setTimestamp` function. +contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { + /// @notice Tests that setting a timestamp updates the stored value and extends the scheduleId. + function test_setTimestamp_updatesTimestampAndScheduleId_succeeds() external { + bytes32 initialScheduleId = protocolVersions.scheduleId(); + vm.prank(_owner); protocolVersions.registerUpgrade("canyon", 1); vm.roll(block.number + 1); vm.warp(block.timestamp + 1); - uint64 ts = uint64(block.timestamp + 100); - vm.prank(owner); + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + vm.prank(_owner); protocolVersions.setTimestamp("canyon", ts); assertEq(protocolVersions.getTimestamp("canyon"), ts); @@ -44,21 +206,22 @@ contract ProtocolVersions_Test is Test { assertNotEq(protocolVersions.scheduleId(), initialScheduleId); } - function testSameTimestampDoesNotChangeScheduleId() public { - vm.prank(owner); + /// @notice Tests that calling `setTimestamp` with the same value is a no-op for scheduleId. + function test_setTimestamp_sameTimestamp_noScheduleIdChange_succeeds() external { + vm.prank(_owner); protocolVersions.registerUpgrade("canyon", 1); vm.roll(block.number + 1); vm.warp(block.timestamp + 1); - uint64 ts = uint64(block.timestamp + 100); - vm.prank(owner); + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + vm.prank(_owner); protocolVersions.setTimestamp("canyon", ts); bytes32 scheduleIdAfterSet = protocolVersions.scheduleId(); uint256 updateBlockAfterSet = protocolVersions.lastUpdatedAtBlock(); vm.roll(block.number + 1); - vm.prank(owner); + vm.prank(_owner); protocolVersions.setTimestamp("canyon", ts); assertEq(protocolVersions.getTimestamp("canyon"), ts); @@ -66,194 +229,156 @@ contract ProtocolVersions_Test is Test { assertEq(protocolVersions.lastUpdatedAtBlock(), updateBlockAfterSet); } - function testClearingSignalResetsTimestamp() public { - vm.prank(owner); + /// @notice Tests that passing 0 clears a scheduled timestamp, changes the scheduleId, and + /// restores it to the value it held immediately after registration (ts=0 link). + function test_setTimestamp_clearTimestamp_succeeds() external { + vm.prank(_owner); protocolVersions.registerUpgrade("canyon", 1); bytes32 scheduleIdAfterRegister = protocolVersions.scheduleId(); vm.roll(block.number + 1); vm.warp(block.timestamp + 1); - vm.prank(owner); - protocolVersions.setTimestamp("canyon", uint64(block.timestamp + 100)); + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + vm.prank(_owner); + protocolVersions.setTimestamp("canyon", ts); + + bytes32 scheduleIdAfterSet = protocolVersions.scheduleId(); + assertNotEq(scheduleIdAfterSet, scheduleIdAfterRegister); vm.roll(block.number + 1); - vm.prank(owner); + vm.prank(_owner); protocolVersions.setTimestamp("canyon", 0); assertEq(protocolVersions.getTimestamp("canyon"), 0); assertEq(protocolVersions.scheduleId(), scheduleIdAfterRegister); } - function testOnlyOwnerCanUpdateSignals() public { - vm.prank(owner); + /// @notice Tests that `setTimestamp` emits a `TimestampSet` event. + function test_setTimestamp_emitsEvent_succeeds() external { + vm.prank(_owner); + protocolVersions.registerUpgrade("canyon", 1); + + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + vm.expectEmit(true, false, false, true, address(protocolVersions)); + emit TimestampSet(bytes32("canyon"), ts); + vm.prank(_owner); + protocolVersions.setTimestamp("canyon", ts); + } + + /// @notice Tests that only the owner can call `setTimestamp`. + function test_setTimestamp_callerNotOwner_reverts() external { + vm.prank(_owner); protocolVersions.registerUpgrade("canyon", 1); + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; vm.expectRevert("Ownable: caller is not the owner"); - vm.prank(nonOwner); - protocolVersions.setTimestamp("canyon", uint64(block.timestamp + 100)); + vm.prank(_nonOwner); + protocolVersions.setTimestamp("canyon", ts); } - function testSetSignalRejectsTimestampInPast() public { - vm.prank(owner); + /// @notice Tests that `setTimestamp` reverts when the timestamp is in the past. + function test_setTimestamp_timestampInPast_reverts() external { + vm.prank(_owner); protocolVersions.registerUpgrade("canyon", 1); vm.warp(1000); vm.expectRevert( - abi.encodeWithSelector(ProtocolVersions.ProtocolVersions_ActivationTimestampInPast.selector, uint64(500)) + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_ActivationTimestampInPast.selector, uint64(500)) ); - vm.prank(owner); + vm.prank(_owner); protocolVersions.setTimestamp("canyon", 500); } - function testSetSignalRejectsChangeAfterActivation() public { - vm.prank(owner); + /// @notice Tests that `setTimestamp` reverts when the timestamp is within MIN_NOTICE of now. + function test_setTimestamp_insufficientNotice_reverts() external { + vm.prank(_owner); protocolVersions.registerUpgrade("canyon", 1); - vm.warp(100); - vm.prank(owner); - protocolVersions.setTimestamp("canyon", 200); - - vm.warp(300); - bytes32 canyonId = bytes32("canyon"); + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() - 1; vm.expectRevert( - abi.encodeWithSelector( - ProtocolVersions.ProtocolVersions_ActivationAlreadyPassed.selector, canyonId, uint64(200) - ) + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_ActivationTimestampInPast.selector, ts) ); - vm.prank(owner); - protocolVersions.setTimestamp("canyon", 400); - } - - function testGetTimestampRejectsUnknownUpgradeName() public { - vm.expectRevert( - abi.encodeWithSelector(ProtocolVersions.ProtocolVersions_UnknownUpgradeName.selector, "unknown") - ); - protocolVersions.getTimestamp("unknown"); - } - - function testRegistryStartsEmpty() public view { - assertEq(protocolVersions.upgradeCount(), 0); - assertEq(protocolVersions.upgradeIds().length, 0); - } - - function testRegisterUpgradeExtendsScheduleAndChangesScheduleId() public { - bytes32 initialScheduleId = protocolVersions.scheduleId(); - - assertEq(protocolVersions.upgradeCount(), 0); - - vm.roll(block.number + 1); - vm.prank(owner); - protocolVersions.registerUpgrade("antares", 1); - - assertEq(protocolVersions.upgradeCount(), 1); - assertEq(protocolVersions.upgradeIdAt(0), bytes32("antares")); - assertNotEq(protocolVersions.scheduleId(), initialScheduleId); - assertEq(protocolVersions.lastUpdatedAtBlock(), block.number); - - vm.warp(block.timestamp + 1); - vm.roll(block.number + 1); - vm.prank(owner); - protocolVersions.setTimestamp("antares", uint64(block.timestamp + 100)); - assertEq(protocolVersions.getTimestamp("antares"), block.timestamp + 100); + vm.prank(_owner); + protocolVersions.setTimestamp("canyon", ts); } - function testUpgradeRegisteredEventEmitsKeyIndexNameAndVersion() public { - // First registration: index 0, with its key, id string, and owner-chosen version. - vm.expectEmit(true, true, true, true, address(protocolVersions)); - emit UpgradeRegistered(bytes32("canyon"), 0, "canyon", 1); - vm.prank(owner); + /// @notice Tests that `setTimestamp` reverts when the upgrade has already activated. + function test_setTimestamp_afterActivation_reverts() external { + vm.prank(_owner); protocolVersions.registerUpgrade("canyon", 1); - // Second registration: index increments to 1. - vm.expectEmit(true, true, true, true, address(protocolVersions)); - emit UpgradeRegistered(bytes32("ecotone"), 1, "ecotone", 2); - vm.prank(owner); - protocolVersions.registerUpgrade("ecotone", 2); - } - - function testRegisterUpgradeRejectsDuplicate() public { - vm.prank(owner); - protocolVersions.registerUpgrade("canyon", 1); + vm.warp(100); + uint64 activationTs = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + vm.prank(_owner); + protocolVersions.setTimestamp("canyon", activationTs); + vm.warp(activationTs + 1); + uint64 laterTs = activationTs + protocolVersions.MIN_NOTICE() + 100; + bytes32 canyonKey = bytes32("canyon"); vm.expectRevert( abi.encodeWithSelector( - ProtocolVersions.ProtocolVersions_UpgradeAlreadyRegistered.selector, bytes32("canyon") + IProtocolVersions.ProtocolVersions_ActivationAlreadyPassed.selector, canyonKey, activationTs ) ); - vm.prank(owner); - protocolVersions.registerUpgrade("canyon", 1); - } - - function testRegisterUpgradeRejectsEmptyName() public { - vm.expectRevert(ProtocolVersions.ProtocolVersions_InvalidUpgradeId.selector); - vm.prank(owner); - protocolVersions.registerUpgrade("", 1); + vm.prank(_owner); + protocolVersions.setTimestamp("canyon", laterTs); } - function testRegisterUpgradeRejectsNameLongerThan32Bytes() public { - vm.expectRevert(ProtocolVersions.ProtocolVersions_InvalidUpgradeId.selector); - vm.prank(owner); - protocolVersions.registerUpgrade("this-upgrade-name-is-definitely-too-long", 1); - } - - function testOnlyOwnerCanRegisterUpgrade() public { - vm.expectRevert("Ownable: caller is not the owner"); - vm.prank(nonOwner); - protocolVersions.registerUpgrade("antares", 1); - } - - function testSetSignalRejectsUnregisteredUpgrade() public { + /// @notice Tests that `setTimestamp` reverts for an unregistered upgrade. + function test_setTimestamp_unregisteredUpgrade_reverts() external { + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; vm.expectRevert( - abi.encodeWithSelector(ProtocolVersions.ProtocolVersions_UnknownUpgradeName.selector, "antares") + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_UnknownUpgradeName.selector, "antares") ); - vm.prank(owner); - protocolVersions.setTimestamp("antares", uint64(block.timestamp + 100)); + vm.prank(_owner); + protocolVersions.setTimestamp("antares", ts); } - function testRegisterUpgradeStoresOwnerChosenProtocolVersion() public { - // Versions are owner-chosen at registration, not derived from registration order. - vm.prank(owner); - protocolVersions.registerUpgrade("canyon", 5); - vm.prank(owner); - protocolVersions.registerUpgrade("ecotone", 9); - - assertEq(protocolVersions.getProtocolVersion("canyon"), 5); - assertEq(protocolVersions.getProtocolVersion("ecotone"), 9); - } + /// @notice Tests that scheduleId is reproducible from (chainId, address, ordered keys, timestamps). + function test_setTimestamp_scheduleIdReproducible_succeeds() external { + vm.prank(_owner); + protocolVersions.registerUpgrade("canyon", 1); + vm.prank(_owner); + protocolVersions.registerUpgrade("ecotone", 2); - function testGetProtocolVersionRejectsUnknownUpgradeName() public { - vm.expectRevert( - abi.encodeWithSelector(ProtocolVersions.ProtocolVersions_UnknownUpgradeName.selector, "unknown") - ); - protocolVersions.getProtocolVersion("unknown"); - } + uint64 ts1 = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + uint64 ts2 = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 200; + vm.prank(_owner); + protocolVersions.setTimestamp("canyon", ts1); + vm.prank(_owner); + protocolVersions.setTimestamp("ecotone", ts2); - function testChainTeamStartsUnset() public view { - assertEq(protocolVersions.chainTeam(), address(0)); - } + // Reproduce the chain from scratch. + bytes32 seed = keccak256(abi.encode(protocolVersions.l2ChainId(), address(protocolVersions))); + bytes32 link0 = keccak256(abi.encode(seed, bytes32("canyon"), ts1)); + bytes32 link1 = keccak256(abi.encode(link0, bytes32("ecotone"), ts2)); - function testOwnerCanSetChainTeam() public { - vm.prank(owner); - protocolVersions.setChainTeam(chainTeam); - assertEq(protocolVersions.chainTeam(), chainTeam); + assertEq(protocolVersions.scheduleId(), link1); } - function testOnlyOwnerCanSetChainTeam() public { - vm.expectRevert("Ownable: caller is not the owner"); - vm.prank(nonOwner); - protocolVersions.setChainTeam(chainTeam); + /// @notice Tests that `getTimestamp` reverts for an unknown upgrade name. + function test_getTimestamp_unknownUpgrade_reverts() external { + vm.expectRevert( + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_UnknownUpgradeName.selector, "unknown") + ); + protocolVersions.getTimestamp("unknown"); } +} - function testDelaySignalPushesTimestampLater() public { +/// @title ProtocolVersions_DelayTimestamp_Test +/// @notice Test contract for the `delayTimestamp` function. +contract ProtocolVersions_DelayTimestamp_Test is ProtocolVersions_TestInit { + /// @notice Tests that `delayTimestamp` pushes the activation timestamp later and updates scheduleId. + function test_delayTimestamp_pushesTimestampLater_succeeds() external { uint64 ts = _scheduleCanyon(100); - vm.prank(owner); - protocolVersions.setChainTeam(chainTeam); + vm.prank(_owner); + protocolVersions.setChainTeam(_chainTeam); bytes32 scheduleIdBefore = protocolVersions.scheduleId(); vm.roll(block.number + 1); uint64 later = ts + 50; - vm.prank(chainTeam); + vm.prank(_chainTeam); protocolVersions.delayTimestamp("canyon", later); assertEq(protocolVersions.getTimestamp("canyon"), later); @@ -261,83 +386,148 @@ contract ProtocolVersions_Test is Test { assertEq(protocolVersions.lastUpdatedAtBlock(), block.number); } - function testOnlyChainTeamCanDelaySignal() public { + /// @notice Tests that only the chainTeam can call `delayTimestamp`. + function test_delayTimestamp_callerNotChainTeam_reverts() external { uint64 ts = _scheduleCanyon(100); - vm.prank(owner); - protocolVersions.setChainTeam(chainTeam); + vm.prank(_owner); + protocolVersions.setChainTeam(_chainTeam); - // Not even the owner may call delayTimestamp — it is chainTeam-only. - vm.expectRevert(ProtocolVersions.ProtocolVersions_NotChainTeam.selector); - vm.prank(owner); + vm.expectRevert(IProtocolVersions.ProtocolVersions_NotChainTeam.selector); + vm.prank(_owner); protocolVersions.delayTimestamp("canyon", ts + 50); - vm.expectRevert(ProtocolVersions.ProtocolVersions_NotChainTeam.selector); - vm.prank(nonOwner); + vm.expectRevert(IProtocolVersions.ProtocolVersions_NotChainTeam.selector); + vm.prank(_nonOwner); protocolVersions.delayTimestamp("canyon", ts + 50); } - function testDelaySignalRejectsEarlierTimestamp() public { + /// @notice Tests that `delayTimestamp` reverts when the new timestamp is earlier than current. + function test_delayTimestamp_earlierTimestamp_reverts() external { uint64 ts = _scheduleCanyon(100); - vm.prank(owner); - protocolVersions.setChainTeam(chainTeam); + vm.prank(_owner); + protocolVersions.setChainTeam(_chainTeam); vm.expectRevert( - abi.encodeWithSelector( - ProtocolVersions.ProtocolVersions_DelayMustBeLater.selector, ts, ts - 10 - ) + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_DelayMustBeLater.selector, ts, ts - 10) ); - vm.prank(chainTeam); + vm.prank(_chainTeam); protocolVersions.delayTimestamp("canyon", ts - 10); } - function testDelaySignalRejectsEqualTimestamp() public { + /// @notice Tests that `delayTimestamp` reverts when the new timestamp equals the current one. + function test_delayTimestamp_equalTimestamp_reverts() external { uint64 ts = _scheduleCanyon(100); - vm.prank(owner); - protocolVersions.setChainTeam(chainTeam); + vm.prank(_owner); + protocolVersions.setChainTeam(_chainTeam); - vm.expectRevert( - abi.encodeWithSelector(ProtocolVersions.ProtocolVersions_DelayMustBeLater.selector, ts, ts) - ); - vm.prank(chainTeam); + vm.expectRevert(abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_DelayMustBeLater.selector, ts, ts)); + vm.prank(_chainTeam); protocolVersions.delayTimestamp("canyon", ts); } - function testDelaySignalRejectsUnscheduledUpgrade() public { - vm.prank(owner); + /// @notice Tests that `delayTimestamp` reverts when the upgrade has no scheduled timestamp. + function test_delayTimestamp_notScheduled_reverts() external { + vm.prank(_owner); protocolVersions.registerUpgrade("canyon", 1); - vm.prank(owner); - protocolVersions.setChainTeam(chainTeam); + vm.prank(_owner); + protocolVersions.setChainTeam(_chainTeam); + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; vm.expectRevert( - abi.encodeWithSelector(ProtocolVersions.ProtocolVersions_NotScheduled.selector, bytes32("canyon")) + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_NotScheduled.selector, bytes32("canyon")) ); - vm.prank(chainTeam); - protocolVersions.delayTimestamp("canyon", uint64(block.timestamp + 100)); + vm.prank(_chainTeam); + protocolVersions.delayTimestamp("canyon", ts); } - function testDelaySignalRejectsAfterActivation() public { + /// @notice Tests that `delayTimestamp` reverts when the upgrade has already activated. + function test_delayTimestamp_afterActivation_reverts() external { uint64 ts = _scheduleCanyon(100); - vm.prank(owner); - protocolVersions.setChainTeam(chainTeam); + vm.prank(_owner); + protocolVersions.setChainTeam(_chainTeam); vm.warp(ts + 1); vm.expectRevert( abi.encodeWithSelector( - ProtocolVersions.ProtocolVersions_ActivationAlreadyPassed.selector, bytes32("canyon"), ts + IProtocolVersions.ProtocolVersions_ActivationAlreadyPassed.selector, bytes32("canyon"), ts ) ); - vm.prank(chainTeam); + vm.prank(_chainTeam); protocolVersions.delayTimestamp("canyon", ts + 100); } - function testDelaySignalRejectsUnregisteredUpgrade() public { - vm.prank(owner); - protocolVersions.setChainTeam(chainTeam); + /// @notice Tests that `delayTimestamp` reverts for an unregistered upgrade. + function test_delayTimestamp_unregisteredUpgrade_reverts() external { + vm.prank(_owner); + protocolVersions.setChainTeam(_chainTeam); + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; vm.expectRevert( - abi.encodeWithSelector(ProtocolVersions.ProtocolVersions_UnknownUpgradeName.selector, "antares") + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_UnknownUpgradeName.selector, "antares") ); - vm.prank(chainTeam); - protocolVersions.delayTimestamp("antares", uint64(block.timestamp + 100)); + vm.prank(_chainTeam); + protocolVersions.delayTimestamp("antares", ts); + } +} + +/// @title ProtocolVersions_ChainTeam_Test +/// @notice Test contract for the `setChainTeam` function and chainTeam role. +contract ProtocolVersions_ChainTeam_Test is ProtocolVersions_TestInit { + /// @notice Tests that `chainTeam` starts as address(0). + function test_chainTeam_startsUnset_succeeds() external view { + assertEq(protocolVersions.chainTeam(), address(0)); + } + + /// @notice Tests that the owner can appoint a chainTeam address. + function test_setChainTeam_setsAddress_succeeds() external { + vm.prank(_owner); + protocolVersions.setChainTeam(_chainTeam); + assertEq(protocolVersions.chainTeam(), _chainTeam); + } + + /// @notice Tests that only the owner can call `setChainTeam`. + function test_setChainTeam_callerNotOwner_reverts() external { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(_nonOwner); + protocolVersions.setChainTeam(_chainTeam); + } + + /// @notice Tests that `setChainTeam` emits a `ChainTeamUpdated` event. + function test_setChainTeam_emitsEvent_succeeds() external { + vm.expectEmit(true, true, false, false, address(protocolVersions)); + emit ChainTeamUpdated(address(0), _chainTeam); + vm.prank(_owner); + protocolVersions.setChainTeam(_chainTeam); + } + + /// @notice Tests that the owner can clear the chainTeam role by setting it to address(0). + function test_setChainTeam_clear_succeeds() external { + vm.prank(_owner); + protocolVersions.setChainTeam(_chainTeam); + + vm.expectEmit(true, true, false, false, address(protocolVersions)); + emit ChainTeamUpdated(_chainTeam, address(0)); + vm.prank(_owner); + protocolVersions.setChainTeam(address(0)); + + assertEq(protocolVersions.chainTeam(), address(0)); + } +} + +/// @title ProtocolVersions_GetView_Test +/// @notice Test contract for view functions and the upgrade registry. +contract ProtocolVersions_GetView_Test is ProtocolVersions_TestInit { + /// @notice Tests that the registry starts empty. + function test_registry_startsEmpty_succeeds() external view { + assertEq(protocolVersions.upgradeCount(), 0); + assertEq(protocolVersions.upgradeIds().length, 0); + } + + /// @notice Tests that `getProtocolVersion` reverts for an unknown upgrade name. + function test_getProtocolVersion_unknownUpgrade_reverts() external { + vm.expectRevert( + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_UnknownUpgradeName.selector, "unknown") + ); + protocolVersions.getProtocolVersion("unknown"); } } From b8e9c391dee6e6436226251f8f71c661e09b9327 Mon Sep 17 00:00:00 2001 From: PelleKrab Date: Thu, 25 Jun 2026 15:53:08 -0700 Subject: [PATCH 03/20] feat(L1): add getSchedule view to ProtocolVersions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Returns the full ordered upgrade schedule in a single eth_call — name, activation timestamp, protocol version, and cumulative scheduleId per entry. Name is recovered from the bytes32 key at read time via _nameFromKey, avoiding a separate storage mapping. Generated with Claude Code Co-Authored-By: Claude --- interfaces/L1/IProtocolVersions.sol | 9 ++++++++ src/L1/ProtocolVersions.sol | 28 +++++++++++++++++++++++++ test/L1/ProtocolVersions.t.sol | 32 +++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/interfaces/L1/IProtocolVersions.sol b/interfaces/L1/IProtocolVersions.sol index 5bc61c257..7a2378300 100644 --- a/interfaces/L1/IProtocolVersions.sol +++ b/interfaces/L1/IProtocolVersions.sol @@ -6,6 +6,13 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; /// @title IProtocolVersions /// @notice Interface for the ProtocolVersions upgrade schedule contract. interface IProtocolVersions is ISemver { + struct Upgrade { + string name; + uint64 timestamp; + uint256 protocolVersion; + bytes32 scheduleId; + } + error ProtocolVersions_ZeroOwner(); error ProtocolVersions_InvalidL2ChainId(); error ProtocolVersions_UnknownUpgradeName(string upgradeId); @@ -34,6 +41,8 @@ interface IProtocolVersions is ISemver { function getTimestamp(string calldata upgradeId) external view returns (uint256); function getProtocolVersion(string calldata upgradeId) external view returns (uint256); + function getSchedule() external view returns (Upgrade[] memory); + function registerUpgrade(string calldata upgradeId, uint256 protocolVersion) external; function setTimestamp(string calldata upgradeId, uint64 timestamp) external; function setChainTeam(address newChainTeam) external; diff --git a/src/L1/ProtocolVersions.sol b/src/L1/ProtocolVersions.sol index 9704401fc..c7f39e08f 100644 --- a/src/L1/ProtocolVersions.sol +++ b/src/L1/ProtocolVersions.sol @@ -127,6 +127,24 @@ contract ProtocolVersions is Ownable, IProtocolVersions { return _protocolVersions[_registeredKey(upgradeId)]; } + /// @notice Returns the full ordered schedule: every registered upgrade with its current + /// activation timestamp, protocol version, and cumulative schedule hash. + /// @dev Calling via eth_call is gas-free; no transaction is submitted. + /// @return schedule_ Ordered array of Upgrade structs, one per registered upgrade. + function getSchedule() external view returns (Upgrade[] memory schedule_) { + uint256 n = _upgradeKeys.length; + schedule_ = new Upgrade[](n); + for (uint256 i = 0; i < n; i++) { + bytes32 key = _upgradeKeys[i]; + schedule_[i] = Upgrade({ + name: _nameFromKey(key), + timestamp: _timestamps[key], + protocolVersion: _protocolVersions[key], + scheduleId: _upgradeScheduleId[key] + }); + } + } + /// @notice Registers a new upgrade by upgradeId with its protocol version. Owner only. /// @dev The upgradeId is packed into a bytes32 key and `protocolVersion` is recorded for it. /// Registration extends the scheduleId chain with the new (key, timestamp=0) link. @@ -260,4 +278,14 @@ contract ProtocolVersions is Ownable, IProtocolVersions { if (raw.length == 0 || raw.length > 32) revert ProtocolVersions_InvalidUpgradeId(); key = bytes32(raw); } + + /// @dev Recovers the original upgradeId string from its bytes32 key by stripping trailing + /// zero bytes. Correct for printable ASCII names, which contain no embedded null bytes. + function _nameFromKey(bytes32 key) internal pure returns (string memory) { + uint256 len = 32; + while (len > 0 && key[len - 1] == 0) len--; + bytes memory b = new bytes(len); + for (uint256 i = 0; i < len; i++) b[i] = key[i]; + return string(b); + } } diff --git a/test/L1/ProtocolVersions.t.sol b/test/L1/ProtocolVersions.t.sol index aaf00dd9f..81e6cf1ad 100644 --- a/test/L1/ProtocolVersions.t.sol +++ b/test/L1/ProtocolVersions.t.sol @@ -530,4 +530,36 @@ contract ProtocolVersions_GetView_Test is ProtocolVersions_TestInit { ); protocolVersions.getProtocolVersion("unknown"); } + + /// @notice Tests that `getSchedule` returns an empty array when no upgrades are registered. + function test_getSchedule_empty_succeeds() external view { + assertEq(protocolVersions.getSchedule().length, 0); + } + + /// @notice Tests that `getSchedule` returns all upgrades in registration order with correct fields. + function test_getSchedule_returnsFullSchedule_succeeds() external { + vm.prank(_owner); + protocolVersions.registerUpgrade("canyon", 1); + vm.prank(_owner); + protocolVersions.registerUpgrade("ecotone", 2); + + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + vm.prank(_owner); + protocolVersions.setTimestamp("canyon", ts); + + IProtocolVersions.Upgrade[] memory s = protocolVersions.getSchedule(); + + assertEq(s.length, 2); + + assertEq(s[0].name, "canyon"); + assertEq(s[0].timestamp, ts); + assertEq(s[0].protocolVersion, 1); + + assertEq(s[1].name, "ecotone"); + assertEq(s[1].timestamp, 0); + assertEq(s[1].protocolVersion, 2); + + // The last entry's scheduleId is the contract's current scheduleId. + assertEq(s[1].scheduleId, protocolVersions.scheduleId()); + } } From e30e60c8fb3a5522c14fd102c45463aedbf2a33f Mon Sep 17 00:00:00 2001 From: PelleKrab Date: Thu, 25 Jun 2026 16:49:41 -0700 Subject: [PATCH 04/20] chore(L1): apply forge fmt and update semver-lock for ProtocolVersions Generated with Claude Code Co-Authored-By: Claude --- interfaces/L1/IProtocolVersions.sol | 4 ++-- snapshots/semver-lock.json | 4 ++++ src/L1/ProtocolVersions.sol | 7 +++++-- test/L1/ProtocolVersions.t.sol | 4 +++- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/interfaces/L1/IProtocolVersions.sol b/interfaces/L1/IProtocolVersions.sol index 7a2378300..75c5b3947 100644 --- a/interfaces/L1/IProtocolVersions.sol +++ b/interfaces/L1/IProtocolVersions.sol @@ -7,8 +7,8 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; /// @notice Interface for the ProtocolVersions upgrade schedule contract. interface IProtocolVersions is ISemver { struct Upgrade { - string name; - uint64 timestamp; + string name; + uint64 timestamp; uint256 protocolVersion; bytes32 scheduleId; } diff --git a/snapshots/semver-lock.json b/snapshots/semver-lock.json index a0b3f4d4e..9b29703dc 100644 --- a/snapshots/semver-lock.json +++ b/snapshots/semver-lock.json @@ -19,6 +19,10 @@ "initCodeHash": "0x2c01bc6c0a55a1a27263224e05c1b28703ff85c61075bae7ab384b3043820ed2", "sourceCodeHash": "0xa1281f5da03fa0431eabad33da23f51a8d835b0dd04554603fd59e11efe9fa51" }, + "src/L1/ProtocolVersions.sol:ProtocolVersions": { + "initCodeHash": "0xf17ad1d8696f97a18ae68f07b0d1732327cce06a46e3a224655622fd9cd2a27d", + "sourceCodeHash": "0xb766b0467cc20a0815857239f1344ed82a8783616cb35eb68ffcc88600b2c184" + }, "src/L1/SuperchainConfig.sol:SuperchainConfig": { "initCodeHash": "0x9b1f3555b499709485d51d5d9665002c0eb1e5eb893be1fb978a30749e894858", "sourceCodeHash": "0x79f0c771fc5f6d222d89b05addedc08ce40a7a42423fbd66f7cbb6cde3c2e74f" diff --git a/src/L1/ProtocolVersions.sol b/src/L1/ProtocolVersions.sol index c7f39e08f..0b05db920 100644 --- a/src/L1/ProtocolVersions.sol +++ b/src/L1/ProtocolVersions.sol @@ -159,7 +159,8 @@ contract ProtocolVersions is Ownable, IProtocolVersions { /// upgrade must not have already activated. Pass 0 to remove a not-yet-activated scheduled /// timestamp; reverts if the upgrade has already passed its activation time. /// @param upgradeId The upgrade to schedule. - /// @param timestamp Future Unix timestamp for L2 activation (must be >= block.timestamp + MIN_NOTICE), or 0 to clear. + /// @param timestamp Future Unix timestamp for L2 activation (must be >= block.timestamp + MIN_NOTICE), or 0 to + /// clear. function setTimestamp(string calldata upgradeId, uint64 timestamp) external onlyOwner { bytes32 key = _registeredKey(upgradeId); if (_applyTimestamp(key, timestamp)) { @@ -285,7 +286,9 @@ contract ProtocolVersions is Ownable, IProtocolVersions { uint256 len = 32; while (len > 0 && key[len - 1] == 0) len--; bytes memory b = new bytes(len); - for (uint256 i = 0; i < len; i++) b[i] = key[i]; + for (uint256 i = 0; i < len; i++) { + b[i] = key[i]; + } return string(b); } } diff --git a/test/L1/ProtocolVersions.t.sol b/test/L1/ProtocolVersions.t.sol index 81e6cf1ad..8f5f8726c 100644 --- a/test/L1/ProtocolVersions.t.sol +++ b/test/L1/ProtocolVersions.t.sol @@ -151,7 +151,9 @@ contract ProtocolVersions_RegisterUpgrade_Test is ProtocolVersions_TestInit { protocolVersions.registerUpgrade("canyon", 1); vm.expectRevert( - abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_UpgradeAlreadyRegistered.selector, bytes32("canyon")) + abi.encodeWithSelector( + IProtocolVersions.ProtocolVersions_UpgradeAlreadyRegistered.selector, bytes32("canyon") + ) ); vm.prank(_owner); protocolVersions.registerUpgrade("canyon", 1); From 018ca7c9f08659bf0fa954a6a14480fd0618ec67 Mon Sep 17 00:00:00 2001 From: PelleKrab Date: Thu, 25 Jun 2026 18:09:27 -0700 Subject: [PATCH 05/20] refactor(L1): cache hash chain seed as immutable in ProtocolVersions Replace the per-call keccak256(abi.encode(l2ChainId, address(this))) in _refreshScheduleId with a cached immutable _seed set once in the constructor, saving a hash + encode on every schedule mutation. Generated with Claude Code Co-Authored-By: Claude --- src/L1/ProtocolVersions.sol | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/L1/ProtocolVersions.sol b/src/L1/ProtocolVersions.sol index 0b05db920..6142e8cb8 100644 --- a/src/L1/ProtocolVersions.sol +++ b/src/L1/ProtocolVersions.sol @@ -37,6 +37,10 @@ contract ProtocolVersions is Ownable, IProtocolVersions { /// @notice The L2 chain ID whose schedule this contract commits to. uint256 public immutable l2ChainId; + /// @notice Hash chain seed: keccak256(abi.encode(l2ChainId, address(this))). Cached to avoid + /// recomputing on every _refreshScheduleId call. + bytes32 internal immutable _seed; + /// @notice The latest block number in which `scheduleId` changed. uint256 public lastUpdatedAtBlock; @@ -76,8 +80,9 @@ contract ProtocolVersions is Ownable, IProtocolVersions { _transferOwnership(_owner); l2ChainId = _l2ChainId; + _seed = keccak256(abi.encode(_l2ChainId, address(this))); - _scheduleId = keccak256(abi.encode(l2ChainId, address(this))); + _scheduleId = _seed; lastUpdatedAtBlock = block.number; emit ScheduleIdUpdated(bytes32(0), _scheduleId, block.number); } @@ -241,14 +246,13 @@ contract ProtocolVersions is Ownable, IProtocolVersions { /// 0-based registration index of `changedKey`. function _refreshScheduleId(bytes32 changedKey) internal { uint256 n = _upgradeKeys.length; - bytes32 seed = keccak256(abi.encode(l2ChainId, address(this))); // Locate changedKey and initialise prev to the hash of the preceding link (or seed). uint256 startIndex; for (startIndex = 0; startIndex < n; startIndex++) { if (_upgradeKeys[startIndex] == changedKey) break; } - bytes32 prev = startIndex == 0 ? seed : _upgradeScheduleId[_upgradeKeys[startIndex - 1]]; + bytes32 prev = startIndex == 0 ? _seed : _upgradeScheduleId[_upgradeKeys[startIndex - 1]]; // Recompute from startIndex onward, storing each link. for (uint256 i = startIndex; i < n; i++) { From 50a37cdd45c80f3d7428c067dfa4e0c464962822 Mon Sep 17 00:00:00 2001 From: PelleKrab Date: Thu, 25 Jun 2026 18:28:32 -0700 Subject: [PATCH 06/20] chore(L1): update semver-lock for ProtocolVersions Reflects bytecode change from caching hash chain seed as immutable. Generated with Claude Code Co-Authored-By: Claude --- snapshots/semver-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/snapshots/semver-lock.json b/snapshots/semver-lock.json index 9b29703dc..d12c170e7 100644 --- a/snapshots/semver-lock.json +++ b/snapshots/semver-lock.json @@ -20,8 +20,8 @@ "sourceCodeHash": "0xa1281f5da03fa0431eabad33da23f51a8d835b0dd04554603fd59e11efe9fa51" }, "src/L1/ProtocolVersions.sol:ProtocolVersions": { - "initCodeHash": "0xf17ad1d8696f97a18ae68f07b0d1732327cce06a46e3a224655622fd9cd2a27d", - "sourceCodeHash": "0xb766b0467cc20a0815857239f1344ed82a8783616cb35eb68ffcc88600b2c184" + "initCodeHash": "0x35b558b4d20df958b619ad9a446477144d9a1f0cd5115ac02f9862f4a044be5f", + "sourceCodeHash": "0x8416897f453c0520dec6eed7592c25bd2b5d29b2d859907043a1170b39134ff3" }, "src/L1/SuperchainConfig.sol:SuperchainConfig": { "initCodeHash": "0x9b1f3555b499709485d51d5d9665002c0eb1e5eb893be1fb978a30749e894858", From ab5924d46b19b0936979f70c134e0724104a70a6 Mon Sep 17 00:00:00 2001 From: PelleKrab Date: Mon, 6 Jul 2026 12:54:25 -0700 Subject: [PATCH 07/20] refactor(L1): simplify ProtocolVersions ownership, storage, and access control - Replace Ownable with ProxyAdminOwnedBase; drop explicit _owner constructor param and ProtocolVersions_ZeroOwner error - Replace per-upgrade protocolVersion mapping with a single latestProtocolVersion uint256 updated on each registerUpgrade call - Remove redundant storage vars: lastUpdatedAtBlock, l2ChainId immutable, _scheduleId (scheduleId() now reads the tip of _upgradeScheduleId directly) - Remove redundant view functions: upgradeCount, upgradeIdAt, upgradeIds, getProtocolVersion (getSchedule() covers all registry reads) - Inline _registerUpgrade into registerUpgrade and _applyTimestamp into setTimestamp; inline onlyChainTeam guard into delayTimestamp - Drop ScheduleIdUpdated old-scheduleId param; enforce MIN_NOTICE in delayTimestamp Generated with Claude Code Co-Authored-By: Claude --- interfaces/L1/IProtocolVersions.sol | 13 +-- src/L1/ProtocolVersions.sol | 159 ++++++++-------------------- test/L1/ProtocolVersions.t.sol | 97 +++++------------ 3 files changed, 77 insertions(+), 192 deletions(-) diff --git a/interfaces/L1/IProtocolVersions.sol b/interfaces/L1/IProtocolVersions.sol index 75c5b3947..0aaef8dbd 100644 --- a/interfaces/L1/IProtocolVersions.sol +++ b/interfaces/L1/IProtocolVersions.sol @@ -9,11 +9,9 @@ interface IProtocolVersions is ISemver { struct Upgrade { string name; uint64 timestamp; - uint256 protocolVersion; bytes32 scheduleId; } - error ProtocolVersions_ZeroOwner(); error ProtocolVersions_InvalidL2ChainId(); error ProtocolVersions_UnknownUpgradeName(string upgradeId); error ProtocolVersions_UpgradeAlreadyRegistered(bytes32 key); @@ -27,19 +25,16 @@ interface IProtocolVersions is ISemver { event UpgradeRegistered(bytes32 indexed key, uint256 indexed index, string upgradeId, uint256 protocolVersion); event TimestampSet(bytes32 indexed key, uint256 timestamp); - event ScheduleIdUpdated(bytes32 indexed oldScheduleId, bytes32 indexed newScheduleId, uint256 indexed blockNumber); + event ChainTeamUpdated(address indexed previousChainTeam, address indexed newChainTeam); function MIN_NOTICE() external view returns (uint64); - function l2ChainId() external view returns (uint256); + function chainTeam() external view returns (address); function scheduleId() external view returns (bytes32); - function lastUpdatedAtBlock() external view returns (uint256); - function upgradeCount() external view returns (uint256); - function upgradeIdAt(uint256 index) external view returns (bytes32); - function upgradeIds() external view returns (bytes32[] memory); + function latestProtocolVersion() external view returns (uint256); + function getTimestamp(string calldata upgradeId) external view returns (uint256); - function getProtocolVersion(string calldata upgradeId) external view returns (uint256); function getSchedule() external view returns (Upgrade[] memory); diff --git a/src/L1/ProtocolVersions.sol b/src/L1/ProtocolVersions.sol index 6142e8cb8..77ae84593 100644 --- a/src/L1/ProtocolVersions.sol +++ b/src/L1/ProtocolVersions.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; // Contracts -import { Ownable } from "lib/openzeppelin-contracts/contracts/access/Ownable.sol"; +import { ProxyAdminOwnedBase } from "src/universal/ProxyAdminOwnedBase.sol"; // Interfaces import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; @@ -26,7 +26,7 @@ import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; /// An `upgradeId` is a human-readable string (e.g. "canyon"); each is packed into a /// bytes32 storage `key`. The registry starts empty; all upgrades are added via /// `registerUpgrade` owner calls. -contract ProtocolVersions is Ownable, IProtocolVersions { +contract ProtocolVersions is ProxyAdminOwnedBase, IProtocolVersions { /// @notice Semantic version. /// @custom:semver 1.0.0 string public constant version = "1.0.0"; @@ -34,19 +34,10 @@ contract ProtocolVersions is Ownable, IProtocolVersions { /// @notice Minimum notice period required when scheduling or modifying an activation timestamp. uint64 public constant MIN_NOTICE = 1 hours; - /// @notice The L2 chain ID whose schedule this contract commits to. - uint256 public immutable l2ChainId; - /// @notice Hash chain seed: keccak256(abi.encode(l2ChainId, address(this))). Cached to avoid /// recomputing on every _refreshScheduleId call. bytes32 internal immutable _seed; - /// @notice The latest block number in which `scheduleId` changed. - uint256 public lastUpdatedAtBlock; - - /// @notice The current schedule commitment hash, extended on every timestamp change. - bytes32 internal _scheduleId; - /// @notice Ordered list of registered upgrade keys. bytes32[] internal _upgradeKeys; @@ -59,10 +50,11 @@ contract ProtocolVersions is Ownable, IProtocolVersions { /// @notice Activation timestamp for each registered upgrade key (0 = not scheduled). mapping(bytes32 => uint64) internal _timestamps; - /// @notice Protocol version for each upgrade, chosen by the owner and set at registration via - /// `registerUpgrade`. Immutable once registered. Non-zero for registered upgrades; - /// zero for unregistered keys (serves as the existence sentinel). - mapping(bytes32 => uint256) internal _protocolVersions; + /// @notice Registration sentinel for each upgrade key. + mapping(bytes32 => bool) internal _registered; + + /// @notice The protocol version set at the most recent `registerUpgrade` call. + uint256 public latestProtocolVersion; /// @notice Address allowed to delay (push out) already-scheduled activation timestamps. /// @dev Appointed and revocable by the owner. This is a restricted secondary role: it can @@ -71,51 +63,24 @@ contract ProtocolVersions is Ownable, IProtocolVersions { /// earlier, or schedule a brand-new activation. Unset (zero) by default. address public chainTeam; - /// @notice Deploys the contract, sets the owner and chain ID, and emits the initial scheduleId. - /// @param _owner Initial Security Council owner. + /// @notice Deploys the contract and sets the chain ID. /// @param _l2ChainId L2 chain ID whose schedule is being committed. - constructor(address _owner, uint256 _l2ChainId) Ownable() { - if (_owner == address(0)) revert ProtocolVersions_ZeroOwner(); + constructor(uint256 _l2ChainId) { if (_l2ChainId == 0) revert ProtocolVersions_InvalidL2ChainId(); - - _transferOwnership(_owner); - l2ChainId = _l2ChainId; _seed = keccak256(abi.encode(_l2ChainId, address(this))); - - _scheduleId = _seed; - lastUpdatedAtBlock = block.number; - emit ScheduleIdUpdated(bytes32(0), _scheduleId, block.number); } - /// @notice Restricts a call to the appointed chainTeam role. - modifier onlyChainTeam() { - if (msg.sender != chainTeam) revert ProtocolVersions_NotChainTeam(); + /// @notice Restricts a call to the ProxyAdmin owner. + modifier onlyProxyAdminOwner() { + _assertOnlyProxyAdminOwner(); _; } /// @notice Returns the canonical schedule commitment. /// @return The current scheduleId hash. function scheduleId() external view returns (bytes32) { - return _scheduleId; - } - - /// @notice Returns the number of registered upgrades. - /// @return The length of the upgrade registry. - function upgradeCount() external view returns (uint256) { - return _upgradeKeys.length; - } - - /// @notice Returns the registered upgrade key at `index` (registration order). - /// @param index 0-based position in the registry. - /// @return The bytes32 key at that position. - function upgradeIdAt(uint256 index) external view returns (bytes32) { - return _upgradeKeys[index]; - } - - /// @notice Returns the full ordered list of registered upgrade keys. - /// @return Ordered array of bytes32 keys in registration order. - function upgradeIds() external view returns (bytes32[] memory) { - return _upgradeKeys; + uint256 n = _upgradeKeys.length; + return n == 0 ? _seed : _upgradeScheduleId[_upgradeKeys[n - 1]]; } /// @notice Returns the activation timestamp for `upgradeId` (0 = not scheduled). @@ -125,15 +90,8 @@ contract ProtocolVersions is Ownable, IProtocolVersions { return _timestamps[_registeredKey(upgradeId)]; } - /// @notice Returns the owner-assigned protocol version for `upgradeId`. Reverts if not registered. - /// @param upgradeId The human-readable upgrade identifier string. - /// @return The protocol version assigned at registration time. - function getProtocolVersion(string calldata upgradeId) external view returns (uint256) { - return _protocolVersions[_registeredKey(upgradeId)]; - } - /// @notice Returns the full ordered schedule: every registered upgrade with its current - /// activation timestamp, protocol version, and cumulative schedule hash. + /// activation timestamp and cumulative schedule hash. /// @dev Calling via eth_call is gas-free; no transaction is submitted. /// @return schedule_ Ordered array of Upgrade structs, one per registered upgrade. function getSchedule() external view returns (Upgrade[] memory schedule_) { @@ -144,19 +102,27 @@ contract ProtocolVersions is Ownable, IProtocolVersions { schedule_[i] = Upgrade({ name: _nameFromKey(key), timestamp: _timestamps[key], - protocolVersion: _protocolVersions[key], scheduleId: _upgradeScheduleId[key] }); } } /// @notice Registers a new upgrade by upgradeId with its protocol version. Owner only. - /// @dev The upgradeId is packed into a bytes32 key and `protocolVersion` is recorded for it. + /// @dev The upgradeId is packed into a bytes32 key and `latestProtocolVersion` is updated. /// Registration extends the scheduleId chain with the new (key, timestamp=0) link. /// @param upgradeId Human-readable upgrade name (1–32 bytes, e.g. "canyon"). - /// @param protocolVersion Owner-assigned protocol version number for this upgrade (must be non-zero). - function registerUpgrade(string calldata upgradeId, uint256 protocolVersion) external onlyOwner { - _registerUpgrade(upgradeId, protocolVersion); + /// @param protocolVersion Packed semver uint256 for this upgrade (must be non-zero). + function registerUpgrade(string calldata upgradeId, uint256 protocolVersion) external onlyProxyAdminOwner { + if (protocolVersion == 0) revert ProtocolVersions_InvalidProtocolVersion(); + bytes32 key = _keyFromUpgradeId(upgradeId); + if (_registered[key]) revert ProtocolVersions_UpgradeAlreadyRegistered(key); + + uint256 index = _upgradeKeys.length; + _upgradeKeys.push(key); + _registered[key] = true; + latestProtocolVersion = protocolVersion; + emit UpgradeRegistered(key, index, upgradeId, protocolVersion); + _refreshScheduleId(key); } /// @notice Sets the activation timestamp for one upgrade by upgradeId. Pass 0 to clear. @@ -166,16 +132,26 @@ contract ProtocolVersions is Ownable, IProtocolVersions { /// @param upgradeId The upgrade to schedule. /// @param timestamp Future Unix timestamp for L2 activation (must be >= block.timestamp + MIN_NOTICE), or 0 to /// clear. - function setTimestamp(string calldata upgradeId, uint64 timestamp) external onlyOwner { + function setTimestamp(string calldata upgradeId, uint64 timestamp) external onlyProxyAdminOwner { bytes32 key = _registeredKey(upgradeId); - if (_applyTimestamp(key, timestamp)) { - _refreshScheduleId(key); + uint64 current = _timestamps[key]; + // Cannot modify a timestamp that has already activated. + if (current != 0 && uint64(block.timestamp) >= current) { + revert ProtocolVersions_ActivationAlreadyPassed(key, current); } + // Non-zero timestamps must be at least MIN_NOTICE seconds in the future. + if (timestamp != 0 && timestamp < uint64(block.timestamp) + MIN_NOTICE) { + revert ProtocolVersions_DelayMustBeLater(current, timestamp); + } + if (current == timestamp) return; + _timestamps[key] = timestamp; + emit TimestampSet(key, timestamp); + _refreshScheduleId(key); } /// @notice Appoints, replaces, or clears (set to zero) the chainTeam role. Owner only. /// @param newChainTeam New chainTeam address, or address(0) to revoke the role. - function setChainTeam(address newChainTeam) external onlyOwner { + function setChainTeam(address newChainTeam) external onlyProxyAdminOwner { emit ChainTeamUpdated(chainTeam, newChainTeam); chainTeam = newChainTeam; } @@ -189,7 +165,8 @@ contract ProtocolVersions is Ownable, IProtocolVersions { /// is later still, the new value is always in the future. /// @param upgradeId The upgrade whose activation to delay. /// @param newTimestamp New activation timestamp, must be strictly later than the current one. - function delayTimestamp(string calldata upgradeId, uint64 newTimestamp) external onlyChainTeam { + function delayTimestamp(string calldata upgradeId, uint64 newTimestamp) external { + if (msg.sender != chainTeam) revert ProtocolVersions_NotChainTeam(); bytes32 key = _registeredKey(upgradeId); uint64 current = _timestamps[key]; @@ -199,48 +176,14 @@ contract ProtocolVersions is Ownable, IProtocolVersions { if (uint64(block.timestamp) >= current) { revert ProtocolVersions_ActivationAlreadyPassed(key, current); } - // The role can only push the activation later, never earlier or to the same time. - if (newTimestamp <= current) revert ProtocolVersions_DelayMustBeLater(current, newTimestamp); + // The role can only push the activation later and must be greater than the MIN_NOTICE buffer, never earlier or to the same time. + if (newTimestamp <= current || newTimestamp < uint64(block.timestamp) + MIN_NOTICE) revert ProtocolVersions_DelayMustBeLater(current, newTimestamp); _timestamps[key] = newTimestamp; emit TimestampSet(key, newTimestamp); _refreshScheduleId(key); } - /// @dev Validates and applies a timestamp update for a registered upgrade key. - /// Returns true if the timestamp changed (caller should then call _refreshScheduleId), - /// false if unchanged (no-op, scheduleId must not be extended). - function _applyTimestamp(bytes32 key, uint64 timestamp) internal returns (bool) { - uint64 current = _timestamps[key]; - // Cannot modify a timestamp that has already activated. - if (current != 0 && uint64(block.timestamp) >= current) { - revert ProtocolVersions_ActivationAlreadyPassed(key, current); - } - // Non-zero timestamps must be at least MIN_NOTICE seconds in the future. - if (timestamp != 0 && timestamp < uint64(block.timestamp) + MIN_NOTICE) { - revert ProtocolVersions_ActivationTimestampInPast(timestamp); - } - if (current == timestamp) return false; - _timestamps[key] = timestamp; - emit TimestampSet(key, timestamp); - return true; - } - - /// @dev Validates and appends a new upgrade to the registry, then extends the scheduleId chain. - /// `_protocolVersions[key] != 0` serves as the existence sentinel, so protocolVersion must - /// be non-zero. The new upgrade is always the last entry, so _refreshScheduleId is O(1). - function _registerUpgrade(string calldata upgradeId, uint256 protocolVersion) internal { - if (protocolVersion == 0) revert ProtocolVersions_InvalidProtocolVersion(); - bytes32 key = _keyFromUpgradeId(upgradeId); - if (_protocolVersions[key] != 0) revert ProtocolVersions_UpgradeAlreadyRegistered(key); - - uint256 index = _upgradeKeys.length; - _upgradeKeys.push(key); - _protocolVersions[key] = protocolVersion; - emit UpgradeRegistered(key, index, upgradeId, protocolVersion); - _refreshScheduleId(key); - } - /// @dev Recomputes the per-upgrade cumulative hash chain starting from `changedKey` and bubbles /// the result through all subsequent registered upgrades. Cost is O(n-j) where j is the /// 0-based registration index of `changedKey`. @@ -253,26 +196,18 @@ contract ProtocolVersions is Ownable, IProtocolVersions { if (_upgradeKeys[startIndex] == changedKey) break; } bytes32 prev = startIndex == 0 ? _seed : _upgradeScheduleId[_upgradeKeys[startIndex - 1]]; - // Recompute from startIndex onward, storing each link. for (uint256 i = startIndex; i < n; i++) { bytes32 k = _upgradeKeys[i]; prev = keccak256(abi.encode(prev, k, _timestamps[k])); _upgradeScheduleId[k] = prev; } - - bytes32 oldScheduleId = _scheduleId; - if (prev != oldScheduleId) { - _scheduleId = prev; - lastUpdatedAtBlock = block.number; - emit ScheduleIdUpdated(oldScheduleId, prev, block.number); - } } /// @dev Resolves an upgradeId string to its storage key, reverting if not registered. function _registeredKey(string calldata upgradeId) internal view returns (bytes32 key) { key = _keyFromUpgradeId(upgradeId); - if (_protocolVersions[key] == 0) revert ProtocolVersions_UnknownUpgradeName(upgradeId); + if (!_registered[key]) revert ProtocolVersions_UnknownUpgradeName(upgradeId); } /// @dev Packs a UTF-8 upgradeId string (1–32 bytes) into a bytes32 key. Shorter strings are diff --git a/test/L1/ProtocolVersions.t.sol b/test/L1/ProtocolVersions.t.sol index 8f5f8726c..d0489cbe0 100644 --- a/test/L1/ProtocolVersions.t.sol +++ b/test/L1/ProtocolVersions.t.sol @@ -6,17 +6,21 @@ import { Test } from "lib/forge-std/src/Test.sol"; // Contracts import { ProtocolVersions } from "src/L1/ProtocolVersions.sol"; +import { ProxyAdminOwnedBase } from "src/universal/ProxyAdminOwnedBase.sol"; // Interfaces import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; +// Mocks +import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; + /// @title ProtocolVersions_TestInit /// @notice Reusable test initialization for ProtocolVersions tests. abstract contract ProtocolVersions_TestInit is Test { event UpgradeRegistered(bytes32 indexed key, uint256 indexed index, string upgradeId, uint256 protocolVersion); event ChainTeamUpdated(address indexed previousChainTeam, address indexed newChainTeam); event TimestampSet(bytes32 indexed key, uint256 timestamp); - event ScheduleIdUpdated(bytes32 indexed oldScheduleId, bytes32 indexed newScheduleId, uint256 indexed blockNumber); + address internal _owner = makeAddr("owner"); address internal _nonOwner = makeAddr("non-owner"); @@ -24,7 +28,10 @@ abstract contract ProtocolVersions_TestInit is Test { ProtocolVersions internal protocolVersions; function setUp() public virtual { - protocolVersions = new ProtocolVersions(_owner, 8453); + protocolVersions = new ProtocolVersions(8453); + address mockProxyAdmin = makeAddr("proxy-admin"); + vm.mockCall(mockProxyAdmin, abi.encodeWithSignature("owner()"), abi.encode(_owner)); + EIP1967Helper.setAdmin(address(protocolVersions), mockProxyAdmin); } /// @dev Registers `canyon` and schedules it for block.timestamp + MIN_NOTICE + delay. @@ -42,22 +49,14 @@ abstract contract ProtocolVersions_TestInit is Test { contract ProtocolVersions_Constructor_Test is ProtocolVersions_TestInit { /// @notice Tests that the constructor sets the correct initial state. function test_constructor_setsInitialState_succeeds() external view { - assertEq(protocolVersions.owner(), _owner); - assertEq(protocolVersions.l2ChainId(), 8453); - assertEq(protocolVersions.lastUpdatedAtBlock(), block.number); + assertEq(protocolVersions.proxyAdminOwner(), _owner); assertNotEq(protocolVersions.scheduleId(), bytes32(0)); } - /// @notice Tests that the constructor reverts when the owner is address(0). - function test_constructor_zeroOwner_reverts() external { - vm.expectRevert(IProtocolVersions.ProtocolVersions_ZeroOwner.selector); - new ProtocolVersions(address(0), 8453); - } - /// @notice Tests that the constructor reverts when the chain ID is zero. function test_constructor_zeroChainId_reverts() external { vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidL2ChainId.selector); - new ProtocolVersions(_owner, 0); + new ProtocolVersions(0); } } @@ -73,7 +72,7 @@ contract ProtocolVersions_Version_Test is ProtocolVersions_TestInit { /// @title ProtocolVersions_RegisterUpgrade_Test /// @notice Test contract for the `registerUpgrade` function. contract ProtocolVersions_RegisterUpgrade_Test is ProtocolVersions_TestInit { - /// @notice Tests that registering an upgrade extends the scheduleId chain and updates lastUpdatedAtBlock. + /// @notice Tests that registering an upgrade extends the scheduleId chain. function test_registerUpgrade_changesScheduleId_succeeds() external { bytes32 idBefore = protocolVersions.scheduleId(); @@ -82,34 +81,6 @@ contract ProtocolVersions_RegisterUpgrade_Test is ProtocolVersions_TestInit { protocolVersions.registerUpgrade("canyon", 1); assertNotEq(protocolVersions.scheduleId(), idBefore); - assertEq(protocolVersions.lastUpdatedAtBlock(), block.number); - } - - /// @notice Tests that registering an upgrade adds it to the ordered registry. - function test_registerUpgrade_extendsRegistry_succeeds() external { - assertEq(protocolVersions.upgradeCount(), 0); - - vm.prank(_owner); - protocolVersions.registerUpgrade("antares", 1); - - assertEq(protocolVersions.upgradeCount(), 1); - assertEq(protocolVersions.upgradeIdAt(0), bytes32("antares")); - } - - /// @notice Tests that registering multiple upgrades preserves registration order. - function test_registerUpgrade_preservesOrder_succeeds() external { - vm.prank(_owner); - protocolVersions.registerUpgrade("canyon", 1); - vm.prank(_owner); - protocolVersions.registerUpgrade("ecotone", 2); - vm.prank(_owner); - protocolVersions.registerUpgrade("fjord", 3); - - bytes32[] memory ids = protocolVersions.upgradeIds(); - assertEq(ids.length, 3); - assertEq(ids[0], bytes32("canyon")); - assertEq(ids[1], bytes32("ecotone")); - assertEq(ids[2], bytes32("fjord")); } /// @notice Tests that `registerUpgrade` emits the `UpgradeRegistered` event with correct fields. @@ -125,15 +96,15 @@ contract ProtocolVersions_RegisterUpgrade_Test is ProtocolVersions_TestInit { protocolVersions.registerUpgrade("ecotone", 2); } - /// @notice Tests that registering an upgrade stores the owner-assigned protocol version. - function test_registerUpgrade_storesProtocolVersion_succeeds() external { + /// @notice Tests that registering upgrades updates latestProtocolVersion to the most recent. + function test_registerUpgrade_updatesLatestProtocolVersion_succeeds() external { vm.prank(_owner); protocolVersions.registerUpgrade("canyon", 5); + assertEq(protocolVersions.latestProtocolVersion(), 5); + vm.prank(_owner); protocolVersions.registerUpgrade("ecotone", 9); - - assertEq(protocolVersions.getProtocolVersion("canyon"), 5); - assertEq(protocolVersions.getProtocolVersion("ecotone"), 9); + assertEq(protocolVersions.latestProtocolVersion(), 9); } /// @notice Tests that registering a 32-byte name (the maximum) succeeds. @@ -141,8 +112,7 @@ contract ProtocolVersions_RegisterUpgrade_Test is ProtocolVersions_TestInit { string memory name32 = "exactly-32-bytes-upgrade-name!!!"; vm.prank(_owner); protocolVersions.registerUpgrade(name32, 7); - assertEq(protocolVersions.upgradeCount(), 1); - assertEq(protocolVersions.getProtocolVersion(name32), 7); + assertEq(protocolVersions.latestProtocolVersion(), 7); } /// @notice Tests that registering the same upgrade twice reverts. @@ -159,7 +129,7 @@ contract ProtocolVersions_RegisterUpgrade_Test is ProtocolVersions_TestInit { protocolVersions.registerUpgrade("canyon", 1); } - /// @notice Tests that registering an upgrade with protocolVersion zero reverts. + /// @notice Tests that registering an upgrade with a zero protocolVersion reverts. function test_registerUpgrade_zeroProtocolVersion_reverts() external { vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidProtocolVersion.selector); vm.prank(_owner); @@ -182,7 +152,7 @@ contract ProtocolVersions_RegisterUpgrade_Test is ProtocolVersions_TestInit { /// @notice Tests that only the owner can call `registerUpgrade`. function test_registerUpgrade_callerNotOwner_reverts() external { - vm.expectRevert("Ownable: caller is not the owner"); + vm.expectRevert(ProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); vm.prank(_nonOwner); protocolVersions.registerUpgrade("antares", 1); } @@ -204,7 +174,6 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { protocolVersions.setTimestamp("canyon", ts); assertEq(protocolVersions.getTimestamp("canyon"), ts); - assertEq(protocolVersions.lastUpdatedAtBlock(), block.number); assertNotEq(protocolVersions.scheduleId(), initialScheduleId); } @@ -220,7 +189,6 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { protocolVersions.setTimestamp("canyon", ts); bytes32 scheduleIdAfterSet = protocolVersions.scheduleId(); - uint256 updateBlockAfterSet = protocolVersions.lastUpdatedAtBlock(); vm.roll(block.number + 1); vm.prank(_owner); @@ -228,7 +196,6 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { assertEq(protocolVersions.getTimestamp("canyon"), ts); assertEq(protocolVersions.scheduleId(), scheduleIdAfterSet); - assertEq(protocolVersions.lastUpdatedAtBlock(), updateBlockAfterSet); } /// @notice Tests that passing 0 clears a scheduled timestamp, changes the scheduleId, and @@ -273,7 +240,7 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { protocolVersions.registerUpgrade("canyon", 1); uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; - vm.expectRevert("Ownable: caller is not the owner"); + vm.expectRevert(ProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); vm.prank(_nonOwner); protocolVersions.setTimestamp("canyon", ts); } @@ -285,7 +252,7 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { vm.warp(1000); vm.expectRevert( - abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_ActivationTimestampInPast.selector, uint64(500)) + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_DelayMustBeLater.selector, uint64(0), uint64(500)) ); vm.prank(_owner); protocolVersions.setTimestamp("canyon", 500); @@ -298,7 +265,7 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() - 1; vm.expectRevert( - abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_ActivationTimestampInPast.selector, ts) + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_DelayMustBeLater.selector, uint64(0), ts) ); vm.prank(_owner); protocolVersions.setTimestamp("canyon", ts); @@ -351,7 +318,7 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { protocolVersions.setTimestamp("ecotone", ts2); // Reproduce the chain from scratch. - bytes32 seed = keccak256(abi.encode(protocolVersions.l2ChainId(), address(protocolVersions))); + bytes32 seed = keccak256(abi.encode(uint256(8453), address(protocolVersions))); bytes32 link0 = keccak256(abi.encode(seed, bytes32("canyon"), ts1)); bytes32 link1 = keccak256(abi.encode(link0, bytes32("ecotone"), ts2)); @@ -385,7 +352,6 @@ contract ProtocolVersions_DelayTimestamp_Test is ProtocolVersions_TestInit { assertEq(protocolVersions.getTimestamp("canyon"), later); assertNotEq(protocolVersions.scheduleId(), scheduleIdBefore); - assertEq(protocolVersions.lastUpdatedAtBlock(), block.number); } /// @notice Tests that only the chainTeam can call `delayTimestamp`. @@ -489,7 +455,7 @@ contract ProtocolVersions_ChainTeam_Test is ProtocolVersions_TestInit { /// @notice Tests that only the owner can call `setChainTeam`. function test_setChainTeam_callerNotOwner_reverts() external { - vm.expectRevert("Ownable: caller is not the owner"); + vm.expectRevert(ProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); vm.prank(_nonOwner); protocolVersions.setChainTeam(_chainTeam); } @@ -521,16 +487,7 @@ contract ProtocolVersions_ChainTeam_Test is ProtocolVersions_TestInit { contract ProtocolVersions_GetView_Test is ProtocolVersions_TestInit { /// @notice Tests that the registry starts empty. function test_registry_startsEmpty_succeeds() external view { - assertEq(protocolVersions.upgradeCount(), 0); - assertEq(protocolVersions.upgradeIds().length, 0); - } - - /// @notice Tests that `getProtocolVersion` reverts for an unknown upgrade name. - function test_getProtocolVersion_unknownUpgrade_reverts() external { - vm.expectRevert( - abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_UnknownUpgradeName.selector, "unknown") - ); - protocolVersions.getProtocolVersion("unknown"); + assertEq(protocolVersions.getSchedule().length, 0); } /// @notice Tests that `getSchedule` returns an empty array when no upgrades are registered. @@ -555,11 +512,9 @@ contract ProtocolVersions_GetView_Test is ProtocolVersions_TestInit { assertEq(s[0].name, "canyon"); assertEq(s[0].timestamp, ts); - assertEq(s[0].protocolVersion, 1); assertEq(s[1].name, "ecotone"); assertEq(s[1].timestamp, 0); - assertEq(s[1].protocolVersion, 2); // The last entry's scheduleId is the contract's current scheduleId. assertEq(s[1].scheduleId, protocolVersions.scheduleId()); From 6cd39d7e97f1106f3d0b021d4d431fc8fdcbb2fc Mon Sep 17 00:00:00 2001 From: PelleKrab Date: Mon, 6 Jul 2026 14:42:00 -0700 Subject: [PATCH 08/20] refactor(L1): switch ProtocolVersions to numeric upgrade IDs and proxy pattern Replace the string-based upgrade registry (bytes32 key from packed upgradeId) with a purely numeric scheme: each upgrade gets an ascending uint256 id equal to its registration index. Human-readable names are kept off-chain; the id is permanent and stable because the registry is strictly append-only. Convert the constructor to an initializer so the contract can be deployed behind an OP proxy. _seed is now stored (not immutable) so it can be set inside initialize() against the proxy's address(this), binding the schedule commitment to the proxy address that callers interact with. Replace the mapping-based storage (_registered, _timestamps, _upgradeScheduleId keyed by bytes32) with parallel arrays indexed by id, simplifying the hash chain and eliminating the three-mapping structure. Drop getTimestamp(string) and the duplicate-registration guard; add setLatestProtocolVersion() so the informational field can be updated independently of registrations. Add LatestProtocolVersionUpdated and ScheduleIdUpdated events; update the interface to inherit IProxyAdminOwnedBase and IReinitializableBase. Generated with Claude Code Co-Authored-By: Claude --- interfaces/L1/IProtocolVersions.sol | 34 +- snapshots/abi/ProtocolVersions.json | 441 ++++++++++++++++++ snapshots/semver-lock.json | 4 +- snapshots/storageLayout/ProtocolVersions.json | 51 ++ src/L1/ProtocolVersions.sol | 257 +++++----- test/L1/ProtocolVersions.t.sol | 306 +++++++----- 6 files changed, 833 insertions(+), 260 deletions(-) create mode 100644 snapshots/abi/ProtocolVersions.json create mode 100644 snapshots/storageLayout/ProtocolVersions.json diff --git a/interfaces/L1/IProtocolVersions.sol b/interfaces/L1/IProtocolVersions.sol index 0aaef8dbd..a5b331c70 100644 --- a/interfaces/L1/IProtocolVersions.sol +++ b/interfaces/L1/IProtocolVersions.sol @@ -2,30 +2,30 @@ pragma solidity ^0.8.0; import { ISemver } from "interfaces/universal/ISemver.sol"; +import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; +import { IReinitializableBase } from "interfaces/universal/IReinitializableBase.sol"; /// @title IProtocolVersions /// @notice Interface for the ProtocolVersions upgrade schedule contract. -interface IProtocolVersions is ISemver { +interface IProtocolVersions is IProxyAdminOwnedBase, ISemver, IReinitializableBase { struct Upgrade { - string name; + uint256 id; uint64 timestamp; bytes32 scheduleId; } error ProtocolVersions_InvalidL2ChainId(); - error ProtocolVersions_UnknownUpgradeName(string upgradeId); - error ProtocolVersions_UpgradeAlreadyRegistered(bytes32 key); - error ProtocolVersions_InvalidUpgradeId(); + error ProtocolVersions_UnknownUpgrade(uint256 id); error ProtocolVersions_InvalidProtocolVersion(); - error ProtocolVersions_ActivationAlreadyPassed(bytes32 key, uint64 activationTimestamp); - error ProtocolVersions_ActivationTimestampInPast(uint64 timestamp); + error ProtocolVersions_ActivationAlreadyPassed(uint256 id, uint64 activationTimestamp); error ProtocolVersions_NotChainTeam(); - error ProtocolVersions_NotScheduled(bytes32 key); + error ProtocolVersions_NotScheduled(uint256 id); error ProtocolVersions_DelayMustBeLater(uint64 currentTimestamp, uint64 newTimestamp); - event UpgradeRegistered(bytes32 indexed key, uint256 indexed index, string upgradeId, uint256 protocolVersion); - event TimestampSet(bytes32 indexed key, uint256 timestamp); - + event UpgradeRegistered(uint256 indexed id, uint256 protocolVersion); + event LatestProtocolVersionUpdated(uint256 indexed protocolVersion); + event TimestampSet(uint256 indexed id, uint256 timestamp); + event ScheduleIdUpdated(bytes32 indexed newScheduleId, uint256 indexed blockNumber); event ChainTeamUpdated(address indexed previousChainTeam, address indexed newChainTeam); function MIN_NOTICE() external view returns (uint64); @@ -34,12 +34,14 @@ interface IProtocolVersions is ISemver { function scheduleId() external view returns (bytes32); function latestProtocolVersion() external view returns (uint256); - function getTimestamp(string calldata upgradeId) external view returns (uint256); - function getSchedule() external view returns (Upgrade[] memory); - function registerUpgrade(string calldata upgradeId, uint256 protocolVersion) external; - function setTimestamp(string calldata upgradeId, uint64 timestamp) external; + function initialize(uint256 _l2ChainId) external; + function registerUpgrade(uint256 protocolVersion) external returns (uint256 id); + function setLatestProtocolVersion(uint256 protocolVersion) external; + function setTimestamp(uint256 id, uint64 timestamp) external; function setChainTeam(address newChainTeam) external; - function delayTimestamp(string calldata upgradeId, uint64 newTimestamp) external; + function delayTimestamp(uint256 id, uint64 newTimestamp) external; + + function __constructor__() external; } diff --git a/snapshots/abi/ProtocolVersions.json b/snapshots/abi/ProtocolVersions.json new file mode 100644 index 000000000..7caefb154 --- /dev/null +++ b/snapshots/abi/ProtocolVersions.json @@ -0,0 +1,441 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "MIN_NOTICE", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "chainTeam", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint64", + "name": "newTimestamp", + "type": "uint64" + } + ], + "name": "delayTimestamp", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getSchedule", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint64", + "name": "timestamp", + "type": "uint64" + }, + { + "internalType": "bytes32", + "name": "scheduleId", + "type": "bytes32" + } + ], + "internalType": "struct ProtocolVersions.Upgrade[]", + "name": "schedule_", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "initVersion", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_l2ChainId", + "type": "uint256" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "latestProtocolVersion", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxyAdmin", + "outputs": [ + { + "internalType": "contract IProxyAdmin", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxyAdminOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "protocolVersion", + "type": "uint256" + } + ], + "name": "registerUpgrade", + "outputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "scheduleId", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newChainTeam", + "type": "address" + } + ], + "name": "setChainTeam", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "protocolVersion", + "type": "uint256" + } + ], + "name": "setLatestProtocolVersion", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint64", + "name": "timestamp", + "type": "uint64" + } + ], + "name": "setTimestamp", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousChainTeam", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newChainTeam", + "type": "address" + } + ], + "name": "ChainTeamUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "protocolVersion", + "type": "uint256" + } + ], + "name": "LatestProtocolVersionUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "newScheduleId", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + } + ], + "name": "ScheduleIdUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "TimestampSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "protocolVersion", + "type": "uint256" + } + ], + "name": "UpgradeRegistered", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint64", + "name": "activationTimestamp", + "type": "uint64" + } + ], + "name": "ProtocolVersions_ActivationAlreadyPassed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "currentTimestamp", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "newTimestamp", + "type": "uint64" + } + ], + "name": "ProtocolVersions_DelayMustBeLater", + "type": "error" + }, + { + "inputs": [], + "name": "ProtocolVersions_InvalidL2ChainId", + "type": "error" + }, + { + "inputs": [], + "name": "ProtocolVersions_InvalidProtocolVersion", + "type": "error" + }, + { + "inputs": [], + "name": "ProtocolVersions_NotChainTeam", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "ProtocolVersions_NotScheduled", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "ProtocolVersions_UnknownUpgrade", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotProxyAdmin", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotResolvedDelegateProxy", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotSharedProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_ProxyAdminNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "ReinitializableBase_ZeroInitVersion", + "type": "error" + } +] \ No newline at end of file diff --git a/snapshots/semver-lock.json b/snapshots/semver-lock.json index d12c170e7..f5c34b32e 100644 --- a/snapshots/semver-lock.json +++ b/snapshots/semver-lock.json @@ -20,8 +20,8 @@ "sourceCodeHash": "0xa1281f5da03fa0431eabad33da23f51a8d835b0dd04554603fd59e11efe9fa51" }, "src/L1/ProtocolVersions.sol:ProtocolVersions": { - "initCodeHash": "0x35b558b4d20df958b619ad9a446477144d9a1f0cd5115ac02f9862f4a044be5f", - "sourceCodeHash": "0x8416897f453c0520dec6eed7592c25bd2b5d29b2d859907043a1170b39134ff3" + "initCodeHash": "0x72a72461ec9510757d04b19f42df5c55277f197af88dbfaa2f76b01afc7f5522", + "sourceCodeHash": "0xd081e861eff3df66d776fc9be34bd93e7b673c46d7a4220f62932d8cdb816bea" }, "src/L1/SuperchainConfig.sol:SuperchainConfig": { "initCodeHash": "0x9b1f3555b499709485d51d5d9665002c0eb1e5eb893be1fb978a30749e894858", diff --git a/snapshots/storageLayout/ProtocolVersions.json b/snapshots/storageLayout/ProtocolVersions.json new file mode 100644 index 000000000..67a9251c3 --- /dev/null +++ b/snapshots/storageLayout/ProtocolVersions.json @@ -0,0 +1,51 @@ +[ + { + "bytes": "1", + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "uint8" + }, + { + "bytes": "1", + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "bool" + }, + { + "bytes": "32", + "label": "_seed", + "offset": 0, + "slot": "1", + "type": "bytes32" + }, + { + "bytes": "32", + "label": "_timestamps", + "offset": 0, + "slot": "2", + "type": "uint64[]" + }, + { + "bytes": "32", + "label": "_upgradeScheduleId", + "offset": 0, + "slot": "3", + "type": "bytes32[]" + }, + { + "bytes": "32", + "label": "latestProtocolVersion", + "offset": 0, + "slot": "4", + "type": "uint256" + }, + { + "bytes": "20", + "label": "chainTeam", + "offset": 0, + "slot": "5", + "type": "address" + } +] \ No newline at end of file diff --git a/src/L1/ProtocolVersions.sol b/src/L1/ProtocolVersions.sol index 77ae84593..1513d4a99 100644 --- a/src/L1/ProtocolVersions.sol +++ b/src/L1/ProtocolVersions.sol @@ -3,30 +3,72 @@ pragma solidity 0.8.15; // Contracts import { ProxyAdminOwnedBase } from "src/universal/ProxyAdminOwnedBase.sol"; +import { ReinitializableBase } from "src/universal/ReinitializableBase.sol"; +import { Initializable } from "lib/openzeppelin-contracts/contracts/proxy/utils/Initializable.sol"; // Interfaces -import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; +import { ISemver } from "interfaces/universal/ISemver.sol"; /// @title ProtocolVersions /// @notice Security Council-controlled upgrade activation schedule contract. /// @dev Maintains an ordered registry of upgrades and their L2 activation timestamps. +/// Each upgrade is identified by an ascending numeric `id` equal to its registration +/// index (0, 1, 2, ...). Human-readable names are intentionally kept off-chain: a client +/// maps `id` => name via its own static configuration. Because the registry is strictly +/// append-only (upgrades are never removed or reordered), an `id` permanently refers to the +/// same upgrade. +/// /// The canonical schedule commitment (`scheduleId`) is the tail of a per-upgrade hash chain: /// /// seed = keccak256(abi.encode(l2ChainId, address(this))) -/// upgradeScheduleId[0] = keccak256(abi.encode(seed, key_0, timestamp_0)) -/// upgradeScheduleId[i] = keccak256(abi.encode(upgradeScheduleId[i-1], key_i, timestamp_i)) +/// upgradeScheduleId[0] = keccak256(abi.encode(seed, 0, timestamp_0)) +/// upgradeScheduleId[i] = keccak256(abi.encode(upgradeScheduleId[i-1], i, timestamp_i)) /// scheduleId = upgradeScheduleId[n-1] (or seed when n == 0) /// /// where timestamp_i is the upgrade's current activation timestamp (0 = not yet scheduled). /// Changing any upgrade's timestamp recomputes its link and all subsequent links, making -/// scheduleId fully reproducible from (l2ChainId, address, ordered keys, current timestamps). +/// scheduleId fully reproducible from (l2ChainId, address, upgrade count, current timestamps). /// Proof journals bind to `scheduleId`, pinning every proof in a dispute game to the schedule /// that was in effect at the game's L1 origin block. /// -/// An `upgradeId` is a human-readable string (e.g. "canyon"); each is packed into a -/// bytes32 storage `key`. The registry starts empty; all upgrades are added via -/// `registerUpgrade` owner calls. -contract ProtocolVersions is ProxyAdminOwnedBase, IProtocolVersions { +/// The contract is deployed behind an OP proxy: the implementation constructor disables +/// initializers, and `initialize` (run through the proxy) computes and stores `_seed` from the +/// proxy's own `address(this)`, so the schedule commitment is bound to the proxy address that +/// callers and off-chain consumers treat as the registry. +contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, ReinitializableBase, ISemver { + /// @notice A registered upgrade's id, current activation timestamp, and cumulative schedule hash. + struct Upgrade { + uint256 id; + uint64 timestamp; + bytes32 scheduleId; + } + + /// @notice Thrown when the L2 chain ID is zero. + error ProtocolVersions_InvalidL2ChainId(); + /// @notice Thrown when an upgrade id is not registered. + error ProtocolVersions_UnknownUpgrade(uint256 id); + /// @notice Thrown when a protocol version is zero. + error ProtocolVersions_InvalidProtocolVersion(); + /// @notice Thrown when modifying a timestamp whose activation has already passed. + error ProtocolVersions_ActivationAlreadyPassed(uint256 id, uint64 activationTimestamp); + /// @notice Thrown when the caller is not the chainTeam. + error ProtocolVersions_NotChainTeam(); + /// @notice Thrown when delaying an upgrade that has no scheduled activation. + error ProtocolVersions_NotScheduled(uint256 id); + /// @notice Thrown when a new timestamp is not sufficiently later than the current one. + error ProtocolVersions_DelayMustBeLater(uint64 currentTimestamp, uint64 newTimestamp); + + /// @notice Emitted when a new upgrade is registered. + event UpgradeRegistered(uint256 indexed id, uint256 protocolVersion); + /// @notice Emitted when the latest protocol version clients should run is updated. + event LatestProtocolVersionUpdated(uint256 indexed protocolVersion); + /// @notice Emitted when an upgrade's activation timestamp is set, cleared, or delayed. + event TimestampSet(uint256 indexed id, uint256 timestamp); + /// @notice Emitted when the schedule commitment changes. + event ScheduleIdUpdated(bytes32 indexed newScheduleId, uint256 indexed blockNumber); + /// @notice Emitted when the chainTeam role changes. + event ChainTeamUpdated(address indexed previousChainTeam, address indexed newChainTeam); + /// @notice Semantic version. /// @custom:semver 1.0.0 string public constant version = "1.0.0"; @@ -34,26 +76,24 @@ contract ProtocolVersions is ProxyAdminOwnedBase, IProtocolVersions { /// @notice Minimum notice period required when scheduling or modifying an activation timestamp. uint64 public constant MIN_NOTICE = 1 hours; - /// @notice Hash chain seed: keccak256(abi.encode(l2ChainId, address(this))). Cached to avoid - /// recomputing on every _refreshScheduleId call. - bytes32 internal immutable _seed; + /// @notice Hash chain seed: keccak256(abi.encode(l2ChainId, address(this))). Set once in + /// `initialize` (run through the proxy, so `address(this)` is the proxy address) and + /// cached in storage to avoid recomputing on every _refreshScheduleId call. + bytes32 internal _seed; - /// @notice Ordered list of registered upgrade keys. - bytes32[] internal _upgradeKeys; + /// @notice Activation timestamp for each registered upgrade, indexed by upgrade id (0 = not scheduled). + /// An upgrade id is registered iff it is a valid index into this array. + uint64[] internal _timestamps; - /// @notice Cumulative schedule hash up to and including each registered upgrade. - /// upgradeScheduleId[i] = keccak256(abi.encode(upgradeScheduleId[i-1], key_i, timestamps[key_i])). - /// Stored per-key so that changing upgrade j's timestamp requires recomputing only + /// @notice Cumulative schedule hash up to and including each registered upgrade, indexed by upgrade id. + /// _upgradeScheduleId[i] = keccak256(abi.encode(_upgradeScheduleId[i-1], i, _timestamps[i])). + /// Stored per-upgrade so that changing upgrade j's timestamp requires recomputing only /// j..n-1 links rather than the entire chain. - mapping(bytes32 => bytes32) internal _upgradeScheduleId; - - /// @notice Activation timestamp for each registered upgrade key (0 = not scheduled). - mapping(bytes32 => uint64) internal _timestamps; + bytes32[] internal _upgradeScheduleId; - /// @notice Registration sentinel for each upgrade key. - mapping(bytes32 => bool) internal _registered; - - /// @notice The protocol version set at the most recent `registerUpgrade` call. + /// @notice The latest protocol version clients should run. Updated on each `registerUpgrade` + /// call and settable directly by the owner via `setLatestProtocolVersion`. Informational + /// only — read off-chain by clients; not part of the scheduleId commitment. uint256 public latestProtocolVersion; /// @notice Address allowed to delay (push out) already-scheduled activation timestamps. @@ -63,11 +103,23 @@ contract ProtocolVersions is ProxyAdminOwnedBase, IProtocolVersions { /// earlier, or schedule a brand-new activation. Unset (zero) by default. address public chainTeam; - /// @notice Deploys the contract and sets the chain ID. + /// @notice Disables initializers on the implementation so it can only be used behind a proxy. + constructor() ReinitializableBase(1) { + _disableInitializers(); + } + + /// @notice Initializes the registry, computing the hash chain seed from the proxy context and + /// emitting the initial scheduleId. Callable only by the ProxyAdmin or its owner. /// @param _l2ChainId L2 chain ID whose schedule is being committed. - constructor(uint256 _l2ChainId) { + function initialize(uint256 _l2ChainId) external reinitializer(initVersion()) { + // Initialization transactions must come from the ProxyAdmin or its owner. + _assertOnlyProxyAdminOrProxyAdminOwner(); if (_l2ChainId == 0) revert ProtocolVersions_InvalidL2ChainId(); + + // address(this) is the proxy address, so the seed binds the schedule to the proxy registry. _seed = keccak256(abi.encode(_l2ChainId, address(this))); + + emit ScheduleIdUpdated(_seed, block.number); } /// @notice Restricts a call to the ProxyAdmin owner. @@ -79,74 +131,72 @@ contract ProtocolVersions is ProxyAdminOwnedBase, IProtocolVersions { /// @notice Returns the canonical schedule commitment. /// @return The current scheduleId hash. function scheduleId() external view returns (bytes32) { - uint256 n = _upgradeKeys.length; - return n == 0 ? _seed : _upgradeScheduleId[_upgradeKeys[n - 1]]; - } - - /// @notice Returns the activation timestamp for `upgradeId` (0 = not scheduled). - /// @param upgradeId The human-readable upgrade identifier string. - /// @return The scheduled L2 activation timestamp, or 0 if unscheduled. - function getTimestamp(string calldata upgradeId) external view returns (uint256) { - return _timestamps[_registeredKey(upgradeId)]; + uint256 n = _upgradeScheduleId.length; + return n == 0 ? _seed : _upgradeScheduleId[n - 1]; } /// @notice Returns the full ordered schedule: every registered upgrade with its current - /// activation timestamp and cumulative schedule hash. + /// activation timestamp and cumulative schedule hash. The array index equals the + /// upgrade `id`; names are resolved off-chain. /// @dev Calling via eth_call is gas-free; no transaction is submitted. /// @return schedule_ Ordered array of Upgrade structs, one per registered upgrade. function getSchedule() external view returns (Upgrade[] memory schedule_) { - uint256 n = _upgradeKeys.length; + uint256 n = _timestamps.length; schedule_ = new Upgrade[](n); for (uint256 i = 0; i < n; i++) { - bytes32 key = _upgradeKeys[i]; - schedule_[i] = Upgrade({ - name: _nameFromKey(key), - timestamp: _timestamps[key], - scheduleId: _upgradeScheduleId[key] - }); + schedule_[i] = Upgrade({ id: i, timestamp: _timestamps[i], scheduleId: _upgradeScheduleId[i] }); } } - /// @notice Registers a new upgrade by upgradeId with its protocol version. Owner only. - /// @dev The upgradeId is packed into a bytes32 key and `latestProtocolVersion` is updated. - /// Registration extends the scheduleId chain with the new (key, timestamp=0) link. - /// @param upgradeId Human-readable upgrade name (1–32 bytes, e.g. "canyon"). + /// @notice Registers a new upgrade, assigning it the next ascending id. Owner only. + /// @dev The upgrade starts unscheduled (timestamp 0) and `latestProtocolVersion` is updated. + /// Registration extends the scheduleId chain with the new (id, timestamp=0) link. /// @param protocolVersion Packed semver uint256 for this upgrade (must be non-zero). - function registerUpgrade(string calldata upgradeId, uint256 protocolVersion) external onlyProxyAdminOwner { + /// @return id The ascending id assigned to the newly registered upgrade. + function registerUpgrade(uint256 protocolVersion) external onlyProxyAdminOwner returns (uint256 id) { if (protocolVersion == 0) revert ProtocolVersions_InvalidProtocolVersion(); - bytes32 key = _keyFromUpgradeId(upgradeId); - if (_registered[key]) revert ProtocolVersions_UpgradeAlreadyRegistered(key); - uint256 index = _upgradeKeys.length; - _upgradeKeys.push(key); - _registered[key] = true; + id = _timestamps.length; + _timestamps.push(0); + _upgradeScheduleId.push(bytes32(0)); + latestProtocolVersion = protocolVersion; + emit UpgradeRegistered(id, protocolVersion); + _refreshScheduleId(id); + } + + /// @notice Sets the latest protocol version clients should run. Owner (Security Council) only. + /// @dev Informational signal for off-chain clients; independent of the upgrade schedule and NOT + /// part of the scheduleId commitment, so it can be updated at any time without shifting any + /// proof binding. `registerUpgrade` also updates this value. + /// @param protocolVersion Packed semver uint256 (must be non-zero). + function setLatestProtocolVersion(uint256 protocolVersion) external onlyProxyAdminOwner { + if (protocolVersion == 0) revert ProtocolVersions_InvalidProtocolVersion(); latestProtocolVersion = protocolVersion; - emit UpgradeRegistered(key, index, upgradeId, protocolVersion); - _refreshScheduleId(key); + emit LatestProtocolVersionUpdated(protocolVersion); } - /// @notice Sets the activation timestamp for one upgrade by upgradeId. Pass 0 to clear. + /// @notice Sets the activation timestamp for one upgrade by id. Pass 0 to clear. /// @dev The activation timestamp must be at least MIN_NOTICE seconds in the future and the /// upgrade must not have already activated. Pass 0 to remove a not-yet-activated scheduled /// timestamp; reverts if the upgrade has already passed its activation time. - /// @param upgradeId The upgrade to schedule. + /// @param id The upgrade to schedule. /// @param timestamp Future Unix timestamp for L2 activation (must be >= block.timestamp + MIN_NOTICE), or 0 to /// clear. - function setTimestamp(string calldata upgradeId, uint64 timestamp) external onlyProxyAdminOwner { - bytes32 key = _registeredKey(upgradeId); - uint64 current = _timestamps[key]; + function setTimestamp(uint256 id, uint64 timestamp) external onlyProxyAdminOwner { + _assertRegistered(id); + uint64 current = _timestamps[id]; // Cannot modify a timestamp that has already activated. if (current != 0 && uint64(block.timestamp) >= current) { - revert ProtocolVersions_ActivationAlreadyPassed(key, current); + revert ProtocolVersions_ActivationAlreadyPassed(id, current); } // Non-zero timestamps must be at least MIN_NOTICE seconds in the future. if (timestamp != 0 && timestamp < uint64(block.timestamp) + MIN_NOTICE) { revert ProtocolVersions_DelayMustBeLater(current, timestamp); } if (current == timestamp) return; - _timestamps[key] = timestamp; - emit TimestampSet(key, timestamp); - _refreshScheduleId(key); + _timestamps[id] = timestamp; + emit TimestampSet(id, timestamp); + _refreshScheduleId(id); } /// @notice Appoints, replaces, or clears (set to zero) the chainTeam role. Owner only. @@ -163,71 +213,50 @@ contract ProtocolVersions is ProxyAdminOwnedBase, IProtocolVersions { /// delay an activation; it cannot pull one earlier, clear it, or schedule a new one — use /// the owner's `setTimestamp` for those. Because `current` is in the future and `newTimestamp` /// is later still, the new value is always in the future. - /// @param upgradeId The upgrade whose activation to delay. + /// @param id The upgrade whose activation to delay. /// @param newTimestamp New activation timestamp, must be strictly later than the current one. - function delayTimestamp(string calldata upgradeId, uint64 newTimestamp) external { + function delayTimestamp(uint256 id, uint64 newTimestamp) external { if (msg.sender != chainTeam) revert ProtocolVersions_NotChainTeam(); - bytes32 key = _registeredKey(upgradeId); - uint64 current = _timestamps[key]; + _assertRegistered(id); + uint64 current = _timestamps[id]; // The upgrade must already have a scheduled activation to delay. - if (current == 0) revert ProtocolVersions_NotScheduled(key); + if (current == 0) revert ProtocolVersions_NotScheduled(id); // Cannot modify an activation that has already passed. if (uint64(block.timestamp) >= current) { - revert ProtocolVersions_ActivationAlreadyPassed(key, current); + revert ProtocolVersions_ActivationAlreadyPassed(id, current); + } + // The role can only push the activation later and must be greater than the MIN_NOTICE buffer, never earlier or + // to the same time. + if (newTimestamp <= current || newTimestamp < uint64(block.timestamp) + MIN_NOTICE) { + revert ProtocolVersions_DelayMustBeLater(current, newTimestamp); } - // The role can only push the activation later and must be greater than the MIN_NOTICE buffer, never earlier or to the same time. - if (newTimestamp <= current || newTimestamp < uint64(block.timestamp) + MIN_NOTICE) revert ProtocolVersions_DelayMustBeLater(current, newTimestamp); - _timestamps[key] = newTimestamp; - emit TimestampSet(key, newTimestamp); - _refreshScheduleId(key); + _timestamps[id] = newTimestamp; + emit TimestampSet(id, newTimestamp); + _refreshScheduleId(id); } - /// @dev Recomputes the per-upgrade cumulative hash chain starting from `changedKey` and bubbles - /// the result through all subsequent registered upgrades. Cost is O(n-j) where j is the - /// 0-based registration index of `changedKey`. - function _refreshScheduleId(bytes32 changedKey) internal { - uint256 n = _upgradeKeys.length; + /// @dev Recomputes the per-upgrade cumulative hash chain starting from upgrade `startIndex` and + /// bubbles the result through all subsequent registered upgrades. Cost is O(n-startIndex). + /// Only ever called after a state change (registration, or a timestamp that actually moved), + /// so the resulting tail is always a new scheduleId. + function _refreshScheduleId(uint256 startIndex) internal { + uint256 n = _timestamps.length; - // Locate changedKey and initialise prev to the hash of the preceding link (or seed). - uint256 startIndex; - for (startIndex = 0; startIndex < n; startIndex++) { - if (_upgradeKeys[startIndex] == changedKey) break; - } - bytes32 prev = startIndex == 0 ? _seed : _upgradeScheduleId[_upgradeKeys[startIndex - 1]]; - // Recompute from startIndex onward, storing each link. + // Initialise prev to the hash of the preceding link (or seed), then recompute from + // startIndex onward, storing each link. + bytes32 prev = startIndex == 0 ? _seed : _upgradeScheduleId[startIndex - 1]; for (uint256 i = startIndex; i < n; i++) { - bytes32 k = _upgradeKeys[i]; - prev = keccak256(abi.encode(prev, k, _timestamps[k])); - _upgradeScheduleId[k] = prev; + prev = keccak256(abi.encode(prev, i, _timestamps[i])); + _upgradeScheduleId[i] = prev; } - } - /// @dev Resolves an upgradeId string to its storage key, reverting if not registered. - function _registeredKey(string calldata upgradeId) internal view returns (bytes32 key) { - key = _keyFromUpgradeId(upgradeId); - if (!_registered[key]) revert ProtocolVersions_UnknownUpgradeName(upgradeId); + emit ScheduleIdUpdated(prev, block.number); } - /// @dev Packs a UTF-8 upgradeId string (1–32 bytes) into a bytes32 key. Shorter strings are - /// zero-padded on the right; strings differing only by trailing null bytes map to the same - /// key. Callers are expected to use printable ASCII names only. - function _keyFromUpgradeId(string calldata upgradeId) internal pure returns (bytes32 key) { - bytes memory raw = bytes(upgradeId); - if (raw.length == 0 || raw.length > 32) revert ProtocolVersions_InvalidUpgradeId(); - key = bytes32(raw); - } - - /// @dev Recovers the original upgradeId string from its bytes32 key by stripping trailing - /// zero bytes. Correct for printable ASCII names, which contain no embedded null bytes. - function _nameFromKey(bytes32 key) internal pure returns (string memory) { - uint256 len = 32; - while (len > 0 && key[len - 1] == 0) len--; - bytes memory b = new bytes(len); - for (uint256 i = 0; i < len; i++) { - b[i] = key[i]; - } - return string(b); + /// @dev Reverts if `id` is not a registered upgrade. + function _assertRegistered(uint256 id) internal view { + if (id >= _timestamps.length) revert ProtocolVersions_UnknownUpgrade(id); } } diff --git a/test/L1/ProtocolVersions.t.sol b/test/L1/ProtocolVersions.t.sol index d0489cbe0..4087db3bb 100644 --- a/test/L1/ProtocolVersions.t.sol +++ b/test/L1/ProtocolVersions.t.sol @@ -7,56 +7,108 @@ import { Test } from "lib/forge-std/src/Test.sol"; // Contracts import { ProtocolVersions } from "src/L1/ProtocolVersions.sol"; import { ProxyAdminOwnedBase } from "src/universal/ProxyAdminOwnedBase.sol"; +import { Proxy } from "src/universal/Proxy.sol"; // Interfaces import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; - -// Mocks -import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; +import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; /// @title ProtocolVersions_TestInit /// @notice Reusable test initialization for ProtocolVersions tests. abstract contract ProtocolVersions_TestInit is Test { - event UpgradeRegistered(bytes32 indexed key, uint256 indexed index, string upgradeId, uint256 protocolVersion); + event UpgradeRegistered(uint256 indexed id, uint256 protocolVersion); + event LatestProtocolVersionUpdated(uint256 indexed protocolVersion); event ChainTeamUpdated(address indexed previousChainTeam, address indexed newChainTeam); - event TimestampSet(bytes32 indexed key, uint256 timestamp); + event TimestampSet(uint256 indexed id, uint256 timestamp); + /// @dev Ascending ids assigned by registration order in these tests. + uint256 internal constant CANYON = 0; + uint256 internal constant ECOTONE = 1; + uint256 internal constant L2_CHAIN_ID = 8453; address internal _owner = makeAddr("owner"); address internal _nonOwner = makeAddr("non-owner"); address internal _chainTeam = makeAddr("chain-team"); + address internal _proxyAdmin = makeAddr("proxy-admin"); + ProtocolVersions internal _impl; ProtocolVersions internal protocolVersions; function setUp() public virtual { - protocolVersions = new ProtocolVersions(8453); - address mockProxyAdmin = makeAddr("proxy-admin"); - vm.mockCall(mockProxyAdmin, abi.encodeWithSignature("owner()"), abi.encode(_owner)); - EIP1967Helper.setAdmin(address(protocolVersions), mockProxyAdmin); + // proxyAdminOwner() resolves by calling owner() on the ProxyAdmin stored in the proxy slot. + vm.mockCall(_proxyAdmin, abi.encodeWithSignature("owner()"), abi.encode(_owner)); + _impl = new ProtocolVersions(); + protocolVersions = ProtocolVersions(_deployInitializedProxy(L2_CHAIN_ID)); + } + + /// @dev Deploys a proxy (admin = _proxyAdmin) over the shared impl and initializes it via the proxy. + function _deployInitializedProxy(uint256 _l2ChainId) internal returns (address proxy_) { + Proxy proxy = new Proxy(_proxyAdmin); + vm.prank(_proxyAdmin); + proxy.upgradeToAndCall(address(_impl), abi.encodeCall(IProtocolVersions.initialize, (_l2ChainId))); + proxy_ = address(proxy); } - /// @dev Registers `canyon` and schedules it for block.timestamp + MIN_NOTICE + delay. + /// @dev Deploys an uninitialized proxy over the shared impl (for initializer tests). + function _deployUninitializedProxy() internal returns (ProtocolVersions) { + Proxy proxy = new Proxy(_proxyAdmin); + vm.prank(_proxyAdmin); + proxy.upgradeTo(address(_impl)); + return ProtocolVersions(address(proxy)); + } + + /// @dev Registers the first upgrade (id CANYON) and schedules it for block.timestamp + MIN_NOTICE + delay. function _scheduleCanyon(uint64 _delay) internal returns (uint64 ts_) { vm.prank(_owner); - protocolVersions.registerUpgrade("canyon", 1); + protocolVersions.registerUpgrade(1); ts_ = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + _delay; vm.prank(_owner); - protocolVersions.setTimestamp("canyon", ts_); + protocolVersions.setTimestamp(CANYON, ts_); } } -/// @title ProtocolVersions_Constructor_Test -/// @notice Test contract for the ProtocolVersions constructor. -contract ProtocolVersions_Constructor_Test is ProtocolVersions_TestInit { - /// @notice Tests that the constructor sets the correct initial state. - function test_constructor_setsInitialState_succeeds() external view { +/// @title ProtocolVersions_Initialize_Test +/// @notice Test contract for the ProtocolVersions initializer. +contract ProtocolVersions_Initialize_Test is ProtocolVersions_TestInit { + /// @notice Tests that initialization sets the correct initial state. + function test_initialize_setsInitialState_succeeds() external view { assertEq(protocolVersions.proxyAdminOwner(), _owner); assertNotEq(protocolVersions.scheduleId(), bytes32(0)); } - /// @notice Tests that the constructor reverts when the chain ID is zero. - function test_constructor_zeroChainId_reverts() external { + /// @notice Tests that the scheduleId seed binds to the proxy address, not the implementation. + function test_initialize_seedBindsToProxyAddress_succeeds() external view { + bytes32 expectedSeed = keccak256(abi.encode(L2_CHAIN_ID, address(protocolVersions))); + assertEq(protocolVersions.scheduleId(), expectedSeed); + } + + /// @notice Tests that initializing with a zero chain ID reverts. + function test_initialize_zeroChainId_reverts() external { + ProtocolVersions uninitialized = _deployUninitializedProxy(); vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidL2ChainId.selector); - new ProtocolVersions(0); + vm.prank(_proxyAdmin); + uninitialized.initialize(0); + } + + /// @notice Tests that only the ProxyAdmin or its owner can initialize. + function test_initialize_notProxyAdminOrOwner_reverts() external { + ProtocolVersions uninitialized = _deployUninitializedProxy(); + vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner.selector); + vm.prank(_nonOwner); + uninitialized.initialize(L2_CHAIN_ID); + } + + /// @notice Tests that the contract cannot be initialized twice. + function test_initialize_alreadyInitialized_reverts() external { + vm.expectRevert("Initializable: contract is already initialized"); + vm.prank(_proxyAdmin); + protocolVersions.initialize(L2_CHAIN_ID); + } + + /// @notice Tests that the implementation itself cannot be initialized (initializers disabled). + function test_initialize_implementationDisabled_reverts() external { + vm.expectRevert("Initializable: contract is already initialized"); + vm.prank(_proxyAdmin); + _impl.initialize(L2_CHAIN_ID); } } @@ -78,83 +130,98 @@ contract ProtocolVersions_RegisterUpgrade_Test is ProtocolVersions_TestInit { vm.roll(block.number + 1); vm.prank(_owner); - protocolVersions.registerUpgrade("canyon", 1); + protocolVersions.registerUpgrade(1); assertNotEq(protocolVersions.scheduleId(), idBefore); } + /// @notice Tests that `registerUpgrade` assigns ascending ids and returns them. + function test_registerUpgrade_returnsAscendingIds_succeeds() external { + vm.prank(_owner); + assertEq(protocolVersions.registerUpgrade(1), 0); + vm.prank(_owner); + assertEq(protocolVersions.registerUpgrade(2), 1); + vm.prank(_owner); + assertEq(protocolVersions.registerUpgrade(3), 2); + } + /// @notice Tests that `registerUpgrade` emits the `UpgradeRegistered` event with correct fields. function test_registerUpgrade_emitsEvent_succeeds() external { - vm.expectEmit(true, true, true, true, address(protocolVersions)); - emit UpgradeRegistered(bytes32("canyon"), 0, "canyon", 1); + vm.expectEmit(true, false, false, true, address(protocolVersions)); + emit UpgradeRegistered(0, 1); vm.prank(_owner); - protocolVersions.registerUpgrade("canyon", 1); + protocolVersions.registerUpgrade(1); - vm.expectEmit(true, true, true, true, address(protocolVersions)); - emit UpgradeRegistered(bytes32("ecotone"), 1, "ecotone", 2); + vm.expectEmit(true, false, false, true, address(protocolVersions)); + emit UpgradeRegistered(1, 2); vm.prank(_owner); - protocolVersions.registerUpgrade("ecotone", 2); + protocolVersions.registerUpgrade(2); } /// @notice Tests that registering upgrades updates latestProtocolVersion to the most recent. function test_registerUpgrade_updatesLatestProtocolVersion_succeeds() external { vm.prank(_owner); - protocolVersions.registerUpgrade("canyon", 5); + protocolVersions.registerUpgrade(5); assertEq(protocolVersions.latestProtocolVersion(), 5); vm.prank(_owner); - protocolVersions.registerUpgrade("ecotone", 9); + protocolVersions.registerUpgrade(9); assertEq(protocolVersions.latestProtocolVersion(), 9); } - /// @notice Tests that registering a 32-byte name (the maximum) succeeds. - function test_registerUpgrade_exactly32ByteName_succeeds() external { - string memory name32 = "exactly-32-bytes-upgrade-name!!!"; + /// @notice Tests that registering an upgrade with a zero protocolVersion reverts. + function test_registerUpgrade_zeroProtocolVersion_reverts() external { + vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidProtocolVersion.selector); vm.prank(_owner); - protocolVersions.registerUpgrade(name32, 7); - assertEq(protocolVersions.latestProtocolVersion(), 7); + protocolVersions.registerUpgrade(0); } - /// @notice Tests that registering the same upgrade twice reverts. - function test_registerUpgrade_duplicate_reverts() external { - vm.prank(_owner); - protocolVersions.registerUpgrade("canyon", 1); + /// @notice Tests that only the owner can call `registerUpgrade`. + function test_registerUpgrade_callerNotOwner_reverts() external { + vm.expectRevert(ProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); + vm.prank(_nonOwner); + protocolVersions.registerUpgrade(1); + } +} - vm.expectRevert( - abi.encodeWithSelector( - IProtocolVersions.ProtocolVersions_UpgradeAlreadyRegistered.selector, bytes32("canyon") - ) - ); +/// @title ProtocolVersions_SetLatestProtocolVersion_Test +/// @notice Test contract for the `setLatestProtocolVersion` function. +contract ProtocolVersions_SetLatestProtocolVersion_Test is ProtocolVersions_TestInit { + /// @notice Tests that the owner can set the latest protocol version directly. + function test_setLatestProtocolVersion_updates_succeeds() external { vm.prank(_owner); - protocolVersions.registerUpgrade("canyon", 1); + protocolVersions.setLatestProtocolVersion(42); + assertEq(protocolVersions.latestProtocolVersion(), 42); } - /// @notice Tests that registering an upgrade with a zero protocolVersion reverts. - function test_registerUpgrade_zeroProtocolVersion_reverts() external { - vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidProtocolVersion.selector); + /// @notice Tests that setting the latest protocol version does not change the scheduleId. + function test_setLatestProtocolVersion_doesNotChangeScheduleId_succeeds() external { + bytes32 scheduleIdBefore = protocolVersions.scheduleId(); vm.prank(_owner); - protocolVersions.registerUpgrade("canyon", 0); + protocolVersions.setLatestProtocolVersion(42); + assertEq(protocolVersions.scheduleId(), scheduleIdBefore); } - /// @notice Tests that registering an upgrade with an empty name reverts. - function test_registerUpgrade_emptyName_reverts() external { - vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidUpgradeId.selector); + /// @notice Tests that `setLatestProtocolVersion` emits the `LatestProtocolVersionUpdated` event. + function test_setLatestProtocolVersion_emitsEvent_succeeds() external { + vm.expectEmit(true, false, false, true, address(protocolVersions)); + emit LatestProtocolVersionUpdated(42); vm.prank(_owner); - protocolVersions.registerUpgrade("", 1); + protocolVersions.setLatestProtocolVersion(42); } - /// @notice Tests that registering an upgrade with a name longer than 32 bytes reverts. - function test_registerUpgrade_nameTooLong_reverts() external { - vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidUpgradeId.selector); + /// @notice Tests that setting a zero protocol version reverts. + function test_setLatestProtocolVersion_zero_reverts() external { + vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidProtocolVersion.selector); vm.prank(_owner); - protocolVersions.registerUpgrade("this-upgrade-name-is-definitely-too-long", 1); + protocolVersions.setLatestProtocolVersion(0); } - /// @notice Tests that only the owner can call `registerUpgrade`. - function test_registerUpgrade_callerNotOwner_reverts() external { + /// @notice Tests that only the owner can call `setLatestProtocolVersion`. + function test_setLatestProtocolVersion_callerNotOwner_reverts() external { vm.expectRevert(ProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); vm.prank(_nonOwner); - protocolVersions.registerUpgrade("antares", 1); + protocolVersions.setLatestProtocolVersion(42); } } @@ -165,36 +232,36 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { function test_setTimestamp_updatesTimestampAndScheduleId_succeeds() external { bytes32 initialScheduleId = protocolVersions.scheduleId(); vm.prank(_owner); - protocolVersions.registerUpgrade("canyon", 1); + protocolVersions.registerUpgrade(1); vm.roll(block.number + 1); vm.warp(block.timestamp + 1); uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; vm.prank(_owner); - protocolVersions.setTimestamp("canyon", ts); + protocolVersions.setTimestamp(CANYON, ts); - assertEq(protocolVersions.getTimestamp("canyon"), ts); + assertEq(protocolVersions.getSchedule()[CANYON].timestamp, ts); assertNotEq(protocolVersions.scheduleId(), initialScheduleId); } /// @notice Tests that calling `setTimestamp` with the same value is a no-op for scheduleId. function test_setTimestamp_sameTimestamp_noScheduleIdChange_succeeds() external { vm.prank(_owner); - protocolVersions.registerUpgrade("canyon", 1); + protocolVersions.registerUpgrade(1); vm.roll(block.number + 1); vm.warp(block.timestamp + 1); uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; vm.prank(_owner); - protocolVersions.setTimestamp("canyon", ts); + protocolVersions.setTimestamp(CANYON, ts); bytes32 scheduleIdAfterSet = protocolVersions.scheduleId(); vm.roll(block.number + 1); vm.prank(_owner); - protocolVersions.setTimestamp("canyon", ts); + protocolVersions.setTimestamp(CANYON, ts); - assertEq(protocolVersions.getTimestamp("canyon"), ts); + assertEq(protocolVersions.getSchedule()[CANYON].timestamp, ts); assertEq(protocolVersions.scheduleId(), scheduleIdAfterSet); } @@ -202,136 +269,125 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { /// restores it to the value it held immediately after registration (ts=0 link). function test_setTimestamp_clearTimestamp_succeeds() external { vm.prank(_owner); - protocolVersions.registerUpgrade("canyon", 1); + protocolVersions.registerUpgrade(1); bytes32 scheduleIdAfterRegister = protocolVersions.scheduleId(); vm.roll(block.number + 1); vm.warp(block.timestamp + 1); uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; vm.prank(_owner); - protocolVersions.setTimestamp("canyon", ts); + protocolVersions.setTimestamp(CANYON, ts); bytes32 scheduleIdAfterSet = protocolVersions.scheduleId(); assertNotEq(scheduleIdAfterSet, scheduleIdAfterRegister); vm.roll(block.number + 1); vm.prank(_owner); - protocolVersions.setTimestamp("canyon", 0); + protocolVersions.setTimestamp(CANYON, 0); - assertEq(protocolVersions.getTimestamp("canyon"), 0); + assertEq(protocolVersions.getSchedule()[CANYON].timestamp, 0); assertEq(protocolVersions.scheduleId(), scheduleIdAfterRegister); } /// @notice Tests that `setTimestamp` emits a `TimestampSet` event. function test_setTimestamp_emitsEvent_succeeds() external { vm.prank(_owner); - protocolVersions.registerUpgrade("canyon", 1); + protocolVersions.registerUpgrade(1); uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; vm.expectEmit(true, false, false, true, address(protocolVersions)); - emit TimestampSet(bytes32("canyon"), ts); + emit TimestampSet(CANYON, ts); vm.prank(_owner); - protocolVersions.setTimestamp("canyon", ts); + protocolVersions.setTimestamp(CANYON, ts); } /// @notice Tests that only the owner can call `setTimestamp`. function test_setTimestamp_callerNotOwner_reverts() external { vm.prank(_owner); - protocolVersions.registerUpgrade("canyon", 1); + protocolVersions.registerUpgrade(1); uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; vm.expectRevert(ProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); vm.prank(_nonOwner); - protocolVersions.setTimestamp("canyon", ts); + protocolVersions.setTimestamp(CANYON, ts); } /// @notice Tests that `setTimestamp` reverts when the timestamp is in the past. function test_setTimestamp_timestampInPast_reverts() external { vm.prank(_owner); - protocolVersions.registerUpgrade("canyon", 1); + protocolVersions.registerUpgrade(1); vm.warp(1000); vm.expectRevert( abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_DelayMustBeLater.selector, uint64(0), uint64(500)) ); vm.prank(_owner); - protocolVersions.setTimestamp("canyon", 500); + protocolVersions.setTimestamp(CANYON, 500); } /// @notice Tests that `setTimestamp` reverts when the timestamp is within MIN_NOTICE of now. function test_setTimestamp_insufficientNotice_reverts() external { vm.prank(_owner); - protocolVersions.registerUpgrade("canyon", 1); + protocolVersions.registerUpgrade(1); uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() - 1; vm.expectRevert( abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_DelayMustBeLater.selector, uint64(0), ts) ); vm.prank(_owner); - protocolVersions.setTimestamp("canyon", ts); + protocolVersions.setTimestamp(CANYON, ts); } /// @notice Tests that `setTimestamp` reverts when the upgrade has already activated. function test_setTimestamp_afterActivation_reverts() external { vm.prank(_owner); - protocolVersions.registerUpgrade("canyon", 1); + protocolVersions.registerUpgrade(1); vm.warp(100); uint64 activationTs = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; vm.prank(_owner); - protocolVersions.setTimestamp("canyon", activationTs); + protocolVersions.setTimestamp(CANYON, activationTs); vm.warp(activationTs + 1); uint64 laterTs = activationTs + protocolVersions.MIN_NOTICE() + 100; - bytes32 canyonKey = bytes32("canyon"); vm.expectRevert( abi.encodeWithSelector( - IProtocolVersions.ProtocolVersions_ActivationAlreadyPassed.selector, canyonKey, activationTs + IProtocolVersions.ProtocolVersions_ActivationAlreadyPassed.selector, CANYON, activationTs ) ); vm.prank(_owner); - protocolVersions.setTimestamp("canyon", laterTs); + protocolVersions.setTimestamp(CANYON, laterTs); } /// @notice Tests that `setTimestamp` reverts for an unregistered upgrade. function test_setTimestamp_unregisteredUpgrade_reverts() external { uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; - vm.expectRevert( - abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_UnknownUpgradeName.selector, "antares") - ); + vm.expectRevert(abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_UnknownUpgrade.selector, uint256(0))); vm.prank(_owner); - protocolVersions.setTimestamp("antares", ts); + protocolVersions.setTimestamp(0, ts); } - /// @notice Tests that scheduleId is reproducible from (chainId, address, ordered keys, timestamps). + /// @notice Tests that scheduleId is reproducible from (chainId, address, ascending ids, timestamps). function test_setTimestamp_scheduleIdReproducible_succeeds() external { vm.prank(_owner); - protocolVersions.registerUpgrade("canyon", 1); + protocolVersions.registerUpgrade(1); vm.prank(_owner); - protocolVersions.registerUpgrade("ecotone", 2); + protocolVersions.registerUpgrade(2); uint64 ts1 = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; uint64 ts2 = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 200; vm.prank(_owner); - protocolVersions.setTimestamp("canyon", ts1); + protocolVersions.setTimestamp(CANYON, ts1); vm.prank(_owner); - protocolVersions.setTimestamp("ecotone", ts2); + protocolVersions.setTimestamp(ECOTONE, ts2); // Reproduce the chain from scratch. bytes32 seed = keccak256(abi.encode(uint256(8453), address(protocolVersions))); - bytes32 link0 = keccak256(abi.encode(seed, bytes32("canyon"), ts1)); - bytes32 link1 = keccak256(abi.encode(link0, bytes32("ecotone"), ts2)); + bytes32 link0 = keccak256(abi.encode(seed, uint256(0), ts1)); + bytes32 link1 = keccak256(abi.encode(link0, uint256(1), ts2)); assertEq(protocolVersions.scheduleId(), link1); } - - /// @notice Tests that `getTimestamp` reverts for an unknown upgrade name. - function test_getTimestamp_unknownUpgrade_reverts() external { - vm.expectRevert( - abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_UnknownUpgradeName.selector, "unknown") - ); - protocolVersions.getTimestamp("unknown"); - } } /// @title ProtocolVersions_DelayTimestamp_Test @@ -348,9 +404,9 @@ contract ProtocolVersions_DelayTimestamp_Test is ProtocolVersions_TestInit { uint64 later = ts + 50; vm.prank(_chainTeam); - protocolVersions.delayTimestamp("canyon", later); + protocolVersions.delayTimestamp(CANYON, later); - assertEq(protocolVersions.getTimestamp("canyon"), later); + assertEq(protocolVersions.getSchedule()[CANYON].timestamp, later); assertNotEq(protocolVersions.scheduleId(), scheduleIdBefore); } @@ -362,11 +418,11 @@ contract ProtocolVersions_DelayTimestamp_Test is ProtocolVersions_TestInit { vm.expectRevert(IProtocolVersions.ProtocolVersions_NotChainTeam.selector); vm.prank(_owner); - protocolVersions.delayTimestamp("canyon", ts + 50); + protocolVersions.delayTimestamp(CANYON, ts + 50); vm.expectRevert(IProtocolVersions.ProtocolVersions_NotChainTeam.selector); vm.prank(_nonOwner); - protocolVersions.delayTimestamp("canyon", ts + 50); + protocolVersions.delayTimestamp(CANYON, ts + 50); } /// @notice Tests that `delayTimestamp` reverts when the new timestamp is earlier than current. @@ -379,7 +435,7 @@ contract ProtocolVersions_DelayTimestamp_Test is ProtocolVersions_TestInit { abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_DelayMustBeLater.selector, ts, ts - 10) ); vm.prank(_chainTeam); - protocolVersions.delayTimestamp("canyon", ts - 10); + protocolVersions.delayTimestamp(CANYON, ts - 10); } /// @notice Tests that `delayTimestamp` reverts when the new timestamp equals the current one. @@ -390,22 +446,20 @@ contract ProtocolVersions_DelayTimestamp_Test is ProtocolVersions_TestInit { vm.expectRevert(abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_DelayMustBeLater.selector, ts, ts)); vm.prank(_chainTeam); - protocolVersions.delayTimestamp("canyon", ts); + protocolVersions.delayTimestamp(CANYON, ts); } /// @notice Tests that `delayTimestamp` reverts when the upgrade has no scheduled timestamp. function test_delayTimestamp_notScheduled_reverts() external { vm.prank(_owner); - protocolVersions.registerUpgrade("canyon", 1); + protocolVersions.registerUpgrade(1); vm.prank(_owner); protocolVersions.setChainTeam(_chainTeam); uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; - vm.expectRevert( - abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_NotScheduled.selector, bytes32("canyon")) - ); + vm.expectRevert(abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_NotScheduled.selector, CANYON)); vm.prank(_chainTeam); - protocolVersions.delayTimestamp("canyon", ts); + protocolVersions.delayTimestamp(CANYON, ts); } /// @notice Tests that `delayTimestamp` reverts when the upgrade has already activated. @@ -416,12 +470,10 @@ contract ProtocolVersions_DelayTimestamp_Test is ProtocolVersions_TestInit { vm.warp(ts + 1); vm.expectRevert( - abi.encodeWithSelector( - IProtocolVersions.ProtocolVersions_ActivationAlreadyPassed.selector, bytes32("canyon"), ts - ) + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_ActivationAlreadyPassed.selector, CANYON, ts) ); vm.prank(_chainTeam); - protocolVersions.delayTimestamp("canyon", ts + 100); + protocolVersions.delayTimestamp(CANYON, ts + 100); } /// @notice Tests that `delayTimestamp` reverts for an unregistered upgrade. @@ -430,11 +482,9 @@ contract ProtocolVersions_DelayTimestamp_Test is ProtocolVersions_TestInit { protocolVersions.setChainTeam(_chainTeam); uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; - vm.expectRevert( - abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_UnknownUpgradeName.selector, "antares") - ); + vm.expectRevert(abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_UnknownUpgrade.selector, uint256(0))); vm.prank(_chainTeam); - protocolVersions.delayTimestamp("antares", ts); + protocolVersions.delayTimestamp(0, ts); } } @@ -498,22 +548,22 @@ contract ProtocolVersions_GetView_Test is ProtocolVersions_TestInit { /// @notice Tests that `getSchedule` returns all upgrades in registration order with correct fields. function test_getSchedule_returnsFullSchedule_succeeds() external { vm.prank(_owner); - protocolVersions.registerUpgrade("canyon", 1); + protocolVersions.registerUpgrade(1); vm.prank(_owner); - protocolVersions.registerUpgrade("ecotone", 2); + protocolVersions.registerUpgrade(2); uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; vm.prank(_owner); - protocolVersions.setTimestamp("canyon", ts); + protocolVersions.setTimestamp(CANYON, ts); - IProtocolVersions.Upgrade[] memory s = protocolVersions.getSchedule(); + ProtocolVersions.Upgrade[] memory s = protocolVersions.getSchedule(); assertEq(s.length, 2); - assertEq(s[0].name, "canyon"); + assertEq(s[0].id, CANYON); assertEq(s[0].timestamp, ts); - assertEq(s[1].name, "ecotone"); + assertEq(s[1].id, ECOTONE); assertEq(s[1].timestamp, 0); // The last entry's scheduleId is the contract's current scheduleId. From b917183433db43df8d6056194495415fd07242e0 Mon Sep 17 00:00:00 2001 From: PelleKrab Date: Mon, 6 Jul 2026 14:42:35 -0700 Subject: [PATCH 09/20] fix(L1): apply code-review corrections to ProtocolVersions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seven findings from a multi-angle review; fixes applied: * emit LatestProtocolVersionUpdated in registerUpgrade — previously only setLatestProtocolVersion fired the event, so indexers missed every version bump that came through registration. * Guard scheduleId() before initialization — returns bytes32(0) while _seed is unset, which could be picked up as a valid schedule commitment; now reverts with ProtocolVersions_NotInitialized. * Dedicate ProtocolVersions_InsufficientNotice for setTimestamp — the MIN_NOTICE check in setTimestamp reused DelayMustBeLater with current=0 as the floor, which was misleading; a new error carries only the offending timestamp. * Split delayTimestamp compound condition — the combined || condition reported current as the floor even when only the MIN_NOTICE leg fired; each constraint now reverts independently with the correct floor value. * Extract _setTimestamp internal — setTimestamp and delayTimestamp duplicated the activation-already-passed check and the write+emit+refresh triple; _setTimestamp holds those shared invariants and each public function keeps only its distinct preconditions. * Drop redundant zero-write in registerUpgrade — push(bytes32(0)) wrote zero to a slot that _refreshScheduleId immediately overwrites; replaced with push() (no argument) to save one SSTORE. Generated with Claude Code Co-Authored-By: Claude --- interfaces/L1/IProtocolVersions.sol | 2 ++ src/L1/ProtocolVersions.sol | 53 ++++++++++++++++------------- test/L1/ProtocolVersions.t.sol | 4 +-- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/interfaces/L1/IProtocolVersions.sol b/interfaces/L1/IProtocolVersions.sol index a5b331c70..5de32a1fc 100644 --- a/interfaces/L1/IProtocolVersions.sol +++ b/interfaces/L1/IProtocolVersions.sol @@ -21,6 +21,8 @@ interface IProtocolVersions is IProxyAdminOwnedBase, ISemver, IReinitializableBa error ProtocolVersions_NotChainTeam(); error ProtocolVersions_NotScheduled(uint256 id); error ProtocolVersions_DelayMustBeLater(uint64 currentTimestamp, uint64 newTimestamp); + error ProtocolVersions_NotInitialized(); + error ProtocolVersions_InsufficientNotice(uint64 timestamp); event UpgradeRegistered(uint256 indexed id, uint256 protocolVersion); event LatestProtocolVersionUpdated(uint256 indexed protocolVersion); diff --git a/src/L1/ProtocolVersions.sol b/src/L1/ProtocolVersions.sol index 1513d4a99..4c77a2e08 100644 --- a/src/L1/ProtocolVersions.sol +++ b/src/L1/ProtocolVersions.sol @@ -57,6 +57,10 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable error ProtocolVersions_NotScheduled(uint256 id); /// @notice Thrown when a new timestamp is not sufficiently later than the current one. error ProtocolVersions_DelayMustBeLater(uint64 currentTimestamp, uint64 newTimestamp); + /// @notice Thrown when scheduleId is read before initialize has been called. + error ProtocolVersions_NotInitialized(); + /// @notice Thrown when a non-zero timestamp does not provide at least MIN_NOTICE seconds of notice. + error ProtocolVersions_InsufficientNotice(uint64 timestamp); /// @notice Emitted when a new upgrade is registered. event UpgradeRegistered(uint256 indexed id, uint256 protocolVersion); @@ -131,6 +135,7 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable /// @notice Returns the canonical schedule commitment. /// @return The current scheduleId hash. function scheduleId() external view returns (bytes32) { + if (_seed == 0) revert ProtocolVersions_NotInitialized(); uint256 n = _upgradeScheduleId.length; return n == 0 ? _seed : _upgradeScheduleId[n - 1]; } @@ -158,9 +163,10 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable id = _timestamps.length; _timestamps.push(0); - _upgradeScheduleId.push(bytes32(0)); + _upgradeScheduleId.push(); latestProtocolVersion = protocolVersion; emit UpgradeRegistered(id, protocolVersion); + emit LatestProtocolVersionUpdated(protocolVersion); _refreshScheduleId(id); } @@ -183,20 +189,11 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable /// @param timestamp Future Unix timestamp for L2 activation (must be >= block.timestamp + MIN_NOTICE), or 0 to /// clear. function setTimestamp(uint256 id, uint64 timestamp) external onlyProxyAdminOwner { - _assertRegistered(id); - uint64 current = _timestamps[id]; - // Cannot modify a timestamp that has already activated. - if (current != 0 && uint64(block.timestamp) >= current) { - revert ProtocolVersions_ActivationAlreadyPassed(id, current); - } - // Non-zero timestamps must be at least MIN_NOTICE seconds in the future. + // Non-zero timestamps must provide at least MIN_NOTICE seconds of notice. if (timestamp != 0 && timestamp < uint64(block.timestamp) + MIN_NOTICE) { - revert ProtocolVersions_DelayMustBeLater(current, timestamp); + revert ProtocolVersions_InsufficientNotice(timestamp); } - if (current == timestamp) return; - _timestamps[id] = timestamp; - emit TimestampSet(id, timestamp); - _refreshScheduleId(id); + _setTimestamp(id, timestamp); } /// @notice Appoints, replaces, or clears (set to zero) the chainTeam role. Owner only. @@ -222,18 +219,28 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable // The upgrade must already have a scheduled activation to delay. if (current == 0) revert ProtocolVersions_NotScheduled(id); - // Cannot modify an activation that has already passed. - if (uint64(block.timestamp) >= current) { + // Cannot delay an activation that has already passed. + if (uint64(block.timestamp) >= current) revert ProtocolVersions_ActivationAlreadyPassed(id, current); + // The role can only push the activation later, never to the same time or earlier. + if (newTimestamp <= current) revert ProtocolVersions_DelayMustBeLater(current, newTimestamp); + // The new timestamp must also provide at least MIN_NOTICE seconds of notice from now. + uint64 minFloor = uint64(block.timestamp) + MIN_NOTICE; + if (newTimestamp < minFloor) revert ProtocolVersions_DelayMustBeLater(minFloor, newTimestamp); + _setTimestamp(id, newTimestamp); + } + + /// @dev Shared write path: validates activation hasn't passed, skips no-ops, then writes newTs, + /// emits TimestampSet, and refreshes the hash chain. Callers handle access control, + /// ordering constraints, and MIN_NOTICE checks before invoking. + function _setTimestamp(uint256 id, uint64 newTs) internal { + _assertRegistered(id); + uint64 current = _timestamps[id]; + if (current != 0 && uint64(block.timestamp) >= current) { revert ProtocolVersions_ActivationAlreadyPassed(id, current); } - // The role can only push the activation later and must be greater than the MIN_NOTICE buffer, never earlier or - // to the same time. - if (newTimestamp <= current || newTimestamp < uint64(block.timestamp) + MIN_NOTICE) { - revert ProtocolVersions_DelayMustBeLater(current, newTimestamp); - } - - _timestamps[id] = newTimestamp; - emit TimestampSet(id, newTimestamp); + if (current == newTs) return; + _timestamps[id] = newTs; + emit TimestampSet(id, newTs); _refreshScheduleId(id); } diff --git a/test/L1/ProtocolVersions.t.sol b/test/L1/ProtocolVersions.t.sol index 4087db3bb..c17d93d23 100644 --- a/test/L1/ProtocolVersions.t.sol +++ b/test/L1/ProtocolVersions.t.sol @@ -319,7 +319,7 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { vm.warp(1000); vm.expectRevert( - abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_DelayMustBeLater.selector, uint64(0), uint64(500)) + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_InsufficientNotice.selector, uint64(500)) ); vm.prank(_owner); protocolVersions.setTimestamp(CANYON, 500); @@ -332,7 +332,7 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() - 1; vm.expectRevert( - abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_DelayMustBeLater.selector, uint64(0), ts) + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_InsufficientNotice.selector, ts) ); vm.prank(_owner); protocolVersions.setTimestamp(CANYON, ts); From c034032658a44bb12d6a9be048d6cbc3a2474520 Mon Sep 17 00:00:00 2001 From: PelleKrab Date: Mon, 6 Jul 2026 15:48:18 -0700 Subject: [PATCH 10/20] refactor(L1): minimumProtocolVersion, no-arg registerUpgrade, write helper, init guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename latestProtocolVersion → minimumProtocolVersion throughout (state variable, event, setter, interface, tests). The field represents the minimum version nodes must run, not an informational latest; the old name implied recommendation rather than a floor. Remove the protocolVersion parameter from registerUpgrade. Upgrade registration is purely a scheduling concern (assign id, extend hash chain); the minimum version is set independently via setMinimumProtocolVersion. Drops the InvalidProtocolVersion check from registerUpgrade and removes the protocolVersion field from the UpgradeRegistered event. Extract _writeTimestamp(id, newTs) as the shared write helper for setTimestamp and delayTimestamp. Each public function owns its complete validation sequence; _writeTimestamp is a pure write (store + emit TimestampSet + refreshScheduleId). Eliminates the mixed-responsibility _setTimestamp that owned some validation but not all, which caused the ActivationAlreadyPassed check to appear in two places. Guard registerUpgrade against pre-initialization calls. _seed is zero between upgradeTo and initialize; any registerUpgrade call in that window would compute hash chain links from a zero seed, permanently desynchronizing scheduleId from its reproducible derivation (l2ChainId, address(this)). Revert with NotInitialized when _seed == 0, consistent with the existing guard in scheduleId(). Regenerate ABI, storage layout, and semver-lock snapshots. Generated with Claude Code Co-Authored-By: Claude --- interfaces/L1/IProtocolVersions.sol | 10 +- snapshots/abi/ProtocolVersions.json | 36 +++--- snapshots/semver-lock.json | 4 +- snapshots/storageLayout/ProtocolVersions.json | 2 +- src/L1/ProtocolVersions.sol | 62 ++++----- test/L1/ProtocolVersions.t.sol | 122 ++++++++---------- 6 files changed, 107 insertions(+), 129 deletions(-) diff --git a/interfaces/L1/IProtocolVersions.sol b/interfaces/L1/IProtocolVersions.sol index 5de32a1fc..0694344bd 100644 --- a/interfaces/L1/IProtocolVersions.sol +++ b/interfaces/L1/IProtocolVersions.sol @@ -24,8 +24,8 @@ interface IProtocolVersions is IProxyAdminOwnedBase, ISemver, IReinitializableBa error ProtocolVersions_NotInitialized(); error ProtocolVersions_InsufficientNotice(uint64 timestamp); - event UpgradeRegistered(uint256 indexed id, uint256 protocolVersion); - event LatestProtocolVersionUpdated(uint256 indexed protocolVersion); + event UpgradeRegistered(uint256 indexed id); + event MinimumProtocolVersionUpdated(uint256 indexed protocolVersion); event TimestampSet(uint256 indexed id, uint256 timestamp); event ScheduleIdUpdated(bytes32 indexed newScheduleId, uint256 indexed blockNumber); event ChainTeamUpdated(address indexed previousChainTeam, address indexed newChainTeam); @@ -34,13 +34,13 @@ interface IProtocolVersions is IProxyAdminOwnedBase, ISemver, IReinitializableBa function chainTeam() external view returns (address); function scheduleId() external view returns (bytes32); - function latestProtocolVersion() external view returns (uint256); + function minimumProtocolVersion() external view returns (uint256); function getSchedule() external view returns (Upgrade[] memory); function initialize(uint256 _l2ChainId) external; - function registerUpgrade(uint256 protocolVersion) external returns (uint256 id); - function setLatestProtocolVersion(uint256 protocolVersion) external; + function registerUpgrade() external returns (uint256 id); + function setMinimumProtocolVersion(uint256 protocolVersion) external; function setTimestamp(uint256 id, uint64 timestamp) external; function setChainTeam(address newChainTeam) external; function delayTimestamp(uint256 id, uint64 newTimestamp) external; diff --git a/snapshots/abi/ProtocolVersions.json b/snapshots/abi/ProtocolVersions.json index 7caefb154..2c789e630 100644 --- a/snapshots/abi/ProtocolVersions.json +++ b/snapshots/abi/ProtocolVersions.json @@ -106,7 +106,7 @@ }, { "inputs": [], - "name": "latestProtocolVersion", + "name": "minimumProtocolVersion", "outputs": [ { "internalType": "uint256", @@ -144,13 +144,7 @@ "type": "function" }, { - "inputs": [ - { - "internalType": "uint256", - "name": "protocolVersion", - "type": "uint256" - } - ], + "inputs": [], "name": "registerUpgrade", "outputs": [ { @@ -196,7 +190,7 @@ "type": "uint256" } ], - "name": "setLatestProtocolVersion", + "name": "setMinimumProtocolVersion", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -274,7 +268,7 @@ "type": "uint256" } ], - "name": "LatestProtocolVersionUpdated", + "name": "MinimumProtocolVersionUpdated", "type": "event" }, { @@ -323,12 +317,6 @@ "internalType": "uint256", "name": "id", "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "protocolVersion", - "type": "uint256" } ], "name": "UpgradeRegistered", @@ -366,6 +354,17 @@ "name": "ProtocolVersions_DelayMustBeLater", "type": "error" }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "timestamp", + "type": "uint64" + } + ], + "name": "ProtocolVersions_InsufficientNotice", + "type": "error" + }, { "inputs": [], "name": "ProtocolVersions_InvalidL2ChainId", @@ -381,6 +380,11 @@ "name": "ProtocolVersions_NotChainTeam", "type": "error" }, + { + "inputs": [], + "name": "ProtocolVersions_NotInitialized", + "type": "error" + }, { "inputs": [ { diff --git a/snapshots/semver-lock.json b/snapshots/semver-lock.json index f5c34b32e..888c1978c 100644 --- a/snapshots/semver-lock.json +++ b/snapshots/semver-lock.json @@ -20,8 +20,8 @@ "sourceCodeHash": "0xa1281f5da03fa0431eabad33da23f51a8d835b0dd04554603fd59e11efe9fa51" }, "src/L1/ProtocolVersions.sol:ProtocolVersions": { - "initCodeHash": "0x72a72461ec9510757d04b19f42df5c55277f197af88dbfaa2f76b01afc7f5522", - "sourceCodeHash": "0xd081e861eff3df66d776fc9be34bd93e7b673c46d7a4220f62932d8cdb816bea" + "initCodeHash": "0x4e5d3a8c5cc76c5a09e3b8f0f24e394c79eb1afedb5695ff7310ad83717636e7", + "sourceCodeHash": "0x5d6a495bcd6ca3989aa50054a0d0f37d4ad5f101619b6dfa1d6b669b1ba9ab7c" }, "src/L1/SuperchainConfig.sol:SuperchainConfig": { "initCodeHash": "0x9b1f3555b499709485d51d5d9665002c0eb1e5eb893be1fb978a30749e894858", diff --git a/snapshots/storageLayout/ProtocolVersions.json b/snapshots/storageLayout/ProtocolVersions.json index 67a9251c3..06436ea46 100644 --- a/snapshots/storageLayout/ProtocolVersions.json +++ b/snapshots/storageLayout/ProtocolVersions.json @@ -36,7 +36,7 @@ }, { "bytes": "32", - "label": "latestProtocolVersion", + "label": "minimumProtocolVersion", "offset": 0, "slot": "4", "type": "uint256" diff --git a/src/L1/ProtocolVersions.sol b/src/L1/ProtocolVersions.sol index 4c77a2e08..89c524baa 100644 --- a/src/L1/ProtocolVersions.sol +++ b/src/L1/ProtocolVersions.sol @@ -63,9 +63,9 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable error ProtocolVersions_InsufficientNotice(uint64 timestamp); /// @notice Emitted when a new upgrade is registered. - event UpgradeRegistered(uint256 indexed id, uint256 protocolVersion); - /// @notice Emitted when the latest protocol version clients should run is updated. - event LatestProtocolVersionUpdated(uint256 indexed protocolVersion); + event UpgradeRegistered(uint256 indexed id); + /// @notice Emitted when the minimum protocol version clients must run is updated. + event MinimumProtocolVersionUpdated(uint256 indexed protocolVersion); /// @notice Emitted when an upgrade's activation timestamp is set, cleared, or delayed. event TimestampSet(uint256 indexed id, uint256 timestamp); /// @notice Emitted when the schedule commitment changes. @@ -95,10 +95,10 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable /// j..n-1 links rather than the entire chain. bytes32[] internal _upgradeScheduleId; - /// @notice The latest protocol version clients should run. Updated on each `registerUpgrade` - /// call and settable directly by the owner via `setLatestProtocolVersion`. Informational - /// only — read off-chain by clients; not part of the scheduleId commitment. - uint256 public latestProtocolVersion; + /// @notice The minimum protocol version clients must run. Settable by the owner via + /// `setMinimumProtocolVersion`. Informational only — read off-chain by clients; + /// not part of the scheduleId commitment. + uint256 public minimumProtocolVersion; /// @notice Address allowed to delay (push out) already-scheduled activation timestamps. /// @dev Appointed and revocable by the owner. This is a restricted secondary role: it can @@ -154,31 +154,27 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable } /// @notice Registers a new upgrade, assigning it the next ascending id. Owner only. - /// @dev The upgrade starts unscheduled (timestamp 0) and `latestProtocolVersion` is updated. - /// Registration extends the scheduleId chain with the new (id, timestamp=0) link. - /// @param protocolVersion Packed semver uint256 for this upgrade (must be non-zero). + /// @dev The upgrade starts unscheduled (timestamp 0). Registration extends the scheduleId + /// chain with the new (id, timestamp=0) link. /// @return id The ascending id assigned to the newly registered upgrade. - function registerUpgrade(uint256 protocolVersion) external onlyProxyAdminOwner returns (uint256 id) { - if (protocolVersion == 0) revert ProtocolVersions_InvalidProtocolVersion(); - + function registerUpgrade() external onlyProxyAdminOwner returns (uint256 id) { + if (_seed == 0) revert ProtocolVersions_NotInitialized(); id = _timestamps.length; _timestamps.push(0); _upgradeScheduleId.push(); - latestProtocolVersion = protocolVersion; - emit UpgradeRegistered(id, protocolVersion); - emit LatestProtocolVersionUpdated(protocolVersion); + emit UpgradeRegistered(id); _refreshScheduleId(id); } - /// @notice Sets the latest protocol version clients should run. Owner (Security Council) only. + /// @notice Sets the minimum protocol version clients must run. Owner (Security Council) only. /// @dev Informational signal for off-chain clients; independent of the upgrade schedule and NOT /// part of the scheduleId commitment, so it can be updated at any time without shifting any - /// proof binding. `registerUpgrade` also updates this value. + /// proof binding. /// @param protocolVersion Packed semver uint256 (must be non-zero). - function setLatestProtocolVersion(uint256 protocolVersion) external onlyProxyAdminOwner { + function setMinimumProtocolVersion(uint256 protocolVersion) external onlyProxyAdminOwner { if (protocolVersion == 0) revert ProtocolVersions_InvalidProtocolVersion(); - latestProtocolVersion = protocolVersion; - emit LatestProtocolVersionUpdated(protocolVersion); + minimumProtocolVersion = protocolVersion; + emit MinimumProtocolVersionUpdated(protocolVersion); } /// @notice Sets the activation timestamp for one upgrade by id. Pass 0 to clear. @@ -189,11 +185,16 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable /// @param timestamp Future Unix timestamp for L2 activation (must be >= block.timestamp + MIN_NOTICE), or 0 to /// clear. function setTimestamp(uint256 id, uint64 timestamp) external onlyProxyAdminOwner { - // Non-zero timestamps must provide at least MIN_NOTICE seconds of notice. + _assertRegistered(id); + uint64 current = _timestamps[id]; + if (current != 0 && uint64(block.timestamp) >= current) { + revert ProtocolVersions_ActivationAlreadyPassed(id, current); + } if (timestamp != 0 && timestamp < uint64(block.timestamp) + MIN_NOTICE) { revert ProtocolVersions_InsufficientNotice(timestamp); } - _setTimestamp(id, timestamp); + if (current == timestamp) return; + _writeTimestamp(id, timestamp); } /// @notice Appoints, replaces, or clears (set to zero) the chainTeam role. Owner only. @@ -226,19 +227,12 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable // The new timestamp must also provide at least MIN_NOTICE seconds of notice from now. uint64 minFloor = uint64(block.timestamp) + MIN_NOTICE; if (newTimestamp < minFloor) revert ProtocolVersions_DelayMustBeLater(minFloor, newTimestamp); - _setTimestamp(id, newTimestamp); + _writeTimestamp(id, newTimestamp); } - /// @dev Shared write path: validates activation hasn't passed, skips no-ops, then writes newTs, - /// emits TimestampSet, and refreshes the hash chain. Callers handle access control, - /// ordering constraints, and MIN_NOTICE checks before invoking. - function _setTimestamp(uint256 id, uint64 newTs) internal { - _assertRegistered(id); - uint64 current = _timestamps[id]; - if (current != 0 && uint64(block.timestamp) >= current) { - revert ProtocolVersions_ActivationAlreadyPassed(id, current); - } - if (current == newTs) return; + /// @dev Writes newTs, emits TimestampSet, and refreshes the hash chain. All validation is + /// the caller's responsibility. + function _writeTimestamp(uint256 id, uint64 newTs) internal { _timestamps[id] = newTs; emit TimestampSet(id, newTs); _refreshScheduleId(id); diff --git a/test/L1/ProtocolVersions.t.sol b/test/L1/ProtocolVersions.t.sol index c17d93d23..833d93b61 100644 --- a/test/L1/ProtocolVersions.t.sol +++ b/test/L1/ProtocolVersions.t.sol @@ -16,8 +16,8 @@ import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; /// @title ProtocolVersions_TestInit /// @notice Reusable test initialization for ProtocolVersions tests. abstract contract ProtocolVersions_TestInit is Test { - event UpgradeRegistered(uint256 indexed id, uint256 protocolVersion); - event LatestProtocolVersionUpdated(uint256 indexed protocolVersion); + event UpgradeRegistered(uint256 indexed id); + event MinimumProtocolVersionUpdated(uint256 indexed protocolVersion); event ChainTeamUpdated(address indexed previousChainTeam, address indexed newChainTeam); event TimestampSet(uint256 indexed id, uint256 timestamp); @@ -59,7 +59,7 @@ abstract contract ProtocolVersions_TestInit is Test { /// @dev Registers the first upgrade (id CANYON) and schedules it for block.timestamp + MIN_NOTICE + delay. function _scheduleCanyon(uint64 _delay) internal returns (uint64 ts_) { vm.prank(_owner); - protocolVersions.registerUpgrade(1); + protocolVersions.registerUpgrade(); ts_ = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + _delay; vm.prank(_owner); protocolVersions.setTimestamp(CANYON, ts_); @@ -130,7 +130,7 @@ contract ProtocolVersions_RegisterUpgrade_Test is ProtocolVersions_TestInit { vm.roll(block.number + 1); vm.prank(_owner); - protocolVersions.registerUpgrade(1); + protocolVersions.registerUpgrade(); assertNotEq(protocolVersions.scheduleId(), idBefore); } @@ -138,90 +138,72 @@ contract ProtocolVersions_RegisterUpgrade_Test is ProtocolVersions_TestInit { /// @notice Tests that `registerUpgrade` assigns ascending ids and returns them. function test_registerUpgrade_returnsAscendingIds_succeeds() external { vm.prank(_owner); - assertEq(protocolVersions.registerUpgrade(1), 0); + assertEq(protocolVersions.registerUpgrade(), 0); vm.prank(_owner); - assertEq(protocolVersions.registerUpgrade(2), 1); + assertEq(protocolVersions.registerUpgrade(), 1); vm.prank(_owner); - assertEq(protocolVersions.registerUpgrade(3), 2); + assertEq(protocolVersions.registerUpgrade(), 2); } - /// @notice Tests that `registerUpgrade` emits the `UpgradeRegistered` event with correct fields. + /// @notice Tests that `registerUpgrade` emits the `UpgradeRegistered` event with the assigned id. function test_registerUpgrade_emitsEvent_succeeds() external { - vm.expectEmit(true, false, false, true, address(protocolVersions)); - emit UpgradeRegistered(0, 1); - vm.prank(_owner); - protocolVersions.registerUpgrade(1); - - vm.expectEmit(true, false, false, true, address(protocolVersions)); - emit UpgradeRegistered(1, 2); - vm.prank(_owner); - protocolVersions.registerUpgrade(2); - } - - /// @notice Tests that registering upgrades updates latestProtocolVersion to the most recent. - function test_registerUpgrade_updatesLatestProtocolVersion_succeeds() external { + vm.expectEmit(true, false, false, false, address(protocolVersions)); + emit UpgradeRegistered(0); vm.prank(_owner); - protocolVersions.registerUpgrade(5); - assertEq(protocolVersions.latestProtocolVersion(), 5); + protocolVersions.registerUpgrade(); + vm.expectEmit(true, false, false, false, address(protocolVersions)); + emit UpgradeRegistered(1); vm.prank(_owner); - protocolVersions.registerUpgrade(9); - assertEq(protocolVersions.latestProtocolVersion(), 9); - } - - /// @notice Tests that registering an upgrade with a zero protocolVersion reverts. - function test_registerUpgrade_zeroProtocolVersion_reverts() external { - vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidProtocolVersion.selector); - vm.prank(_owner); - protocolVersions.registerUpgrade(0); + protocolVersions.registerUpgrade(); } /// @notice Tests that only the owner can call `registerUpgrade`. function test_registerUpgrade_callerNotOwner_reverts() external { vm.expectRevert(ProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); vm.prank(_nonOwner); - protocolVersions.registerUpgrade(1); + protocolVersions.registerUpgrade(); } } -/// @title ProtocolVersions_SetLatestProtocolVersion_Test -/// @notice Test contract for the `setLatestProtocolVersion` function. -contract ProtocolVersions_SetLatestProtocolVersion_Test is ProtocolVersions_TestInit { - /// @notice Tests that the owner can set the latest protocol version directly. - function test_setLatestProtocolVersion_updates_succeeds() external { +/// @title ProtocolVersions_SetMinimumProtocolVersion_Test +/// @notice Test contract for the `setMinimumProtocolVersion` function. +contract ProtocolVersions_SetMinimumProtocolVersion_Test is ProtocolVersions_TestInit { + /// @notice Tests that the owner can set the minimum protocol version. + function test_setMinimumProtocolVersion_updates_succeeds() external { vm.prank(_owner); - protocolVersions.setLatestProtocolVersion(42); - assertEq(protocolVersions.latestProtocolVersion(), 42); + protocolVersions.setMinimumProtocolVersion(42); + assertEq(protocolVersions.minimumProtocolVersion(), 42); } - /// @notice Tests that setting the latest protocol version does not change the scheduleId. - function test_setLatestProtocolVersion_doesNotChangeScheduleId_succeeds() external { + /// @notice Tests that setting the minimum protocol version does not change the scheduleId. + function test_setMinimumProtocolVersion_doesNotChangeScheduleId_succeeds() external { bytes32 scheduleIdBefore = protocolVersions.scheduleId(); vm.prank(_owner); - protocolVersions.setLatestProtocolVersion(42); + protocolVersions.setMinimumProtocolVersion(42); assertEq(protocolVersions.scheduleId(), scheduleIdBefore); } - /// @notice Tests that `setLatestProtocolVersion` emits the `LatestProtocolVersionUpdated` event. - function test_setLatestProtocolVersion_emitsEvent_succeeds() external { + /// @notice Tests that `setMinimumProtocolVersion` emits the `MinimumProtocolVersionUpdated` event. + function test_setMinimumProtocolVersion_emitsEvent_succeeds() external { vm.expectEmit(true, false, false, true, address(protocolVersions)); - emit LatestProtocolVersionUpdated(42); + emit MinimumProtocolVersionUpdated(42); vm.prank(_owner); - protocolVersions.setLatestProtocolVersion(42); + protocolVersions.setMinimumProtocolVersion(42); } /// @notice Tests that setting a zero protocol version reverts. - function test_setLatestProtocolVersion_zero_reverts() external { + function test_setMinimumProtocolVersion_zero_reverts() external { vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidProtocolVersion.selector); vm.prank(_owner); - protocolVersions.setLatestProtocolVersion(0); + protocolVersions.setMinimumProtocolVersion(0); } - /// @notice Tests that only the owner can call `setLatestProtocolVersion`. - function test_setLatestProtocolVersion_callerNotOwner_reverts() external { + /// @notice Tests that only the owner can call `setMinimumProtocolVersion`. + function test_setMinimumProtocolVersion_callerNotOwner_reverts() external { vm.expectRevert(ProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); vm.prank(_nonOwner); - protocolVersions.setLatestProtocolVersion(42); + protocolVersions.setMinimumProtocolVersion(42); } } @@ -232,7 +214,7 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { function test_setTimestamp_updatesTimestampAndScheduleId_succeeds() external { bytes32 initialScheduleId = protocolVersions.scheduleId(); vm.prank(_owner); - protocolVersions.registerUpgrade(1); + protocolVersions.registerUpgrade(); vm.roll(block.number + 1); vm.warp(block.timestamp + 1); @@ -245,9 +227,9 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { } /// @notice Tests that calling `setTimestamp` with the same value is a no-op for scheduleId. - function test_setTimestamp_sameTimestamp_noScheduleIdChange_succeeds() external { + function test_setTimestamp_sameTimestamp_succeeds() external { vm.prank(_owner); - protocolVersions.registerUpgrade(1); + protocolVersions.registerUpgrade(); vm.roll(block.number + 1); vm.warp(block.timestamp + 1); @@ -269,7 +251,7 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { /// restores it to the value it held immediately after registration (ts=0 link). function test_setTimestamp_clearTimestamp_succeeds() external { vm.prank(_owner); - protocolVersions.registerUpgrade(1); + protocolVersions.registerUpgrade(); bytes32 scheduleIdAfterRegister = protocolVersions.scheduleId(); vm.roll(block.number + 1); @@ -292,7 +274,7 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { /// @notice Tests that `setTimestamp` emits a `TimestampSet` event. function test_setTimestamp_emitsEvent_succeeds() external { vm.prank(_owner); - protocolVersions.registerUpgrade(1); + protocolVersions.registerUpgrade(); uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; vm.expectEmit(true, false, false, true, address(protocolVersions)); @@ -304,7 +286,7 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { /// @notice Tests that only the owner can call `setTimestamp`. function test_setTimestamp_callerNotOwner_reverts() external { vm.prank(_owner); - protocolVersions.registerUpgrade(1); + protocolVersions.registerUpgrade(); uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; vm.expectRevert(ProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); @@ -315,7 +297,7 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { /// @notice Tests that `setTimestamp` reverts when the timestamp is in the past. function test_setTimestamp_timestampInPast_reverts() external { vm.prank(_owner); - protocolVersions.registerUpgrade(1); + protocolVersions.registerUpgrade(); vm.warp(1000); vm.expectRevert( @@ -328,12 +310,10 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { /// @notice Tests that `setTimestamp` reverts when the timestamp is within MIN_NOTICE of now. function test_setTimestamp_insufficientNotice_reverts() external { vm.prank(_owner); - protocolVersions.registerUpgrade(1); + protocolVersions.registerUpgrade(); uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() - 1; - vm.expectRevert( - abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_InsufficientNotice.selector, ts) - ); + vm.expectRevert(abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_InsufficientNotice.selector, ts)); vm.prank(_owner); protocolVersions.setTimestamp(CANYON, ts); } @@ -341,7 +321,7 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { /// @notice Tests that `setTimestamp` reverts when the upgrade has already activated. function test_setTimestamp_afterActivation_reverts() external { vm.prank(_owner); - protocolVersions.registerUpgrade(1); + protocolVersions.registerUpgrade(); vm.warp(100); uint64 activationTs = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; @@ -370,9 +350,9 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { /// @notice Tests that scheduleId is reproducible from (chainId, address, ascending ids, timestamps). function test_setTimestamp_scheduleIdReproducible_succeeds() external { vm.prank(_owner); - protocolVersions.registerUpgrade(1); + protocolVersions.registerUpgrade(); vm.prank(_owner); - protocolVersions.registerUpgrade(2); + protocolVersions.registerUpgrade(); uint64 ts1 = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; uint64 ts2 = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 200; @@ -452,7 +432,7 @@ contract ProtocolVersions_DelayTimestamp_Test is ProtocolVersions_TestInit { /// @notice Tests that `delayTimestamp` reverts when the upgrade has no scheduled timestamp. function test_delayTimestamp_notScheduled_reverts() external { vm.prank(_owner); - protocolVersions.registerUpgrade(1); + protocolVersions.registerUpgrade(); vm.prank(_owner); protocolVersions.setChainTeam(_chainTeam); @@ -532,9 +512,9 @@ contract ProtocolVersions_ChainTeam_Test is ProtocolVersions_TestInit { } } -/// @title ProtocolVersions_GetView_Test +/// @title ProtocolVersions_Uncategorized_Test /// @notice Test contract for view functions and the upgrade registry. -contract ProtocolVersions_GetView_Test is ProtocolVersions_TestInit { +contract ProtocolVersions_Uncategorized_Test is ProtocolVersions_TestInit { /// @notice Tests that the registry starts empty. function test_registry_startsEmpty_succeeds() external view { assertEq(protocolVersions.getSchedule().length, 0); @@ -548,9 +528,9 @@ contract ProtocolVersions_GetView_Test is ProtocolVersions_TestInit { /// @notice Tests that `getSchedule` returns all upgrades in registration order with correct fields. function test_getSchedule_returnsFullSchedule_succeeds() external { vm.prank(_owner); - protocolVersions.registerUpgrade(1); + protocolVersions.registerUpgrade(); vm.prank(_owner); - protocolVersions.registerUpgrade(2); + protocolVersions.registerUpgrade(); uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; vm.prank(_owner); From 1f39756feb037aa3582a3b9b56d457189c77c594 Mon Sep 17 00:00:00 2001 From: PelleKrab Date: Mon, 6 Jul 2026 16:33:40 -0700 Subject: [PATCH 11/20] test: exclude ProtocolVersions from initializer tracking Co-authored-by: Codex --- interfaces/L1/IProtocolVersions.sol | 3 ++- test/vendor/Initializable.t.sol | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/interfaces/L1/IProtocolVersions.sol b/interfaces/L1/IProtocolVersions.sol index 0694344bd..da1daedc6 100644 --- a/interfaces/L1/IProtocolVersions.sol +++ b/interfaces/L1/IProtocolVersions.sol @@ -29,6 +29,7 @@ interface IProtocolVersions is IProxyAdminOwnedBase, ISemver, IReinitializableBa event TimestampSet(uint256 indexed id, uint256 timestamp); event ScheduleIdUpdated(bytes32 indexed newScheduleId, uint256 indexed blockNumber); event ChainTeamUpdated(address indexed previousChainTeam, address indexed newChainTeam); + event Initialized(uint8 version); function MIN_NOTICE() external view returns (uint64); @@ -36,7 +37,7 @@ interface IProtocolVersions is IProxyAdminOwnedBase, ISemver, IReinitializableBa function scheduleId() external view returns (bytes32); function minimumProtocolVersion() external view returns (uint256); - function getSchedule() external view returns (Upgrade[] memory); + function getSchedule() external view returns (Upgrade[] memory schedule_); function initialize(uint256 _l2ChainId) external; function registerUpgrade() external returns (uint256 id); diff --git a/test/vendor/Initializable.t.sol b/test/vendor/Initializable.t.sol index 9bdd9df94..0052a674a 100644 --- a/test/vendor/Initializable.t.sol +++ b/test/vendor/Initializable.t.sol @@ -256,8 +256,9 @@ contract Initializer_Test is CommonTest { excludes[j++] = "src/periphery/*"; // L2 contract initialization is tested in Predeploys.t.sol excludes[j++] = "src/L2/*"; - // Contract is not deployed as part of the standard deployment script. + // Contracts not deployed as part of the standard deployment script. excludes[j++] = "src/L1/BalanceTracker.sol"; + excludes[j++] = "src/L1/ProtocolVersions.sol"; // AggregateVerifier uses a custom `bool initialized` instead of OpenZeppelin's `_initialized` uint8. excludes[j++] = "src/L1/proofs/AggregateVerifier.sol"; // ETHLockbox is only deployed when interop is enabled. From 871e6d4742b7da0fa7cd28823b3fd99033e0017a Mon Sep 17 00:00:00 2001 From: PelleKrab Date: Tue, 7 Jul 2026 09:50:49 -0700 Subject: [PATCH 12/20] feat(L1): deploy ProtocolVersions in standard deployment script Deploy the ProtocolVersions impl + per-chain proxy as part of SystemDeploy and initialize it via initialize(l2ChainId), rather than excluding it from initializer tracking. Adds it to Types.Implementations/DeployOutput, artifact saves, getImplementations, and _assertValidImplementations. Tags the contract @custom:proxied true so the initializer test verifies both impl and proxy, and wires it into Setup (via getAddress for fork safety). Updates the ProtocolVersions sourceCodeHash in semver-lock accordingly. Generated with Claude Code Co-Authored-By: Claude --- scripts/deploy/SystemDeploy.s.sol | 25 +++++++++++++++++++++++++ scripts/libraries/Types.sol | 3 +++ snapshots/semver-lock.json | 2 +- src/L1/ProtocolVersions.sol | 1 + test/setup/Setup.sol | 5 +++++ test/vendor/Initializable.t.sol | 26 ++++++++++++++++++++++++-- 6 files changed, 59 insertions(+), 3 deletions(-) diff --git a/scripts/deploy/SystemDeploy.s.sol b/scripts/deploy/SystemDeploy.s.sol index 227d123d6..4ad5e3ddb 100644 --- a/scripts/deploy/SystemDeploy.s.sol +++ b/scripts/deploy/SystemDeploy.s.sol @@ -17,6 +17,7 @@ import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.s import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; import { IL1StandardBridge } from "interfaces/L1/IL1StandardBridge.sol"; import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IAddressManager } from "interfaces/legacy/IAddressManager.sol"; @@ -218,6 +219,7 @@ contract SystemDeploy is Script { disputeGameFactoryImpl: artifacts.mustGetAddress("DisputeGameFactoryImpl"), anchorStateRegistryImpl: artifacts.mustGetAddress("AnchorStateRegistryImpl"), delayedWETHImpl: artifacts.mustGetAddress("DelayedWETHImpl"), + protocolVersionsImpl: artifacts.mustGetAddress("ProtocolVersionsImpl"), aggregateVerifierImpl: artifacts.getAddress("AggregateVerifier"), teeProverRegistryImpl: artifacts.getAddress("TEEProverRegistryImpl"), teeVerifierImpl: artifacts.getAddress("TEEVerifier"), @@ -447,6 +449,7 @@ contract SystemDeploy is Script { output_.delayedWETHImpl = address(_deployDelayedWETHImpl(_input)); output_.disputeGameFactoryImpl = address(_deployDisputeGameFactoryImpl()); output_.anchorStateRegistryImpl = address(_deployAnchorStateRegistryImpl(_input)); + output_.protocolVersionsImpl = address(_deployProtocolVersionsImpl()); } function _deployOPChain( @@ -487,6 +490,8 @@ contract SystemDeploy is Script { output_.anchorStateRegistryProxy = IAnchorStateRegistry(_deployProxy(_input, output_.opChainProxyAdmin, "AnchorStateRegistry")); output_.delayedWETHProxy = IDelayedWETH(payable(_deployProxy(_input, output_.opChainProxyAdmin, "DelayedWETH"))); + output_.protocolVersionsProxy = + IProtocolVersions(_deployProxy(_input, output_.opChainProxyAdmin, "ProtocolVersions")); output_.l1StandardBridgeProxy = IL1StandardBridge( payable(_createDeterministic( @@ -619,6 +624,13 @@ contract SystemDeploy is Script { _impls.anchorStateRegistryImpl, _encodeAnchorStateRegistryInitializer(_input, _output) ); + + _upgradeToAndCall( + _output.opChainProxyAdmin, + address(_output.protocolVersionsProxy), + _impls.protocolVersionsImpl, + abi.encodeCall(IProtocolVersions.initialize, (_input.l2ChainId)) + ); } function _upgradeSuperchainConfigIfNeeded( @@ -947,6 +959,16 @@ contract SystemDeploy is Script { ); } + function _deployProtocolVersionsImpl() internal returns (IProtocolVersions) { + return IProtocolVersions( + DeployUtils.createDeterministic({ + _name: "ProtocolVersions", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IProtocolVersions.__constructor__, ())), + _salt: DeployUtils.DEFAULT_SALT + }) + ); + } + function _deployMultiproofContracts( Types.DeployInput memory _opChainInput, ImplementationInput memory _input, @@ -1100,6 +1122,7 @@ contract SystemDeploy is Script { DeployUtils.assertValidContractAddress(_impls.disputeGameFactoryImpl); DeployUtils.assertValidContractAddress(_impls.anchorStateRegistryImpl); DeployUtils.assertValidContractAddress(_impls.delayedWETHImpl); + DeployUtils.assertValidContractAddress(_impls.protocolVersionsImpl); } function _implementationsEmpty(Types.Implementations memory _impls) internal pure returns (bool) { @@ -1125,6 +1148,7 @@ contract SystemDeploy is Script { artifacts.save("DisputeGameFactoryProxy", address(chain.disputeGameFactoryProxy)); artifacts.save("DelayedWETHProxy", address(chain.delayedWETHProxy)); artifacts.save("AnchorStateRegistryProxy", address(chain.anchorStateRegistryProxy)); + artifacts.save("ProtocolVersionsProxy", address(chain.protocolVersionsProxy)); artifacts.save("OptimismPortalProxy", address(chain.optimismPortalProxy)); artifacts.save("OptimismPortal2Proxy", address(chain.optimismPortalProxy)); _saveIfSet("TEEProverRegistryProxy", address(chain.teeProverRegistryProxy)); @@ -1151,6 +1175,7 @@ contract SystemDeploy is Script { artifacts.save("DisputeGameFactoryImpl", _impls.disputeGameFactoryImpl); artifacts.save("AnchorStateRegistryImpl", _impls.anchorStateRegistryImpl); artifacts.save("DelayedWETHImpl", _impls.delayedWETHImpl); + artifacts.save("ProtocolVersionsImpl", _impls.protocolVersionsImpl); _saveIfSet("AggregateVerifier", _impls.aggregateVerifierImpl); _saveIfSet("TEEProverRegistryImpl", _impls.teeProverRegistryImpl); _saveIfSet("TEEVerifier", _impls.teeVerifierImpl); diff --git a/scripts/libraries/Types.sol b/scripts/libraries/Types.sol index 2431d705c..cdde01db4 100644 --- a/scripts/libraries/Types.sol +++ b/scripts/libraries/Types.sol @@ -17,6 +17,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 { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { Proposal } from "src/libraries/bridge/Types.sol"; import { Claim } from "src/libraries/bridge/LibUDT.sol"; @@ -55,6 +56,7 @@ library Types { IDisputeGameFactory disputeGameFactoryProxy; IAnchorStateRegistry anchorStateRegistryProxy; IDelayedWETH delayedWETHProxy; + IProtocolVersions protocolVersionsProxy; IVerifier aggregateVerifier; ITEEProverRegistry teeProverRegistryProxy; IVerifier teeVerifier; @@ -76,6 +78,7 @@ library Types { address disputeGameFactoryImpl; address anchorStateRegistryImpl; address delayedWETHImpl; + address protocolVersionsImpl; address aggregateVerifierImpl; address teeProverRegistryImpl; address teeVerifierImpl; diff --git a/snapshots/semver-lock.json b/snapshots/semver-lock.json index 888c1978c..82434443e 100644 --- a/snapshots/semver-lock.json +++ b/snapshots/semver-lock.json @@ -21,7 +21,7 @@ }, "src/L1/ProtocolVersions.sol:ProtocolVersions": { "initCodeHash": "0x4e5d3a8c5cc76c5a09e3b8f0f24e394c79eb1afedb5695ff7310ad83717636e7", - "sourceCodeHash": "0x5d6a495bcd6ca3989aa50054a0d0f37d4ad5f101619b6dfa1d6b669b1ba9ab7c" + "sourceCodeHash": "0x6e5f0cd213d3bf954dd7c305be0fa6771dae4744d7220ced042ce351464efaee" }, "src/L1/SuperchainConfig.sol:SuperchainConfig": { "initCodeHash": "0x9b1f3555b499709485d51d5d9665002c0eb1e5eb893be1fb978a30749e894858", diff --git a/src/L1/ProtocolVersions.sol b/src/L1/ProtocolVersions.sol index 89c524baa..6af4b0f41 100644 --- a/src/L1/ProtocolVersions.sol +++ b/src/L1/ProtocolVersions.sol @@ -9,6 +9,7 @@ import { Initializable } from "lib/openzeppelin-contracts/contracts/proxy/utils/ // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; +/// @custom:proxied true /// @title ProtocolVersions /// @notice Security Council-controlled upgrade activation schedule contract. /// @dev Maintains an ordered registry of upgrades and their L2 activation timestamps. diff --git a/test/setup/Setup.sol b/test/setup/Setup.sol index a415ccfd1..df4a6d05e 100644 --- a/test/setup/Setup.sol +++ b/test/setup/Setup.sol @@ -29,6 +29,7 @@ import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { IL1StandardBridge } from "interfaces/L1/IL1StandardBridge.sol"; import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; import { IOptimismMintableERC721Factory } from "interfaces/L2/IOptimismMintableERC721Factory.sol"; @@ -105,6 +106,7 @@ abstract contract Setup is FeatureFlags { IL1ERC721Bridge l1ERC721Bridge; IOptimismMintableERC20Factory l1OptimismMintableERC20Factory; ISuperchainConfig superchainConfig; + IProtocolVersions protocolVersions; // L2 contracts IL2CrossDomainMessenger l2CrossDomainMessenger = @@ -241,6 +243,9 @@ abstract contract Setup is FeatureFlags { anchorStateRegistry = IAnchorStateRegistry(artifacts.mustGetAddress("AnchorStateRegistryProxy")); disputeGameFactory = IDisputeGameFactory(artifacts.mustGetAddress("DisputeGameFactoryProxy")); delayedWeth = IDelayedWETH(artifacts.mustGetAddress("DelayedWETHProxy")); + // Use getAddress instead of mustGetAddress because forked production chains predating + // ProtocolVersions won't have the proxy; those return address(0) rather than reverting. + protocolVersions = IProtocolVersions(artifacts.getAddress("ProtocolVersionsProxy")); proxyAdmin = IProxyAdmin(artifacts.mustGetAddress("ProxyAdmin")); proxyAdminOwner = proxyAdmin.owner(); superchainProxyAdmin = IProxyAdmin(EIP1967Helper.getAdmin(address(superchainConfig))); diff --git a/test/vendor/Initializable.t.sol b/test/vendor/Initializable.t.sol index 0052a674a..18e6e9cdd 100644 --- a/test/vendor/Initializable.t.sol +++ b/test/vendor/Initializable.t.sol @@ -15,6 +15,7 @@ import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; // Interfaces import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; import { IDisputeGameFactory } from "interfaces/L1/proofs/IDisputeGameFactory.sol"; import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; @@ -210,6 +211,24 @@ 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, (deploy.cfg().l2ChainId())); + contracts.push( + InitializeableContract({ + name: "ProtocolVersionsImpl", + target: EIP1967Helper.getImplementation(address(protocolVersions)), + initCalldata: initCalldata + }) + ); + contracts.push( + InitializeableContract({ + name: "ProtocolVersionsProxy", target: address(protocolVersions), initCalldata: initCalldata + }) + ); + } + // ETHLockbox is only deployed when interop is enabled if (address(ethLockbox) != address(0)) { initCalldata = abi.encodeCall(ethLockbox.initialize, (ISystemConfig(address(0)), new IOptimismPortal2[](0))); @@ -256,15 +275,18 @@ contract Initializer_Test is CommonTest { excludes[j++] = "src/periphery/*"; // L2 contract initialization is tested in Predeploys.t.sol excludes[j++] = "src/L2/*"; - // Contracts not deployed as part of the standard deployment script. + // Contract is not deployed as part of the standard deployment script. excludes[j++] = "src/L1/BalanceTracker.sol"; - excludes[j++] = "src/L1/ProtocolVersions.sol"; // AggregateVerifier uses a custom `bool initialized` instead of OpenZeppelin's `_initialized` uint8. excludes[j++] = "src/L1/proofs/AggregateVerifier.sol"; // ETHLockbox is only deployed when interop is enabled. if (address(ethLockbox) == address(0)) { excludes[j++] = "src/L1/ETHLockbox.sol"; } + // ProtocolVersions is not deployed on older forked chains. + if (address(protocolVersions) == address(0)) { + excludes[j++] = "src/L1/ProtocolVersions.sol"; + } // TEEProverRegistry is only deployed when multiproof is enabled. if (address(teeProverRegistry) == address(0)) { excludes[j++] = "src/L1/proofs/tee/TEEProverRegistry.sol"; From 766f563b82fef73967e1b4d13b1df4b5aac4265d Mon Sep 17 00:00:00 2001 From: PelleKrab Date: Tue, 7 Jul 2026 11:06:25 -0700 Subject: [PATCH 13/20] refactor(L1): simplify ProtocolVersions schedule seed Co-authored-by: Codex --- interfaces/L1/IProtocolVersions.sol | 3 +- scripts/deploy/SystemDeploy.s.sol | 2 +- snapshots/abi/ProtocolVersions.json | 13 +--- snapshots/semver-lock.json | 4 +- snapshots/storageLayout/ProtocolVersions.json | 15 +--- src/L1/ProtocolVersions.sol | 75 +++++++++---------- test/L1/ProtocolVersions.t.sol | 38 +++------- test/vendor/Initializable.t.sol | 2 +- 8 files changed, 58 insertions(+), 94 deletions(-) diff --git a/interfaces/L1/IProtocolVersions.sol b/interfaces/L1/IProtocolVersions.sol index da1daedc6..159ab158e 100644 --- a/interfaces/L1/IProtocolVersions.sol +++ b/interfaces/L1/IProtocolVersions.sol @@ -14,7 +14,6 @@ interface IProtocolVersions is IProxyAdminOwnedBase, ISemver, IReinitializableBa bytes32 scheduleId; } - error ProtocolVersions_InvalidL2ChainId(); error ProtocolVersions_UnknownUpgrade(uint256 id); error ProtocolVersions_InvalidProtocolVersion(); error ProtocolVersions_ActivationAlreadyPassed(uint256 id, uint64 activationTimestamp); @@ -39,7 +38,7 @@ interface IProtocolVersions is IProxyAdminOwnedBase, ISemver, IReinitializableBa function getSchedule() external view returns (Upgrade[] memory schedule_); - function initialize(uint256 _l2ChainId) external; + function initialize() external; function registerUpgrade() external returns (uint256 id); function setMinimumProtocolVersion(uint256 protocolVersion) external; function setTimestamp(uint256 id, uint64 timestamp) external; diff --git a/scripts/deploy/SystemDeploy.s.sol b/scripts/deploy/SystemDeploy.s.sol index 4ad5e3ddb..cdad82131 100644 --- a/scripts/deploy/SystemDeploy.s.sol +++ b/scripts/deploy/SystemDeploy.s.sol @@ -629,7 +629,7 @@ contract SystemDeploy is Script { _output.opChainProxyAdmin, address(_output.protocolVersionsProxy), _impls.protocolVersionsImpl, - abi.encodeCall(IProtocolVersions.initialize, (_input.l2ChainId)) + abi.encodeCall(IProtocolVersions.initialize, ()) ); } diff --git a/snapshots/abi/ProtocolVersions.json b/snapshots/abi/ProtocolVersions.json index 2c789e630..b0cdd59fd 100644 --- a/snapshots/abi/ProtocolVersions.json +++ b/snapshots/abi/ProtocolVersions.json @@ -92,13 +92,7 @@ "type": "function" }, { - "inputs": [ - { - "internalType": "uint256", - "name": "_l2ChainId", - "type": "uint256" - } - ], + "inputs": [], "name": "initialize", "outputs": [], "stateMutability": "nonpayable", @@ -365,11 +359,6 @@ "name": "ProtocolVersions_InsufficientNotice", "type": "error" }, - { - "inputs": [], - "name": "ProtocolVersions_InvalidL2ChainId", - "type": "error" - }, { "inputs": [], "name": "ProtocolVersions_InvalidProtocolVersion", diff --git a/snapshots/semver-lock.json b/snapshots/semver-lock.json index 82434443e..d64948b38 100644 --- a/snapshots/semver-lock.json +++ b/snapshots/semver-lock.json @@ -20,8 +20,8 @@ "sourceCodeHash": "0xa1281f5da03fa0431eabad33da23f51a8d835b0dd04554603fd59e11efe9fa51" }, "src/L1/ProtocolVersions.sol:ProtocolVersions": { - "initCodeHash": "0x4e5d3a8c5cc76c5a09e3b8f0f24e394c79eb1afedb5695ff7310ad83717636e7", - "sourceCodeHash": "0x6e5f0cd213d3bf954dd7c305be0fa6771dae4744d7220ced042ce351464efaee" + "initCodeHash": "0x0641815cc45d4f321a4d37336b7b2c871a84e1d84a9c1bc4f5e0d35b33eb87f0", + "sourceCodeHash": "0x860e3ef7f0ccf377a63649a95066f33cf09d0a5b94dac790b1cd3ee83216ee03" }, "src/L1/SuperchainConfig.sol:SuperchainConfig": { "initCodeHash": "0x9b1f3555b499709485d51d5d9665002c0eb1e5eb893be1fb978a30749e894858", diff --git a/snapshots/storageLayout/ProtocolVersions.json b/snapshots/storageLayout/ProtocolVersions.json index 06436ea46..80b1b935f 100644 --- a/snapshots/storageLayout/ProtocolVersions.json +++ b/snapshots/storageLayout/ProtocolVersions.json @@ -13,39 +13,32 @@ "slot": "0", "type": "bool" }, - { - "bytes": "32", - "label": "_seed", - "offset": 0, - "slot": "1", - "type": "bytes32" - }, { "bytes": "32", "label": "_timestamps", "offset": 0, - "slot": "2", + "slot": "1", "type": "uint64[]" }, { "bytes": "32", "label": "_upgradeScheduleId", "offset": 0, - "slot": "3", + "slot": "2", "type": "bytes32[]" }, { "bytes": "32", "label": "minimumProtocolVersion", "offset": 0, - "slot": "4", + "slot": "3", "type": "uint256" }, { "bytes": "20", "label": "chainTeam", "offset": 0, - "slot": "5", + "slot": "4", "type": "address" } ] \ No newline at end of file diff --git a/src/L1/ProtocolVersions.sol b/src/L1/ProtocolVersions.sol index 6af4b0f41..a39169442 100644 --- a/src/L1/ProtocolVersions.sol +++ b/src/L1/ProtocolVersions.sol @@ -19,23 +19,24 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; /// append-only (upgrades are never removed or reordered), an `id` permanently refers to the /// same upgrade. /// -/// The canonical schedule commitment (`scheduleId`) is the tail of a per-upgrade hash chain: +/// The canonical schedule commitment (`scheduleId`) is the tail of a hash chain that is +/// seeded at index 0 and extended by one link per registered upgrade: /// -/// seed = keccak256(abi.encode(l2ChainId, address(this))) -/// upgradeScheduleId[0] = keccak256(abi.encode(seed, 0, timestamp_0)) -/// upgradeScheduleId[i] = keccak256(abi.encode(upgradeScheduleId[i-1], i, timestamp_i)) -/// scheduleId = upgradeScheduleId[n-1] (or seed when n == 0) +/// _upgradeScheduleId[0] = bytes32(0) (seed) +/// _upgradeScheduleId[i+1] = keccak256(abi.encode(_upgradeScheduleId[i], i, timestamp_i)) +/// scheduleId = _upgradeScheduleId[last] /// -/// where timestamp_i is the upgrade's current activation timestamp (0 = not yet scheduled). +/// where timestamp_i is upgrade i's current activation timestamp (0 = not yet scheduled). /// Changing any upgrade's timestamp recomputes its link and all subsequent links, making -/// scheduleId fully reproducible from (l2ChainId, address, upgrade count, current timestamps). -/// Proof journals bind to `scheduleId`, pinning every proof in a dispute game to the schedule -/// that was in effect at the game's L1 origin block. +/// scheduleId fully reproducible from (upgrade count, current timestamps). Keeping the seed as +/// the array's first element lets both `scheduleId` and the refresh loop avoid an +/// empty-registry special case. Proof journals bind to `scheduleId`, pinning every proof in a +/// dispute game to the schedule in effect at the game's L1 origin block; cross-chain domain +/// separation is provided by the journal itself, which commits the L2 chain id and registry +/// address alongside `scheduleId`. /// /// The contract is deployed behind an OP proxy: the implementation constructor disables -/// initializers, and `initialize` (run through the proxy) computes and stores `_seed` from the -/// proxy's own `address(this)`, so the schedule commitment is bound to the proxy address that -/// callers and off-chain consumers treat as the registry. +/// initializers, and `initialize` (run through the proxy) seeds the hash chain. contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, ReinitializableBase, ISemver { /// @notice A registered upgrade's id, current activation timestamp, and cumulative schedule hash. struct Upgrade { @@ -44,8 +45,6 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable bytes32 scheduleId; } - /// @notice Thrown when the L2 chain ID is zero. - error ProtocolVersions_InvalidL2ChainId(); /// @notice Thrown when an upgrade id is not registered. error ProtocolVersions_UnknownUpgrade(uint256 id); /// @notice Thrown when a protocol version is zero. @@ -81,19 +80,15 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable /// @notice Minimum notice period required when scheduling or modifying an activation timestamp. uint64 public constant MIN_NOTICE = 1 hours; - /// @notice Hash chain seed: keccak256(abi.encode(l2ChainId, address(this))). Set once in - /// `initialize` (run through the proxy, so `address(this)` is the proxy address) and - /// cached in storage to avoid recomputing on every _refreshScheduleId call. - bytes32 internal _seed; - /// @notice Activation timestamp for each registered upgrade, indexed by upgrade id (0 = not scheduled). /// An upgrade id is registered iff it is a valid index into this array. uint64[] internal _timestamps; - /// @notice Cumulative schedule hash up to and including each registered upgrade, indexed by upgrade id. - /// _upgradeScheduleId[i] = keccak256(abi.encode(_upgradeScheduleId[i-1], i, _timestamps[i])). - /// Stored per-upgrade so that changing upgrade j's timestamp requires recomputing only - /// j..n-1 links rather than the entire chain. + /// @notice Hash chain links. Element 0 is the seed (`bytes32(0)`), pushed in `initialize`; + /// element `i + 1` is the cumulative hash through upgrade `i`: + /// _upgradeScheduleId[i + 1] = keccak256(abi.encode(_upgradeScheduleId[i], i, _timestamps[i])). + /// Stored per-link so that changing upgrade j's timestamp recomputes only j..n-1 links + /// rather than the entire chain. Non-empty iff the contract has been initialized. bytes32[] internal _upgradeScheduleId; /// @notice The minimum protocol version clients must run. Settable by the owner via @@ -113,18 +108,18 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable _disableInitializers(); } - /// @notice Initializes the registry, computing the hash chain seed from the proxy context and - /// emitting the initial scheduleId. Callable only by the ProxyAdmin or its owner. - /// @param _l2ChainId L2 chain ID whose schedule is being committed. - function initialize(uint256 _l2ChainId) external reinitializer(initVersion()) { + /// @notice Initializes the registry by seeding the hash chain. Callable only by the ProxyAdmin + /// or its owner. + function initialize() external reinitializer(initVersion()) { // Initialization transactions must come from the ProxyAdmin or its owner. _assertOnlyProxyAdminOrProxyAdminOwner(); - if (_l2ChainId == 0) revert ProtocolVersions_InvalidL2ChainId(); - // address(this) is the proxy address, so the seed binds the schedule to the proxy registry. - _seed = keccak256(abi.encode(_l2ChainId, address(this))); + // 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 + // non-empty array double as the "initialized" flag. + _upgradeScheduleId.push(bytes32(0)); - emit ScheduleIdUpdated(_seed, block.number); + emit ScheduleIdUpdated(bytes32(0), block.number); } /// @notice Restricts a call to the ProxyAdmin owner. @@ -136,9 +131,9 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable /// @notice Returns the canonical schedule commitment. /// @return The current scheduleId hash. function scheduleId() external view returns (bytes32) { - if (_seed == 0) revert ProtocolVersions_NotInitialized(); uint256 n = _upgradeScheduleId.length; - return n == 0 ? _seed : _upgradeScheduleId[n - 1]; + if (n == 0) revert ProtocolVersions_NotInitialized(); + return _upgradeScheduleId[n - 1]; } /// @notice Returns the full ordered schedule: every registered upgrade with its current @@ -150,7 +145,8 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable uint256 n = _timestamps.length; schedule_ = new Upgrade[](n); for (uint256 i = 0; i < n; i++) { - schedule_[i] = Upgrade({ id: i, timestamp: _timestamps[i], scheduleId: _upgradeScheduleId[i] }); + // Upgrade i's cumulative hash lives at index i + 1 (index 0 is the seed). + schedule_[i] = Upgrade({ id: i, timestamp: _timestamps[i], scheduleId: _upgradeScheduleId[i + 1] }); } } @@ -159,9 +155,10 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable /// chain with the new (id, timestamp=0) link. /// @return id The ascending id assigned to the newly registered upgrade. function registerUpgrade() external onlyProxyAdminOwner returns (uint256 id) { - if (_seed == 0) revert ProtocolVersions_NotInitialized(); + if (_upgradeScheduleId.length == 0) revert ProtocolVersions_NotInitialized(); id = _timestamps.length; _timestamps.push(0); + // Reserve the link slot for this upgrade at index id + 1; _refreshScheduleId fills it. _upgradeScheduleId.push(); emit UpgradeRegistered(id); _refreshScheduleId(id); @@ -246,12 +243,12 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable function _refreshScheduleId(uint256 startIndex) internal { uint256 n = _timestamps.length; - // Initialise prev to the hash of the preceding link (or seed), then recompute from - // startIndex onward, storing each link. - bytes32 prev = startIndex == 0 ? _seed : _upgradeScheduleId[startIndex - 1]; + // _upgradeScheduleId[startIndex] is the link preceding upgrade `startIndex` (the seed when + // startIndex == 0). Recompute from startIndex onward, storing each link at index i + 1. + bytes32 prev = _upgradeScheduleId[startIndex]; for (uint256 i = startIndex; i < n; i++) { prev = keccak256(abi.encode(prev, i, _timestamps[i])); - _upgradeScheduleId[i] = prev; + _upgradeScheduleId[i + 1] = prev; } emit ScheduleIdUpdated(prev, block.number); diff --git a/test/L1/ProtocolVersions.t.sol b/test/L1/ProtocolVersions.t.sol index 833d93b61..a036d761b 100644 --- a/test/L1/ProtocolVersions.t.sol +++ b/test/L1/ProtocolVersions.t.sol @@ -24,7 +24,6 @@ abstract contract ProtocolVersions_TestInit is Test { /// @dev Ascending ids assigned by registration order in these tests. uint256 internal constant CANYON = 0; uint256 internal constant ECOTONE = 1; - uint256 internal constant L2_CHAIN_ID = 8453; address internal _owner = makeAddr("owner"); address internal _nonOwner = makeAddr("non-owner"); @@ -37,14 +36,14 @@ abstract contract ProtocolVersions_TestInit is Test { // proxyAdminOwner() resolves by calling owner() on the ProxyAdmin stored in the proxy slot. vm.mockCall(_proxyAdmin, abi.encodeWithSignature("owner()"), abi.encode(_owner)); _impl = new ProtocolVersions(); - protocolVersions = ProtocolVersions(_deployInitializedProxy(L2_CHAIN_ID)); + protocolVersions = ProtocolVersions(_deployInitializedProxy()); } /// @dev Deploys a proxy (admin = _proxyAdmin) over the shared impl and initializes it via the proxy. - function _deployInitializedProxy(uint256 _l2ChainId) internal returns (address proxy_) { + function _deployInitializedProxy() internal returns (address proxy_) { Proxy proxy = new Proxy(_proxyAdmin); vm.prank(_proxyAdmin); - proxy.upgradeToAndCall(address(_impl), abi.encodeCall(IProtocolVersions.initialize, (_l2ChainId))); + proxy.upgradeToAndCall(address(_impl), abi.encodeCall(IProtocolVersions.initialize, ())); proxy_ = address(proxy); } @@ -69,24 +68,11 @@ abstract contract ProtocolVersions_TestInit is Test { /// @title ProtocolVersions_Initialize_Test /// @notice Test contract for the ProtocolVersions initializer. contract ProtocolVersions_Initialize_Test is ProtocolVersions_TestInit { - /// @notice Tests that initialization sets the correct initial state. + /// @notice Tests that initialization sets the correct initial state. The seed is bytes32(0), so + /// the initial scheduleId is bytes32(0) until the first upgrade is registered. function test_initialize_setsInitialState_succeeds() external view { assertEq(protocolVersions.proxyAdminOwner(), _owner); - assertNotEq(protocolVersions.scheduleId(), bytes32(0)); - } - - /// @notice Tests that the scheduleId seed binds to the proxy address, not the implementation. - function test_initialize_seedBindsToProxyAddress_succeeds() external view { - bytes32 expectedSeed = keccak256(abi.encode(L2_CHAIN_ID, address(protocolVersions))); - assertEq(protocolVersions.scheduleId(), expectedSeed); - } - - /// @notice Tests that initializing with a zero chain ID reverts. - function test_initialize_zeroChainId_reverts() external { - ProtocolVersions uninitialized = _deployUninitializedProxy(); - vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidL2ChainId.selector); - vm.prank(_proxyAdmin); - uninitialized.initialize(0); + assertEq(protocolVersions.scheduleId(), bytes32(0)); } /// @notice Tests that only the ProxyAdmin or its owner can initialize. @@ -94,21 +80,21 @@ contract ProtocolVersions_Initialize_Test is ProtocolVersions_TestInit { ProtocolVersions uninitialized = _deployUninitializedProxy(); vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner.selector); vm.prank(_nonOwner); - uninitialized.initialize(L2_CHAIN_ID); + uninitialized.initialize(); } /// @notice Tests that the contract cannot be initialized twice. function test_initialize_alreadyInitialized_reverts() external { vm.expectRevert("Initializable: contract is already initialized"); vm.prank(_proxyAdmin); - protocolVersions.initialize(L2_CHAIN_ID); + protocolVersions.initialize(); } /// @notice Tests that the implementation itself cannot be initialized (initializers disabled). function test_initialize_implementationDisabled_reverts() external { vm.expectRevert("Initializable: contract is already initialized"); vm.prank(_proxyAdmin); - _impl.initialize(L2_CHAIN_ID); + _impl.initialize(); } } @@ -347,7 +333,7 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { protocolVersions.setTimestamp(0, ts); } - /// @notice Tests that scheduleId is reproducible from (chainId, address, ascending ids, timestamps). + /// @notice Tests that scheduleId is reproducible from (ascending ids, timestamps). function test_setTimestamp_scheduleIdReproducible_succeeds() external { vm.prank(_owner); protocolVersions.registerUpgrade(); @@ -361,8 +347,8 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { vm.prank(_owner); protocolVersions.setTimestamp(ECOTONE, ts2); - // Reproduce the chain from scratch. - bytes32 seed = keccak256(abi.encode(uint256(8453), address(protocolVersions))); + // Reproduce the chain from scratch, starting from the bytes32(0) seed. + bytes32 seed = bytes32(0); bytes32 link0 = keccak256(abi.encode(seed, uint256(0), ts1)); bytes32 link1 = keccak256(abi.encode(link0, uint256(1), ts2)); diff --git a/test/vendor/Initializable.t.sol b/test/vendor/Initializable.t.sol index 18e6e9cdd..5f21b74c0 100644 --- a/test/vendor/Initializable.t.sol +++ b/test/vendor/Initializable.t.sol @@ -214,7 +214,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, (deploy.cfg().l2ChainId())); + initCalldata = abi.encodeCall(protocolVersions.initialize, ()); contracts.push( InitializeableContract({ name: "ProtocolVersionsImpl", From 29fff2f731fe50064745e3d9d0eea7476b62e513 Mon Sep 17 00:00:00 2001 From: PelleKrab Date: Tue, 7 Jul 2026 13:38:31 -0700 Subject: [PATCH 14/20] fix(L1): apply ProtocolVersions review nits Co-authored-by: Codex --- interfaces/L1/IProtocolVersions.sol | 7 ++-- scripts/deploy/DeployConfig.s.sol | 2 ++ scripts/deploy/SystemDeploy.s.sol | 5 +-- scripts/libraries/Types.sol | 1 + snapshots/abi/ProtocolVersions.json | 33 +++++++++++++++--- snapshots/semver-lock.json | 4 +-- src/L1/ProtocolVersions.sol | 54 +++++++++++++++++++---------- test/L1/ProtocolVersions.t.sol | 45 ++++++++++++++++++++---- test/deploy/SystemDeploy.t.sol | 6 +++- test/vendor/Initializable.t.sol | 2 +- 10 files changed, 120 insertions(+), 39 deletions(-) diff --git a/interfaces/L1/IProtocolVersions.sol b/interfaces/L1/IProtocolVersions.sol index 159ab158e..c39afe511 100644 --- a/interfaces/L1/IProtocolVersions.sol +++ b/interfaces/L1/IProtocolVersions.sol @@ -34,12 +34,13 @@ interface IProtocolVersions is IProxyAdminOwnedBase, ISemver, IReinitializableBa function chainTeam() external view returns (address); function scheduleId() external view returns (bytes32); + function scheduleId(uint256 id) external view returns (bytes32); function minimumProtocolVersion() external view returns (uint256); - function getSchedule() external view returns (Upgrade[] memory schedule_); + function getSchedule() external view returns (Upgrade[] memory); - function initialize() external; - function registerUpgrade() external returns (uint256 id); + function initialize(address _chainTeam) external; + function registerUpgrade() external returns (uint256); function setMinimumProtocolVersion(uint256 protocolVersion) external; function setTimestamp(uint256 id, uint64 timestamp) external; function setChainTeam(address newChainTeam) external; diff --git a/scripts/deploy/DeployConfig.s.sol b/scripts/deploy/DeployConfig.s.sol index 7fe484580..c098e1232 100644 --- a/scripts/deploy/DeployConfig.s.sol +++ b/scripts/deploy/DeployConfig.s.sol @@ -12,6 +12,7 @@ contract DeployConfig is Script { address public baseFeeVaultRecipient; address public batchSenderAddress; + address public chainTeam; address public finalSystemOwner; address public l1FeeVaultRecipient; address public nitroEnclaveVerifier; @@ -65,6 +66,7 @@ contract DeployConfig is Script { baseFeeVaultRecipient = _json.readAddress("$.baseFeeVaultRecipient"); batchSenderAddress = _json.readAddress("$.batchSenderAddress"); + chainTeam = _json.readAddressOr("$.chainTeam", address(0)); finalSystemOwner = _json.readAddress("$.finalSystemOwner"); l1FeeVaultRecipient = _json.readAddress("$.l1FeeVaultRecipient"); nitroEnclaveVerifier = _json.readAddress("$.nitroEnclaveVerifier"); diff --git a/scripts/deploy/SystemDeploy.s.sol b/scripts/deploy/SystemDeploy.s.sol index cdad82131..7cc217376 100644 --- a/scripts/deploy/SystemDeploy.s.sol +++ b/scripts/deploy/SystemDeploy.s.sol @@ -281,7 +281,8 @@ contract SystemDeploy is Script { opChainProxyAdminOwner: cfg.finalSystemOwner(), systemConfigOwner: cfg.finalSystemOwner(), batcher: cfg.batchSenderAddress(), - unsafeBlockSigner: cfg.p2pSequencerAddress() + unsafeBlockSigner: cfg.p2pSequencerAddress(), + chainTeam: cfg.chainTeam() }), basefeeScalar: cfg.basefeeScalar(), blobBasefeeScalar: cfg.blobbasefeeScalar(), @@ -629,7 +630,7 @@ contract SystemDeploy is Script { _output.opChainProxyAdmin, address(_output.protocolVersionsProxy), _impls.protocolVersionsImpl, - abi.encodeCall(IProtocolVersions.initialize, ()) + abi.encodeCall(IProtocolVersions.initialize, (_input.roles.chainTeam)) ); } diff --git a/scripts/libraries/Types.sol b/scripts/libraries/Types.sol index cdde01db4..17c574dd9 100644 --- a/scripts/libraries/Types.sol +++ b/scripts/libraries/Types.sol @@ -29,6 +29,7 @@ library Types { address systemConfigOwner; address batcher; address unsafeBlockSigner; + address chainTeam; } /// @notice The full set of inputs to deploy a new OP Stack chain. diff --git a/snapshots/abi/ProtocolVersions.json b/snapshots/abi/ProtocolVersions.json index b0cdd59fd..93f5999cd 100644 --- a/snapshots/abi/ProtocolVersions.json +++ b/snapshots/abi/ProtocolVersions.json @@ -71,7 +71,7 @@ } ], "internalType": "struct ProtocolVersions.Upgrade[]", - "name": "schedule_", + "name": "", "type": "tuple[]" } ], @@ -92,7 +92,13 @@ "type": "function" }, { - "inputs": [], + "inputs": [ + { + "internalType": "address", + "name": "_chainTeam", + "type": "address" + } + ], "name": "initialize", "outputs": [], "stateMutability": "nonpayable", @@ -143,7 +149,7 @@ "outputs": [ { "internalType": "uint256", - "name": "id", + "name": "", "type": "uint256" } ], @@ -163,6 +169,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "scheduleId", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -217,7 +242,7 @@ "type": "string" } ], - "stateMutability": "view", + "stateMutability": "pure", "type": "function" }, { diff --git a/snapshots/semver-lock.json b/snapshots/semver-lock.json index d64948b38..fa371ca9e 100644 --- a/snapshots/semver-lock.json +++ b/snapshots/semver-lock.json @@ -20,8 +20,8 @@ "sourceCodeHash": "0xa1281f5da03fa0431eabad33da23f51a8d835b0dd04554603fd59e11efe9fa51" }, "src/L1/ProtocolVersions.sol:ProtocolVersions": { - "initCodeHash": "0x0641815cc45d4f321a4d37336b7b2c871a84e1d84a9c1bc4f5e0d35b33eb87f0", - "sourceCodeHash": "0x860e3ef7f0ccf377a63649a95066f33cf09d0a5b94dac790b1cd3ee83216ee03" + "initCodeHash": "0x5237d46f7f1564996b5a4cd44ac31f9b7ce012b3408718a2ea5f9f2ab1f67f9e", + "sourceCodeHash": "0x0ad6d58ee1081b2e1cbb3203ae48269e7fa3e7c2c43376db6642300d9fa85da1" }, "src/L1/SuperchainConfig.sol:SuperchainConfig": { "initCodeHash": "0x9b1f3555b499709485d51d5d9665002c0eb1e5eb893be1fb978a30749e894858", diff --git a/src/L1/ProtocolVersions.sol b/src/L1/ProtocolVersions.sol index a39169442..4e0d262d1 100644 --- a/src/L1/ProtocolVersions.sol +++ b/src/L1/ProtocolVersions.sol @@ -14,7 +14,7 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; /// @notice Security Council-controlled upgrade activation schedule contract. /// @dev Maintains an ordered registry of upgrades and their L2 activation timestamps. /// Each upgrade is identified by an ascending numeric `id` equal to its registration -/// index (0, 1, 2, ...). Human-readable names are intentionally kept off-chain: a client +/// index (0, 1, 2, ...). Human-readable names are intentionally kept offchain: a client /// maps `id` => name via its own static configuration. Because the registry is strictly /// append-only (upgrades are never removed or reordered), an `id` permanently refers to the /// same upgrade. @@ -73,10 +73,6 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable /// @notice Emitted when the chainTeam role changes. event ChainTeamUpdated(address indexed previousChainTeam, address indexed newChainTeam); - /// @notice Semantic version. - /// @custom:semver 1.0.0 - string public constant version = "1.0.0"; - /// @notice Minimum notice period required when scheduling or modifying an activation timestamp. uint64 public constant MIN_NOTICE = 1 hours; @@ -92,7 +88,7 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable bytes32[] internal _upgradeScheduleId; /// @notice The minimum protocol version clients must run. Settable by the owner via - /// `setMinimumProtocolVersion`. Informational only — read off-chain by clients; + /// `setMinimumProtocolVersion`. Informational only — read offchain by clients; /// not part of the scheduleId commitment. uint256 public minimumProtocolVersion; @@ -103,14 +99,21 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable /// earlier, or schedule a brand-new activation. Unset (zero) by default. address public chainTeam; + /// @notice Semantic version. + /// @custom:semver 1.0.0 + function version() public pure virtual returns (string memory) { + return "1.0.0"; + } + /// @notice Disables initializers on the implementation so it can only be used behind a proxy. constructor() ReinitializableBase(1) { _disableInitializers(); } - /// @notice Initializes the registry by seeding the hash chain. Callable only by the ProxyAdmin - /// or its owner. - function initialize() external reinitializer(initVersion()) { + /// @notice Initializes the registry by seeding the hash chain and appointing the initial + /// chainTeam. Callable only by the ProxyAdmin or its owner. + /// @param _chainTeam Initial chainTeam allowed to delay activations, or address(0) to leave unset. + function initialize(address _chainTeam) external reinitializer(initVersion()) { // Initialization transactions must come from the ProxyAdmin or its owner. _assertOnlyProxyAdminOrProxyAdminOwner(); @@ -118,8 +121,10 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable // `scheduleId` and `_refreshScheduleId` avoid an empty-registry special case, and makes a // non-empty array double as the "initialized" flag. _upgradeScheduleId.push(bytes32(0)); - emit ScheduleIdUpdated(bytes32(0), block.number); + + chainTeam = _chainTeam; + emit ChainTeamUpdated(address(0), _chainTeam); } /// @notice Restricts a call to the ProxyAdmin owner. @@ -136,36 +141,47 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable return _upgradeScheduleId[n - 1]; } + /// @notice Returns the schedule commitment as of a specific registered upgrade. + /// @param id The upgrade id to query. + /// @return The scheduleId hash committing to upgrades 0 through `id`. + function scheduleId(uint256 id) external view returns (bytes32) { + _assertRegistered(id); + // Upgrade `id`'s cumulative hash lives at index id + 1 (index 0 is the seed). + return _upgradeScheduleId[id + 1]; + } + /// @notice Returns the full ordered schedule: every registered upgrade with its current /// activation timestamp and cumulative schedule hash. The array index equals the - /// upgrade `id`; names are resolved off-chain. + /// upgrade `id`; names are resolved offchain. /// @dev Calling via eth_call is gas-free; no transaction is submitted. - /// @return schedule_ Ordered array of Upgrade structs, one per registered upgrade. - function getSchedule() external view returns (Upgrade[] memory schedule_) { + /// @return Ordered array of Upgrade structs, one per registered upgrade. + function getSchedule() external view returns (Upgrade[] memory) { uint256 n = _timestamps.length; - schedule_ = new Upgrade[](n); + Upgrade[] memory schedule = new Upgrade[](n); for (uint256 i = 0; i < n; i++) { // Upgrade i's cumulative hash lives at index i + 1 (index 0 is the seed). - schedule_[i] = Upgrade({ id: i, timestamp: _timestamps[i], scheduleId: _upgradeScheduleId[i + 1] }); + schedule[i] = Upgrade({ id: i, timestamp: _timestamps[i], scheduleId: _upgradeScheduleId[i + 1] }); } + return schedule; } /// @notice Registers a new upgrade, assigning it the next ascending id. Owner only. /// @dev The upgrade starts unscheduled (timestamp 0). Registration extends the scheduleId /// chain with the new (id, timestamp=0) link. - /// @return id The ascending id assigned to the newly registered upgrade. - function registerUpgrade() external onlyProxyAdminOwner returns (uint256 id) { + /// @return The ascending id assigned to the newly registered upgrade. + function registerUpgrade() external onlyProxyAdminOwner returns (uint256) { if (_upgradeScheduleId.length == 0) revert ProtocolVersions_NotInitialized(); - id = _timestamps.length; + uint256 id = _timestamps.length; _timestamps.push(0); // Reserve the link slot for this upgrade at index id + 1; _refreshScheduleId fills it. _upgradeScheduleId.push(); emit UpgradeRegistered(id); _refreshScheduleId(id); + return id; } /// @notice Sets the minimum protocol version clients must run. Owner (Security Council) only. - /// @dev Informational signal for off-chain clients; independent of the upgrade schedule and NOT + /// @dev Informational signal for offchain clients; independent of the upgrade schedule and NOT /// part of the scheduleId commitment, so it can be updated at any time without shifting any /// proof binding. /// @param protocolVersion Packed semver uint256 (must be non-zero). diff --git a/test/L1/ProtocolVersions.t.sol b/test/L1/ProtocolVersions.t.sol index a036d761b..ca4e9f31d 100644 --- a/test/L1/ProtocolVersions.t.sol +++ b/test/L1/ProtocolVersions.t.sol @@ -36,14 +36,15 @@ abstract contract ProtocolVersions_TestInit is Test { // proxyAdminOwner() resolves by calling owner() on the ProxyAdmin stored in the proxy slot. vm.mockCall(_proxyAdmin, abi.encodeWithSignature("owner()"), abi.encode(_owner)); _impl = new ProtocolVersions(); - protocolVersions = ProtocolVersions(_deployInitializedProxy()); + protocolVersions = ProtocolVersions(_deployInitializedProxy(address(0))); } - /// @dev Deploys a proxy (admin = _proxyAdmin) over the shared impl and initializes it via the proxy. - function _deployInitializedProxy() internal returns (address proxy_) { + /// @dev Deploys a proxy (admin = _proxyAdmin) over the shared impl and initializes it via the + /// proxy, appointing `_team` as the chainTeam. + function _deployInitializedProxy(address _team) internal returns (address proxy_) { Proxy proxy = new Proxy(_proxyAdmin); vm.prank(_proxyAdmin); - proxy.upgradeToAndCall(address(_impl), abi.encodeCall(IProtocolVersions.initialize, ())); + proxy.upgradeToAndCall(address(_impl), abi.encodeCall(IProtocolVersions.initialize, (_team))); proxy_ = address(proxy); } @@ -75,26 +76,36 @@ contract ProtocolVersions_Initialize_Test is ProtocolVersions_TestInit { assertEq(protocolVersions.scheduleId(), bytes32(0)); } + /// @notice Tests that initialization appoints the provided chainTeam and emits the event. + function test_initialize_setsChainTeam_succeeds() external { + ProtocolVersions uninitialized = _deployUninitializedProxy(); + vm.expectEmit(true, true, false, false, address(uninitialized)); + emit ChainTeamUpdated(address(0), _chainTeam); + vm.prank(_proxyAdmin); + uninitialized.initialize(_chainTeam); + assertEq(uninitialized.chainTeam(), _chainTeam); + } + /// @notice Tests that only the ProxyAdmin or its owner can initialize. function test_initialize_notProxyAdminOrOwner_reverts() external { ProtocolVersions uninitialized = _deployUninitializedProxy(); vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner.selector); vm.prank(_nonOwner); - uninitialized.initialize(); + uninitialized.initialize(_chainTeam); } /// @notice Tests that the contract cannot be initialized twice. function test_initialize_alreadyInitialized_reverts() external { vm.expectRevert("Initializable: contract is already initialized"); vm.prank(_proxyAdmin); - protocolVersions.initialize(); + protocolVersions.initialize(address(0)); } /// @notice Tests that the implementation itself cannot be initialized (initializers disabled). function test_initialize_implementationDisabled_reverts() external { vm.expectRevert("Initializable: contract is already initialized"); vm.prank(_proxyAdmin); - _impl.initialize(); + _impl.initialize(address(0)); } } @@ -535,4 +546,24 @@ contract ProtocolVersions_Uncategorized_Test is ProtocolVersions_TestInit { // The last entry's scheduleId is the contract's current scheduleId. assertEq(s[1].scheduleId, protocolVersions.scheduleId()); } + + /// @notice Tests that `scheduleId(id)` returns each upgrade's cumulative commitment, matching + /// `getSchedule`, and that the last upgrade's commitment equals the current scheduleId. + function test_scheduleId_byId_succeeds() external { + vm.prank(_owner); + protocolVersions.registerUpgrade(); + vm.prank(_owner); + protocolVersions.registerUpgrade(); + + ProtocolVersions.Upgrade[] memory s = protocolVersions.getSchedule(); + assertEq(protocolVersions.scheduleId(CANYON), s[CANYON].scheduleId); + assertEq(protocolVersions.scheduleId(ECOTONE), s[ECOTONE].scheduleId); + assertEq(protocolVersions.scheduleId(ECOTONE), protocolVersions.scheduleId()); + } + + /// @notice Tests that `scheduleId(id)` reverts for an unregistered upgrade. + function test_scheduleId_byId_unregistered_reverts() external { + vm.expectRevert(abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_UnknownUpgrade.selector, uint256(0))); + protocolVersions.scheduleId(0); + } } diff --git a/test/deploy/SystemDeploy.t.sol b/test/deploy/SystemDeploy.t.sol index 6a94439d6..ea6e45de1 100644 --- a/test/deploy/SystemDeploy.t.sol +++ b/test/deploy/SystemDeploy.t.sol @@ -39,6 +39,7 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { address internal unsafeBlockSigner = makeAddr("unsafeBlockSigner"); address internal proposer = makeAddr("proposer"); address internal challenger = makeAddr("challenger"); + address internal chainTeam = makeAddr("chainTeam"); MockNitroEnclaveVerifier internal nitroEnclaveVerifier; MockSP1Verifier internal sp1Verifier; @@ -119,6 +120,8 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { assertNotEq(address(output.opChain.optimismPortalProxy), address(0), "portal"); assertNotEq(address(output.opChain.ethLockboxProxy), address(0), "lockbox"); assertNotEq(address(output.opChain.delayedWETHProxy), address(0), "delayed weth"); + assertNotEq(address(output.opChain.protocolVersionsProxy), address(0), "protocol versions"); + assertEq(output.opChain.protocolVersionsProxy.chainTeam(), chainTeam, "protocol versions chain team"); assertEq(output.opChain.opChainProxyAdmin.owner(), owner, "op chain proxy admin owner"); assertEq(output.opChain.systemConfigProxy.batchInbox(), Types.chainIdToBatchInboxAddress(l2ChainId), "inbox"); @@ -203,7 +206,8 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { opChainProxyAdminOwner: owner, systemConfigOwner: owner, batcher: batcher, - unsafeBlockSigner: unsafeBlockSigner + unsafeBlockSigner: unsafeBlockSigner, + chainTeam: chainTeam }), basefeeScalar: 100, blobBasefeeScalar: 200, diff --git a/test/vendor/Initializable.t.sol b/test/vendor/Initializable.t.sol index 5f21b74c0..b1c762141 100644 --- a/test/vendor/Initializable.t.sol +++ b/test/vendor/Initializable.t.sol @@ -214,7 +214,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, ()); + initCalldata = abi.encodeCall(protocolVersions.initialize, (address(0))); contracts.push( InitializeableContract({ name: "ProtocolVersionsImpl", From 2257727990b3c622e83c33e1667ab38c48f2b2ba Mon Sep 17 00:00:00 2001 From: PelleKrab Date: Tue, 7 Jul 2026 15:23:59 -0700 Subject: [PATCH 15/20] refactor(L1): reduce ProtocolVersions API surface Co-authored-by: Codex --- interfaces/L1/IProtocolVersions.sol | 12 +-- snapshots/abi/ProtocolVersions.json | 40 ++++----- snapshots/semver-lock.json | 4 +- src/L1/ProtocolVersions.sol | 89 +++++++++++--------- test/L1/ProtocolVersions.t.sol | 125 +++++++++++++++++----------- 5 files changed, 146 insertions(+), 124 deletions(-) diff --git a/interfaces/L1/IProtocolVersions.sol b/interfaces/L1/IProtocolVersions.sol index c39afe511..4dc9d86de 100644 --- a/interfaces/L1/IProtocolVersions.sol +++ b/interfaces/L1/IProtocolVersions.sol @@ -8,12 +8,6 @@ import { IReinitializableBase } from "interfaces/universal/IReinitializableBase. /// @title IProtocolVersions /// @notice Interface for the ProtocolVersions upgrade schedule contract. interface IProtocolVersions is IProxyAdminOwnedBase, ISemver, IReinitializableBase { - struct Upgrade { - uint256 id; - uint64 timestamp; - bytes32 scheduleId; - } - error ProtocolVersions_UnknownUpgrade(uint256 id); error ProtocolVersions_InvalidProtocolVersion(); error ProtocolVersions_ActivationAlreadyPassed(uint256 id, uint64 activationTimestamp); @@ -26,7 +20,7 @@ interface IProtocolVersions is IProxyAdminOwnedBase, ISemver, IReinitializableBa event UpgradeRegistered(uint256 indexed id); event MinimumProtocolVersionUpdated(uint256 indexed protocolVersion); event TimestampSet(uint256 indexed id, uint256 timestamp); - event ScheduleIdUpdated(bytes32 indexed newScheduleId, uint256 indexed blockNumber); + event ScheduleIdUpdated(bytes32 indexed newScheduleId); event ChainTeamUpdated(address indexed previousChainTeam, address indexed newChainTeam); event Initialized(uint8 version); @@ -37,10 +31,10 @@ interface IProtocolVersions is IProxyAdminOwnedBase, ISemver, IReinitializableBa function scheduleId(uint256 id) external view returns (bytes32); function minimumProtocolVersion() external view returns (uint256); - function getSchedule() external view returns (Upgrade[] memory); + function getSchedule() external view returns (uint64[] memory); function initialize(address _chainTeam) external; - function registerUpgrade() external returns (uint256); + function registerUpgrade(uint64 timestamp, uint256 minProtocolVersion) external returns (uint256); function setMinimumProtocolVersion(uint256 protocolVersion) external; function setTimestamp(uint256 id, uint64 timestamp) external; function setChainTeam(address newChainTeam) external; diff --git a/snapshots/abi/ProtocolVersions.json b/snapshots/abi/ProtocolVersions.json index 93f5999cd..9ad7ad31b 100644 --- a/snapshots/abi/ProtocolVersions.json +++ b/snapshots/abi/ProtocolVersions.json @@ -53,26 +53,9 @@ "name": "getSchedule", "outputs": [ { - "components": [ - { - "internalType": "uint256", - "name": "id", - "type": "uint256" - }, - { - "internalType": "uint64", - "name": "timestamp", - "type": "uint64" - }, - { - "internalType": "bytes32", - "name": "scheduleId", - "type": "bytes32" - } - ], - "internalType": "struct ProtocolVersions.Upgrade[]", + "internalType": "uint64[]", "name": "", - "type": "tuple[]" + "type": "uint64[]" } ], "stateMutability": "view", @@ -144,7 +127,18 @@ "type": "function" }, { - "inputs": [], + "inputs": [ + { + "internalType": "uint64", + "name": "timestamp", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "minProtocolVersion", + "type": "uint256" + } + ], "name": "registerUpgrade", "outputs": [ { @@ -298,12 +292,6 @@ "internalType": "bytes32", "name": "newScheduleId", "type": "bytes32" - }, - { - "indexed": true, - "internalType": "uint256", - "name": "blockNumber", - "type": "uint256" } ], "name": "ScheduleIdUpdated", diff --git a/snapshots/semver-lock.json b/snapshots/semver-lock.json index fa371ca9e..7a6bc3ea5 100644 --- a/snapshots/semver-lock.json +++ b/snapshots/semver-lock.json @@ -20,8 +20,8 @@ "sourceCodeHash": "0xa1281f5da03fa0431eabad33da23f51a8d835b0dd04554603fd59e11efe9fa51" }, "src/L1/ProtocolVersions.sol:ProtocolVersions": { - "initCodeHash": "0x5237d46f7f1564996b5a4cd44ac31f9b7ce012b3408718a2ea5f9f2ab1f67f9e", - "sourceCodeHash": "0x0ad6d58ee1081b2e1cbb3203ae48269e7fa3e7c2c43376db6642300d9fa85da1" + "initCodeHash": "0x201b442493a591859132351e024a6edbb99d76bd5a6672a0587652038fad3f52", + "sourceCodeHash": "0x20d923ffd5a5a871a49901546e8d83a0f048a2e985371e40a099262713c0c8b1" }, "src/L1/SuperchainConfig.sol:SuperchainConfig": { "initCodeHash": "0x9b1f3555b499709485d51d5d9665002c0eb1e5eb893be1fb978a30749e894858", diff --git a/src/L1/ProtocolVersions.sol b/src/L1/ProtocolVersions.sol index 4e0d262d1..83e5baea5 100644 --- a/src/L1/ProtocolVersions.sol +++ b/src/L1/ProtocolVersions.sol @@ -38,13 +38,6 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; /// The contract is deployed behind an OP proxy: the implementation constructor disables /// initializers, and `initialize` (run through the proxy) seeds the hash chain. contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, ReinitializableBase, ISemver { - /// @notice A registered upgrade's id, current activation timestamp, and cumulative schedule hash. - struct Upgrade { - uint256 id; - uint64 timestamp; - bytes32 scheduleId; - } - /// @notice Thrown when an upgrade id is not registered. error ProtocolVersions_UnknownUpgrade(uint256 id); /// @notice Thrown when a protocol version is zero. @@ -69,7 +62,7 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable /// @notice Emitted when an upgrade's activation timestamp is set, cleared, or delayed. event TimestampSet(uint256 indexed id, uint256 timestamp); /// @notice Emitted when the schedule commitment changes. - event ScheduleIdUpdated(bytes32 indexed newScheduleId, uint256 indexed blockNumber); + event ScheduleIdUpdated(bytes32 indexed newScheduleId); /// @notice Emitted when the chainTeam role changes. event ChainTeamUpdated(address indexed previousChainTeam, address indexed newChainTeam); @@ -121,18 +114,12 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable // `scheduleId` and `_refreshScheduleId` avoid an empty-registry special case, and makes a // non-empty array double as the "initialized" flag. _upgradeScheduleId.push(bytes32(0)); - emit ScheduleIdUpdated(bytes32(0), block.number); + emit ScheduleIdUpdated(bytes32(0)); chainTeam = _chainTeam; emit ChainTeamUpdated(address(0), _chainTeam); } - /// @notice Restricts a call to the ProxyAdmin owner. - modifier onlyProxyAdminOwner() { - _assertOnlyProxyAdminOwner(); - _; - } - /// @notice Returns the canonical schedule commitment. /// @return The current scheduleId hash. function scheduleId() external view returns (bytes32) { @@ -150,33 +137,46 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable return _upgradeScheduleId[id + 1]; } - /// @notice Returns the full ordered schedule: every registered upgrade with its current - /// activation timestamp and cumulative schedule hash. The array index equals the - /// upgrade `id`; names are resolved offchain. + /// @notice Returns the activation timestamp for every registered upgrade, ordered by upgrade id + /// (0 = not scheduled). The array index equals the upgrade `id`; names are resolved + /// offchain, and per-upgrade schedule hashes can be reproduced from these timestamps and + /// the seed or read via `scheduleId(id)`. /// @dev Calling via eth_call is gas-free; no transaction is submitted. - /// @return Ordered array of Upgrade structs, one per registered upgrade. - function getSchedule() external view returns (Upgrade[] memory) { - uint256 n = _timestamps.length; - Upgrade[] memory schedule = new Upgrade[](n); - for (uint256 i = 0; i < n; i++) { - // Upgrade i's cumulative hash lives at index i + 1 (index 0 is the seed). - schedule[i] = Upgrade({ id: i, timestamp: _timestamps[i], scheduleId: _upgradeScheduleId[i + 1] }); - } - return schedule; + /// @return Ordered activation timestamps, one per registered upgrade. + function getSchedule() external view returns (uint64[] memory) { + return _timestamps; } - /// @notice Registers a new upgrade, assigning it the next ascending id. Owner only. - /// @dev The upgrade starts unscheduled (timestamp 0). Registration extends the scheduleId - /// chain with the new (id, timestamp=0) link. + /// @notice Registers a new upgrade, assigning it the next ascending id, optionally scheduling its + /// activation and bumping the minimum protocol version in the same call. Owner only. + /// @dev Pass `timestamp` 0 to register without scheduling (schedule later via `setTimestamp`), or + /// a value at least MIN_NOTICE seconds in the future to register and schedule at once. + /// Either way registration extends the scheduleId chain with the new upgrade's link. + /// @param timestamp Future Unix activation timestamp (>= block.timestamp + MIN_NOTICE), or 0 to + /// leave the upgrade unscheduled. + /// @param minProtocolVersion New minimum protocol version to set at registration, or 0 to leave + /// the current minimum unchanged. /// @return The ascending id assigned to the newly registered upgrade. - function registerUpgrade() external onlyProxyAdminOwner returns (uint256) { + function registerUpgrade(uint64 timestamp, uint256 minProtocolVersion) external returns (uint256) { + _assertOnlyProxyAdminOwner(); if (_upgradeScheduleId.length == 0) revert ProtocolVersions_NotInitialized(); + if (timestamp != 0 && timestamp < uint64(block.timestamp) + MIN_NOTICE) { + revert ProtocolVersions_InsufficientNotice(timestamp); + } uint256 id = _timestamps.length; _timestamps.push(0); - // Reserve the link slot for this upgrade at index id + 1; _refreshScheduleId fills it. + // Reserve the link slot for this upgrade at index id + 1. _upgradeScheduleId.push(); emit UpgradeRegistered(id); - _refreshScheduleId(id); + if (timestamp == 0) { + // Register-only: commit the new (id, 0) link. + _refreshScheduleId(id); + } else { + // Register and schedule at once via the shared pure-write helper. + _writeTimestamp(id, timestamp); + } + // Optionally bump the global minimum protocol version in the same call (0 = leave unchanged). + if (minProtocolVersion != 0) _writeMinimumProtocolVersion(minProtocolVersion); return id; } @@ -185,10 +185,10 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable /// part of the scheduleId commitment, so it can be updated at any time without shifting any /// proof binding. /// @param protocolVersion Packed semver uint256 (must be non-zero). - function setMinimumProtocolVersion(uint256 protocolVersion) external onlyProxyAdminOwner { + function setMinimumProtocolVersion(uint256 protocolVersion) external { + _assertOnlyProxyAdminOwner(); if (protocolVersion == 0) revert ProtocolVersions_InvalidProtocolVersion(); - minimumProtocolVersion = protocolVersion; - emit MinimumProtocolVersionUpdated(protocolVersion); + _writeMinimumProtocolVersion(protocolVersion); } /// @notice Sets the activation timestamp for one upgrade by id. Pass 0 to clear. @@ -198,7 +198,8 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable /// @param id The upgrade to schedule. /// @param timestamp Future Unix timestamp for L2 activation (must be >= block.timestamp + MIN_NOTICE), or 0 to /// clear. - function setTimestamp(uint256 id, uint64 timestamp) external onlyProxyAdminOwner { + function setTimestamp(uint256 id, uint64 timestamp) external { + _assertOnlyProxyAdminOwner(); _assertRegistered(id); uint64 current = _timestamps[id]; if (current != 0 && uint64(block.timestamp) >= current) { @@ -213,7 +214,8 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable /// @notice Appoints, replaces, or clears (set to zero) the chainTeam role. Owner only. /// @param newChainTeam New chainTeam address, or address(0) to revoke the role. - function setChainTeam(address newChainTeam) external onlyProxyAdminOwner { + function setChainTeam(address newChainTeam) external { + _assertOnlyProxyAdminOwner(); emit ChainTeamUpdated(chainTeam, newChainTeam); chainTeam = newChainTeam; } @@ -240,7 +242,7 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable if (newTimestamp <= current) revert ProtocolVersions_DelayMustBeLater(current, newTimestamp); // The new timestamp must also provide at least MIN_NOTICE seconds of notice from now. uint64 minFloor = uint64(block.timestamp) + MIN_NOTICE; - if (newTimestamp < minFloor) revert ProtocolVersions_DelayMustBeLater(minFloor, newTimestamp); + if (newTimestamp < minFloor) revert ProtocolVersions_InsufficientNotice(newTimestamp); _writeTimestamp(id, newTimestamp); } @@ -252,6 +254,13 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable _refreshScheduleId(id); } + /// @dev Writes the minimum protocol version and emits the update. Validation is the caller's + /// responsibility. + function _writeMinimumProtocolVersion(uint256 protocolVersion) internal { + minimumProtocolVersion = protocolVersion; + emit MinimumProtocolVersionUpdated(protocolVersion); + } + /// @dev Recomputes the per-upgrade cumulative hash chain starting from upgrade `startIndex` and /// bubbles the result through all subsequent registered upgrades. Cost is O(n-startIndex). /// Only ever called after a state change (registration, or a timestamp that actually moved), @@ -267,7 +276,7 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable _upgradeScheduleId[i + 1] = prev; } - emit ScheduleIdUpdated(prev, block.number); + emit ScheduleIdUpdated(prev); } /// @dev Reverts if `id` is not a registered upgrade. diff --git a/test/L1/ProtocolVersions.t.sol b/test/L1/ProtocolVersions.t.sol index ca4e9f31d..601af38e5 100644 --- a/test/L1/ProtocolVersions.t.sol +++ b/test/L1/ProtocolVersions.t.sol @@ -59,7 +59,7 @@ abstract contract ProtocolVersions_TestInit is Test { /// @dev Registers the first upgrade (id CANYON) and schedules it for block.timestamp + MIN_NOTICE + delay. function _scheduleCanyon(uint64 _delay) internal returns (uint64 ts_) { vm.prank(_owner); - protocolVersions.registerUpgrade(); + protocolVersions.registerUpgrade(0, 0); ts_ = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + _delay; vm.prank(_owner); protocolVersions.setTimestamp(CANYON, ts_); @@ -127,7 +127,7 @@ contract ProtocolVersions_RegisterUpgrade_Test is ProtocolVersions_TestInit { vm.roll(block.number + 1); vm.prank(_owner); - protocolVersions.registerUpgrade(); + protocolVersions.registerUpgrade(0, 0); assertNotEq(protocolVersions.scheduleId(), idBefore); } @@ -135,11 +135,11 @@ contract ProtocolVersions_RegisterUpgrade_Test is ProtocolVersions_TestInit { /// @notice Tests that `registerUpgrade` assigns ascending ids and returns them. function test_registerUpgrade_returnsAscendingIds_succeeds() external { vm.prank(_owner); - assertEq(protocolVersions.registerUpgrade(), 0); + assertEq(protocolVersions.registerUpgrade(0, 0), 0); vm.prank(_owner); - assertEq(protocolVersions.registerUpgrade(), 1); + assertEq(protocolVersions.registerUpgrade(0, 0), 1); vm.prank(_owner); - assertEq(protocolVersions.registerUpgrade(), 2); + assertEq(protocolVersions.registerUpgrade(0, 0), 2); } /// @notice Tests that `registerUpgrade` emits the `UpgradeRegistered` event with the assigned id. @@ -147,19 +147,64 @@ contract ProtocolVersions_RegisterUpgrade_Test is ProtocolVersions_TestInit { vm.expectEmit(true, false, false, false, address(protocolVersions)); emit UpgradeRegistered(0); vm.prank(_owner); - protocolVersions.registerUpgrade(); + protocolVersions.registerUpgrade(0, 0); vm.expectEmit(true, false, false, false, address(protocolVersions)); emit UpgradeRegistered(1); vm.prank(_owner); - protocolVersions.registerUpgrade(); + protocolVersions.registerUpgrade(0, 0); } /// @notice Tests that only the owner can call `registerUpgrade`. function test_registerUpgrade_callerNotOwner_reverts() external { vm.expectRevert(ProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); vm.prank(_nonOwner); - protocolVersions.registerUpgrade(); + protocolVersions.registerUpgrade(0, 0); + } + + /// @notice Tests that registering with a future timestamp schedules the upgrade in one call. + function test_registerUpgrade_withTimestamp_succeeds() external { + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + + vm.expectEmit(true, false, false, false, address(protocolVersions)); + emit UpgradeRegistered(CANYON); + vm.expectEmit(true, false, false, true, address(protocolVersions)); + emit TimestampSet(CANYON, ts); + vm.prank(_owner); + uint256 id = protocolVersions.registerUpgrade(ts, 0); + + assertEq(id, CANYON); + assertEq(protocolVersions.getSchedule()[CANYON], ts); + assertNotEq(protocolVersions.scheduleId(), bytes32(0)); + } + + /// @notice Tests that registering with a timestamp inside the notice window reverts. + function test_registerUpgrade_insufficientNotice_reverts() external { + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() - 1; + vm.expectRevert(abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_InsufficientNotice.selector, ts)); + vm.prank(_owner); + protocolVersions.registerUpgrade(ts, 0); + } + + /// @notice Tests that a non-zero minProtocolVersion bumps the minimum during registration. + function test_registerUpgrade_setsMinProtocolVersion_succeeds() external { + vm.expectEmit(true, false, false, false, address(protocolVersions)); + emit MinimumProtocolVersionUpdated(42); + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 42); + + assertEq(protocolVersions.minimumProtocolVersion(), 42); + } + + /// @notice Tests that a zero minProtocolVersion leaves the current minimum unchanged. + function test_registerUpgrade_zeroMinProtocolVersion_leavesUnchanged_succeeds() external { + vm.prank(_owner); + protocolVersions.setMinimumProtocolVersion(7); + + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + + assertEq(protocolVersions.minimumProtocolVersion(), 7); } } @@ -211,7 +256,7 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { function test_setTimestamp_updatesTimestampAndScheduleId_succeeds() external { bytes32 initialScheduleId = protocolVersions.scheduleId(); vm.prank(_owner); - protocolVersions.registerUpgrade(); + protocolVersions.registerUpgrade(0, 0); vm.roll(block.number + 1); vm.warp(block.timestamp + 1); @@ -219,14 +264,14 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { vm.prank(_owner); protocolVersions.setTimestamp(CANYON, ts); - assertEq(protocolVersions.getSchedule()[CANYON].timestamp, ts); + assertEq(protocolVersions.getSchedule()[CANYON], ts); assertNotEq(protocolVersions.scheduleId(), initialScheduleId); } /// @notice Tests that calling `setTimestamp` with the same value is a no-op for scheduleId. function test_setTimestamp_sameTimestamp_succeeds() external { vm.prank(_owner); - protocolVersions.registerUpgrade(); + protocolVersions.registerUpgrade(0, 0); vm.roll(block.number + 1); vm.warp(block.timestamp + 1); @@ -240,7 +285,7 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { vm.prank(_owner); protocolVersions.setTimestamp(CANYON, ts); - assertEq(protocolVersions.getSchedule()[CANYON].timestamp, ts); + assertEq(protocolVersions.getSchedule()[CANYON], ts); assertEq(protocolVersions.scheduleId(), scheduleIdAfterSet); } @@ -248,7 +293,7 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { /// restores it to the value it held immediately after registration (ts=0 link). function test_setTimestamp_clearTimestamp_succeeds() external { vm.prank(_owner); - protocolVersions.registerUpgrade(); + protocolVersions.registerUpgrade(0, 0); bytes32 scheduleIdAfterRegister = protocolVersions.scheduleId(); vm.roll(block.number + 1); @@ -264,14 +309,14 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { vm.prank(_owner); protocolVersions.setTimestamp(CANYON, 0); - assertEq(protocolVersions.getSchedule()[CANYON].timestamp, 0); + assertEq(protocolVersions.getSchedule()[CANYON], 0); assertEq(protocolVersions.scheduleId(), scheduleIdAfterRegister); } /// @notice Tests that `setTimestamp` emits a `TimestampSet` event. function test_setTimestamp_emitsEvent_succeeds() external { vm.prank(_owner); - protocolVersions.registerUpgrade(); + protocolVersions.registerUpgrade(0, 0); uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; vm.expectEmit(true, false, false, true, address(protocolVersions)); @@ -283,7 +328,7 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { /// @notice Tests that only the owner can call `setTimestamp`. function test_setTimestamp_callerNotOwner_reverts() external { vm.prank(_owner); - protocolVersions.registerUpgrade(); + protocolVersions.registerUpgrade(0, 0); uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; vm.expectRevert(ProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); @@ -294,7 +339,7 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { /// @notice Tests that `setTimestamp` reverts when the timestamp is in the past. function test_setTimestamp_timestampInPast_reverts() external { vm.prank(_owner); - protocolVersions.registerUpgrade(); + protocolVersions.registerUpgrade(0, 0); vm.warp(1000); vm.expectRevert( @@ -307,7 +352,7 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { /// @notice Tests that `setTimestamp` reverts when the timestamp is within MIN_NOTICE of now. function test_setTimestamp_insufficientNotice_reverts() external { vm.prank(_owner); - protocolVersions.registerUpgrade(); + protocolVersions.registerUpgrade(0, 0); uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() - 1; vm.expectRevert(abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_InsufficientNotice.selector, ts)); @@ -318,7 +363,7 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { /// @notice Tests that `setTimestamp` reverts when the upgrade has already activated. function test_setTimestamp_afterActivation_reverts() external { vm.prank(_owner); - protocolVersions.registerUpgrade(); + protocolVersions.registerUpgrade(0, 0); vm.warp(100); uint64 activationTs = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; @@ -347,9 +392,9 @@ contract ProtocolVersions_SetTimestamp_Test is ProtocolVersions_TestInit { /// @notice Tests that scheduleId is reproducible from (ascending ids, timestamps). function test_setTimestamp_scheduleIdReproducible_succeeds() external { vm.prank(_owner); - protocolVersions.registerUpgrade(); + protocolVersions.registerUpgrade(0, 0); vm.prank(_owner); - protocolVersions.registerUpgrade(); + protocolVersions.registerUpgrade(0, 0); uint64 ts1 = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; uint64 ts2 = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 200; @@ -383,7 +428,7 @@ contract ProtocolVersions_DelayTimestamp_Test is ProtocolVersions_TestInit { vm.prank(_chainTeam); protocolVersions.delayTimestamp(CANYON, later); - assertEq(protocolVersions.getSchedule()[CANYON].timestamp, later); + assertEq(protocolVersions.getSchedule()[CANYON], later); assertNotEq(protocolVersions.scheduleId(), scheduleIdBefore); } @@ -429,7 +474,7 @@ contract ProtocolVersions_DelayTimestamp_Test is ProtocolVersions_TestInit { /// @notice Tests that `delayTimestamp` reverts when the upgrade has no scheduled timestamp. function test_delayTimestamp_notScheduled_reverts() external { vm.prank(_owner); - protocolVersions.registerUpgrade(); + protocolVersions.registerUpgrade(0, 0); vm.prank(_owner); protocolVersions.setChainTeam(_chainTeam); @@ -512,11 +557,6 @@ contract ProtocolVersions_ChainTeam_Test is ProtocolVersions_TestInit { /// @title ProtocolVersions_Uncategorized_Test /// @notice Test contract for view functions and the upgrade registry. contract ProtocolVersions_Uncategorized_Test is ProtocolVersions_TestInit { - /// @notice Tests that the registry starts empty. - function test_registry_startsEmpty_succeeds() external view { - assertEq(protocolVersions.getSchedule().length, 0); - } - /// @notice Tests that `getSchedule` returns an empty array when no upgrades are registered. function test_getSchedule_empty_succeeds() external view { assertEq(protocolVersions.getSchedule().length, 0); @@ -525,39 +565,30 @@ contract ProtocolVersions_Uncategorized_Test is ProtocolVersions_TestInit { /// @notice Tests that `getSchedule` returns all upgrades in registration order with correct fields. function test_getSchedule_returnsFullSchedule_succeeds() external { vm.prank(_owner); - protocolVersions.registerUpgrade(); + protocolVersions.registerUpgrade(0, 0); vm.prank(_owner); - protocolVersions.registerUpgrade(); + protocolVersions.registerUpgrade(0, 0); uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; vm.prank(_owner); protocolVersions.setTimestamp(CANYON, ts); - ProtocolVersions.Upgrade[] memory s = protocolVersions.getSchedule(); + uint64[] memory s = protocolVersions.getSchedule(); assertEq(s.length, 2); - - assertEq(s[0].id, CANYON); - assertEq(s[0].timestamp, ts); - - assertEq(s[1].id, ECOTONE); - assertEq(s[1].timestamp, 0); - - // The last entry's scheduleId is the contract's current scheduleId. - assertEq(s[1].scheduleId, protocolVersions.scheduleId()); + assertEq(s[CANYON], ts); + assertEq(s[ECOTONE], 0); } - /// @notice Tests that `scheduleId(id)` returns each upgrade's cumulative commitment, matching - /// `getSchedule`, and that the last upgrade's commitment equals the current scheduleId. + /// @notice Tests that `scheduleId(id)` returns each upgrade's cumulative commitment, and that + /// the last upgrade's commitment equals the current scheduleId. function test_scheduleId_byId_succeeds() external { vm.prank(_owner); - protocolVersions.registerUpgrade(); + protocolVersions.registerUpgrade(0, 0); vm.prank(_owner); - protocolVersions.registerUpgrade(); + protocolVersions.registerUpgrade(0, 0); - ProtocolVersions.Upgrade[] memory s = protocolVersions.getSchedule(); - assertEq(protocolVersions.scheduleId(CANYON), s[CANYON].scheduleId); - assertEq(protocolVersions.scheduleId(ECOTONE), s[ECOTONE].scheduleId); + assertNotEq(protocolVersions.scheduleId(CANYON), protocolVersions.scheduleId(ECOTONE)); assertEq(protocolVersions.scheduleId(ECOTONE), protocolVersions.scheduleId()); } From e1ebc491cc62b49355e996a24f5e4e809a571af9 Mon Sep 17 00:00:00 2001 From: PelleKrab Date: Thu, 9 Jul 2026 10:05:35 -0700 Subject: [PATCH 16/20] refactor(L1): rename chainTeam to incidentResponder, combine config, no-op setTimestamp Apply @jackchuma review nits on ProtocolVersions: - Rename the chainTeam role to incidentResponder; delaying a bad hardfork activation is an incident-response action. NatSpec scopes the power so the shared name doesn't imply SuperchainConfig's pause authority. - Drop the redundant chainTeam deploy config key and source the role from superchainConfigIncidentResponder (same entity). - setTimestamp: short-circuit no-op writes ahead of the change-validation guards so idempotent resubmissions succeed. Generated with Claude Code Co-Authored-By: Claude --- interfaces/L1/IProtocolVersions.sol | 10 +- scripts/deploy/DeployConfig.s.sol | 2 - scripts/deploy/SystemDeploy.s.sol | 4 +- scripts/libraries/Types.sol | 2 +- snapshots/abi/ProtocolVersions.json | 40 ++++---- snapshots/semver-lock.json | 4 +- snapshots/storageLayout/ProtocolVersions.json | 2 +- src/L1/ProtocolVersions.sol | 36 +++---- test/L1/ProtocolVersions.t.sol | 98 +++++++++---------- test/deploy/SystemDeploy.t.sol | 9 +- 10 files changed, 104 insertions(+), 103 deletions(-) diff --git a/interfaces/L1/IProtocolVersions.sol b/interfaces/L1/IProtocolVersions.sol index 4dc9d86de..79e8ef8b2 100644 --- a/interfaces/L1/IProtocolVersions.sol +++ b/interfaces/L1/IProtocolVersions.sol @@ -11,7 +11,7 @@ interface IProtocolVersions is IProxyAdminOwnedBase, ISemver, IReinitializableBa error ProtocolVersions_UnknownUpgrade(uint256 id); error ProtocolVersions_InvalidProtocolVersion(); error ProtocolVersions_ActivationAlreadyPassed(uint256 id, uint64 activationTimestamp); - error ProtocolVersions_NotChainTeam(); + error ProtocolVersions_NotIncidentResponder(); error ProtocolVersions_NotScheduled(uint256 id); error ProtocolVersions_DelayMustBeLater(uint64 currentTimestamp, uint64 newTimestamp); error ProtocolVersions_NotInitialized(); @@ -21,23 +21,23 @@ interface IProtocolVersions is IProxyAdminOwnedBase, ISemver, IReinitializableBa event MinimumProtocolVersionUpdated(uint256 indexed protocolVersion); event TimestampSet(uint256 indexed id, uint256 timestamp); event ScheduleIdUpdated(bytes32 indexed newScheduleId); - event ChainTeamUpdated(address indexed previousChainTeam, address indexed newChainTeam); + event IncidentResponderUpdated(address indexed previousIncidentResponder, address indexed newIncidentResponder); event Initialized(uint8 version); function MIN_NOTICE() external view returns (uint64); - function chainTeam() external view returns (address); + function incidentResponder() external view returns (address); function scheduleId() external view returns (bytes32); function scheduleId(uint256 id) external view returns (bytes32); function minimumProtocolVersion() external view returns (uint256); function getSchedule() external view returns (uint64[] memory); - function initialize(address _chainTeam) external; + function initialize(address _incidentResponder) external; function registerUpgrade(uint64 timestamp, uint256 minProtocolVersion) external returns (uint256); function setMinimumProtocolVersion(uint256 protocolVersion) external; function setTimestamp(uint256 id, uint64 timestamp) external; - function setChainTeam(address newChainTeam) external; + function setIncidentResponder(address newIncidentResponder) external; function delayTimestamp(uint256 id, uint64 newTimestamp) external; function __constructor__() external; diff --git a/scripts/deploy/DeployConfig.s.sol b/scripts/deploy/DeployConfig.s.sol index c098e1232..7fe484580 100644 --- a/scripts/deploy/DeployConfig.s.sol +++ b/scripts/deploy/DeployConfig.s.sol @@ -12,7 +12,6 @@ contract DeployConfig is Script { address public baseFeeVaultRecipient; address public batchSenderAddress; - address public chainTeam; address public finalSystemOwner; address public l1FeeVaultRecipient; address public nitroEnclaveVerifier; @@ -66,7 +65,6 @@ contract DeployConfig is Script { baseFeeVaultRecipient = _json.readAddress("$.baseFeeVaultRecipient"); batchSenderAddress = _json.readAddress("$.batchSenderAddress"); - chainTeam = _json.readAddressOr("$.chainTeam", address(0)); finalSystemOwner = _json.readAddress("$.finalSystemOwner"); l1FeeVaultRecipient = _json.readAddress("$.l1FeeVaultRecipient"); nitroEnclaveVerifier = _json.readAddress("$.nitroEnclaveVerifier"); diff --git a/scripts/deploy/SystemDeploy.s.sol b/scripts/deploy/SystemDeploy.s.sol index 7cc217376..739d7275f 100644 --- a/scripts/deploy/SystemDeploy.s.sol +++ b/scripts/deploy/SystemDeploy.s.sol @@ -282,7 +282,7 @@ contract SystemDeploy is Script { systemConfigOwner: cfg.finalSystemOwner(), batcher: cfg.batchSenderAddress(), unsafeBlockSigner: cfg.p2pSequencerAddress(), - chainTeam: cfg.chainTeam() + incidentResponder: cfg.superchainConfigIncidentResponder() }), basefeeScalar: cfg.basefeeScalar(), blobBasefeeScalar: cfg.blobbasefeeScalar(), @@ -630,7 +630,7 @@ contract SystemDeploy is Script { _output.opChainProxyAdmin, address(_output.protocolVersionsProxy), _impls.protocolVersionsImpl, - abi.encodeCall(IProtocolVersions.initialize, (_input.roles.chainTeam)) + abi.encodeCall(IProtocolVersions.initialize, (_input.roles.incidentResponder)) ); } diff --git a/scripts/libraries/Types.sol b/scripts/libraries/Types.sol index 17c574dd9..9f26a5e75 100644 --- a/scripts/libraries/Types.sol +++ b/scripts/libraries/Types.sol @@ -29,7 +29,7 @@ library Types { address systemConfigOwner; address batcher; address unsafeBlockSigner; - address chainTeam; + address incidentResponder; } /// @notice The full set of inputs to deploy a new OP Stack chain. diff --git a/snapshots/abi/ProtocolVersions.json b/snapshots/abi/ProtocolVersions.json index 9ad7ad31b..33ef5fc0a 100644 --- a/snapshots/abi/ProtocolVersions.json +++ b/snapshots/abi/ProtocolVersions.json @@ -17,19 +17,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "chainTeam", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -61,6 +48,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "incidentResponder", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "initVersion", @@ -78,7 +78,7 @@ "inputs": [ { "internalType": "address", - "name": "_chainTeam", + "name": "_incidentResponder", "type": "address" } ], @@ -186,11 +186,11 @@ "inputs": [ { "internalType": "address", - "name": "newChainTeam", + "name": "newIncidentResponder", "type": "address" } ], - "name": "setChainTeam", + "name": "setIncidentResponder", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -245,17 +245,17 @@ { "indexed": true, "internalType": "address", - "name": "previousChainTeam", + "name": "previousIncidentResponder", "type": "address" }, { "indexed": true, "internalType": "address", - "name": "newChainTeam", + "name": "newIncidentResponder", "type": "address" } ], - "name": "ChainTeamUpdated", + "name": "IncidentResponderUpdated", "type": "event" }, { @@ -379,7 +379,7 @@ }, { "inputs": [], - "name": "ProtocolVersions_NotChainTeam", + "name": "ProtocolVersions_NotIncidentResponder", "type": "error" }, { diff --git a/snapshots/semver-lock.json b/snapshots/semver-lock.json index 7a6bc3ea5..b45691057 100644 --- a/snapshots/semver-lock.json +++ b/snapshots/semver-lock.json @@ -20,8 +20,8 @@ "sourceCodeHash": "0xa1281f5da03fa0431eabad33da23f51a8d835b0dd04554603fd59e11efe9fa51" }, "src/L1/ProtocolVersions.sol:ProtocolVersions": { - "initCodeHash": "0x201b442493a591859132351e024a6edbb99d76bd5a6672a0587652038fad3f52", - "sourceCodeHash": "0x20d923ffd5a5a871a49901546e8d83a0f048a2e985371e40a099262713c0c8b1" + "initCodeHash": "0x0d47628779e604e08a8787c299411ca1f9ed411052921aec53047748a5d7e40f", + "sourceCodeHash": "0x34ad9da6bd0a217a9a8cd0751954ecb9ea2fcb546fecd512f15625c79ff27b1e" }, "src/L1/SuperchainConfig.sol:SuperchainConfig": { "initCodeHash": "0x9b1f3555b499709485d51d5d9665002c0eb1e5eb893be1fb978a30749e894858", diff --git a/snapshots/storageLayout/ProtocolVersions.json b/snapshots/storageLayout/ProtocolVersions.json index 80b1b935f..27001da1d 100644 --- a/snapshots/storageLayout/ProtocolVersions.json +++ b/snapshots/storageLayout/ProtocolVersions.json @@ -36,7 +36,7 @@ }, { "bytes": "20", - "label": "chainTeam", + "label": "incidentResponder", "offset": 0, "slot": "4", "type": "address" diff --git a/src/L1/ProtocolVersions.sol b/src/L1/ProtocolVersions.sol index 83e5baea5..f678989f7 100644 --- a/src/L1/ProtocolVersions.sol +++ b/src/L1/ProtocolVersions.sol @@ -44,8 +44,8 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable error ProtocolVersions_InvalidProtocolVersion(); /// @notice Thrown when modifying a timestamp whose activation has already passed. error ProtocolVersions_ActivationAlreadyPassed(uint256 id, uint64 activationTimestamp); - /// @notice Thrown when the caller is not the chainTeam. - error ProtocolVersions_NotChainTeam(); + /// @notice Thrown when the caller is not the incidentResponder. + error ProtocolVersions_NotIncidentResponder(); /// @notice Thrown when delaying an upgrade that has no scheduled activation. error ProtocolVersions_NotScheduled(uint256 id); /// @notice Thrown when a new timestamp is not sufficiently later than the current one. @@ -63,8 +63,8 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable event TimestampSet(uint256 indexed id, uint256 timestamp); /// @notice Emitted when the schedule commitment changes. event ScheduleIdUpdated(bytes32 indexed newScheduleId); - /// @notice Emitted when the chainTeam role changes. - event ChainTeamUpdated(address indexed previousChainTeam, address indexed newChainTeam); + /// @notice Emitted when the incidentResponder role changes. + event IncidentResponderUpdated(address indexed previousIncidentResponder, address indexed newIncidentResponder); /// @notice Minimum notice period required when scheduling or modifying an activation timestamp. uint64 public constant MIN_NOTICE = 1 hours; @@ -90,7 +90,7 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable /// only move an already-scheduled, not-yet-activated upgrade further into the future via /// `delayTimestamp`. It cannot register upgrades, clear timestamps, pull an activation /// earlier, or schedule a brand-new activation. Unset (zero) by default. - address public chainTeam; + address public incidentResponder; /// @notice Semantic version. /// @custom:semver 1.0.0 @@ -104,9 +104,9 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable } /// @notice Initializes the registry by seeding the hash chain and appointing the initial - /// chainTeam. Callable only by the ProxyAdmin or its owner. - /// @param _chainTeam Initial chainTeam allowed to delay activations, or address(0) to leave unset. - function initialize(address _chainTeam) external reinitializer(initVersion()) { + /// incidentResponder. Callable only by the ProxyAdmin or its owner. + /// @param _incidentResponder Initial incidentResponder allowed to delay activations, or address(0) to leave unset. + function initialize(address _incidentResponder) external reinitializer(initVersion()) { // Initialization transactions must come from the ProxyAdmin or its owner. _assertOnlyProxyAdminOrProxyAdminOwner(); @@ -116,8 +116,8 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable _upgradeScheduleId.push(bytes32(0)); emit ScheduleIdUpdated(bytes32(0)); - chainTeam = _chainTeam; - emit ChainTeamUpdated(address(0), _chainTeam); + incidentResponder = _incidentResponder; + emit IncidentResponderUpdated(address(0), _incidentResponder); } /// @notice Returns the canonical schedule commitment. @@ -202,26 +202,26 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable _assertOnlyProxyAdminOwner(); _assertRegistered(id); uint64 current = _timestamps[id]; + if (current == timestamp) return; if (current != 0 && uint64(block.timestamp) >= current) { revert ProtocolVersions_ActivationAlreadyPassed(id, current); } if (timestamp != 0 && timestamp < uint64(block.timestamp) + MIN_NOTICE) { revert ProtocolVersions_InsufficientNotice(timestamp); } - if (current == timestamp) return; _writeTimestamp(id, timestamp); } - /// @notice Appoints, replaces, or clears (set to zero) the chainTeam role. Owner only. - /// @param newChainTeam New chainTeam address, or address(0) to revoke the role. - function setChainTeam(address newChainTeam) external { + /// @notice Appoints, replaces, or clears (set to zero) the incidentResponder role. Owner only. + /// @param newIncidentResponder New incidentResponder address, or address(0) to revoke the role. + function setIncidentResponder(address newIncidentResponder) external { _assertOnlyProxyAdminOwner(); - emit ChainTeamUpdated(chainTeam, newChainTeam); - chainTeam = newChainTeam; + emit IncidentResponderUpdated(incidentResponder, newIncidentResponder); + incidentResponder = newIncidentResponder; } /// @notice Pushes an already-scheduled upgrade's activation timestamp further into the future. - /// Can only be called by the chainTeam. + /// Can only be called by the incidentResponder. /// @dev The upgrade must already have a non-zero activation timestamp that has not yet passed, /// and `newTimestamp` must be strictly later than the current value. This role can only /// delay an activation; it cannot pull one earlier, clear it, or schedule a new one — use @@ -230,7 +230,7 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable /// @param id The upgrade whose activation to delay. /// @param newTimestamp New activation timestamp, must be strictly later than the current one. function delayTimestamp(uint256 id, uint64 newTimestamp) external { - if (msg.sender != chainTeam) revert ProtocolVersions_NotChainTeam(); + if (msg.sender != incidentResponder) revert ProtocolVersions_NotIncidentResponder(); _assertRegistered(id); uint64 current = _timestamps[id]; diff --git a/test/L1/ProtocolVersions.t.sol b/test/L1/ProtocolVersions.t.sol index 601af38e5..315face06 100644 --- a/test/L1/ProtocolVersions.t.sol +++ b/test/L1/ProtocolVersions.t.sol @@ -18,7 +18,7 @@ import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; abstract contract ProtocolVersions_TestInit is Test { event UpgradeRegistered(uint256 indexed id); event MinimumProtocolVersionUpdated(uint256 indexed protocolVersion); - event ChainTeamUpdated(address indexed previousChainTeam, address indexed newChainTeam); + event IncidentResponderUpdated(address indexed previousIncidentResponder, address indexed newIncidentResponder); event TimestampSet(uint256 indexed id, uint256 timestamp); /// @dev Ascending ids assigned by registration order in these tests. @@ -27,7 +27,7 @@ abstract contract ProtocolVersions_TestInit is Test { address internal _owner = makeAddr("owner"); address internal _nonOwner = makeAddr("non-owner"); - address internal _chainTeam = makeAddr("chain-team"); + address internal _incidentResponder = makeAddr("incident-responder"); address internal _proxyAdmin = makeAddr("proxy-admin"); ProtocolVersions internal _impl; ProtocolVersions internal protocolVersions; @@ -40,7 +40,7 @@ abstract contract ProtocolVersions_TestInit is Test { } /// @dev Deploys a proxy (admin = _proxyAdmin) over the shared impl and initializes it via the - /// proxy, appointing `_team` as the chainTeam. + /// proxy, appointing `_team` as the incidentResponder. function _deployInitializedProxy(address _team) internal returns (address proxy_) { Proxy proxy = new Proxy(_proxyAdmin); vm.prank(_proxyAdmin); @@ -76,14 +76,14 @@ contract ProtocolVersions_Initialize_Test is ProtocolVersions_TestInit { assertEq(protocolVersions.scheduleId(), bytes32(0)); } - /// @notice Tests that initialization appoints the provided chainTeam and emits the event. - function test_initialize_setsChainTeam_succeeds() external { + /// @notice Tests that initialization appoints the provided incidentResponder and emits the event. + function test_initialize_setsIncidentResponder_succeeds() external { ProtocolVersions uninitialized = _deployUninitializedProxy(); vm.expectEmit(true, true, false, false, address(uninitialized)); - emit ChainTeamUpdated(address(0), _chainTeam); + emit IncidentResponderUpdated(address(0), _incidentResponder); vm.prank(_proxyAdmin); - uninitialized.initialize(_chainTeam); - assertEq(uninitialized.chainTeam(), _chainTeam); + uninitialized.initialize(_incidentResponder); + assertEq(uninitialized.incidentResponder(), _incidentResponder); } /// @notice Tests that only the ProxyAdmin or its owner can initialize. @@ -91,7 +91,7 @@ contract ProtocolVersions_Initialize_Test is ProtocolVersions_TestInit { ProtocolVersions uninitialized = _deployUninitializedProxy(); vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner.selector); vm.prank(_nonOwner); - uninitialized.initialize(_chainTeam); + uninitialized.initialize(_incidentResponder); } /// @notice Tests that the contract cannot be initialized twice. @@ -419,30 +419,30 @@ contract ProtocolVersions_DelayTimestamp_Test is ProtocolVersions_TestInit { function test_delayTimestamp_pushesTimestampLater_succeeds() external { uint64 ts = _scheduleCanyon(100); vm.prank(_owner); - protocolVersions.setChainTeam(_chainTeam); + protocolVersions.setIncidentResponder(_incidentResponder); bytes32 scheduleIdBefore = protocolVersions.scheduleId(); vm.roll(block.number + 1); uint64 later = ts + 50; - vm.prank(_chainTeam); + vm.prank(_incidentResponder); protocolVersions.delayTimestamp(CANYON, later); assertEq(protocolVersions.getSchedule()[CANYON], later); assertNotEq(protocolVersions.scheduleId(), scheduleIdBefore); } - /// @notice Tests that only the chainTeam can call `delayTimestamp`. - function test_delayTimestamp_callerNotChainTeam_reverts() external { + /// @notice Tests that only the incidentResponder can call `delayTimestamp`. + function test_delayTimestamp_callerNotIncidentResponder_reverts() external { uint64 ts = _scheduleCanyon(100); vm.prank(_owner); - protocolVersions.setChainTeam(_chainTeam); + protocolVersions.setIncidentResponder(_incidentResponder); - vm.expectRevert(IProtocolVersions.ProtocolVersions_NotChainTeam.selector); + vm.expectRevert(IProtocolVersions.ProtocolVersions_NotIncidentResponder.selector); vm.prank(_owner); protocolVersions.delayTimestamp(CANYON, ts + 50); - vm.expectRevert(IProtocolVersions.ProtocolVersions_NotChainTeam.selector); + vm.expectRevert(IProtocolVersions.ProtocolVersions_NotIncidentResponder.selector); vm.prank(_nonOwner); protocolVersions.delayTimestamp(CANYON, ts + 50); } @@ -451,12 +451,12 @@ contract ProtocolVersions_DelayTimestamp_Test is ProtocolVersions_TestInit { function test_delayTimestamp_earlierTimestamp_reverts() external { uint64 ts = _scheduleCanyon(100); vm.prank(_owner); - protocolVersions.setChainTeam(_chainTeam); + protocolVersions.setIncidentResponder(_incidentResponder); vm.expectRevert( abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_DelayMustBeLater.selector, ts, ts - 10) ); - vm.prank(_chainTeam); + vm.prank(_incidentResponder); protocolVersions.delayTimestamp(CANYON, ts - 10); } @@ -464,10 +464,10 @@ contract ProtocolVersions_DelayTimestamp_Test is ProtocolVersions_TestInit { function test_delayTimestamp_equalTimestamp_reverts() external { uint64 ts = _scheduleCanyon(100); vm.prank(_owner); - protocolVersions.setChainTeam(_chainTeam); + protocolVersions.setIncidentResponder(_incidentResponder); vm.expectRevert(abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_DelayMustBeLater.selector, ts, ts)); - vm.prank(_chainTeam); + vm.prank(_incidentResponder); protocolVersions.delayTimestamp(CANYON, ts); } @@ -476,11 +476,11 @@ contract ProtocolVersions_DelayTimestamp_Test is ProtocolVersions_TestInit { vm.prank(_owner); protocolVersions.registerUpgrade(0, 0); vm.prank(_owner); - protocolVersions.setChainTeam(_chainTeam); + protocolVersions.setIncidentResponder(_incidentResponder); uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; vm.expectRevert(abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_NotScheduled.selector, CANYON)); - vm.prank(_chainTeam); + vm.prank(_incidentResponder); protocolVersions.delayTimestamp(CANYON, ts); } @@ -488,69 +488,69 @@ contract ProtocolVersions_DelayTimestamp_Test is ProtocolVersions_TestInit { function test_delayTimestamp_afterActivation_reverts() external { uint64 ts = _scheduleCanyon(100); vm.prank(_owner); - protocolVersions.setChainTeam(_chainTeam); + protocolVersions.setIncidentResponder(_incidentResponder); vm.warp(ts + 1); vm.expectRevert( abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_ActivationAlreadyPassed.selector, CANYON, ts) ); - vm.prank(_chainTeam); + vm.prank(_incidentResponder); protocolVersions.delayTimestamp(CANYON, ts + 100); } /// @notice Tests that `delayTimestamp` reverts for an unregistered upgrade. function test_delayTimestamp_unregisteredUpgrade_reverts() external { vm.prank(_owner); - protocolVersions.setChainTeam(_chainTeam); + protocolVersions.setIncidentResponder(_incidentResponder); uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; vm.expectRevert(abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_UnknownUpgrade.selector, uint256(0))); - vm.prank(_chainTeam); + vm.prank(_incidentResponder); protocolVersions.delayTimestamp(0, ts); } } -/// @title ProtocolVersions_ChainTeam_Test -/// @notice Test contract for the `setChainTeam` function and chainTeam role. -contract ProtocolVersions_ChainTeam_Test is ProtocolVersions_TestInit { - /// @notice Tests that `chainTeam` starts as address(0). - function test_chainTeam_startsUnset_succeeds() external view { - assertEq(protocolVersions.chainTeam(), address(0)); +/// @title ProtocolVersions_IncidentResponder_Test +/// @notice Test contract for the `setIncidentResponder` function and incidentResponder role. +contract ProtocolVersions_IncidentResponder_Test is ProtocolVersions_TestInit { + /// @notice Tests that `incidentResponder` starts as address(0). + function test_incidentResponder_startsUnset_succeeds() external view { + assertEq(protocolVersions.incidentResponder(), address(0)); } - /// @notice Tests that the owner can appoint a chainTeam address. - function test_setChainTeam_setsAddress_succeeds() external { + /// @notice Tests that the owner can appoint a incidentResponder address. + function test_setIncidentResponder_setsAddress_succeeds() external { vm.prank(_owner); - protocolVersions.setChainTeam(_chainTeam); - assertEq(protocolVersions.chainTeam(), _chainTeam); + protocolVersions.setIncidentResponder(_incidentResponder); + assertEq(protocolVersions.incidentResponder(), _incidentResponder); } - /// @notice Tests that only the owner can call `setChainTeam`. - function test_setChainTeam_callerNotOwner_reverts() external { + /// @notice Tests that only the owner can call `setIncidentResponder`. + function test_setIncidentResponder_callerNotOwner_reverts() external { vm.expectRevert(ProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); vm.prank(_nonOwner); - protocolVersions.setChainTeam(_chainTeam); + protocolVersions.setIncidentResponder(_incidentResponder); } - /// @notice Tests that `setChainTeam` emits a `ChainTeamUpdated` event. - function test_setChainTeam_emitsEvent_succeeds() external { + /// @notice Tests that `setIncidentResponder` emits a `IncidentResponderUpdated` event. + function test_setIncidentResponder_emitsEvent_succeeds() external { vm.expectEmit(true, true, false, false, address(protocolVersions)); - emit ChainTeamUpdated(address(0), _chainTeam); + emit IncidentResponderUpdated(address(0), _incidentResponder); vm.prank(_owner); - protocolVersions.setChainTeam(_chainTeam); + protocolVersions.setIncidentResponder(_incidentResponder); } - /// @notice Tests that the owner can clear the chainTeam role by setting it to address(0). - function test_setChainTeam_clear_succeeds() external { + /// @notice Tests that the owner can clear the incidentResponder role by setting it to address(0). + function test_setIncidentResponder_clear_succeeds() external { vm.prank(_owner); - protocolVersions.setChainTeam(_chainTeam); + protocolVersions.setIncidentResponder(_incidentResponder); vm.expectEmit(true, true, false, false, address(protocolVersions)); - emit ChainTeamUpdated(_chainTeam, address(0)); + emit IncidentResponderUpdated(_incidentResponder, address(0)); vm.prank(_owner); - protocolVersions.setChainTeam(address(0)); + protocolVersions.setIncidentResponder(address(0)); - assertEq(protocolVersions.chainTeam(), address(0)); + assertEq(protocolVersions.incidentResponder(), address(0)); } } diff --git a/test/deploy/SystemDeploy.t.sol b/test/deploy/SystemDeploy.t.sol index ea6e45de1..616cea3bf 100644 --- a/test/deploy/SystemDeploy.t.sol +++ b/test/deploy/SystemDeploy.t.sol @@ -39,7 +39,6 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { address internal unsafeBlockSigner = makeAddr("unsafeBlockSigner"); address internal proposer = makeAddr("proposer"); address internal challenger = makeAddr("challenger"); - address internal chainTeam = makeAddr("chainTeam"); MockNitroEnclaveVerifier internal nitroEnclaveVerifier; MockSP1Verifier internal sp1Verifier; @@ -121,7 +120,11 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { assertNotEq(address(output.opChain.ethLockboxProxy), address(0), "lockbox"); assertNotEq(address(output.opChain.delayedWETHProxy), address(0), "delayed weth"); assertNotEq(address(output.opChain.protocolVersionsProxy), address(0), "protocol versions"); - assertEq(output.opChain.protocolVersionsProxy.chainTeam(), chainTeam, "protocol versions chain team"); + assertEq( + output.opChain.protocolVersionsProxy.incidentResponder(), + incidentResponder, + "protocol versions incident responder" + ); assertEq(output.opChain.opChainProxyAdmin.owner(), owner, "op chain proxy admin owner"); assertEq(output.opChain.systemConfigProxy.batchInbox(), Types.chainIdToBatchInboxAddress(l2ChainId), "inbox"); @@ -207,7 +210,7 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { systemConfigOwner: owner, batcher: batcher, unsafeBlockSigner: unsafeBlockSigner, - chainTeam: chainTeam + incidentResponder: incidentResponder }), basefeeScalar: 100, blobBasefeeScalar: 200, From b465d9580e055ea1e01c639abb89c59037dd5047 Mon Sep 17 00:00:00 2001 From: PelleKrab Date: Thu, 9 Jul 2026 14:32:27 -0700 Subject: [PATCH 17/20] refactor(L1): address ProtocolVersions review nits Run ProtocolVersions tests through CommonTest so they use the SystemDeploy-created proxy and implementation. Reorder ProtocolVersions members to match the Solidity style guide and restrict implementation internals to private. Co-authored-by: Codex --- snapshots/semver-lock.json | 2 +- src/L1/ProtocolVersions.sol | 132 ++++++++++++++++----------------- test/L1/ProtocolVersions.t.sol | 72 +++++++++--------- 3 files changed, 102 insertions(+), 104 deletions(-) diff --git a/snapshots/semver-lock.json b/snapshots/semver-lock.json index b45691057..22cbff899 100644 --- a/snapshots/semver-lock.json +++ b/snapshots/semver-lock.json @@ -21,7 +21,7 @@ }, "src/L1/ProtocolVersions.sol:ProtocolVersions": { "initCodeHash": "0x0d47628779e604e08a8787c299411ca1f9ed411052921aec53047748a5d7e40f", - "sourceCodeHash": "0x34ad9da6bd0a217a9a8cd0751954ecb9ea2fcb546fecd512f15625c79ff27b1e" + "sourceCodeHash": "0x6bb512df396c0fe7216d160f0a4637cd690b4b5b8515096799d946b6bbcdaa5f" }, "src/L1/SuperchainConfig.sol:SuperchainConfig": { "initCodeHash": "0x9b1f3555b499709485d51d5d9665002c0eb1e5eb893be1fb978a30749e894858", diff --git a/src/L1/ProtocolVersions.sol b/src/L1/ProtocolVersions.sol index f678989f7..a30f21305 100644 --- a/src/L1/ProtocolVersions.sol +++ b/src/L1/ProtocolVersions.sol @@ -38,47 +38,19 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; /// The contract is deployed behind an OP proxy: the implementation constructor disables /// initializers, and `initialize` (run through the proxy) seeds the hash chain. contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, ReinitializableBase, ISemver { - /// @notice Thrown when an upgrade id is not registered. - error ProtocolVersions_UnknownUpgrade(uint256 id); - /// @notice Thrown when a protocol version is zero. - error ProtocolVersions_InvalidProtocolVersion(); - /// @notice Thrown when modifying a timestamp whose activation has already passed. - error ProtocolVersions_ActivationAlreadyPassed(uint256 id, uint64 activationTimestamp); - /// @notice Thrown when the caller is not the incidentResponder. - error ProtocolVersions_NotIncidentResponder(); - /// @notice Thrown when delaying an upgrade that has no scheduled activation. - error ProtocolVersions_NotScheduled(uint256 id); - /// @notice Thrown when a new timestamp is not sufficiently later than the current one. - error ProtocolVersions_DelayMustBeLater(uint64 currentTimestamp, uint64 newTimestamp); - /// @notice Thrown when scheduleId is read before initialize has been called. - error ProtocolVersions_NotInitialized(); - /// @notice Thrown when a non-zero timestamp does not provide at least MIN_NOTICE seconds of notice. - error ProtocolVersions_InsufficientNotice(uint64 timestamp); - - /// @notice Emitted when a new upgrade is registered. - event UpgradeRegistered(uint256 indexed id); - /// @notice Emitted when the minimum protocol version clients must run is updated. - event MinimumProtocolVersionUpdated(uint256 indexed protocolVersion); - /// @notice Emitted when an upgrade's activation timestamp is set, cleared, or delayed. - event TimestampSet(uint256 indexed id, uint256 timestamp); - /// @notice Emitted when the schedule commitment changes. - event ScheduleIdUpdated(bytes32 indexed newScheduleId); - /// @notice Emitted when the incidentResponder role changes. - event IncidentResponderUpdated(address indexed previousIncidentResponder, address indexed newIncidentResponder); - /// @notice Minimum notice period required when scheduling or modifying an activation timestamp. uint64 public constant MIN_NOTICE = 1 hours; /// @notice Activation timestamp for each registered upgrade, indexed by upgrade id (0 = not scheduled). /// An upgrade id is registered iff it is a valid index into this array. - uint64[] internal _timestamps; + uint64[] private _timestamps; /// @notice Hash chain links. Element 0 is the seed (`bytes32(0)`), pushed in `initialize`; /// element `i + 1` is the cumulative hash through upgrade `i`: /// _upgradeScheduleId[i + 1] = keccak256(abi.encode(_upgradeScheduleId[i], i, _timestamps[i])). /// Stored per-link so that changing upgrade j's timestamp recomputes only j..n-1 links /// rather than the entire chain. Non-empty iff the contract has been initialized. - bytes32[] internal _upgradeScheduleId; + bytes32[] private _upgradeScheduleId; /// @notice The minimum protocol version clients must run. Settable by the owner via /// `setMinimumProtocolVersion`. Informational only — read offchain by clients; @@ -92,11 +64,33 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable /// earlier, or schedule a brand-new activation. Unset (zero) by default. address public incidentResponder; - /// @notice Semantic version. - /// @custom:semver 1.0.0 - function version() public pure virtual returns (string memory) { - return "1.0.0"; - } + /// @notice Emitted when a new upgrade is registered. + event UpgradeRegistered(uint256 indexed id); + /// @notice Emitted when the minimum protocol version clients must run is updated. + event MinimumProtocolVersionUpdated(uint256 indexed protocolVersion); + /// @notice Emitted when an upgrade's activation timestamp is set, cleared, or delayed. + event TimestampSet(uint256 indexed id, uint256 timestamp); + /// @notice Emitted when the schedule commitment changes. + event ScheduleIdUpdated(bytes32 indexed newScheduleId); + /// @notice Emitted when the incidentResponder role changes. + event IncidentResponderUpdated(address indexed previousIncidentResponder, address indexed newIncidentResponder); + + /// @notice Thrown when an upgrade id is not registered. + error ProtocolVersions_UnknownUpgrade(uint256 id); + /// @notice Thrown when a protocol version is zero. + error ProtocolVersions_InvalidProtocolVersion(); + /// @notice Thrown when modifying a timestamp whose activation has already passed. + error ProtocolVersions_ActivationAlreadyPassed(uint256 id, uint64 activationTimestamp); + /// @notice Thrown when the caller is not the incidentResponder. + error ProtocolVersions_NotIncidentResponder(); + /// @notice Thrown when delaying an upgrade that has no scheduled activation. + error ProtocolVersions_NotScheduled(uint256 id); + /// @notice Thrown when a new timestamp is not sufficiently later than the current one. + error ProtocolVersions_DelayMustBeLater(uint64 currentTimestamp, uint64 newTimestamp); + /// @notice Thrown when scheduleId is read before initialize has been called. + error ProtocolVersions_NotInitialized(); + /// @notice Thrown when a non-zero timestamp does not provide at least MIN_NOTICE seconds of notice. + error ProtocolVersions_InsufficientNotice(uint64 timestamp); /// @notice Disables initializers on the implementation so it can only be used behind a proxy. constructor() ReinitializableBase(1) { @@ -120,33 +114,6 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable emit IncidentResponderUpdated(address(0), _incidentResponder); } - /// @notice Returns the canonical schedule commitment. - /// @return The current scheduleId hash. - function scheduleId() external view returns (bytes32) { - uint256 n = _upgradeScheduleId.length; - if (n == 0) revert ProtocolVersions_NotInitialized(); - return _upgradeScheduleId[n - 1]; - } - - /// @notice Returns the schedule commitment as of a specific registered upgrade. - /// @param id The upgrade id to query. - /// @return The scheduleId hash committing to upgrades 0 through `id`. - function scheduleId(uint256 id) external view returns (bytes32) { - _assertRegistered(id); - // Upgrade `id`'s cumulative hash lives at index id + 1 (index 0 is the seed). - return _upgradeScheduleId[id + 1]; - } - - /// @notice Returns the activation timestamp for every registered upgrade, ordered by upgrade id - /// (0 = not scheduled). The array index equals the upgrade `id`; names are resolved - /// offchain, and per-upgrade schedule hashes can be reproduced from these timestamps and - /// the seed or read via `scheduleId(id)`. - /// @dev Calling via eth_call is gas-free; no transaction is submitted. - /// @return Ordered activation timestamps, one per registered upgrade. - function getSchedule() external view returns (uint64[] memory) { - return _timestamps; - } - /// @notice Registers a new upgrade, assigning it the next ascending id, optionally scheduling its /// activation and bumping the minimum protocol version in the same call. Owner only. /// @dev Pass `timestamp` 0 to register without scheduling (schedule later via `setTimestamp`), or @@ -246,9 +213,42 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable _writeTimestamp(id, newTimestamp); } + /// @notice Returns the canonical schedule commitment. + /// @return The current scheduleId hash. + function scheduleId() external view returns (bytes32) { + uint256 n = _upgradeScheduleId.length; + if (n == 0) revert ProtocolVersions_NotInitialized(); + return _upgradeScheduleId[n - 1]; + } + + /// @notice Returns the schedule commitment as of a specific registered upgrade. + /// @param id The upgrade id to query. + /// @return The scheduleId hash committing to upgrades 0 through `id`. + function scheduleId(uint256 id) external view returns (bytes32) { + _assertRegistered(id); + // Upgrade `id`'s cumulative hash lives at index id + 1 (index 0 is the seed). + return _upgradeScheduleId[id + 1]; + } + + /// @notice Returns the activation timestamp for every registered upgrade, ordered by upgrade id + /// (0 = not scheduled). The array index equals the upgrade `id`; names are resolved + /// offchain, and per-upgrade schedule hashes can be reproduced from these timestamps and + /// the seed or read via `scheduleId(id)`. + /// @dev Calling via eth_call is gas-free; no transaction is submitted. + /// @return Ordered activation timestamps, one per registered upgrade. + function getSchedule() external view returns (uint64[] memory) { + return _timestamps; + } + + /// @notice Semantic version. + /// @custom:semver 1.0.0 + function version() public pure virtual returns (string memory) { + return "1.0.0"; + } + /// @dev Writes newTs, emits TimestampSet, and refreshes the hash chain. All validation is /// the caller's responsibility. - function _writeTimestamp(uint256 id, uint64 newTs) internal { + function _writeTimestamp(uint256 id, uint64 newTs) private { _timestamps[id] = newTs; emit TimestampSet(id, newTs); _refreshScheduleId(id); @@ -256,7 +256,7 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable /// @dev Writes the minimum protocol version and emits the update. Validation is the caller's /// responsibility. - function _writeMinimumProtocolVersion(uint256 protocolVersion) internal { + function _writeMinimumProtocolVersion(uint256 protocolVersion) private { minimumProtocolVersion = protocolVersion; emit MinimumProtocolVersionUpdated(protocolVersion); } @@ -265,7 +265,7 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable /// bubbles the result through all subsequent registered upgrades. Cost is O(n-startIndex). /// Only ever called after a state change (registration, or a timestamp that actually moved), /// so the resulting tail is always a new scheduleId. - function _refreshScheduleId(uint256 startIndex) internal { + function _refreshScheduleId(uint256 startIndex) private { uint256 n = _timestamps.length; // _upgradeScheduleId[startIndex] is the link preceding upgrade `startIndex` (the seed when @@ -280,7 +280,7 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable } /// @dev Reverts if `id` is not a registered upgrade. - function _assertRegistered(uint256 id) internal view { + function _assertRegistered(uint256 id) private view { if (id >= _timestamps.length) revert ProtocolVersions_UnknownUpgrade(id); } } diff --git a/test/L1/ProtocolVersions.t.sol b/test/L1/ProtocolVersions.t.sol index 315face06..0b1f184a6 100644 --- a/test/L1/ProtocolVersions.t.sol +++ b/test/L1/ProtocolVersions.t.sol @@ -2,10 +2,10 @@ pragma solidity 0.8.15; // Testing -import { Test } from "lib/forge-std/src/Test.sol"; +import { CommonTest } from "test/setup/CommonTest.sol"; +import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; // Contracts -import { ProtocolVersions } from "src/L1/ProtocolVersions.sol"; import { ProxyAdminOwnedBase } from "src/universal/ProxyAdminOwnedBase.sol"; import { Proxy } from "src/universal/Proxy.sol"; @@ -14,8 +14,9 @@ import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; /// @title ProtocolVersions_TestInit -/// @notice Reusable test initialization for ProtocolVersions tests. -abstract contract ProtocolVersions_TestInit is Test { +/// @notice Reusable test initialization for ProtocolVersions tests. Runs against the +/// `protocolVersions` instance deployed by the standard SystemDeploy script. +abstract contract ProtocolVersions_TestInit is CommonTest { event UpgradeRegistered(uint256 indexed id); event MinimumProtocolVersionUpdated(uint256 indexed protocolVersion); event IncidentResponderUpdated(address indexed previousIncidentResponder, address indexed newIncidentResponder); @@ -25,35 +26,14 @@ abstract contract ProtocolVersions_TestInit is Test { uint256 internal constant CANYON = 0; uint256 internal constant ECOTONE = 1; - address internal _owner = makeAddr("owner"); + address internal _owner; address internal _nonOwner = makeAddr("non-owner"); address internal _incidentResponder = makeAddr("incident-responder"); - address internal _proxyAdmin = makeAddr("proxy-admin"); - ProtocolVersions internal _impl; - ProtocolVersions internal protocolVersions; - function setUp() public virtual { - // proxyAdminOwner() resolves by calling owner() on the ProxyAdmin stored in the proxy slot. - vm.mockCall(_proxyAdmin, abi.encodeWithSignature("owner()"), abi.encode(_owner)); - _impl = new ProtocolVersions(); - protocolVersions = ProtocolVersions(_deployInitializedProxy(address(0))); - } - - /// @dev Deploys a proxy (admin = _proxyAdmin) over the shared impl and initializes it via the - /// proxy, appointing `_team` as the incidentResponder. - function _deployInitializedProxy(address _team) internal returns (address proxy_) { - Proxy proxy = new Proxy(_proxyAdmin); - vm.prank(_proxyAdmin); - proxy.upgradeToAndCall(address(_impl), abi.encodeCall(IProtocolVersions.initialize, (_team))); - proxy_ = address(proxy); - } - - /// @dev Deploys an uninitialized proxy over the shared impl (for initializer tests). - function _deployUninitializedProxy() internal returns (ProtocolVersions) { - Proxy proxy = new Proxy(_proxyAdmin); - vm.prank(_proxyAdmin); - proxy.upgradeTo(address(_impl)); - return ProtocolVersions(address(proxy)); + function setUp() public virtual override { + super.setUp(); + skipIfForkTest("ProtocolVersions_TestInit: cannot test on forked network"); + _owner = proxyAdminOwner; } /// @dev Registers the first upgrade (id CANYON) and schedules it for block.timestamp + MIN_NOTICE + delay. @@ -72,23 +52,28 @@ contract ProtocolVersions_Initialize_Test is ProtocolVersions_TestInit { /// @notice Tests that initialization sets the correct initial state. The seed is bytes32(0), so /// the initial scheduleId is bytes32(0) until the first upgrade is registered. function test_initialize_setsInitialState_succeeds() external view { - assertEq(protocolVersions.proxyAdminOwner(), _owner); + // The owner is inherited from the shared ProxyAdmin; initialize records the incident + // 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.scheduleId(), bytes32(0)); } /// @notice Tests that initialization appoints the provided incidentResponder and emits the event. + /// @dev Requires a fresh uninitialized proxy rather than the already-initialized shared instance. function test_initialize_setsIncidentResponder_succeeds() external { - ProtocolVersions uninitialized = _deployUninitializedProxy(); + IProtocolVersions uninitialized = _deployUninitializedProxy(); vm.expectEmit(true, true, false, false, address(uninitialized)); emit IncidentResponderUpdated(address(0), _incidentResponder); - vm.prank(_proxyAdmin); + vm.prank(EIP1967Helper.getAdmin(address(uninitialized))); uninitialized.initialize(_incidentResponder); assertEq(uninitialized.incidentResponder(), _incidentResponder); } /// @notice Tests that only the ProxyAdmin or its owner can initialize. + /// @dev Requires a fresh uninitialized proxy rather than the already-initialized shared instance. function test_initialize_notProxyAdminOrOwner_reverts() external { - ProtocolVersions uninitialized = _deployUninitializedProxy(); + IProtocolVersions uninitialized = _deployUninitializedProxy(); vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner.selector); vm.prank(_nonOwner); uninitialized.initialize(_incidentResponder); @@ -97,15 +82,28 @@ contract ProtocolVersions_Initialize_Test is ProtocolVersions_TestInit { /// @notice Tests that the contract cannot be initialized twice. function test_initialize_alreadyInitialized_reverts() external { vm.expectRevert("Initializable: contract is already initialized"); - vm.prank(_proxyAdmin); + vm.prank(EIP1967Helper.getAdmin(address(protocolVersions))); protocolVersions.initialize(address(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"); - vm.prank(_proxyAdmin); - _impl.initialize(address(0)); + impl.initialize(address(0)); + } + + /// @dev Deploys a fresh uninitialized proxy over the impl produced by SystemDeploy for the two + /// initializer tests that genuinely need one. proxyAdminOwner() resolves by calling owner() + /// on the ProxyAdmin stored in the proxy slot, so the mock provides one. + function _deployUninitializedProxy() internal returns (IProtocolVersions) { + address proxyAdmin = makeAddr("proxy-admin"); + vm.mockCall(proxyAdmin, abi.encodeWithSignature("owner()"), abi.encode(_owner)); + Proxy proxy = new Proxy(proxyAdmin); + address impl = EIP1967Helper.getImplementation(address(protocolVersions)); + vm.prank(proxyAdmin); + proxy.upgradeTo(impl); + return IProtocolVersions(address(proxy)); } } From 21e6cc7ac6b9688d894e48c5ee26fdaac51c1d9f Mon Sep 17 00:00:00 2001 From: PelleKrab Date: Fri, 10 Jul 2026 10:51:44 -0700 Subject: [PATCH 18/20] fix(deploy): upgrade ProtocolVersions proxy --- scripts/deploy/SystemDeploy.s.sol | 20 +++++++++++-- test/deploy/SystemDeploy.t.sol | 50 +++++++++++++++++++++++++++++-- test/setup/ForkLive.s.sol | 8 +++-- 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/scripts/deploy/SystemDeploy.s.sol b/scripts/deploy/SystemDeploy.s.sol index 739d7275f..d12aa6c08 100644 --- a/scripts/deploy/SystemDeploy.s.sol +++ b/scripts/deploy/SystemDeploy.s.sol @@ -108,6 +108,7 @@ contract SystemDeploy is Script { ISuperchainConfig superchainConfigProxy; Types.Implementations implementations; ISystemConfig systemConfigProxy; + IProtocolVersions protocolVersionsProxy; } struct UpgradeOutput { @@ -339,7 +340,12 @@ contract SystemDeploy is Script { revert SuperchainConfigNeedsUpgrade(); } - _upgradeOPChain(systemConfigProxy, _input.implementations); + IProtocolVersions protocolVersionsProxy = _input.protocolVersionsProxy; + if (address(protocolVersionsProxy) == address(0) && address(artifacts).code.length != 0) { + protocolVersionsProxy = IProtocolVersions(artifacts.getAddress("ProtocolVersionsProxy")); + } + + _upgradeOPChain(systemConfigProxy, _input.implementations, protocolVersionsProxy); output_.chainUpgraded = true; } @@ -650,7 +656,13 @@ contract SystemDeploy is Script { upgraded_ = true; } - function _upgradeOPChain(ISystemConfig _systemConfigProxy, Types.Implementations memory _impls) internal { + function _upgradeOPChain( + ISystemConfig _systemConfigProxy, + Types.Implementations memory _impls, + IProtocolVersions _protocolVersionsProxy + ) + internal + { IProxyAdmin proxyAdmin = _systemConfigProxy.proxyAdmin(); uint256 l2ChainId = _systemConfigProxy.l2ChainId(); @@ -675,6 +687,10 @@ contract SystemDeploy is Script { _upgradeTo(proxyAdmin, opChainAddrs.delayedWETH, _impls.delayedWETHImpl); } + if (address(_protocolVersionsProxy) != address(0)) { + _upgradeTo(proxyAdmin, address(_protocolVersionsProxy), _impls.protocolVersionsImpl); + } + emit Upgraded(l2ChainId, _systemConfigProxy, msg.sender); } diff --git a/test/deploy/SystemDeploy.t.sol b/test/deploy/SystemDeploy.t.sol index 616cea3bf..5011d14f3 100644 --- a/test/deploy/SystemDeploy.t.sol +++ b/test/deploy/SystemDeploy.t.sol @@ -9,6 +9,8 @@ import { Types } from "scripts/libraries/Types.sol"; import { SystemDeployAssertions } from "test/deploy/SystemDeployAssertions.sol"; import { ISP1Verifier } from "interfaces/L1/proofs/zk/ISP1Verifier.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; +import { ProtocolVersions } from "src/L1/ProtocolVersions.sol"; import { TEEProverRegistry } from "src/L1/proofs/tee/TEEProverRegistry.sol"; import { TEEVerifier } from "src/L1/proofs/tee/TEEVerifier.sol"; import { ZKVerifier } from "src/L1/proofs/zk/ZKVerifier.sol"; @@ -140,13 +142,17 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { function test_upgrade_withoutManagerDelegatecall_succeeds() public { SystemDeploy.DeployInput memory input = _defaultDeployInput(); SystemDeploy.DeployOutput memory output = systemDeploy.deploy(input); + Types.Implementations memory implementations = output.impls; + ProtocolVersions protocolVersionsImpl = new ProtocolVersions(); + implementations.protocolVersionsImpl = address(protocolVersionsImpl); SystemDeploy.UpgradeOutput memory upgradeOutput = systemDeploy.upgrade( SystemDeploy.UpgradeInput({ saveArtifacts: false, superchainConfigProxy: output.superchain.superchainConfigProxy, - implementations: output.impls, - systemConfigProxy: output.opChain.systemConfigProxy + implementations: implementations, + systemConfigProxy: output.opChain.systemConfigProxy, + protocolVersionsProxy: output.opChain.protocolVersionsProxy }) ); @@ -158,9 +164,42 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { output.impls.superchainConfigImpl, "superchain config impl" ); + assertEq( + output.opChain.opChainProxyAdmin.getProxyImplementation(address(output.opChain.protocolVersionsProxy)), + address(protocolVersionsImpl), + "protocol versions impl" + ); assertValidStandardSystem(_expected(output, input)); } + function test_upgrade_discoversProtocolVersionsProxyFromArtifacts_succeeds() public { + SystemDeploy.DeployInput memory input = _defaultDeployInput(); + SystemDeploy.DeployOutput memory output = systemDeploy.deploy(input); + Types.Implementations memory implementations = output.impls; + ProtocolVersions protocolVersionsImpl = new ProtocolVersions(); + implementations.protocolVersionsImpl = address(protocolVersionsImpl); + + _saveArtifact("ProtocolVersionsProxy", address(output.opChain.protocolVersionsProxy)); + + SystemDeploy.UpgradeOutput memory upgradeOutput = systemDeploy.upgrade( + SystemDeploy.UpgradeInput({ + saveArtifacts: false, + superchainConfigProxy: output.superchain.superchainConfigProxy, + implementations: implementations, + systemConfigProxy: output.opChain.systemConfigProxy, + protocolVersionsProxy: IProtocolVersions(address(0)) + }) + ); + + assertFalse(upgradeOutput.superchainConfigUpgraded, "superchain already current"); + assertTrue(upgradeOutput.chainUpgraded, "chain upgraded"); + assertEq( + output.opChain.opChainProxyAdmin.getProxyImplementation(address(output.opChain.protocolVersionsProxy)), + address(protocolVersionsImpl), + "protocol versions impl" + ); + } + function test_deploy_reusingImplementations_doesNotSaveZeroImplementationOnlyArtifacts() public { SystemDeploy.DeployInput memory input = _defaultDeployInput(); SystemDeploy.DeployOutput memory output = systemDeploy.deploy(input); @@ -278,6 +317,13 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { ); } + function _saveArtifact(string memory _name, address _addr) internal { + vm.etch(address(artifacts), vm.getDeployedCode("Artifacts.s.sol:Artifacts")); + bytes32 slot = keccak256(abi.encodePacked(_name, uint256(0))); + vm.store(address(artifacts), slot, bytes32(uint256(uint160(_addr)))); + assertEq(artifacts.getAddress(_name), _addr, "artifact saved"); + } + function _expected( SystemDeploy.DeployOutput memory _output, SystemDeploy.DeployInput memory _input diff --git a/test/setup/ForkLive.s.sol b/test/setup/ForkLive.s.sol index 2b1b0cea2..e606b5f64 100644 --- a/test/setup/ForkLive.s.sol +++ b/test/setup/ForkLive.s.sol @@ -21,6 +21,7 @@ import { IDisputeGameFactory } from "interfaces/L1/proofs/IDisputeGameFactory.so import { IAggregateVerifier } from "interfaces/L1/proofs/IAggregateVerifier.sol"; import { IAddressManager } from "interfaces/legacy/IAddressManager.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; @@ -190,6 +191,7 @@ contract ForkLive is Script { ISuperchainConfig superchainConfig = ISuperchainConfig(artifacts.mustGetAddress("SuperchainConfigProxy")); IProxyAdmin superchainProxyAdmin = IProxyAdmin(EIP1967Helper.getAdmin(address(superchainConfig))); address superchainPAO = superchainProxyAdmin.owner(); + IProtocolVersions protocolVersionsProxy = IProtocolVersions(artifacts.getAddress("ProtocolVersionsProxy")); // Run the shared SuperchainConfig upgrade as the Superchain ProxyAdmin owner. The script // skips this step when the proxy is already at or above the target implementation version. @@ -199,7 +201,8 @@ contract ForkLive is Script { saveArtifacts: false, superchainConfigProxy: superchainConfig, implementations: implementations, - systemConfigProxy: ISystemConfig(address(0)) + systemConfigProxy: ISystemConfig(address(0)), + protocolVersionsProxy: IProtocolVersions(address(0)) }) ); @@ -210,7 +213,8 @@ contract ForkLive is Script { saveArtifacts: false, superchainConfigProxy: ISuperchainConfig(address(0)), implementations: implementations, - systemConfigProxy: _systemConfigProxy + systemConfigProxy: _systemConfigProxy, + protocolVersionsProxy: protocolVersionsProxy }) ); } From 2e348fac214e71c2da457bd1fc62b6cf482bffa1 Mon Sep 17 00:00:00 2001 From: PelleKrab Date: Fri, 10 Jul 2026 12:20:30 -0700 Subject: [PATCH 19/20] refactor(L1): reorder IProtocolVersions members to match style guide Events before errors, and mutating externals before view getters, mirroring the ProtocolVersions contract body and the repo style-guide ordering. ABI is generator-sorted, so no snapshot or semver-lock impact. Generated with Claude Code Co-Authored-By: Claude --- interfaces/L1/IProtocolVersions.sol | 30 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/interfaces/L1/IProtocolVersions.sol b/interfaces/L1/IProtocolVersions.sol index 79e8ef8b2..ccb41815d 100644 --- a/interfaces/L1/IProtocolVersions.sol +++ b/interfaces/L1/IProtocolVersions.sol @@ -8,6 +8,13 @@ import { IReinitializableBase } from "interfaces/universal/IReinitializableBase. /// @title IProtocolVersions /// @notice Interface for the ProtocolVersions upgrade schedule contract. interface IProtocolVersions is IProxyAdminOwnedBase, ISemver, IReinitializableBase { + event UpgradeRegistered(uint256 indexed id); + event MinimumProtocolVersionUpdated(uint256 indexed protocolVersion); + event TimestampSet(uint256 indexed id, uint256 timestamp); + event ScheduleIdUpdated(bytes32 indexed newScheduleId); + event IncidentResponderUpdated(address indexed previousIncidentResponder, address indexed newIncidentResponder); + event Initialized(uint8 version); + error ProtocolVersions_UnknownUpgrade(uint256 id); error ProtocolVersions_InvalidProtocolVersion(); error ProtocolVersions_ActivationAlreadyPassed(uint256 id, uint64 activationTimestamp); @@ -17,22 +24,6 @@ interface IProtocolVersions is IProxyAdminOwnedBase, ISemver, IReinitializableBa error ProtocolVersions_NotInitialized(); error ProtocolVersions_InsufficientNotice(uint64 timestamp); - event UpgradeRegistered(uint256 indexed id); - event MinimumProtocolVersionUpdated(uint256 indexed protocolVersion); - event TimestampSet(uint256 indexed id, uint256 timestamp); - event ScheduleIdUpdated(bytes32 indexed newScheduleId); - event IncidentResponderUpdated(address indexed previousIncidentResponder, address indexed newIncidentResponder); - event Initialized(uint8 version); - - function MIN_NOTICE() external view returns (uint64); - - function incidentResponder() external view returns (address); - function scheduleId() external view returns (bytes32); - function scheduleId(uint256 id) external view returns (bytes32); - function minimumProtocolVersion() external view returns (uint256); - - function getSchedule() external view returns (uint64[] memory); - function initialize(address _incidentResponder) external; function registerUpgrade(uint64 timestamp, uint256 minProtocolVersion) external returns (uint256); function setMinimumProtocolVersion(uint256 protocolVersion) external; @@ -40,5 +31,12 @@ interface IProtocolVersions is IProxyAdminOwnedBase, ISemver, IReinitializableBa function setIncidentResponder(address newIncidentResponder) external; function delayTimestamp(uint256 id, uint64 newTimestamp) external; + function MIN_NOTICE() external view returns (uint64); + function minimumProtocolVersion() external view returns (uint256); + function incidentResponder() external view returns (address); + function scheduleId() external view returns (bytes32); + function scheduleId(uint256 id) external view returns (bytes32); + function getSchedule() external view returns (uint64[] memory); + function __constructor__() external; } From 90bf7d32a5e180b67322043dd245d4d5e09f31ef Mon Sep 17 00:00:00 2001 From: PelleKrab Date: Wed, 8 Jul 2026 13:09:20 -0700 Subject: [PATCH 20/20] feat(L1): bind aggregate proofs to ProtocolVersions schedule Co-authored-by: Codex --- interfaces/L1/proofs/IAggregateVerifier.sol | 3 ++ scripts/deploy/SystemDeploy.s.sol | 7 +++-- scripts/multiproof/DeployDevBase.s.sol | 11 ++++++- snapshots/abi/AggregateVerifier.json | 31 +++++++++++++++++++ snapshots/semver-lock.json | 4 +-- .../storageLayout/AggregateVerifier.json | 7 +++++ src/L1/proofs/AggregateVerifier.sol | 24 ++++++++++++-- test/L1/OptimismPortal2.t.sol | 3 +- test/L1/proofs/AggregateVerifier.t.sol | 29 ++++++++++++++++- test/L1/proofs/BaseTest.t.sol | 10 +++++- test/L1/proofs/DisputeGameFactory.t.sol | 3 +- 11 files changed, 120 insertions(+), 12 deletions(-) diff --git a/interfaces/L1/proofs/IAggregateVerifier.sol b/interfaces/L1/proofs/IAggregateVerifier.sol index dd588e4ec..125fc3aa4 100644 --- a/interfaces/L1/proofs/IAggregateVerifier.sol +++ b/interfaces/L1/proofs/IAggregateVerifier.sol @@ -5,6 +5,7 @@ import { IDisputeGame } from "./IDisputeGame.sol"; import { IDisputeGameFactory } from "./IDisputeGameFactory.sol"; import { IDelayedWETH } from "./IDelayedWETH.sol"; import { IVerifier } from "./IVerifier.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { Proposal, Hash } from "src/libraries/bridge/Types.sol"; import { Timestamp } from "src/libraries/bridge/LibUDT.sol"; @@ -23,6 +24,7 @@ interface IAggregateVerifier is IDisputeGame { function ZK_RANGE_HASH() external view returns (bytes32); function ZK_AGGREGATE_HASH() external view returns (bytes32); function CONFIG_HASH() external view returns (bytes32); + function PROTOCOL_VERSIONS() external view returns (IProtocolVersions); function L2_CHAIN_ID() external view returns (uint256); function BLOCK_INTERVAL() external view returns (uint256); function INTERMEDIATE_BLOCK_INTERVAL() external view returns (uint256); @@ -35,6 +37,7 @@ interface IAggregateVerifier is IDisputeGame { function counteredByIntermediateRootIndexPlusOne() external view returns (uint256); function expectedResolution() external view returns (Timestamp); function proofCount() external view returns (uint8); + function scheduleId() external view returns (bytes32); function initializeWithInitData(bytes calldata proof) external payable; function verifyProposalProof(bytes calldata proofBytes) external; diff --git a/scripts/deploy/SystemDeploy.s.sol b/scripts/deploy/SystemDeploy.s.sol index d12aa6c08..e8e1a19cf 100644 --- a/scripts/deploy/SystemDeploy.s.sol +++ b/scripts/deploy/SystemDeploy.s.sol @@ -129,6 +129,7 @@ contract SystemDeploy is Script { uint256 l2ChainId; uint256 multiproofBlockInterval; uint256 multiproofIntermediateBlockInterval; + IProtocolVersions protocolVersions; } struct MultiproofOutput { @@ -1048,7 +1049,8 @@ contract SystemDeploy is Script { multiproofConfigHash: _input.multiproofConfigHash, l2ChainId: _opChainInput.l2ChainId, multiproofBlockInterval: _input.multiproofBlockInterval, - multiproofIntermediateBlockInterval: _input.multiproofIntermediateBlockInterval + multiproofIntermediateBlockInterval: _input.multiproofIntermediateBlockInterval, + protocolVersions: _output.protocolVersionsProxy }) ); @@ -1077,7 +1079,8 @@ contract SystemDeploy is Script { _input.multiproofConfigHash, _input.l2ChainId, _input.multiproofBlockInterval, - _input.multiproofIntermediateBlockInterval + _input.multiproofIntermediateBlockInterval, + _input.protocolVersions ) ) ); diff --git a/scripts/multiproof/DeployDevBase.s.sol b/scripts/multiproof/DeployDevBase.s.sol index 31d009270..dd4cc4a06 100644 --- a/scripts/multiproof/DeployDevBase.s.sol +++ b/scripts/multiproof/DeployDevBase.s.sol @@ -16,6 +16,8 @@ import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; import { AggregateVerifier } from "src/L1/proofs/AggregateVerifier.sol"; import { IVerifier } from "interfaces/L1/proofs/IVerifier.sol"; +import { ProtocolVersions } from "src/L1/ProtocolVersions.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { MockVerifier } from "test/mocks/MockVerifier.sol"; import { TEEProverRegistry } from "src/L1/proofs/tee/TEEProverRegistry.sol"; import { TEEVerifier } from "src/L1/proofs/tee/TEEVerifier.sol"; @@ -104,6 +106,12 @@ abstract contract DeployDevBase is Script { AggregateVerifier.ZkHashes memory zkHashes = AggregateVerifier.ZkHashes({ rangeHash: cfg.zkRangeHash(), aggregateHash: cfg.zkAggregationHash() }); + Proxy protocolVersionsProxy = new Proxy(msg.sender); + protocolVersionsProxy.upgradeToAndCall( + address(new ProtocolVersions()), abi.encodeCall(IProtocolVersions.initialize, (address(0))) + ); + protocolVersionsProxy.changeAdmin(address(0xdead)); + aggregateVerifier = address( new AggregateVerifier( gameType, @@ -116,7 +124,8 @@ abstract contract DeployDevBase is Script { cfg.multiproofConfigHash(), cfg.l2ChainId(), _blockInterval(), - _intermediateBlockInterval() + _intermediateBlockInterval(), + IProtocolVersions(address(protocolVersionsProxy)) ) ); diff --git a/snapshots/abi/AggregateVerifier.json b/snapshots/abi/AggregateVerifier.json index d8ee465e9..6b5d6357c 100644 --- a/snapshots/abi/AggregateVerifier.json +++ b/snapshots/abi/AggregateVerifier.json @@ -67,6 +67,11 @@ "internalType": "uint256", "name": "intermediateBlockInterval", "type": "uint256" + }, + { + "internalType": "contract IProtocolVersions", + "name": "protocolVersions", + "type": "address" } ], "stateMutability": "nonpayable", @@ -215,6 +220,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "PROTOCOL_VERSIONS", + "outputs": [ + { + "internalType": "contract IProtocolVersions", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "SLOW_FINALIZATION_DELAY", @@ -681,6 +699,19 @@ "stateMutability": "pure", "type": "function" }, + { + "inputs": [], + "name": "scheduleId", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "startingBlockNumber", diff --git a/snapshots/semver-lock.json b/snapshots/semver-lock.json index 22cbff899..98b556fdb 100644 --- a/snapshots/semver-lock.json +++ b/snapshots/semver-lock.json @@ -32,8 +32,8 @@ "sourceCodeHash": "0xf122a50487efe9bd5a620262ba20ef4adbca14eeec2af7fd32e6e16739001596" }, "src/L1/proofs/AggregateVerifier.sol:AggregateVerifier": { - "initCodeHash": "0x90629c679e821a70553d899a68e818bcddb672a004a0d2e09078d17028a2f082", - "sourceCodeHash": "0x4835d3de998b2951898f0e427c61b0c330ac3feb5f7a8dc9fd0deb78912a9b7a" + "initCodeHash": "0x6f3c53245830b9c72c8199695e93959d5ed3b0cb8352e5b50b3dfa28183ac787", + "sourceCodeHash": "0x1f2e2b07ebc4359fa31ea7400ace9bab0e4f6099d8ff3ff7b6a2b14cccfb89ca" }, "src/L1/proofs/AnchorStateRegistry.sol:AnchorStateRegistry": { "initCodeHash": "0x6f3afd2d0ef97a82ca3111976322b99343a270e54cd4a405028f2f29c75f7fb1", diff --git a/snapshots/storageLayout/AggregateVerifier.json b/snapshots/storageLayout/AggregateVerifier.json index 08fd716cb..1226af645 100644 --- a/snapshots/storageLayout/AggregateVerifier.json +++ b/snapshots/storageLayout/AggregateVerifier.json @@ -96,5 +96,12 @@ "offset": 8, "slot": "7", "type": "uint8" + }, + { + "bytes": "32", + "label": "scheduleId", + "offset": 0, + "slot": "8", + "type": "bytes32" } ] \ No newline at end of file diff --git a/src/L1/proofs/AggregateVerifier.sol b/src/L1/proofs/AggregateVerifier.sol index a468dc058..fd447fe6a 100644 --- a/src/L1/proofs/AggregateVerifier.sol +++ b/src/L1/proofs/AggregateVerifier.sol @@ -26,6 +26,7 @@ import { ReentrancyGuard } from "lib/solady/src/utils/ReentrancyGuard.sol"; import { IVerifier } from "interfaces/L1/proofs/IVerifier.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { //////////////////////////////////////////////////////////////// @@ -113,6 +114,9 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { /// @notice The game type ID. GameType internal immutable GAME_TYPE; + /// @notice The ProtocolVersions upgrade schedule contract. + IProtocolVersions public immutable PROTOCOL_VERSIONS; + //////////////////////////////////////////////////////////////// // State Vars // //////////////////////////////////////////////////////////////// @@ -162,6 +166,11 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { /// @notice The number of proofs provided. uint8 public proofCount; + /// @notice The ProtocolVersions scheduleId snapshotted at game initialization. + /// @dev Pinned once at init; every proof in this game commits to this value, binding it to the + /// upgrade schedule in effect when the game was created. + bytes32 public scheduleId; + //////////////////////////////////////////////////////////////// // Events // //////////////////////////////////////////////////////////////// @@ -259,6 +268,7 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { /// @param l2ChainId The chain ID of the L2 network. /// @param blockInterval The block interval. /// @param intermediateBlockInterval The intermediate block interval. + /// @param protocolVersions The ProtocolVersions upgrade schedule contract. constructor( GameType gameType_, IAnchorStateRegistry anchorStateRegistry_, @@ -270,7 +280,8 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { bytes32 configHash, uint256 l2ChainId, uint256 blockInterval, - uint256 intermediateBlockInterval + uint256 intermediateBlockInterval, + IProtocolVersions protocolVersions ) { // Block interval and intermediate block interval must be positive and divisible. if (blockInterval == 0 || intermediateBlockInterval == 0 || blockInterval % intermediateBlockInterval != 0) { @@ -291,6 +302,7 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { L2_CHAIN_ID = l2ChainId; BLOCK_INTERVAL = blockInterval; INTERMEDIATE_BLOCK_INTERVAL = intermediateBlockInterval; + PROTOCOL_VERSIONS = protocolVersions; INITIALIZE_CALLDATA_SIZE = 0x8E + 0x20 * intermediateOutputRootsCount(); } @@ -364,6 +376,10 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { // Set the game as initialized. initialized = true; + // Snapshot the scheduleId from ProtocolVersions. Every proof in this game commits to this + // value, pinning them to the upgrade schedule in effect at the game's creation block. + scheduleId = PROTOCOL_VERSIONS.scheduleId(); + // Set the game's starting timestamp. createdAt = Timestamp.wrap(uint64(block.timestamp)); @@ -899,7 +915,8 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { endingL2SequenceNumber, intermediateRoots, CONFIG_HASH, - TEE_IMAGE_HASH + TEE_IMAGE_HASH, + scheduleId ) ); @@ -933,7 +950,8 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { endingL2SequenceNumber, intermediateRoots, CONFIG_HASH, - ZK_RANGE_HASH + ZK_RANGE_HASH, + scheduleId ) ); diff --git a/test/L1/OptimismPortal2.t.sol b/test/L1/OptimismPortal2.t.sol index bf93e25f4..04e8d1df5 100644 --- a/test/L1/OptimismPortal2.t.sol +++ b/test/L1/OptimismPortal2.t.sol @@ -90,7 +90,8 @@ abstract contract OptimismPortal2_TestInit is DisputeGameFactory_TestInit { bytes32(uint256(4)), deploy.cfg().l2ChainId(), 100, - 10 + 10, + protocolVersions ); disputeGameFactory.setImplementation(respectedGameType, IDisputeGame(address(gameImpl))); disputeGameFactory.setInitBond(respectedGameType, 0); diff --git a/test/L1/proofs/AggregateVerifier.t.sol b/test/L1/proofs/AggregateVerifier.t.sol index 30907b20e..a6d34f7fb 100644 --- a/test/L1/proofs/AggregateVerifier.t.sol +++ b/test/L1/proofs/AggregateVerifier.t.sol @@ -11,6 +11,7 @@ import { Claim, Timestamp } from "src/libraries/bridge/LibUDT.sol"; import { AggregateVerifier } from "src/L1/proofs/AggregateVerifier.sol"; import { IVerifier } from "interfaces/L1/proofs/IVerifier.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { LibClone } from "lib/solady/src/utils/LibClone.sol"; @@ -36,6 +37,31 @@ contract AggregateVerifierTest is BaseTest { _createAndAssertInitializedGame("zk-proof", AggregateVerifier.ProofType.ZK, ZK_PROVER, address(0), ZK_PROVER); } + /// @notice init snapshots the live scheduleId into storage and pins it: a later schedule change + /// must not alter the game's snapshot (proves it is stored once, not read live). + /// @dev Only covers the stored getter. That the snapshot flows into the proof journal is not + /// asserted here — MockVerifier ignores the journal, so journal binding is untestable. + function test_initialize_pinsScheduleId_succeeds() public { + // Register an upgrade so the schedule commitment is non-trivial (non-zero). + protocolVersions.registerUpgrade(uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100, 1); + bytes32 pinned = protocolVersions.scheduleId(); + assertTrue(pinned != bytes32(0)); + + Claim rootClaim = _advanceL2BlockAndClaim(); + bytes memory proof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE); + + AggregateVerifier game = _createAggregateVerifierGame( + TEE_PROVER, rootClaim, currentL2BlockNumber, address(anchorStateRegistry), proof + ); + + assertEq(game.scheduleId(), pinned); + + // Move the live schedule after creation; the game's snapshot must stay pinned. + protocolVersions.registerUpgrade(uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 200, 2); + assertNotEq(protocolVersions.scheduleId(), pinned); + assertEq(game.scheduleId(), pinned); + } + function testInitializeFailsIfInvalidCallDataSize() public { Claim rootClaim = _advanceL2BlockAndClaim(); @@ -417,7 +443,8 @@ contract AggregateVerifierTest is BaseTest { CONFIG_HASH, L2_CHAIN_ID, blockInterval, - intermediateBlockInterval + intermediateBlockInterval, + IProtocolVersions(address(protocolVersions)) ); } } diff --git a/test/L1/proofs/BaseTest.t.sol b/test/L1/proofs/BaseTest.t.sol index 2016de959..87e6c96f5 100644 --- a/test/L1/proofs/BaseTest.t.sol +++ b/test/L1/proofs/BaseTest.t.sol @@ -19,6 +19,8 @@ import { ProxyAdmin } from "src/universal/ProxyAdmin.sol"; import { AggregateVerifier } from "src/L1/proofs/AggregateVerifier.sol"; import { IVerifier } from "interfaces/L1/proofs/IVerifier.sol"; +import { ProtocolVersions } from "src/L1/ProtocolVersions.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { MockVerifier } from "test/mocks/MockVerifier.sol"; @@ -54,6 +56,7 @@ contract BaseTest is Test { MockVerifier internal teeVerifier; MockVerifier internal zkVerifier; + ProtocolVersions internal protocolVersions; function setUp() public virtual { _deployContractsAndProxies(); @@ -78,9 +81,12 @@ contract BaseTest is Test { proxyAdmin = new ProxyAdmin(address(this)); + ProtocolVersions _protocolVersions = new ProtocolVersions(); + anchorStateRegistry = AnchorStateRegistry(_deployProxy(address(_anchorStateRegistry))); factory = DisputeGameFactory(_deployProxy(address(_factory))); delayedWETH = DelayedWETH(payable(_deployProxy(address(_delayedWETH)))); + protocolVersions = ProtocolVersions(_deployProxy(address(_protocolVersions))); teeVerifier = new MockVerifier(IAnchorStateRegistry(address(anchorStateRegistry))); zkVerifier = new MockVerifier(IAnchorStateRegistry(address(anchorStateRegistry))); @@ -103,6 +109,7 @@ contract BaseTest is Test { ); factory.initialize(address(this)); delayedWETH.initialize(systemConfig); + protocolVersions.initialize(address(0)); } function _deployAndSetAggregateVerifier() internal { @@ -117,7 +124,8 @@ contract BaseTest is Test { CONFIG_HASH, L2_CHAIN_ID, BLOCK_INTERVAL, - INTERMEDIATE_BLOCK_INTERVAL + INTERMEDIATE_BLOCK_INTERVAL, + IProtocolVersions(address(protocolVersions)) ); factory.setImplementation(GameTypes.AGGREGATE_VERIFIER, IDisputeGame(address(aggregateVerifierImpl))); diff --git a/test/L1/proofs/DisputeGameFactory.t.sol b/test/L1/proofs/DisputeGameFactory.t.sol index 6cab78c26..768fff7ce 100644 --- a/test/L1/proofs/DisputeGameFactory.t.sol +++ b/test/L1/proofs/DisputeGameFactory.t.sol @@ -230,7 +230,8 @@ contract DisputeGameFactory_Create_Test is DisputeGameFactory_TestInit { bytes32(uint256(4)), L2_CHAIN_ID, AGGREGATE_BLOCK_INTERVAL, - AGGREGATE_INTERMEDIATE_BLOCK_INTERVAL + AGGREGATE_INTERMEDIATE_BLOCK_INTERVAL, + protocolVersions ); _setGame(address(gameImpl), GameTypes.AGGREGATE_VERIFIER);