diff --git a/interfaces/L1/IProtocolVersions.sol b/interfaces/L1/IProtocolVersions.sol new file mode 100644 index 000000000..ccb41815d --- /dev/null +++ b/interfaces/L1/IProtocolVersions.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +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 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); + error ProtocolVersions_NotIncidentResponder(); + error ProtocolVersions_NotScheduled(uint256 id); + error ProtocolVersions_DelayMustBeLater(uint64 currentTimestamp, uint64 newTimestamp); + error ProtocolVersions_NotInitialized(); + error ProtocolVersions_InsufficientNotice(uint64 timestamp); + + 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 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; +} diff --git a/scripts/deploy/SystemDeploy.s.sol b/scripts/deploy/SystemDeploy.s.sol index 227d123d6..d12aa6c08 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"; @@ -107,6 +108,7 @@ contract SystemDeploy is Script { ISuperchainConfig superchainConfigProxy; Types.Implementations implementations; ISystemConfig systemConfigProxy; + IProtocolVersions protocolVersionsProxy; } struct UpgradeOutput { @@ -218,6 +220,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"), @@ -279,7 +282,8 @@ contract SystemDeploy is Script { opChainProxyAdminOwner: cfg.finalSystemOwner(), systemConfigOwner: cfg.finalSystemOwner(), batcher: cfg.batchSenderAddress(), - unsafeBlockSigner: cfg.p2pSequencerAddress() + unsafeBlockSigner: cfg.p2pSequencerAddress(), + incidentResponder: cfg.superchainConfigIncidentResponder() }), basefeeScalar: cfg.basefeeScalar(), blobBasefeeScalar: cfg.blobbasefeeScalar(), @@ -336,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; } @@ -447,6 +456,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 +497,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 +631,13 @@ contract SystemDeploy is Script { _impls.anchorStateRegistryImpl, _encodeAnchorStateRegistryInitializer(_input, _output) ); + + _upgradeToAndCall( + _output.opChainProxyAdmin, + address(_output.protocolVersionsProxy), + _impls.protocolVersionsImpl, + abi.encodeCall(IProtocolVersions.initialize, (_input.roles.incidentResponder)) + ); } function _upgradeSuperchainConfigIfNeeded( @@ -637,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(); @@ -662,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); } @@ -947,6 +976,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 +1139,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 +1165,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 +1192,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..9f26a5e75 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"; @@ -28,6 +29,7 @@ library Types { address systemConfigOwner; address batcher; address unsafeBlockSigner; + address incidentResponder; } /// @notice The full set of inputs to deploy a new OP Stack chain. @@ -55,6 +57,7 @@ library Types { IDisputeGameFactory disputeGameFactoryProxy; IAnchorStateRegistry anchorStateRegistryProxy; IDelayedWETH delayedWETHProxy; + IProtocolVersions protocolVersionsProxy; IVerifier aggregateVerifier; ITEEProverRegistry teeProverRegistryProxy; IVerifier teeVerifier; @@ -76,6 +79,7 @@ library Types { address disputeGameFactoryImpl; address anchorStateRegistryImpl; address delayedWETHImpl; + address protocolVersionsImpl; address aggregateVerifierImpl; address teeProverRegistryImpl; address teeVerifierImpl; diff --git a/snapshots/abi/ProtocolVersions.json b/snapshots/abi/ProtocolVersions.json new file mode 100644 index 000000000..33ef5fc0a --- /dev/null +++ b/snapshots/abi/ProtocolVersions.json @@ -0,0 +1,447 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "MIN_NOTICE", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "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": [ + { + "internalType": "uint64[]", + "name": "", + "type": "uint64[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "incidentResponder", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "initVersion", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_incidentResponder", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "minimumProtocolVersion", + "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": "uint64", + "name": "timestamp", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "minProtocolVersion", + "type": "uint256" + } + ], + "name": "registerUpgrade", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "scheduleId", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "scheduleId", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newIncidentResponder", + "type": "address" + } + ], + "name": "setIncidentResponder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "protocolVersion", + "type": "uint256" + } + ], + "name": "setMinimumProtocolVersion", + "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": "pure", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousIncidentResponder", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newIncidentResponder", + "type": "address" + } + ], + "name": "IncidentResponderUpdated", + "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": "MinimumProtocolVersionUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "newScheduleId", + "type": "bytes32" + } + ], + "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" + } + ], + "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": [ + { + "internalType": "uint64", + "name": "timestamp", + "type": "uint64" + } + ], + "name": "ProtocolVersions_InsufficientNotice", + "type": "error" + }, + { + "inputs": [], + "name": "ProtocolVersions_InvalidProtocolVersion", + "type": "error" + }, + { + "inputs": [], + "name": "ProtocolVersions_NotIncidentResponder", + "type": "error" + }, + { + "inputs": [], + "name": "ProtocolVersions_NotInitialized", + "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 a0b3f4d4e..22cbff899 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": "0x0d47628779e604e08a8787c299411ca1f9ed411052921aec53047748a5d7e40f", + "sourceCodeHash": "0x6bb512df396c0fe7216d160f0a4637cd690b4b5b8515096799d946b6bbcdaa5f" + }, "src/L1/SuperchainConfig.sol:SuperchainConfig": { "initCodeHash": "0x9b1f3555b499709485d51d5d9665002c0eb1e5eb893be1fb978a30749e894858", "sourceCodeHash": "0x79f0c771fc5f6d222d89b05addedc08ce40a7a42423fbd66f7cbb6cde3c2e74f" diff --git a/snapshots/storageLayout/ProtocolVersions.json b/snapshots/storageLayout/ProtocolVersions.json new file mode 100644 index 000000000..27001da1d --- /dev/null +++ b/snapshots/storageLayout/ProtocolVersions.json @@ -0,0 +1,44 @@ +[ + { + "bytes": "1", + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "uint8" + }, + { + "bytes": "1", + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "bool" + }, + { + "bytes": "32", + "label": "_timestamps", + "offset": 0, + "slot": "1", + "type": "uint64[]" + }, + { + "bytes": "32", + "label": "_upgradeScheduleId", + "offset": 0, + "slot": "2", + "type": "bytes32[]" + }, + { + "bytes": "32", + "label": "minimumProtocolVersion", + "offset": 0, + "slot": "3", + "type": "uint256" + }, + { + "bytes": "20", + "label": "incidentResponder", + "offset": 0, + "slot": "4", + "type": "address" + } +] \ No newline at end of file diff --git a/src/L1/ProtocolVersions.sol b/src/L1/ProtocolVersions.sol new file mode 100644 index 000000000..a30f21305 --- /dev/null +++ b/src/L1/ProtocolVersions.sol @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: MIT +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 { 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. +/// Each upgrade is identified by an ascending numeric `id` equal to its registration +/// 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. +/// +/// 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: +/// +/// _upgradeScheduleId[0] = bytes32(0) (seed) +/// _upgradeScheduleId[i+1] = keccak256(abi.encode(_upgradeScheduleId[i], i, timestamp_i)) +/// scheduleId = _upgradeScheduleId[last] +/// +/// 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 (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) seeds the hash chain. +contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, ReinitializableBase, ISemver { + /// @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[] 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[] private _upgradeScheduleId; + + /// @notice The minimum protocol version clients must run. Settable by the owner via + /// `setMinimumProtocolVersion`. Informational only — read offchain 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 + /// 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 incidentResponder; + + /// @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) { + _disableInitializers(); + } + + /// @notice Initializes the registry by seeding the hash chain and appointing the initial + /// 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(); + + // 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(bytes32(0)); + + incidentResponder = _incidentResponder; + emit IncidentResponderUpdated(address(0), _incidentResponder); + } + + /// @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(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. + _upgradeScheduleId.push(); + emit UpgradeRegistered(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; + } + + /// @notice Sets the minimum protocol version clients must run. Owner (Security Council) only. + /// @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). + function setMinimumProtocolVersion(uint256 protocolVersion) external { + _assertOnlyProxyAdminOwner(); + if (protocolVersion == 0) revert ProtocolVersions_InvalidProtocolVersion(); + _writeMinimumProtocolVersion(protocolVersion); + } + + /// @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 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 { + _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); + } + _writeTimestamp(id, timestamp); + } + + /// @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 IncidentResponderUpdated(incidentResponder, newIncidentResponder); + incidentResponder = newIncidentResponder; + } + + /// @notice Pushes an already-scheduled upgrade's activation timestamp further into the future. + /// 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 + /// 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 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 != incidentResponder) revert ProtocolVersions_NotIncidentResponder(); + _assertRegistered(id); + uint64 current = _timestamps[id]; + + // The upgrade must already have a scheduled activation to delay. + if (current == 0) revert ProtocolVersions_NotScheduled(id); + // 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_InsufficientNotice(newTimestamp); + _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) private { + _timestamps[id] = newTs; + emit TimestampSet(id, newTs); + _refreshScheduleId(id); + } + + /// @dev Writes the minimum protocol version and emits the update. Validation is the caller's + /// responsibility. + function _writeMinimumProtocolVersion(uint256 protocolVersion) private { + 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), + /// so the resulting tail is always a new scheduleId. + function _refreshScheduleId(uint256 startIndex) private { + uint256 n = _timestamps.length; + + // _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 + 1] = prev; + } + + emit ScheduleIdUpdated(prev); + } + + /// @dev Reverts if `id` is not a registered upgrade. + 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 new file mode 100644 index 000000000..0b1f184a6 --- /dev/null +++ b/test/L1/ProtocolVersions.t.sol @@ -0,0 +1,598 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing +import { CommonTest } from "test/setup/CommonTest.sol"; +import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; + +// Contracts +import { ProxyAdminOwnedBase } from "src/universal/ProxyAdminOwnedBase.sol"; +import { Proxy } from "src/universal/Proxy.sol"; + +// Interfaces +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; +import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; + +/// @title ProtocolVersions_TestInit +/// @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); + 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; + + address internal _owner; + address internal _nonOwner = makeAddr("non-owner"); + address internal _incidentResponder = makeAddr("incident-responder"); + + 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. + function _scheduleCanyon(uint64 _delay) internal returns (uint64 ts_) { + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + ts_ = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + _delay; + vm.prank(_owner); + protocolVersions.setTimestamp(CANYON, ts_); + } +} + +/// @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. 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 { + // 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 { + IProtocolVersions uninitialized = _deployUninitializedProxy(); + vm.expectEmit(true, true, false, false, address(uninitialized)); + emit IncidentResponderUpdated(address(0), _incidentResponder); + 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 { + IProtocolVersions uninitialized = _deployUninitializedProxy(); + vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner.selector); + vm.prank(_nonOwner); + uninitialized.initialize(_incidentResponder); + } + + /// @notice Tests that the contract cannot be initialized twice. + function test_initialize_alreadyInitialized_reverts() external { + vm.expectRevert("Initializable: contract is already initialized"); + vm.prank(EIP1967Helper.getAdmin(address(protocolVersions))); + protocolVersions.initialize(address(0)); + } + + /// @notice Tests that the implementation itself cannot be initialized (initializers disabled). + function test_initialize_implementationDisabled_reverts() external { + IProtocolVersions impl = IProtocolVersions(EIP1967Helper.getImplementation(address(protocolVersions))); + vm.expectRevert("Initializable: contract is already initialized"); + impl.initialize(address(0)); + } + + /// @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)); + } +} + +/// @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. + function test_registerUpgrade_changesScheduleId_succeeds() external { + bytes32 idBefore = protocolVersions.scheduleId(); + + vm.roll(block.number + 1); + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + + 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(0, 0), 0); + vm.prank(_owner); + assertEq(protocolVersions.registerUpgrade(0, 0), 1); + vm.prank(_owner); + assertEq(protocolVersions.registerUpgrade(0, 0), 2); + } + + /// @notice Tests that `registerUpgrade` emits the `UpgradeRegistered` event with the assigned id. + function test_registerUpgrade_emitsEvent_succeeds() external { + vm.expectEmit(true, false, false, false, address(protocolVersions)); + emit UpgradeRegistered(0); + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + + vm.expectEmit(true, false, false, false, address(protocolVersions)); + emit UpgradeRegistered(1); + vm.prank(_owner); + 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(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); + } +} + +/// @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.setMinimumProtocolVersion(42); + assertEq(protocolVersions.minimumProtocolVersion(), 42); + } + + /// @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.setMinimumProtocolVersion(42); + assertEq(protocolVersions.scheduleId(), scheduleIdBefore); + } + + /// @notice Tests that `setMinimumProtocolVersion` emits the `MinimumProtocolVersionUpdated` event. + function test_setMinimumProtocolVersion_emitsEvent_succeeds() external { + vm.expectEmit(true, false, false, true, address(protocolVersions)); + emit MinimumProtocolVersionUpdated(42); + vm.prank(_owner); + protocolVersions.setMinimumProtocolVersion(42); + } + + /// @notice Tests that setting a zero protocol version reverts. + function test_setMinimumProtocolVersion_zero_reverts() external { + vm.expectRevert(IProtocolVersions.ProtocolVersions_InvalidProtocolVersion.selector); + vm.prank(_owner); + protocolVersions.setMinimumProtocolVersion(0); + } + + /// @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.setMinimumProtocolVersion(42); + } +} + +/// @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(0, 0); + + 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); + + 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(0, 0); + + 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); + + bytes32 scheduleIdAfterSet = protocolVersions.scheduleId(); + + vm.roll(block.number + 1); + vm.prank(_owner); + protocolVersions.setTimestamp(CANYON, ts); + + assertEq(protocolVersions.getSchedule()[CANYON], ts); + assertEq(protocolVersions.scheduleId(), scheduleIdAfterSet); + } + + /// @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(0, 0); + 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); + + bytes32 scheduleIdAfterSet = protocolVersions.scheduleId(); + assertNotEq(scheduleIdAfterSet, scheduleIdAfterRegister); + + vm.roll(block.number + 1); + vm.prank(_owner); + protocolVersions.setTimestamp(CANYON, 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(0, 0); + + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + vm.expectEmit(true, false, false, true, address(protocolVersions)); + emit TimestampSet(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(0, 0); + + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + vm.expectRevert(ProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); + vm.prank(_nonOwner); + 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(0, 0); + + vm.warp(1000); + vm.expectRevert( + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_InsufficientNotice.selector, uint64(500)) + ); + vm.prank(_owner); + 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(0, 0); + + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() - 1; + vm.expectRevert(abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_InsufficientNotice.selector, ts)); + vm.prank(_owner); + 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(0, 0); + + 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; + vm.expectRevert( + abi.encodeWithSelector( + IProtocolVersions.ProtocolVersions_ActivationAlreadyPassed.selector, CANYON, activationTs + ) + ); + vm.prank(_owner); + 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_UnknownUpgrade.selector, uint256(0))); + vm.prank(_owner); + protocolVersions.setTimestamp(0, ts); + } + + /// @notice Tests that scheduleId is reproducible from (ascending ids, timestamps). + function test_setTimestamp_scheduleIdReproducible_succeeds() external { + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + + 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); + + // 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)); + + assertEq(protocolVersions.scheduleId(), link1); + } +} + +/// @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.setIncidentResponder(_incidentResponder); + + bytes32 scheduleIdBefore = protocolVersions.scheduleId(); + vm.roll(block.number + 1); + + uint64 later = ts + 50; + vm.prank(_incidentResponder); + protocolVersions.delayTimestamp(CANYON, later); + + assertEq(protocolVersions.getSchedule()[CANYON], later); + assertNotEq(protocolVersions.scheduleId(), scheduleIdBefore); + } + + /// @notice Tests that only the incidentResponder can call `delayTimestamp`. + function test_delayTimestamp_callerNotIncidentResponder_reverts() external { + uint64 ts = _scheduleCanyon(100); + vm.prank(_owner); + protocolVersions.setIncidentResponder(_incidentResponder); + + vm.expectRevert(IProtocolVersions.ProtocolVersions_NotIncidentResponder.selector); + vm.prank(_owner); + protocolVersions.delayTimestamp(CANYON, ts + 50); + + vm.expectRevert(IProtocolVersions.ProtocolVersions_NotIncidentResponder.selector); + vm.prank(_nonOwner); + protocolVersions.delayTimestamp(CANYON, ts + 50); + } + + /// @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.setIncidentResponder(_incidentResponder); + + vm.expectRevert( + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_DelayMustBeLater.selector, ts, ts - 10) + ); + vm.prank(_incidentResponder); + protocolVersions.delayTimestamp(CANYON, ts - 10); + } + + /// @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.setIncidentResponder(_incidentResponder); + + vm.expectRevert(abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_DelayMustBeLater.selector, ts, ts)); + vm.prank(_incidentResponder); + 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(0, 0); + vm.prank(_owner); + protocolVersions.setIncidentResponder(_incidentResponder); + + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + vm.expectRevert(abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_NotScheduled.selector, CANYON)); + vm.prank(_incidentResponder); + protocolVersions.delayTimestamp(CANYON, ts); + } + + /// @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.setIncidentResponder(_incidentResponder); + + vm.warp(ts + 1); + vm.expectRevert( + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_ActivationAlreadyPassed.selector, CANYON, ts) + ); + 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.setIncidentResponder(_incidentResponder); + + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + vm.expectRevert(abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_UnknownUpgrade.selector, uint256(0))); + vm.prank(_incidentResponder); + protocolVersions.delayTimestamp(0, ts); + } +} + +/// @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 incidentResponder address. + function test_setIncidentResponder_setsAddress_succeeds() external { + vm.prank(_owner); + protocolVersions.setIncidentResponder(_incidentResponder); + assertEq(protocolVersions.incidentResponder(), _incidentResponder); + } + + /// @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.setIncidentResponder(_incidentResponder); + } + + /// @notice Tests that `setIncidentResponder` emits a `IncidentResponderUpdated` event. + function test_setIncidentResponder_emitsEvent_succeeds() external { + vm.expectEmit(true, true, false, false, address(protocolVersions)); + emit IncidentResponderUpdated(address(0), _incidentResponder); + vm.prank(_owner); + protocolVersions.setIncidentResponder(_incidentResponder); + } + + /// @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.setIncidentResponder(_incidentResponder); + + vm.expectEmit(true, true, false, false, address(protocolVersions)); + emit IncidentResponderUpdated(_incidentResponder, address(0)); + vm.prank(_owner); + protocolVersions.setIncidentResponder(address(0)); + + assertEq(protocolVersions.incidentResponder(), address(0)); + } +} + +/// @title ProtocolVersions_Uncategorized_Test +/// @notice Test contract for view functions and the upgrade registry. +contract ProtocolVersions_Uncategorized_Test is ProtocolVersions_TestInit { + /// @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(0, 0); + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + + uint64 ts = uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100; + vm.prank(_owner); + protocolVersions.setTimestamp(CANYON, ts); + + uint64[] memory s = protocolVersions.getSchedule(); + + assertEq(s.length, 2); + assertEq(s[CANYON], ts); + assertEq(s[ECOTONE], 0); + } + + /// @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(0, 0); + vm.prank(_owner); + protocolVersions.registerUpgrade(0, 0); + + assertNotEq(protocolVersions.scheduleId(CANYON), protocolVersions.scheduleId(ECOTONE)); + 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..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"; @@ -119,6 +121,12 @@ 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.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"); @@ -134,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 }) ); @@ -152,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); @@ -203,7 +248,8 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { opChainProxyAdminOwner: owner, systemConfigOwner: owner, batcher: batcher, - unsafeBlockSigner: unsafeBlockSigner + unsafeBlockSigner: unsafeBlockSigner, + incidentResponder: incidentResponder }), basefeeScalar: 100, blobBasefeeScalar: 200, @@ -271,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 }) ); } 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 9bdd9df94..b1c762141 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, (address(0))); + 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))); @@ -264,6 +283,10 @@ contract Initializer_Test is CommonTest { 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";