Randomness and Entropy

There is no on-chain source of true randomness. Every value visible to a smart contract is either deterministic (block fields, transaction inputs, state) or predictable shortly before its use (validator-influenceable values). Any protocol whose outcome depends on randomness — lotteries, raffles, NFT mint reveals, game mechanics, sortition for committees — must source that randomness from outside the chain or accept that block producers can influence it.

The Entropy Illusion

The recurring bug is "we hashed some block fields and a counter, that should be unpredictable enough." It is not. Every input to that hash is either known in advance by the validator producing the block, or known to anyone who can observe the mempool and front-run the result.

// VULNERABLE
function rollDie() external returns (uint256) {
    return uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender))) % 6;
}

block.timestamp is set by the validator producing the block. It is bounded loosely (must be greater than the parent timestamp, not too far in the future) but otherwise chosen by them. A validator who controls block inclusion can:

  • Pick the timestamp that produces the desired outcome.
  • Skip producing the block entirely if no acceptable timestamp exists.
  • Combine the timestamp choice with transaction ordering to maximize their winnings.

Even non-validator attackers can observe the protocol's inputs in the mempool, compute what block.timestamp would need to be, and submit their transaction only when the timestamp window produces a win — or wait through unfavorable blocks until one arrives. A 12-second slot time on Ethereum mainnet is not a meaningful obstacle.

The same logic applies to block.number, block.coinbase, block.gaslimit, and block.basefee.

blockhash

// VULNERABLE
uint256 r = uint256(blockhash(block.number - 1)) % outcomes;

blockhash(n) returns the hash of block n for n in [block.number - 256, block.number - 1], and zero otherwise. Two failure modes:

  • The hash of the current block (or earlier blocks not yet visible to mempool observers) is unknown to outside attackers, but the proposer producing the block knows it before broadcast. A validator-aware exploit picks the winning outcome.
  • A "commit now, reveal at block.number + N" pattern that uses blockhash(commitBlock + N) is exploitable if N is small enough that the user can choose to act or not act based on the eventual hash, and the protocol allows the user to abandon a losing claim.

blockhash returns zero outside the 256-block window. Any contract that references blockhash for a far-future block effectively gets 0 as its randomness — exploitable trivially.

block.difficulty and PREVRANDAO

Pre-merge, block.difficulty returned the proof-of-work difficulty — predictable from chain state, useless as randomness. Post-merge (EIP-4399), the same opcode returns the prevRandao value: a 256-bit number contributed by the previous slot's RANDAO mixing. This is less manipulable than block.timestamp — the value is fixed one slot in advance and a validator who controls the producing slot has only a one-bit influence (include their slot, or skip and lose the block reward). For low-stakes applications this can be sufficient. For high-value lotteries or randomness-determining-large-payouts, the validator's expected-value calculation may still favor skipping.

uint256 r = block.prevrandao;   // post-merge, was block.difficulty

The Solidity block.prevrandao global aliases the same opcode. Treat it as: predictable to validators one slot ahead, biasable by validators willing to forgo block rewards, observable to everyone else only after the producing slot commits.

Acceptable Patterns

Chainlink VRF, Pyth Entropy, API3 QRNG, and similar services provide cryptographically-verifiable randomness from an off-chain source. The pattern:

  1. The contract requests randomness, paying a fee.
  2. The oracle generates the value off-chain and posts it on-chain along with a proof.
  3. The contract verifies the proof and consumes the value.

The trust assumption shifts from "no validator can manipulate this" to "the oracle network does not collude with the protocol's adversary." For most use cases that is a substantial improvement. Audit considerations:

  • Subscription / funding state. A VRF callback that runs out of LINK silently fails to deliver — the protocol must handle the no-delivery case.
  • Callback gas limit. Set high enough to cover the full consumer logic; too low truncates the callback.
  • Fulfillment race. The fulfillment transaction is in the public mempool. Anything the contract reveals in the callback is observable; structure the protocol so observers cannot front-run user decisions after the random value is known.
  • Re-request semantics. If the contract allows re-requesting on timeout, the first eventual fulfillment must take precedence to prevent the requester from cherry-picking.

Commit-Reveal

Two transactions, separated by enough blocks to prevent the committer from acting on the eventual reveal:

  1. Commit. User submits keccak256(secret || nonce). Stored on-chain.
  2. Reveal. User submits secret. Contract verifies it matches the commitment and uses secret (perhaps combined with prevrandao or other users' secrets) as the random input.

The protocol must:

  • Require all participants to reveal, or treat non-revealers as having committed address(0) / 0x00. Otherwise an adversary who sees the eventual outcome will likely lose simply refuses to reveal.
  • Use enough independent commits that no single non-revealer can determine the outcome.
  • Mix in prevrandao or a future block hash at reveal time so the committer cannot fully control the result by choosing their secret.

Commit-reveal is exploitable when there is only one participant (the only "committer" is also the only "consumer of the result," so they can simply not reveal losing outcomes) and when reveal deadlines allow rational non-reveal.

Threshold and BLS-Based Randomness

drand, RANDAO with delays, and similar threshold-randomness networks produce values that no single participant can predict, with cryptographic verification on-chain. These are most useful for protocols willing to consume randomness with a multi-block delay (drand emits values on a fixed cadence) and to integrate the verification logic (BLS signature verification, possibly via the EIP-2537 precompile — see §4.14.5).

Common Misuses Even With Good Sources

  • Revealing the random value in the same transaction the user can react to. If requestRandomness and the action gated by the result are in the same callable function, the user can simulate the call and revert if the outcome is unfavorable. Always separate request from consumption — the random source delivers via callback or in a subsequent transaction the user does not control.
  • Allowing users to choose which random value applies to them. Multi-batch fulfillment that lets a user pick the favorable batch defeats the source.
  • Using the random value modulo n where n does not evenly divide 2^256. Introduces statistical bias toward small values. For game-theoretic randomness this is rarely exploitable, but for cryptographic uses (key generation, nonce derivation) it matters. Use rejection sampling or wider domains.
  • Combining a verifiable random value with block.timestamp. The good source is degraded by the manipulable one. If the protocol needs to combine multiple inputs, every input must be unmanipulable.

Auditor Checklist

  • No block.timestamp, block.number, block.prevrandao, block.coinbase, block.gaslimit, block.basefee, or blockhash used as the sole randomness source for any outcome with value greater than the cost of a validator skipping a block.
  • No private state variable used as a "seed" (see §4.18.3private is not hidden).
  • Randomness sourced from VRF, Pyth Entropy, drand, or commit-reveal with multiple participants and enforced reveal.
  • Request and consumption of randomness are in separate transactions or behind a callback the user cannot block.
  • Callback gas limits, subscription funding, and timeout/re-request semantics are handled.
  • Modulo-bias is acceptable for the use case, or rejection sampling is implemented.
  • No combination of a good random source with a manipulable input.