Skip to content

bug(cheatcodes): blockhash returns zero value if vm.rolled beyond type(uint64).max value, raise a warning to user #10367

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
2 tasks done
pcaversaccio opened this issue Apr 24, 2025 · 2 comments · May be fixed by #10183
Open
2 tasks done
Assignees
Labels
A-cheatcodes Area: cheatcodes T-bug Type: bug

Comments

@pcaversaccio
Copy link
Contributor

pcaversaccio commented Apr 24, 2025

Component

Forge

Have you ensured that all of these are up to date?

  • Foundry
  • Foundryup

What version of Foundry are you on?

55802bad5f9068d969df4273b5c2a960332e8e42

What version of Foundryup are you on?

1.0.1

What command(s) is the bug in?

No response

Operating System

Linux

Describe the bug

// SPDX-License-Identifier: WTFPL
pragma solidity ^0.8.29;

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

contract SimpleTest is Test {
    function testBlockHashSimple() public {
        vm.roll(type(uint72).max); // If you replace this with `vm.roll(type(uint64).max);` (or below) it will work properly.
        uint256 blockNumber = vm.getBlockNumber() - 1;
        vm.setBlockhash(blockNumber, keccak256("vyper"));
        emit log_bytes32(blockhash(blockNumber));
    }
}
Logs:
  0x0000000000000000000000000000000000000000000000000000000000000000

Traces:
  [4964] SimpleTest::testBlockHashSimple()
    ├─ [0] VM::roll(4722366482869645213695 [4.722e21])
    │   └─ ← [Return]
    ├─ [0] VM::getBlockNumber() [staticcall]
    │   └─ ← [Return] 4722366482869645213695 [4.722e21]
    ├─ [0] VM::setBlockhash(4722366482869645213694 [4.722e21], 0xcde04c9d98738375f7f1bbf067b84e35f9327a84c3d709815de017a16d090c11)
    │   └─ ← [Return]
    ├─ emit log_bytes32(val: 0x0000000000000000000000000000000000000000000000000000000000000000)
    └─ ← [Stop]

The correct output should be:

[PASS] testBlockHashSimple() (gas: 4964)
Logs:
  0xcde04c9d98738375f7f1bbf067b84e35f9327a84c3d709815de017a16d090c11

Traces:
  [4964] SimpleTest::testBlockHashSimple()
    ├─ [0] VM::roll(18446744073709551615 [1.844e19])
    │   └─ ← [Return]
    ├─ [0] VM::getBlockNumber() [staticcall]
    │   └─ ← [Return] 18446744073709551615 [1.844e19]
    ├─ [0] VM::setBlockhash(18446744073709551614 [1.844e19], 0xcde04c9d98738375f7f1bbf067b84e35f9327a84c3d709815de017a16d090c11)
    │   └─ ← [Return]
    ├─ emit log_bytes32(val: 0xcde04c9d98738375f7f1bbf067b84e35f9327a84c3d709815de017a16d090c11)
    └─ ← [Stop]
@pcaversaccio pcaversaccio added T-bug Type: bug T-needs-triage Type: this issue needs to be labelled labels Apr 24, 2025
@github-project-automation github-project-automation bot moved this to Backlog in Foundry Apr 24, 2025
@pcaversaccio
Copy link
Contributor Author

pcaversaccio commented Apr 25, 2025

So this happens because revm internally uses a saturated conversion from U256 to u64 for the block number in the block_hash function (see as_u64_saturated!). So if both env.block.number and the requested block number are ≥ u64::MAX, they get clamped to u64::MAX. This causes requested_number >= block_number to evaluate true, which triggers an early return of U256::ZERO without querying the DB. As a result, even though the original U256 values may differ, revm treats them as equal at the u64 level. To avoid this, it's safer for now to work with u64 block numbers when interacting with block_hash().

@pcaversaccio
Copy link
Contributor Author

@grandizzy @DaniPopes I think Foundry should raise an informative error if 2**64-1.

pcaversaccio added a commit to pcaversaccio/snekmate that referenced this issue Apr 29, 2025
### 🕓 Changelog

This PR adds [EIP-2935](https://eips.ethereum.org/EIPS/eip-2935)-based
utility functions. These functions can be used to access the historical
block hashes beyond the default `256`-block limit. We use the
[EIP-2935](https://eips.ethereum.org/EIPS/eip-2935) history contract,
which maintains a ring buffer of the last `8,191` block hashes stored in
state. For the blocks within the last `256` blocks, we use the native
[`BLOCKHASH`](https://www.evm.codes/?fork=cancun#40) opcode. For blocks
between `257` and `8,191` blocks ago, the function `_block_hash` queries
via the specified [`get`](https://eips.ethereum.org/EIPS/eip-2935#get)
method the [EIP-2935](https://eips.ethereum.org/EIPS/eip-2935) history
contract. For blocks older than `8,191` or future blocks (including the
current one), we return zero, matching the
[`BLOCKHASH`](https://www.evm.codes/?fork=cancun#40) behaviour.

It's important to note that the Vyper built-in function
[`blockhash`](https://docs.vyperlang.org/en/stable/built-in-functions.html#blockhash)
reverts if the block number is more than `256` blocks behind the current
block. We explicitly handle this case (i.e. `delta > 8191`) to ensure
the function returns an empty `bytes32` value rather than reverting
(i.e. exactly matching the
[`BLOCKHASH`](https://www.evm.codes/?fork=cancun#40) opcode behaviour).

In the stateless fuzzing tests we use `uint64` as upper bound for the
`block.number` due to Revm's internal
[saturation](https://github.com/bluealloy/revm/blob/b2c789d42d4eee93ce111f1a7d3d0708f1e34180/crates/interpreter/src/instructions/host.rs#L144)
of `block.number` to `u64::MAX` (see also my issue
[here](foundry-rs/foundry#10367)). If
`requested_number >= block_number` (after saturation), Revm returns
`U256::ZERO`
[early](https://github.com/bluealloy/revm/blob/b2c789d42d4eee93ce111f1a7d3d0708f1e34180/crates/interpreter/src/instructions/host.rs#L148-L157)
without querying the database.

Eventually, `block.number` is replaced with the
[`vm.getBlockNumber()`](https://book.getfoundry.sh/cheatcodes/get-block-number)
cheat code in the tests.

---------

Signed-off-by: Pascal Marco Caversaccio <[email protected]>
@zerosnacks zerosnacks changed the title blockhash returns zero value if rolled beyond type(uint64).max value bug(cheatcodes): blockhash returns zero value if vm.rolled beyond type(uint64).max value, raise a warning to user May 1, 2025
@zerosnacks zerosnacks added A-cheatcodes Area: cheatcodes and removed T-needs-triage Type: this issue needs to be labelled labels May 1, 2025
@zerosnacks zerosnacks self-assigned this May 1, 2025
@zerosnacks zerosnacks linked a pull request May 1, 2025 that will close this issue
26 tasks
@zerosnacks zerosnacks moved this from Backlog to In Progress in Foundry May 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-cheatcodes Area: cheatcodes T-bug Type: bug
Projects
Status: In Progress
Development

Successfully merging a pull request may close this issue.

2 participants