diff --git a/snapshots/semver-lock.json b/snapshots/semver-lock.json index b3c09b855..8d1aa7b2c 100644 --- a/snapshots/semver-lock.json +++ b/snapshots/semver-lock.json @@ -16,8 +16,8 @@ "sourceCodeHash": "0x811596e7486cab9ceeeb61405b9ae510d93fcbbdfa11949201a367506c53194a" }, "src/L1/ProtocolVersions.sol:ProtocolVersions": { - "initCodeHash": "0x909a923012cab53fdfc4ea8f9f54173d64d2e3c250e8d69cb10ea226e21a4adf", - "sourceCodeHash": "0x2b338e2c3cb163445841a1bfca2370e731b4cba4be35018cb07b2d22a3465f67" + "initCodeHash": "0x07398627bcea4571951b4f6fe9d0b28505cb5d896f4eb13cde870a52dfffff3e", + "sourceCodeHash": "0xac3cde345f37efbf3c3dcc481d2a5c64dfbc971f2bb1492272acdb61892684eb" }, "src/L1/SuperchainConfig.sol:SuperchainConfig": { "initCodeHash": "0x9b1f3555b499709485d51d5d9665002c0eb1e5eb893be1fb978a30749e894858", diff --git a/src/L1/ProtocolVersions.sol b/src/L1/ProtocolVersions.sol index 93988f282..868721328 100644 --- a/src/L1/ProtocolVersions.sol +++ b/src/L1/ProtocolVersions.sol @@ -125,15 +125,16 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable /// @notice Initializes the registry by seeding the hash chain, importing any preexisting upgrade /// schedule, and appointing the initial incidentResponder. Callable only by the /// ProxyAdmin or its owner. - /// @dev `_initialSchedule` is the only path that can enter an activation which is not at least - /// MIN_NOTICE in the future. It exists so a chain that already has a hardfork history can be - /// represented faithfully at deployment, while it is still impossible for any proof game to - /// have pinned a commitment from this registry. Every later write goes through - /// `registerUpgrade`, `setTimestamp`, or `delayTimestamp`, which together guarantee that an - /// activation is never created or moved once L1 is within FREEZE_WINDOW of it. + /// + /// @dev `_initialSchedule` may import historical activations without MIN_NOTICE so a chain that + /// already has a hardfork history can be represented faithfully at deployment, while it is + /// still impossible for any proof game to have pinned a commitment from this registry. + /// Future activations must provide MIN_NOTICE, matching every post-initialization write path. + /// /// @param _incidentResponder Initial incidentResponder allowed to delay activations, or address(0) to leave unset. /// @param _initialSchedule Activation timestamps for already-known upgrades, ordered by ascending /// upgrade id, using 0 for an upgrade that is registered but unscheduled. + /// Future timestamps must be at least MIN_NOTICE from block.timestamp. /// Pass an empty array for a chain with no upgrade history. function initialize( address _incidentResponder, @@ -152,6 +153,9 @@ contract ProtocolVersions is ProxyAdminOwnedBase, Initializable, Reinitializable for (uint256 id = 0; id < _initialSchedule.length; id++) { uint64 timestamp = _initialSchedule[id]; + if (timestamp > uint64(block.timestamp) && timestamp < uint64(block.timestamp) + MIN_NOTICE) { + revert ProtocolVersions_InsufficientNotice(timestamp); + } // `id` is the index about to be appended, so this reads only the entries already imported. _assertTimestampAfterPrevious(id, timestamp); _timestamps.push(timestamp); diff --git a/test/L1/ProtocolVersions.t.sol b/test/L1/ProtocolVersions.t.sol index 290f19b65..db2eb4e87 100644 --- a/test/L1/ProtocolVersions.t.sol +++ b/test/L1/ProtocolVersions.t.sol @@ -96,6 +96,7 @@ contract ProtocolVersions_Initialize_Test is ProtocolVersions_TestInit { /// @notice Tests that the initializer imports a preexisting schedule, building the same hash /// chain the equivalent sequence of registrations would have. function test_initialize_importsSchedule_succeeds() external { + vm.warp(31); uint64[] memory schedule = new uint64[](3); schedule[0] = 10; schedule[1] = 0; @@ -118,8 +119,36 @@ contract ProtocolVersions_Initialize_Test is ProtocolVersions_TestInit { assertEq(imported.scheduleId(), link2); } + /// @notice Tests that initialization rejects a future activation already inside its freeze window. + function test_initialize_importFutureActivationInsufficientNotice_reverts() external { + uint64 activation = uint64(block.timestamp) + protocolVersions.FREEZE_WINDOW(); + uint64[] memory schedule = new uint64[](1); + schedule[0] = activation; + + IProtocolVersions imported = _deployUninitializedProxy(); + vm.expectRevert( + abi.encodeWithSelector(IProtocolVersions.ProtocolVersions_InsufficientNotice.selector, activation) + ); + vm.prank(EIP1967Helper.getAdmin(address(imported))); + imported.initialize(_incidentResponder, schedule); + } + + /// @notice Tests that the initializer uses the same inclusive MIN_NOTICE floor as later write paths. + function test_initialize_importFutureActivationWithMinimumNotice_succeeds() external { + uint64 activation = uint64(block.timestamp) + protocolVersions.MIN_NOTICE(); + uint64[] memory schedule = new uint64[](1); + schedule[0] = activation; + + IProtocolVersions imported = _deployUninitializedProxy(); + vm.prank(EIP1967Helper.getAdmin(address(imported))); + imported.initialize(_incidentResponder, schedule); + + assertEq(imported.getSchedule()[0], activation); + } + /// @notice Tests that an imported schedule is held to the same ordering rule as registration. function test_initialize_importUnorderedSchedule_reverts() external { + vm.warp(31); uint64[] memory schedule = new uint64[](2); schedule[0] = 30; schedule[1] = 10; @@ -1088,6 +1117,12 @@ contract ProtocolVersions_ActivatedScheduleId_Test is ProtocolVersions_TestInit /// @dev These cases pin `activatedScheduleId` against activations that are already in the past, /// which only the initializer's import can produce. function _importSchedule(uint64[] memory schedule) private returns (IProtocolVersions) { + uint64 latest; + for (uint256 i = 0; i < schedule.length; i++) { + if (schedule[i] > latest) latest = schedule[i]; + } + vm.warp(uint256(latest) + 1); + IProtocolVersions imported = _deployUninitializedProxy(); vm.prank(EIP1967Helper.getAdmin(address(imported))); imported.initialize(address(0), schedule); diff --git a/test/L1/proofs/AggregateVerifier.t.sol b/test/L1/proofs/AggregateVerifier.t.sol index e7403a995..4b714779f 100644 --- a/test/L1/proofs/AggregateVerifier.t.sol +++ b/test/L1/proofs/AggregateVerifier.t.sol @@ -40,20 +40,22 @@ contract AggregateVerifierTest is BaseTest { /// @notice Initialization pins the upgrades active at the claimed L2 block, independently of /// the L1 game-creation timestamp, and later schedule changes cannot alter the pin. function test_initialize_pinsScheduleId_succeeds() public { - // The first game ends at L2 block 100, whose deterministic timestamp is 200. The first - // upgrade is active there; the second is not. + uint64 firstGameTimestamp = _l2Timestamp(BLOCK_INTERVAL); + uint64 secondActivationTimestamp = _l2Timestamp(BLOCK_INTERVAL + BLOCK_INTERVAL / 2); + + // The first upgrade is active at the first game's L2 timestamp; the second is not. uint64[] memory schedule = new uint64[](2); - schedule[0] = 200; - schedule[1] = 300; + schedule[0] = firstGameTimestamp; + schedule[1] = secondActivationTimestamp; _importProtocolVersionsSchedule(schedule); - bytes32 pinned = protocolVersions.activatedScheduleId(200); + bytes32 pinned = protocolVersions.activatedScheduleId(firstGameTimestamp); assertTrue(pinned != bytes32(0)); assertNotEq(protocolVersions.scheduleId(), pinned); // Even though the L1 clock has passed the second activation, the game's schedule is // selected by its claimed L2 block timestamp. - vm.warp(300); + vm.warp(secondActivationTimestamp); Claim rootClaim = _advanceL2BlockAndClaim(); bytes memory proof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE); @@ -75,8 +77,9 @@ contract AggregateVerifierTest is BaseTest { /// @notice Consecutive games on opposite sides of an L2 activation boundary pin different /// schedule commitments. function test_initialize_scheduleChangesAtL2ActivationBoundary_succeeds() public { + uint64 activationTimestamp = _l2Timestamp(BLOCK_INTERVAL + BLOCK_INTERVAL / 2); uint64[] memory schedule = new uint64[](1); - schedule[0] = 300; + schedule[0] = activationTimestamp; _importProtocolVersionsSchedule(schedule); Claim firstClaim = _advanceL2BlockAndClaim(); @@ -97,15 +100,15 @@ contract AggregateVerifierTest is BaseTest { address(firstGame), _generateProof("second", AggregateVerifier.ProofType.TEE) ); - assertEq(secondGame.scheduleId(), protocolVersions.activatedScheduleId(400)); + assertEq(secondGame.scheduleId(), protocolVersions.activatedScheduleId(_l2Timestamp(currentL2BlockNumber))); } /// @notice A claim whose L2 timestamp L1 has not yet reached cannot open a game, so a game can /// never pin an activation that the owner is still able to clear or delay. function test_initialize_l2TimestampInFuture_reverts() public { - // The first game ends at L2 block 100, whose deterministic timestamp is 200. Scheduling the - // upgrade there and leaving the L1 clock short of it is the window the finding exploits. - uint64 activationTimestamp = uint64(BLOCK_INTERVAL * L2_BLOCK_TIME); + // Scheduling the upgrade at the first game's deterministic L2 timestamp and leaving the L1 + // clock short of it is the window the finding exploits. + uint64 activationTimestamp = _l2Timestamp(BLOCK_INTERVAL); uint64[] memory schedule = new uint64[](1); schedule[0] = activationTimestamp; _importProtocolVersionsSchedule(schedule); diff --git a/test/L1/proofs/BaseTest.t.sol b/test/L1/proofs/BaseTest.t.sol index a45a75f25..d41add534 100644 --- a/test/L1/proofs/BaseTest.t.sol +++ b/test/L1/proofs/BaseTest.t.sol @@ -27,7 +27,7 @@ import { MockVerifier } from "test/mocks/MockVerifier.sol"; contract BaseTest is Test { uint256 internal constant L2_CHAIN_ID = 8453; uint256 internal constant L2_GENESIS_BLOCK_NUMBER = 0; - uint64 internal constant L2_GENESIS_TIMESTAMP = 0; + uint64 internal constant L2_GENESIS_TIMESTAMP = 1 days; uint64 internal constant L2_BLOCK_TIME = 2; // AggregateVerifier expects evenly spaced intermediate roots. @@ -116,8 +116,8 @@ contract BaseTest is Test { } /// @dev Rebuilds the schedule registry around a preset schedule and rebinds the verifier to it. - /// These tests run on a compressed L2 timescale whose activations can never clear - /// MIN_NOTICE, so a schedule has to be imported at initialization rather than registered. + /// Future test activations derive from L2_GENESIS_TIMESTAMP, which is far enough ahead of + /// the initial L1 clock to clear MIN_NOTICE. /// Must be called before any game is created, since games pin the registry they see. function _importProtocolVersionsSchedule(uint64[] memory schedule) internal { protocolVersions = ProtocolVersions(_deployProxy(address(new ProtocolVersions()))); @@ -175,10 +175,14 @@ contract BaseTest is Test { ); } + function _l2Timestamp(uint256 l2BlockNumber) internal pure returns (uint64) { + return L2_GENESIS_TIMESTAMP + uint64((l2BlockNumber - L2_GENESIS_BLOCK_NUMBER) * L2_BLOCK_TIME); + } + /// @dev A game is only creatable once L1 has reached the claimed L2 block's deterministic /// timestamp, which mirrors production: a block is proven well after it is produced. function _warpToL2Timestamp(uint256 l2BlockNumber) internal { - uint256 claimTimestamp = L2_GENESIS_TIMESTAMP + (l2BlockNumber - L2_GENESIS_BLOCK_NUMBER) * L2_BLOCK_TIME; + uint64 claimTimestamp = _l2Timestamp(l2BlockNumber); if (block.timestamp < claimTimestamp) vm.warp(claimTimestamp); }