Skip to content
5 changes: 5 additions & 0 deletions .changeset/wet-dodos-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`Blockhash`: Add a library that provides access to historical block hashes using EIP-2935's history storage, extending the standard 256-block limit to 8191 blocks.
53 changes: 53 additions & 0 deletions contracts/utils/Blockhash.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/**
* @dev Library for accessing historical block hashes beyond the standard 256 block limit.
* Uses EIP-2935's history storage contract which maintains a ring buffer of the last
* 8191 block hashes in state.
*
* For blocks within the last 256 blocks, it uses the native `BLOCKHASH` opcode.
* For blocks between 257 and 8191 blocks ago, it queries the EIP-2935 history storage.
* For blocks older than 8191 or future blocks, it returns zero, matching the `BLOCKHASH` behavior.
*
* NOTE: After EIP-2935 activation, it takes 8191 blocks to completely fill the history.
* Before that, only block hashes since the fork block will be available.
*/
library Blockhash {
address internal constant HISTORY_STORAGE_ADDRESS = 0x0000F90827F1C53a10cb7A02335B175320002935;

/**
* @dev Retrieves the block hash for any historical block within the supported range.
*
* NOTE: The function gracefully handles future blocks and blocks beyond the history window
* by returning zero, consistent with the EVM's native `BLOCKHASH` behavior.
*/
function blockHash(uint256 blockNumber) internal view returns (bytes32) {
uint256 current = block.number;
uint256 distance;

unchecked {
// Can only wrap around to `current + 1` given `block.number - (2**256 - 1) = block.number + 1`
distance = current - blockNumber;
}

return distance > 256 && distance <= 8191 ? _historyStorageCall(blockNumber) : blockhash(blockNumber);
}

/// @dev Internal function to query the EIP-2935 history storage contract.
function _historyStorageCall(uint256 blockNumber) private view returns (bytes32 hash) {
assembly ("memory-safe") {
// Use scratch space to allocate blockNumber
mstore(0, blockNumber) // Store the blockNumber in scratch space

// In case the history storage address is not deployed, the call will succeed
// without returndata, so the hash will be 0 just as querying `blockhash` directly.
let success := staticcall(gas(), HISTORY_STORAGE_ADDRESS, 0, 0x20, 0, 0x20)

// In case of failure, the returndata might include the revert reason or custom error
if success {
hash := mload(0)
}
}
}
}
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[profile.default]
solc_version = '0.8.24'
evm_version = 'cancun'
evm_version = 'prague'
optimizer = true
optimizer-runs = 200
src = 'contracts'
Expand Down
84 changes: 84 additions & 0 deletions test/utils/Blockhash.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Test} from "forge-std/Test.sol";
import {Blockhash} from "../../contracts/utils/Blockhash.sol";

contract BlockhashTest is Test {
uint256 internal startingBlock;

address internal constant SYSTEM_ADDRESS = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE;

// See https://eips.ethereum.org/EIPS/eip-2935#bytecode
// Generated using https://www.evm.codes/playground
bytes private HISTORY_STORAGE_BYTECODE =

Check failure on line 14 in test/utils/Blockhash.t.sol

View workflow job for this annotation

GitHub Actions / lint

Variable name must be in mixedCase
hex"3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500";

function setUp() public {
startingBlock = block.number;
vm.etch(Blockhash.HISTORY_STORAGE_ADDRESS, HISTORY_STORAGE_BYTECODE);
}

function testFuzzRecentBlocks(uint256 offset, uint256 currentBlock, bytes32 expectedHash) public {
// Recent blocks (1-256 blocks old)
offset = bound(offset, 1, 256);
vm.assume(currentBlock > offset);
vm.roll(currentBlock);

uint256 targetBlock = currentBlock - offset;
vm.setBlockhash(targetBlock, expectedHash);

bytes32 result = Blockhash.blockHash(targetBlock);
assertEq(result, blockhash(targetBlock));
assertEq(result, expectedHash);
}

function testFuzzHistoryBlocks(uint256 offset, uint256 currentBlock, bytes32 expectedHash) public {
// History blocks (257-8191 blocks old)
offset = bound(offset, 257, 8191);
vm.assume(currentBlock > offset);
vm.roll(currentBlock);
_setHistoryBlockhash(expectedHash);

uint256 targetBlock = currentBlock - offset;
bytes32 result = Blockhash.blockHash(targetBlock);
(bool success, bytes memory returndata) = Blockhash.HISTORY_STORAGE_ADDRESS.staticcall(
abi.encodePacked(bytes32(targetBlock))
);
assertTrue(success);
assertEq(result, abi.decode(returndata, (bytes32)));
assertEq(result, expectedHash);
}

function testFuzzVeryOldBlocks(uint256 offset, uint256 currentBlock) public {
// Very old blocks (>8191 blocks old)
offset = bound(offset, 8192, type(uint256).max);
vm.assume(currentBlock > offset);
vm.roll(currentBlock);

uint256 targetBlock = currentBlock - offset;
bytes32 result = Blockhash.blockHash(targetBlock);
assertEq(result, bytes32(0));
}

function testFuzzFutureBlocks(uint256 offset, uint256 currentBlock) public {
// Future blocks
offset = bound(offset, 1, type(uint256).max);
vm.roll(currentBlock);

unchecked {
uint256 targetBlock = currentBlock + offset;
bytes32 result = Blockhash.blockHash(targetBlock);
assertEq(result, blockhash(targetBlock));
}
}

function _setHistoryBlockhash(bytes32 blockHash) internal {
vm.assume(block.number < type(uint256).max);
vm.roll(block.number + 1); // roll to the next block so the storage contract sets the parent's blockhash
vm.prank(SYSTEM_ADDRESS);
(bool success, ) = Blockhash.HISTORY_STORAGE_ADDRESS.call(abi.encode(blockHash)); // set parent's blockhash
assertTrue(success);
vm.roll(block.number - 1);
}
}
Loading