Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions snapshots/semver-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
16 changes: 10 additions & 6 deletions src/L1/ProtocolVersions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand Down
35 changes: 35 additions & 0 deletions test/L1/ProtocolVersions.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
25 changes: 14 additions & 11 deletions test/L1/proofs/AggregateVerifier.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
Expand All @@ -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);
Expand Down
12 changes: 8 additions & 4 deletions test/L1/proofs/BaseTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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())));
Expand Down Expand Up @@ -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);
}

Expand Down
Loading