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
16 changes: 16 additions & 0 deletions snapshots/abi/AggregateVerifier.json
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,22 @@
"name": "L2BlockBeforeGenesis",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint64",
"name": "claimTimestamp",
"type": "uint64"
},
{
"internalType": "uint256",
"name": "l1Timestamp",
"type": "uint256"
}
],
"name": "L2TimestampInFuture",
"type": "error"
},
{
"inputs": [
{
Expand Down
4 changes: 2 additions & 2 deletions snapshots/semver-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
"sourceCodeHash": "0x780ff372493ba9010bc0d13100ac896f2bf75730a9b17d4bb63aaf694dc3c634"
},
"src/L1/proofs/AggregateVerifier.sol:AggregateVerifier": {
"initCodeHash": "0x83b451d1b77752c41bef7a038cf9ce9a7c7d3b59ef601acd0239a15a8ce32279",
"sourceCodeHash": "0x0dc7ebcb331c4ea3ac23cace04e69995589828a5ab9087376fbf1241db7a62ea"
"initCodeHash": "0x84c6bf00e337c889993c409f18b17986f9d9546ac6754f7730962459d301c21d",
"sourceCodeHash": "0xab669e0a55462740b4c1c06fe94aa709044146e3eeec4a0123fbc67dd7f35c28"
},
"src/L1/proofs/AnchorStateRegistry.sol:AnchorStateRegistry": {
"initCodeHash": "0x6f3afd2d0ef97a82ca3111976322b99343a270e54cd4a405028f2f29c75f7fb1",
Expand Down
9 changes: 9 additions & 0 deletions src/L1/proofs/AggregateVerifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,9 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver {
/// @notice Thrown when an L2 block timestamp cannot be represented as a uint64.
error L2TimestampOverflow(uint256 blockNumber);

/// @notice Thrown when the claimed L2 block's timestamp has not yet been reached on L1.
error L2TimestampInFuture(uint64 claimTimestamp, uint256 l1Timestamp);

/// @notice Thrown when there are not enough proofs to resolve the game.
error NotEnoughProofs();

Expand Down Expand Up @@ -423,6 +426,12 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver {
if (blocksSinceGenesis > maxBlocks) revert L2TimestampOverflow(claimBlock);

uint64 claimTimestamp = L2_GENESIS_TIMESTAMP + uint64(blocksSinceGenesis * uint256(L2_BLOCK_TIME));

// `ProtocolVersions` only freezes an activation once L1 time reaches it, so a claim whose L2
// timestamp is still in L1's future would pin a schedule the owner can afterwards clear or
// delay. Requiring the claim to have already passed on L1 keeps the pin canonical for life.
if (claimTimestamp > block.timestamp) revert L2TimestampInFuture(claimTimestamp, block.timestamp);

scheduleId = PROTOCOL_VERSIONS.activatedScheduleId(claimTimestamp);

// Set the game's starting timestamp.
Expand Down
9 changes: 7 additions & 2 deletions test/L1/OptimismPortal2.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,13 @@ abstract contract OptimismPortal2_TestInit is DisputeGameFactory_TestInit {

depositor = makeAddr("depositor");

// Warp forward in time to ensure that the game is created after the retirement timestamp.
vm.warp(anchorStateRegistry.retirementTimestamp() + 1);
// Warp forward in time to ensure that the game is created after the retirement timestamp,
// and after the proposed block's deterministic L2 timestamp, which L1 must have reached
// before a game claiming that block can be created.
uint256 retirementFloor = anchorStateRegistry.retirementTimestamp() + 1;
uint256 proposedBlockTimestamp = gameImpl.L2_GENESIS_TIMESTAMP()
+ (_proposedBlockNumber - gameImpl.L2_GENESIS_BLOCK_NUMBER()) * gameImpl.L2_BLOCK_TIME();
vm.warp(retirementFloor > proposedBlockTimestamp ? retirementFloor : proposedBlockTimestamp);

game = _createDisputeGame(Claim.wrap(_outputRoot), 0);

Expand Down
39 changes: 39 additions & 0 deletions test/L1/proofs/AggregateVerifier.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,43 @@ contract AggregateVerifierTest is BaseTest {
assertEq(secondGame.scheduleId(), protocolVersions.activatedScheduleId(400));
}

/// @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);
protocolVersions.registerUpgrade(activationTimestamp, 1);
assertLt(block.timestamp, activationTimestamp);

// Created straight through the factory, since the shared helper advances L1 to the claim.
Claim rootClaim = _advanceL2BlockAndClaim();
bytes memory proof = _generateProof("future-l2", AggregateVerifier.ProofType.TEE);
bytes memory extraData =
_aggregateVerifierExtraData(rootClaim, currentL2BlockNumber, address(anchorStateRegistry));

vm.deal(TEE_PROVER, INIT_BOND);
vm.prank(TEE_PROVER);
vm.expectRevert(
abi.encodeWithSelector(AggregateVerifier.L2TimestampInFuture.selector, activationTimestamp, block.timestamp)
);
factory.createWithInitData{ value: INIT_BOND }(GameTypes.AGGREGATE_VERIFIER, rootClaim, extraData, proof);

// Once L1 reaches the activation, the schedule is frozen and the game becomes creatable.
vm.warp(activationTimestamp);
vm.expectRevert(
abi.encodeWithSelector(
IProtocolVersions.ProtocolVersions_ActivationAlreadyPassed.selector, 0, activationTimestamp
)
);
protocolVersions.setTimestamp(0, 0);

AggregateVerifier game = _createAggregateVerifierGame(
TEE_PROVER, rootClaim, currentL2BlockNumber, address(anchorStateRegistry), proof
);
assertEq(game.scheduleId(), protocolVersions.activatedScheduleId(activationTimestamp));
}

function test_constructor_zeroL2BlockTime_reverts() public {
AggregateVerifier.ScheduleConfig memory scheduleConfig = AggregateVerifier.ScheduleConfig({
protocolVersions: IProtocolVersions(address(protocolVersions)),
Expand Down Expand Up @@ -507,6 +544,8 @@ contract AggregateVerifierTest is BaseTest {
bytes32 l1Head = blockhash(block.number - 1);
address clone = address(impl).clone(abi.encodePacked(creator, rootClaim, l1Head, extraData));

_warpToL2Timestamp(l2BlockNumber);

vm.deal(creator, INIT_BOND);
vm.prank(creator);
AggregateVerifier(payable(clone)).initializeWithInitData{ value: INIT_BOND }(proof);
Expand Down
9 changes: 9 additions & 0 deletions test/L1/proofs/BaseTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ contract BaseTest is Test {
{
bytes memory extraData = _aggregateVerifierExtraData(rootClaim, l2BlockNumber, parentAddress);

_warpToL2Timestamp(l2BlockNumber);

vm.deal(creator, INIT_BOND);
vm.prank(creator);
return AggregateVerifier(
Expand All @@ -163,6 +165,13 @@ contract BaseTest is Test {
);
}

/// @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;
if (block.timestamp < claimTimestamp) vm.warp(claimTimestamp);
}

function _provideProof(AggregateVerifier game, address prover, bytes memory proofBytes) internal {
vm.prank(prover);
game.verifyProposalProof(proofBytes);
Expand Down
4 changes: 4 additions & 0 deletions test/L1/proofs/DisputeGameFactory.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,10 @@ contract DisputeGameFactory_Create_Test is DisputeGameFactory_TestInit {
uint256 bondAmount = disputeGameFactory.initBonds(GameTypes.AGGREGATE_VERIFIER);
vm.deal(address(this), bondAmount);

// The claimed L2 block's deterministic timestamp (block number x blockTime) must have
// already passed on L1 for the game to be creatable.
vm.warp((startingRoot.l2SequenceNumber + AGGREGATE_BLOCK_INTERVAL) * 2);

uint256 gameCountBefore = disputeGameFactory.gameCount();
IDisputeGame proxy = disputeGameFactory.createWithInitData{ value: bondAmount }(
GameTypes.AGGREGATE_VERIFIER, rootClaim, extraData, proof
Expand Down
Loading