Skip to content
Open
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: 4 additions & 0 deletions snapshots/semver-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@
"initCodeHash": "0xdaae3903628f760e36da47c8f8d75d20962d1811fb5129cb09eb01803e67c095",
"sourceCodeHash": "0x95dd8da08e907fa398c98710bb12fda9fb50d9688c5d2144fd9a424c99e672c5"
},
"src/L2/FlashblockIndex.sol:FlashblockIndex": {
"initCodeHash": "0x7e09adc445d209875b09e70f0ea025ec45e071f7ca6c1239187f08edc1645b04",
"sourceCodeHash": "0x7e52ea8b5725107344b51b89a365de33d0c8d158a3c32b539298b707149d9787"
},
"src/L2/GasPriceOracle.sol:GasPriceOracle": {
"initCodeHash": "0xf72c23d9c3775afd7b645fde429d09800622d329116feb5ff9829634655123ca",
"sourceCodeHash": "0xb4d1bf3669ba87bbeaf4373145c7e1490478c4a05ba4838a524aa6f0ce7348a6"
Expand Down
58 changes: 58 additions & 0 deletions src/L2/FlashblockIndex.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

import { ISemver } from "interfaces/universal/ISemver.sol";

/// @custom:upgradeable
/// @title FlashblockIndex
/// @notice Stores the current flashblock index alongside block.number.
/// @dev The builder calls this via fallback with 1 byte of calldata (the index as uint8).
/// Both values are manually packed into a single uint256 to guarantee 1 SSTORE per write.
contract FlashblockIndex is ISemver {
/// @notice Semantic version.
/// @custom:semver 1.0.0
string public constant override version = "1.0.0";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think most other contracts define this as an external view function instead of a constant variable

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel strongly but most of the contracts in the repo do use the constant version.


/// @notice The authorized builder address, set at deploy time.
address public immutable BUILDER;

/// @notice Packed storage: blockNumber (uint48) in bits [55:8] | flashblockIndex (uint8) in bits [7:0].
/// @dev Using uint48 for block numbers is safe for the foreseeable future (~281 trillion blocks).
uint256 private _packed;

/// @notice Emitted when the flashblock index is updated.
/// @param flashblockIndex The new flashblock index.
/// @param blockNumber The block number at which the index was set.
event FlashblockIndexUpdated(uint8 indexed flashblockIndex, uint48 indexed blockNumber);

/// @notice Thrown when the caller is not the authorized builder.
error OnlyBuilder();

/// @notice Thrown when calldata is not exactly 1 byte.
error InvalidCalldata();

/// @notice Constructor.
/// @param builder The address authorized to update the flashblock index.
constructor(address builder) {
BUILDER = builder;
}

/// @notice Sets the flashblock index for the current block.
/// @dev Calldata must be exactly 1 byte representing the flashblock index (uint8).
/// Stores `(block.number << 8) | index` in a single SSTORE.
fallback() external {
if (msg.sender != BUILDER) revert OnlyBuilder();
if (msg.data.length != 1) revert InvalidCalldata();
_packed = (uint256(uint48(block.number)) << 8) | uint256(uint8(msg.data[0]));
emit FlashblockIndexUpdated(uint8(msg.data[0]), uint48(block.number));
}

/// @notice Returns the last stored flashblock index and its associated block number.
/// @return flashblockIndex The flashblock index.
/// @return blockNumber The block number at which the index was set.
function get() external view returns (uint8 flashblockIndex, uint48 blockNumber) {
uint256 packed = _packed;
flashblockIndex = uint8(packed);
blockNumber = uint48(packed >> 8);
}
}
166 changes: 166 additions & 0 deletions test/L2/FlashblockIndex.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

import { Test } from "forge-std/Test.sol";
import { FlashblockIndex } from "src/L2/FlashblockIndex.sol";

contract FlashblockIndexTest is Test {
event FlashblockIndexUpdated(uint8 indexed flashblockIndex, uint48 indexed blockNumber);

FlashblockIndex flashblockIndex;
address builder;

function setUp() public {
builder = makeAddr("builder");
flashblockIndex = new FlashblockIndex(builder);
}

/// @notice Tests that the constructor correctly sets the BUILDER immutable.
function test_constructor_setsBuilder() external view {
assertEq(flashblockIndex.BUILDER(), builder);
}

/// @notice Tests that version() returns "1.0.0".
function test_version_returnsCorrectValue() external view {
assertEq(flashblockIndex.version(), "1.0.0");
}

/// @notice Tests that get() returns (0, 0) when no index has ever been written.
function test_get_returnsZeros_whenNeverWritten() external view {
(uint8 index, uint48 blockNumber) = flashblockIndex.get();
assertEq(index, 0);
assertEq(blockNumber, 0);
}

/// @notice Tests that get() returns the correct index and block number after a write.
function test_get_returnsCorrectValues(uint8 index, uint48 blockNumber) external {
vm.roll(blockNumber);
(bool success,) = _callFallback({ caller: builder, index: index });
assertTrue(success);

(uint8 actualIndex, uint48 actualBlock) = flashblockIndex.get();
assertEq(actualIndex, index);
assertEq(actualBlock, blockNumber);
}

/// @notice Tests that the fallback reverts with OnlyBuilder when called by a non-builder address.
function test_fallback_reverts_whenCallerIsNotBuilder(address caller, uint8 index) external {
vm.assume(caller != builder);
(bool success, bytes memory returnData) = _callFallback({ caller: caller, index: index });
assertFalse(success);
assertEq(bytes4(returnData), FlashblockIndex.OnlyBuilder.selector);
}

/// @notice Tests that the fallback reverts with InvalidCalldata when called with zero bytes.
function test_fallback_reverts_whenCalldataIsEmpty() external {
vm.prank(builder);
(bool success, bytes memory returnData) = address(flashblockIndex).call("");
assertFalse(success);
assertEq(bytes4(returnData), FlashblockIndex.InvalidCalldata.selector);
}

/// @notice Tests that the fallback reverts with InvalidCalldata when called with more than 1 byte.
function test_fallback_reverts_whenCalldataIsTooLong(uint8 extra) external {
vm.prank(builder);
(bool success, bytes memory returnData) = address(flashblockIndex).call(abi.encodePacked(uint8(1), extra));
assertFalse(success);
assertEq(bytes4(returnData), FlashblockIndex.InvalidCalldata.selector);
}

/// @notice Tests that the fallback stores the index and block number correctly when called by the builder.
function test_fallback_setsIndex(uint8 index, uint48 blockNumber) external {
vm.roll(blockNumber);
(bool success,) = _callFallback({ caller: builder, index: index });
assertTrue(success);

(uint8 actualIndex, uint48 actualBlock) = flashblockIndex.get();
assertEq(actualIndex, index);
assertEq(actualBlock, blockNumber);
}

/// @notice Tests that the fallback emits FlashblockIndexUpdated with the correct parameters.
function test_fallback_emitsFlashblockIndexUpdated(uint8 index, uint48 blockNumber) external {
vm.roll(blockNumber);
vm.expectEmit(address(flashblockIndex));
emit FlashblockIndexUpdated(index, blockNumber);
(bool success,) = _callFallback({ caller: builder, index: index });
assertTrue(success);
}

/// @notice Tests that a second fallback call overwrites the previous value at a different block.
function test_fallback_overwritesPreviousValue() external {
uint48 firstBlock = 100;
uint8 firstIndex = 5;
vm.roll(firstBlock);
(bool s1,) = _callFallback({ caller: builder, index: firstIndex });
assertTrue(s1);

uint48 secondBlock = 200;
uint8 secondIndex = 10;
vm.roll(secondBlock);
(bool s2,) = _callFallback({ caller: builder, index: secondIndex });
assertTrue(s2);

(uint8 actualIndex, uint48 actualBlock) = flashblockIndex.get();
assertEq(actualIndex, secondIndex);
assertEq(actualBlock, secondBlock);
}

/// @notice Tests that a second fallback call overwrites the previous value within the same block.
function test_fallback_overwritesWithinSameBlock() external {
uint48 blockNumber = 100;
uint8 firstIndex = 5;
uint8 secondIndex = 10;

vm.roll(blockNumber);
(bool s1,) = _callFallback({ caller: builder, index: firstIndex });
assertTrue(s1);

(bool s2,) = _callFallback({ caller: builder, index: secondIndex });
assertTrue(s2);

(uint8 actualIndex, uint48 actualBlock) = flashblockIndex.get();
assertEq(actualIndex, secondIndex);
assertEq(actualBlock, blockNumber);
}

/// @notice Tests that the fallback correctly stores the maximum uint48 block number.
function test_fallback_storesMaxBlockNumber() external {
uint48 maxBlock = type(uint48).max;
uint8 index = 1;

vm.roll(maxBlock);
(bool success,) = _callFallback({ caller: builder, index: index });
assertTrue(success);

(uint8 actualIndex, uint48 actualBlock) = flashblockIndex.get();
assertEq(actualIndex, index);
assertEq(actualBlock, maxBlock);
}

/// @notice Tests that the block number is truncated to uint48 when it exceeds the max value.
function test_fallback_truncatesBlockNumber() external {
uint256 overflowBlock = uint256(type(uint48).max) + 1;
uint8 index = 1;

vm.roll(overflowBlock);
(bool success,) = _callFallback({ caller: builder, index: index });
assertTrue(success);

(uint8 actualIndex, uint48 actualBlock) = flashblockIndex.get();
assertEq(actualIndex, index);
assertEq(actualBlock, 0);
}

// --- Helper ---

/// @notice Helper function to call the fallback with the given caller and index.
/// @param caller The address of the caller.
/// @param index The index to call the fallback with.
/// @return success True if the fallback call succeeded.
/// @return returnData The return data from the fallback call.
function _callFallback(address caller, uint8 index) private returns (bool success, bytes memory returnData) {
vm.prank(caller);
(success, returnData) = address(flashblockIndex).call(abi.encodePacked(index));
}
}
Loading