Opcode Summary

PropertyValue
Opcode0x43
MnemonicNUMBER
Gas2
Stack Input(none)
Stack Outputblock.number (uint256)
BehaviorPushes the number of the current block onto the stack. On Ethereum mainnet, block numbers increment by one per slot (12 seconds post-Merge). Solidity exposes this as block.number. On L2s, the semantics vary dramatically: Arbitrum’s block.number returns the L1 block number (not the L2 block number), Optimism returns an L2 block number that increments per L2 block (every 2 seconds), and other L2s have their own conventions.

Threat Surface

NUMBER is the EVM’s block-height primitive and one of the most widely misused opcodes in deployed contracts. Developers routinely treat block.number as a reliable clock, a source of entropy, or a cross-chain-portable constant. None of these assumptions hold.

The threat surface centers on three properties:

  1. Block numbers are not a reliable time measure. Before the Merge, Ethereum’s PoW block time averaged ~13.5 seconds but fluctuated between 1 and 60+ seconds depending on hash rate and difficulty adjustments. Post-Merge, PoS fixed block production at exactly 12-second slots, but missed slots (where no validator proposes) cause gaps. On L2s, block times range from sub-second (Arbitrum) to 2 seconds (Optimism) to variable (other rollups). Any contract that converts block numbers to calendar time using a fixed multiplier (block.number * 15, block.number * 12, etc.) will drift, sometimes catastrophically. Governance voting periods, vesting schedules, and auction deadlines built on block-number math silently break when deployed across chains with different block cadences.

  2. Block numbers are fully predictable and make terrible randomness. Every participant can observe the current block number and calculate the next one with near-certainty. Using block.number (or any derivation like block.number % N) as a random seed is equivalent to using a public counter — miners/validators can choose whether to include or exclude transactions based on the resulting “random” outcome, and regular users can front-run by computing the outcome in advance. Approximately 15% of smart contract audits flag insecure randomness derived from block variables.

  3. Block number semantics fragment across L2s. Arbitrum’s block.number returns the L1 block number (the Ethereum mainnet block at which the sequencer received the transaction), not the Arbitrum-native block number. Optimism returns an L2 block number that increments every 2 seconds. Other rollups and sidechains have their own conventions. A contract hardcoding block.number + 100 as a deadline behaves completely differently on Ethereum (20 minutes), Optimism (3.3 minutes), and Arbitrum (where “100 L1 blocks” is ~20 minutes but the L2 has produced thousands of its own blocks in that time). Multi-chain deployments that don’t account for this break silently.


Smart Contract Threats

T1: block.number as a Time Proxy — Inaccurate Scheduling (High)

Using block.number as a clock is a pervasive anti-pattern. Contracts compute deadlines, lock durations, or cooldown periods as a block count and assume a fixed block time:

  • Pre-Merge drift. PoW block times fluctuated between ~1s and ~60s. A lock set to block.number + 40320 (targeting “~1 week” at 15s/block) could expire in anywhere from 3 days to 2+ weeks depending on network conditions. The Constantinople difficulty bomb caused block times to exceed 30 seconds in late 2018, halving the effective duration of all block-number-based schedules.

  • Post-Merge precision. PoS fixed block production at 12-second slots. Contracts assuming 15-second blocks (the pre-Merge convention) now overestimate durations by 25%. A “30-day” vesting period calculated as block.number + 172800 (at 15s/block) actually completes in ~24 days at 12s/slot.

  • Missed slots. On PoS Ethereum, a validator can miss their slot (offline, slashed, or deliberately abstaining). Block numbers still increment by 1 per produced block, but the wall-clock gap between blocks jumps to 24s, 36s, or more. Over time, this causes block-number-based schedules to lag behind calendar time.

  • L2 divergence. On Optimism (2s block time), block.number + 172800 expires in ~4 days, not 30. On Arbitrum, where block.number returns L1 block numbers (~12s), the same expression takes ~24 days. Contracts deployed across chains with identical bytecode will have wildly different timing behavior.

Why it matters: OpenZeppelin’s governance contracts (Governor, TimelockController) historically used block.number for voting periods and proposal deadlines. The inconsistency was severe enough that OpenZeppelin migrated to block.timestamp in v4.9+ and introduced EIP-6372 (Clock mode) to let contracts declare their time basis. Any governance contract still using block numbers on an L2 has incorrect voting windows.

T2: block.number as a Randomness Source (Critical)

Using block.number for randomness is fundamentally broken across all consensus mechanisms:

  • Fully predictable. The next block number is always current + 1 (barring missed slots, which only delay it). Any computation f(block.number) is known before the block is produced. Attackers can simulate the outcome off-chain, decide whether it’s favorable, and submit (or withhold) their transaction accordingly.

  • Miner/validator manipulation. On PoW, miners could choose which transactions to include based on the block number’s effect on a randomness computation. On PoS, the proposer can reorder transactions within a block, selectively include or exclude them, or skip their slot entirely. If the “random” outcome depends on block.number, the block producer has influence over it.

  • Front-running. Even without block producer collusion, any user can compute f(block.number + 1) at the start of a block and submit a transaction in the same block if the outcome is favorable. This is a trivially simple MEV opportunity.

  • Combination with other block variables doesn’t help. Patterns like keccak256(abi.encodePacked(block.number, block.timestamp, msg.sender)) add no real entropy. block.timestamp is also known to the block producer, and msg.sender is attacker-controlled.

Why it matters: Lottery contracts, NFT mint randomizers, gaming contracts, and fair-distribution mechanisms that derive randomness from block.number are all exploitable. The SmartBillions lottery hack ($120K, October 2017) and numerous Ethernaut CTF challenges demonstrate this conclusively.

T3: Hardcoded Block Number Deadlines (High)

Contracts that embed specific block numbers for upgrade deadlines, token unlocks, or feature toggles create brittle, non-portable code:

  • Chain-specific assumptions. A contract deployed on Ethereum mainnet with if (block.number >= 18000000) as an activation condition will never trigger on a testnet, L2, or fork with a different block numbering scheme. Deploying the same bytecode on Arbitrum (which returns L1 block numbers) produces unpredictable behavior.

  • Incorrect migration estimates. Teams estimating “block 18,000,000 is approximately October 2023” bake in a block-time assumption. If the chain’s block time changes (as it did at the Merge, shifting from ~13.5s to 12s), the activation date shifts. For the Merge itself, the transition used total difficulty (not block number) precisely because block-number-based targets would have been unreliable.

  • No recourse on immutable contracts. If the target block number passes without the intended action (e.g., a multisig missed the upgrade window) or arrives earlier than expected, immutable contracts have no mechanism to adjust. Funds can be locked or unlocked at the wrong time.

Why it matters: The Ethereum protocol itself moved away from block-number-based fork activation after the Merge, switching to timestamps (EIP-3675) for Shapella and subsequent upgrades. Smart contracts should follow the same pattern.

T4: L2 Block Number Semantic Divergence (High)

The meaning of block.number varies dramatically across L2s, breaking contracts that assume Ethereum mainnet semantics:

  • Arbitrum. block.number returns the L1 block number at the time the sequencer received the transaction. The actual Arbitrum L2 block number is only accessible via the ArbSys precompile (ArbSys(address(100)).arbBlockNumber()). This means two transactions in different L2 blocks can report the same block.number if they were sequenced during the same L1 block. Contracts using block.number for uniqueness, ordering, or per-block rate limiting on Arbitrum will malfunction.

  • Optimism. block.number returns the L2 block number, which increments every ~2 seconds (6x faster than L1). A governance contract with a 50,400-block voting period (intended as ~7 days on mainnet at 12s/block) would last only ~28 hours on Optimism.

  • zkSync Era. block.number is the L2 batch number, which advances irregularly depending on batch submission frequency. It is not a reliable proxy for either time or transaction ordering.

  • Cross-chain governance. DAOs deploying governance contracts on multiple chains with identical block.number-based voting periods get wildly inconsistent windows. Voters on Optimism have 6x less real time to vote than voters on mainnet for the same block count.

Why it matters: Multi-chain DeFi protocols (Uniswap, Aave, Compound) must account for L2 divergence. Compound’s governance framework originally used block numbers, and OpenZeppelin raised this as a critical issue for L2 deployments, leading to the EIP-6372 clock mode standard.

T5: Governance Time-Lock Bypass via Block Number Manipulation (Medium)

Governance protocols using block numbers for proposal timing are vulnerable to manipulation on chains where block production is irregular or controllable:

  • Snapshot timing attacks. Governance systems snapshot voting power at a specific block number (e.g., proposalSnapshot = block.number + votingDelay). An attacker can accumulate tokens, submit a proposal, and ensure the snapshot captures their peak voting power. On chains with fast block times, the voting delay measured in blocks can be trivially short in wall-clock time.

  • Compressed voting windows on L2s. A governance contract deployed on Optimism with a 17,280-block voting period (intended as ~2.4 days on mainnet) resolves in ~9.6 hours, giving defenders less time to organize opposition to a malicious proposal.

  • Sequencer-influenced timing. On L2s with centralized sequencers, the sequencer controls when blocks are produced. A malicious or compromised sequencer could delay or accelerate block production around governance-critical moments, though this would be visible on-chain and damage the sequencer’s reputation.

  • Compound governance attack (July 2024). While not a pure block-number exploit, the “Golden Boys” coordinated governance capture of Compound Finance leveraged the block-number-based snapshot and voting mechanics to time their $25M treasury drain proposal. They accumulated COMP tokens and timed the proposal submission so the voting power snapshot captured their peak holdings.

Why it matters: Governance time-locks are a critical security mechanism. If an attacker can compress the effective time window (by deploying on a fast L2 or exploiting irregular block production), they reduce the community’s ability to respond to malicious proposals.


Protocol-Level Threats

P1: L2 Block Number Divergence Creates Cross-Chain Incompatibility (High)

The lack of a standard block.number semantic across L2s is a protocol-level design issue, not just a smart contract concern. Different L2 architectures made fundamentally different choices:

  • Arbitrum chose L1 block numbers for block.number to maintain composability with L1 contracts that reason about block numbers. This breaks contracts that assume block.number increments once per transaction or per L2 block.

  • Optimism chose L2 block numbers for block.number because it more closely matches developer expectations. This breaks contracts that assume L1-like block times.

  • EIP-6372 (Contract Clock Mode) was introduced to let contracts declare whether they use block numbers or timestamps, but adoption is not universal and it doesn’t solve the problem for already-deployed contracts.

The result is that “portable” Solidity code using block.number is not portable in practice. Protocol teams must audit and adapt every block-number reference when deploying across chains.

P2: Block Time Changes Invalidate Historical Assumptions (Medium)

Ethereum’s block time has changed twice in significant ways:

  • PoW era: Average ~13-15 seconds, with high variance. The ice age / difficulty bomb periodically increased block times to 20-30+ seconds before being defused.

  • PoS era (post-Merge, September 2022): Fixed at exactly 12-second slots. This was a permanent ~15-20% speedup relative to the PoW average.

Any contract deployed before the Merge that used block numbers for time estimation (e.g., blocksPerYear = 2102400 based on 15s/block) now has incorrect constants. Post-Merge, blocksPerYear ≈ 2628000 (at 12s/slot, minus missed slots). Compound, Aave, and other lending protocols that calculated interest accrual per-block had to adjust their rate models.

P3: No DoS Vector (Low)

NUMBER costs a fixed 2 gas with no dynamic component. It reads from the block header (a register, not storage). It cannot be used for gas griefing or state bloat.


Edge Cases

Edge CaseBehaviorSecurity Implication
Block 0 (genesis)block.number == 0 for transactions in the genesis blockContracts using block.number == 0 as a special case may trigger unexpectedly on new chains or testnets; genesis block transactions are rare but possible
Very large block numberblock.number is a uint256 but practically bounded. Ethereum mainnet is at ~19M blocks after ~9 years. Overflow is not a realistic concern for billions of years.Safe from overflow, but contracts casting block.number to smaller types (uint32, uint64) may truncate on chains with very large block numbers
Missed slots (PoS)Block numbers skip no values; each produced block gets the next sequential number. But wall-clock time between blocks increases (24s, 36s, etc.)Time-based assumptions using block numbers break during periods of high missed-slot rates
Arbitrum block.numberReturns the L1 block number, not the L2 block numberTwo Arbitrum transactions in different L2 blocks can share the same block.number; per-block uniqueness assumptions fail
Optimism block.numberReturns L2 block number (~1 block per 2 seconds)Block-number-based durations are 6x shorter in wall-clock time compared to Ethereum mainnet
zkSync Era block.numberReturns L2 batch number, which advances irregularlyCannot be used for any time-based or ordering logic
block.number in STATICCALLReturns the same value as in a regular callNo special behavior, but view functions that return block.number for off-chain consumption are subject to the same L2 divergence
block.number during CREATE/CREATE2Returns the same block number as the deploying transactionConstructor logic using block.number as a seed or deadline is as exploitable as runtime usage

Real-World Exploits

Exploit 1: SmartBillions Lottery — $120K Stolen via Predictable block.number Randomness (October 2017)

Root cause: Lottery contract used block.number and related block variables as a randomness source, allowing attackers to predict and manipulate outcomes.

Details: SmartBillions launched an Ethereum-based lottery platform and publicly challenged hackers to break their smart contract, offering 1,500 ETH as a bounty. The contract derived its “random” lottery numbers from block variables including block.number, blockhash, and other on-chain data. Two independent attackers recognized that these values are fully predictable and deterministic. They wrote attack contracts that computed the lottery outcome in the same transaction, only proceeding when the result was favorable.

The attackers drained 400 ETH (~$120,000 at the time) within days of the hackathon launch. SmartBillions had to withdraw the remaining prize pool to prevent further losses. The platform subsequently redesigned their randomness mechanism, but the damage to their credibility was permanent.

NUMBER’s role: block.number was a key input to the randomness derivation. Because the next block number is always current + 1, an attacker contract executing in the same block could compute the identical “random” value and make decisions accordingly.

Impact: ~$120K stolen. The incident became a canonical example of insecure on-chain randomness in smart contract security education (Ethernaut, Damn Vulnerable DeFi).

References:


Exploit 2: Fomo3D — 10,469 ETH Won via Block Stuffing Attack (August 2018)

Root cause: Last-player-wins game mechanism combined with block-number-based countdown allowed a player to guarantee they were the last buyer by preventing anyone else from transacting.

Details: Fomo3D was a “last key buyer wins the pot” game where each key purchase reset a countdown timer. The timer was measured in block numbers — if N blocks passed without a new purchase, the last buyer won the pot. An attacker devised a “block stuffing” strategy: immediately after buying a key, they submitted multiple transactions with extremely high gas prices (up to 501 Gwei) that consumed entire blocks’ gas limits (~8M gas per block). Over 6 consecutive blocks (1.5 minutes), the attacker’s high-gas transactions filled every block, preventing any other player’s key purchase transaction from being included. The countdown timer expired, and the attacker won the 10,469 ETH pot ($3M at the time).

The attack cost approximately 19 ETH in gas fees — a 550x return on investment.

NUMBER’s role: The game’s countdown was block-number-based: “if block.number - lastPurchaseBlock >= threshold, the round ends.” The attacker exploited the fact that block numbers advance predictably (one per ~15 seconds on PoW) and that filling blocks with junk transactions prevents competing purchases from being mined within the threshold.

Impact: ~10,469 ETH ($3M+) won by a single attacker. The exploit demonstrated that block-number-based deadlines in adversarial settings can be gamed through economic attacks on block space, without requiring any 51% attack or protocol-level vulnerability.

References:


Exploit 3: Compound Governance Block-Number Timing Exploitation — $25M Treasury Drain Attempt (July 2024)

Root cause: Governance proposal timing, voting snapshots, and execution delays all denominated in block numbers, enabling a coordinated group to time a treasury drain proposal around peak voting power.

Details: A group known as the “Golden Boys,” led by a pseudonymous actor “Humpy,” accumulated a controlling share of COMP voting power through token purchases and delegation. They submitted Proposal 247 to redirect $25M in COMP tokens from the DAO treasury into a yield vault they controlled. The attack leveraged Compound’s block-number-based governance mechanics: a 2-day review period (~14,400 blocks), a voting power snapshot at a specific block number, and a 3-day voting window (~21,600 blocks).

The attackers timed their token accumulation so the voting power snapshot (taken at a specific block number after the review period) captured their maximum holdings. They passed the proposal with concentrated voting power before the broader community could organize opposition.

NUMBER’s role: Every timing parameter in Compound’s governance — review period, snapshot block, voting window, timelock delay — was denominated in block numbers. The attackers exploited the predictability of these block-based windows to coordinate their accumulation and voting. Had the governance system used timestamps with longer real-time windows, defenders would have had more time to react.

Impact: $25M in COMP tokens were at risk. The community ultimately negotiated a resolution, but the exploit exposed fundamental weaknesses in block-number-based governance timing, particularly the risk of compressed real-time windows.

References:


Attack Scenarios

Scenario A: Predictable Lottery via block.number Randomness

// Vulnerable: uses block.number for "randomness"
contract VulnerableLottery {
    uint256 public ticketPrice = 0.1 ether;
    address[] public players;
 
    function buyTicket() external payable {
        require(msg.value == ticketPrice);
        players.push(msg.sender);
    }
 
    function pickWinner() external {
        require(players.length >= 10);
        // block.number is publicly known; attacker predicts the winner
        uint256 winnerIndex = uint256(
            keccak256(abi.encodePacked(block.number, players.length))
        ) % players.length;
        payable(players[winnerIndex]).transfer(address(this).balance);
        delete players;
    }
}
 
// Attack: compute outcome in the same block and only participate when winning
contract LotteryAttacker {
    VulnerableLottery immutable lottery;
 
    constructor(VulnerableLottery _lottery) {
        lottery = _lottery;
    }
 
    function attackIfWinning() external payable {
        uint256 playerCount = lottery.players.length;
        // Simulate the randomness computation for the NEXT call
        uint256 winnerIndex = uint256(
            keccak256(abi.encodePacked(block.number, playerCount + 1))
        ) % (playerCount + 1);
 
        // Only buy ticket if we'd be the winner
        require(winnerIndex == playerCount, "not winning this block");
        lottery.buyTicket{value: 0.1 ether}();
        lottery.pickWinner();
    }
}

Scenario B: Hardcoded Block Deadline Breaks on L2

// Vulnerable: hardcoded block number for token unlock
contract VestingVault {
    address public beneficiary;
    uint256 public unlockBlock;
 
    constructor(address _beneficiary) {
        beneficiary = _beneficiary;
        // "6 months" at 12s/block on mainnet = ~1,314,000 blocks
        unlockBlock = block.number + 1_314_000;
    }
 
    function withdraw() external {
        require(msg.sender == beneficiary);
        require(block.number >= unlockBlock, "still locked");
        payable(beneficiary).transfer(address(this).balance);
    }
}
 
// On Ethereum mainnet: unlocks in ~182 days (correct).
// On Optimism (2s blocks): unlocks in ~30 days (6x faster).
// On Arbitrum (block.number = L1 block number): unlocks in ~182 days,
//   but any L2-specific time reasoning is broken.

Scenario C: Block Stuffing to Win a Time-Limited Auction

// Vulnerable: auction with block-number-based deadline
contract VulnerableAuction {
    address public highestBidder;
    uint256 public highestBid;
    uint256 public endBlock;
 
    constructor(uint256 durationBlocks) {
        endBlock = block.number + durationBlocks;
    }
 
    function bid() external payable {
        require(block.number < endBlock, "auction ended");
        require(msg.value > highestBid, "bid too low");
        if (highestBidder != address(0)) {
            payable(highestBidder).transfer(highestBid);
        }
        highestBidder = msg.sender;
        highestBid = msg.value;
    }
 
    function settle() external {
        require(block.number >= endBlock, "auction not ended");
        // Transfer asset to highestBidder...
    }
}
 
// Attack: bid near endBlock, then stuff remaining blocks with
// high-gas transactions to prevent competing bids.
// Cost: gas fees for stuffing ~3-5 blocks.
// Reward: winning asset at a below-market bid.

Scenario D: Governance Voting Period Compressed on L2

// Governance contract using block-number voting period
contract SimpleGovernor {
    uint256 public constant VOTING_PERIOD = 50_400; // ~7 days at 12s/block
 
    struct Proposal {
        uint256 startBlock;
        uint256 forVotes;
        uint256 againstVotes;
        bool executed;
    }
 
    mapping(uint256 => Proposal) public proposals;
    mapping(address => uint256) public votingPower;
 
    function propose(uint256 proposalId) external {
        proposals[proposalId].startBlock = block.number;
    }
 
    function vote(uint256 proposalId, bool support) external {
        Proposal storage p = proposals[proposalId];
        require(block.number >= p.startBlock, "not started");
        require(block.number < p.startBlock + VOTING_PERIOD, "ended");
 
        uint256 power = votingPower[msg.sender];
        if (support) p.forVotes += power;
        else p.againstVotes += power;
    }
 
    // On Ethereum mainnet: 50,400 blocks * 12s = 7 days (intended).
    // On Optimism: 50,400 blocks * 2s = 28 hours.
    // On Arbitrum (L1 block.number): 50,400 * 12s = 7 days,
    //   but L2-native blocks advance much faster, confusing users.
}

Mitigations

ThreatMitigationImplementation
T1: block.number as time proxyUse block.timestamp for time-dependent logicReplace block.number + N with block.timestamp + duration_seconds; adopt EIP-6372 clock mode for governance contracts
T1: Post-Merge block time assumption driftUse chain-agnostic time primitivesOpenZeppelin Governor v4.9+ supports both block.number and block.timestamp via clock() / CLOCK_MODE() (EIP-6372)
T2: block.number for randomnessUse verifiable randomness oraclesChainlink VRF for production randomness; commit-reveal schemes for lower-stakes applications; never derive entropy from block variables
T2: Front-running randomnessCommit-reveal patternUsers commit a hash of their choice in block N, reveal in block N+k; outcome depends on both reveals, preventing single-block prediction
T3: Hardcoded block deadlinesUse timestamps or configurable parametersStore deadlines as block.timestamp + duration; if block numbers are required, make the target configurable via governance
T4: L2 block.number divergenceAbstract chain-specific block semanticsOn Arbitrum, use ArbSys(address(100)).arbBlockNumber() for L2 block numbers; on all chains, prefer block.timestamp for time logic
T4: Cross-chain deployment inconsistencyTest block behavior on each target chainIntegration tests asserting block cadence and block.number semantics per chain; CI pipelines for L2 fork testing
T5: Governance timing compressionUse timestamp-based governanceMigrate to timestamp-based voting periods (OpenZeppelin Governor + EIP-6372); set minimum real-time durations, not block counts
General: Block stuffingUse timestamp-based deadlines with bufferTime-based deadlines (block.timestamp) resist block stuffing because wall-clock time passes regardless of block space saturation

Compiler/EIP-Based Protections

  • EIP-6372 (Contract Clock Mode): Defines a standard interface (clock(), CLOCK_MODE()) for contracts to declare whether they use block numbers or timestamps. Adopted by OpenZeppelin Governor v4.9+ to enable timestamp-based governance that works correctly across L1 and L2.

  • Chainlink VRF v2/v2.5: Provides verifiable, tamper-proof randomness that is economically infeasible to manipulate. Uses a public key and proof system that guarantees the random number was generated honestly, regardless of block variables.

  • EIP-4399 / PREVRANDAO (post-Merge): Replaced the DIFFICULTY opcode with PREVRANDAO, providing validator-contributed randomness. While better than block.number, it is still biasable by validators (who can choose to propose or skip a slot) and should not be used alone for high-stakes randomness.

  • EIP-3675 (The Merge): Fixed block production at 12-second slots, making block.number more predictable on Ethereum mainnet. Protocol-level fork activation moved from block numbers to timestamps after this EIP.


Severity Summary

Threat IDCategorySeverityLikelihoodReal-World Precedent
T1Smart ContractHighHighOpenZeppelin governance migration (EIP-6372); L2 deployment timing failures
T2Smart ContractCriticalHighSmartBillions ($120K, 2017); ~15% of audits flag insecure randomness
T3Smart ContractHighMediumEthereum protocol moved fork activation from block numbers to timestamps post-Merge
T4Smart ContractHighHighArbitrum/Optimism semantic divergence; Compound/OZ governance issues
T5Smart ContractMediumMediumCompound governance capture ($25M at risk, July 2024)
P1ProtocolHighHighArbitrum vs Optimism vs zkSync divergence is a known, ongoing issue
P2ProtocolMediumMediumPost-Merge block time change invalidated pre-Merge assumptions in deployed contracts
P3ProtocolLowN/A

OpcodeRelationship
TIMESTAMP (0x42)Returns block.timestamp (seconds since epoch). Preferred over NUMBER for time-dependent logic because it directly represents wall-clock time. Post-Merge, timestamps are deterministic (genesis_time + slot × 12). Subject to its own manipulation risks on PoW and L2 chains, but avoids the block-cadence portability problem that plagues NUMBER.
BLOCKHASH (0x40)Returns the hash of a recent block (up to 256 blocks back). Often combined with block.number in broken randomness patterns (blockhash(block.number - 1)). The hash adds entropy but is still known to the block producer and predictable for future blocks. blockhash(block.number) always returns 0x0 (current block’s hash isn’t available during execution).
PREVRANDAO (0x44)Returns the beacon chain’s RANDAO mix (post-Merge) or block difficulty (pre-Merge). Provides better entropy than NUMBER or TIMESTAMP but is biasable by validators who can choose whether to propose. Should be combined with a VRF for high-stakes randomness, not used alone.