diff --git a/src/utils/LibBlockHash.sol b/src/utils/LibBlockHash.sol new file mode 100644 index 0000000000..62012815f7 --- /dev/null +++ b/src/utils/LibBlockHash.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +/// @notice Library for accessing block hashes way beyond the 256-block limit. ref: EIP-2935 +/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/LibBlockHash.sol) +library LibBlockHash { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Address of the EIP-2935 history storage contract. + /// See: https://eips.ethereum.org/EIPS/eip-2935 + address internal constant HISTORY_STORAGE_ADDRESS = 0x0000F90827F1C53a10cb7A02335B175320002935; + + /// @dev Retrieves the block hash for any historical block within the supported range. + /// 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 hash) { + assembly { + let current := number() + let distance := sub(current, blockNumber) + + // Check if distance < 257 + if lt(distance, 257) { + // Return blockhash(blockNumber) + mstore(0x00, blockhash(blockNumber)) + return(0x00, 0x20) + } + + // Store the blockNumber in scratch space + mstore(0x00, blockNumber) + mstore(0x20, 0) + + // call history storage address + pop(staticcall(gas(), HISTORY_STORAGE_ADDRESS, 0x00, 0x20, 0x20, 0x20)) + + // load result + hash := mload(0x20) + } + } +} diff --git a/src/utils/LibTransient.sol b/src/utils/LibTransient.sol index b7151a4198..614ba336d7 100644 --- a/src/utils/LibTransient.sol +++ b/src/utils/LibTransient.sol @@ -406,6 +406,8 @@ library LibTransient { _compat(ptr)._spacer = 0; } + /// @dev + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* ADDRESS OPERATIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ diff --git a/test/LibBlockHash.t.sol b/test/LibBlockHash.t.sol new file mode 100644 index 0000000000..6cfbc7e52c --- /dev/null +++ b/test/LibBlockHash.t.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "./utils/SoladyTest.sol"; +import {LibBlockHash} from "../src/utils/LibBlockHash.sol"; + +contract LibBlockHashTest is SoladyTest { + uint256 internal startingBlock; + + address internal constant SYSTEM_ADDRESS = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE; + + bytes private constant _HISTORY_STORAGE_BYTECODE = + hex"3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500"; + + function setUp() public { + vm.roll(block.number + 100); + startingBlock = block.number; + vm.etch(LibBlockHash.HISTORY_STORAGE_ADDRESS, _HISTORY_STORAGE_BYTECODE); + } + + function __blockHash(uint256 blockNumber, bytes32 expectedHash, bytes32 sysExpectedHash) + internal + view + returns (bool) + { + if (expectedHash != sysExpectedHash) return false; + return sysExpectedHash == LibBlockHash.blockHash(blockNumber); + } + + function testFuzzRecentBlocks(uint8 offset, uint64 currentBlock, bytes32 expectedHash) public { + // Recent blocks (1-256 blocks old) + uint256 boundedOffset = uint256(offset) + 1; + vm.assume(currentBlock > boundedOffset); + vm.roll(currentBlock); + + uint256 targetBlock = currentBlock - boundedOffset; + vm.setBlockhash(targetBlock, expectedHash); + + assertTrue(__blockHash(targetBlock, expectedHash, blockhash(targetBlock))); + } + + 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; + assertTrue(__blockHash(targetBlock, bytes32(0), bytes32(0))); + } + + function testFuzzFutureBlocks(uint256 offset, uint256 currentBlock) public { + // Future blocks + offset = _bound(offset, 1, type(uint256).max); + currentBlock = _bound(currentBlock, 0, type(uint256).max - offset); + vm.roll(currentBlock); + + unchecked { + uint256 targetBlock = currentBlock + offset; + assertTrue(__blockHash(targetBlock, blockhash(targetBlock), blockhash(targetBlock))); + } + } + + function testUnsupportedChainsReturnZeroWhenOutOfRange() public { + vm.etch(LibBlockHash.HISTORY_STORAGE_ADDRESS, hex""); + + vm.roll(block.number + 1000); + assertEq(LibBlockHash.blockHash(block.number - 1000), bytes32(0)); + } +} diff --git a/test/utils/forge-std/Vm.sol b/test/utils/forge-std/Vm.sol index 2614f4ecba..dc644b9c26 100644 --- a/test/utils/forge-std/Vm.sol +++ b/test/utils/forge-std/Vm.sol @@ -1988,4 +1988,7 @@ interface Vm is VmSafe { /// Stops all safe memory expectation in the current subcontext. function stopExpectSafeMemory() external; + + /// Sets the blockhash for a given block number. + function setBlockhash(uint256 blockNumber, bytes32 blockHash) external; }