diff --git a/foundry.toml b/foundry.toml index 6afdd63321..0534ad6552 100644 --- a/foundry.toml +++ b/foundry.toml @@ -10,7 +10,7 @@ auto_detect_solc = false optimizer = true optimizer_runs = 1_000 gas_limit = 100_000_000 # ETH is 30M, but we use a higher value. -skip = ["*/*7702*", "*/*Transient*", "*/ext/ithaca/*", "*/ext/zksync/*"] +skip = ["*/*7702*", "*/*BlockHashLib*", "*/*Transient*", "*/ext/ithaca/*", "*/ext/zksync/*"] fs_permissions = [{ access = "read", path = "./test/data"}] remappings = [ "forge-std=test/utils/forge-std/" diff --git a/src/utils/BlockHashLib.sol b/src/utils/BlockHashLib.sol new file mode 100644 index 0000000000..faa5d16853 --- /dev/null +++ b/src/utils/BlockHashLib.sol @@ -0,0 +1,38 @@ +// 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/BlockHashLib.sol) +/// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Blockhash.sol) +library BlockHashLib { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* 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; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* OPERATIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @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 result) { + unchecked { + // If `blockNumber + 256` overflows: + // - Typical chain height (`block.number > 255`) -> `staticcall` -> 0. + // - Very early chain (`block.number <= 255`) -> `blockhash` -> 0. + if (block.number <= blockNumber + 256) return blockhash(blockNumber); + } + /// @solidity memory-safe-assembly + assembly { + mstore(0x20, blockNumber) + mstore(0x00, 0) + pop(staticcall(gas(), HISTORY_STORAGE_ADDRESS, 0x20, 0x20, 0x00, 0x20)) + result := mload(0x00) + } + } +} diff --git a/test/BlockHashLib.t.sol b/test/BlockHashLib.t.sol new file mode 100644 index 0000000000..100d2194c5 --- /dev/null +++ b/test/BlockHashLib.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "./utils/SoladyTest.sol"; +import {BlockHashLib} from "../src/utils/BlockHashLib.sol"; + +contract BlockHashLibTest is SoladyTest { + uint256 internal startingBlock; + + address internal constant SYSTEM_ADDRESS = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE; + + bytes private constant _HISTORY_STORAGE_BYTECODE = + hex"3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500"; + + function testBlockHash( + uint256 simulationBlockNumber, + uint256 queryBlockNumber, + uint256 savedBlockedNumber, + bytes32 h + ) public { + if (_randomChance(2)) { + vm.etch(BlockHashLib.HISTORY_STORAGE_ADDRESS, _HISTORY_STORAGE_BYTECODE); + } + + savedBlockedNumber = _bound(savedBlockedNumber, 0, 2 ** 64 - 1); + + vm.roll(savedBlockedNumber + 1); + vm.prank(SYSTEM_ADDRESS); + (bool success,) = BlockHashLib.HISTORY_STORAGE_ADDRESS.call(abi.encode(h)); + require(success); + + vm.setBlockhash(savedBlockedNumber, h); + + vm.roll(simulationBlockNumber); + + assertEq(BlockHashLib.blockHash(queryBlockNumber), _blockHash(queryBlockNumber)); + } + + function _blockHash(uint256 blockNumber) internal view returns (bytes32) { + (bool success, bytes memory result) = + address(this).staticcall(abi.encodeWithSignature("blockHash(uint256)", blockNumber)); + if (!success) return 0; + return abi.decode(result, (bytes32)); + } + + function blockHash(uint256 blockNumber) public view returns (bytes32 result) { + if (block.number <= blockNumber + 256) return blockhash(blockNumber); + address a = BlockHashLib.HISTORY_STORAGE_ADDRESS; + if (a.code.length == 0) return 0; + /// @solidity memory-safe-assembly + assembly { + mstore(0x20, blockNumber) + mstore(0x00, 0) + pop(staticcall(gas(), a, 0x20, 0x20, 0x00, 0x20)) + result := mload(0x00) + } + } +} 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; }