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/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 227d123d6..e8e1a19cf 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 { @@ -127,6 +129,7 @@ contract SystemDeploy is Script { uint256 l2ChainId; uint256 multiproofBlockInterval; uint256 multiproofIntermediateBlockInterval; + IProtocolVersions protocolVersions; } struct MultiproofOutput { @@ -218,6 +221,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 +283,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 +341,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 +457,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 +498,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 +632,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 +657,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 +688,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 +977,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, @@ -1009,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 }) ); @@ -1038,7 +1079,8 @@ contract SystemDeploy is Script { _input.multiproofConfigHash, _input.l2ChainId, _input.multiproofBlockInterval, - _input.multiproofIntermediateBlockInterval + _input.multiproofIntermediateBlockInterval, + _input.protocolVersions ) ) ); @@ -1100,6 +1142,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 +1168,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 +1195,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/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/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..98b556fdb 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" @@ -28,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/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/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/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/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); 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";