-
Notifications
You must be signed in to change notification settings - Fork 174
feat(CHAIN-3335): add FlashblockIndex contract #198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
xenoliss
wants to merge
5
commits into
main
Choose a base branch
from
bo/chain-3335
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+228
−0
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
c1b7178
feat: add FlashblockIndex contract
xenoliss 005d64c
refactor: make FlashblockIndex upgradeable and emit FlashblockIndexUp…
xenoliss 3d64a2c
chore: update semver-lock snapshot
xenoliss 3bb0a60
refactor: remove Initializable from FlashblockIndex
xenoliss 7ef587b
refactor: reorder declarations per style guide
xenoliss File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"; | ||
|
|
||
| /// @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); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.