Back to Cheat Sheets

🎲 Insecure Randomness

OWASP Smart Contract Top 10 (2025) - SC09

MEDIUM RISK

📋 What Is It?

Insecure Randomness occurs when a smart contract derives a "random" number from on-chain values that are predictable or manipulable. A blockchain is deterministic and fully public: every node must re-execute a transaction and reach the same state, so nothing in the EVM is genuinely random. Values like block.timestamp, a block hash, or block.prevrandao are visible before the transaction commits, computable within the same transaction, or influenceable by the block proposer. An attacker who predicts or biases the outcome wins whatever the randomness was meant to guard-a lottery draw, a game result, a rare NFT mint, or a reward allocation.

SC09 OWASP Rank
Predictable Root Cause
Easy To Exploit

⚠️ Common Exploits

  • Compute-then-revert: attacker recomputes the draw in the same tx, plays only on a win
  • Predictable block hash: blockhash(n-1) is already fixed and public before your tx runs
  • Proposer bias: validator nudges timestamp or skips a slot to steer prevrandao
  • Mempool reordering: front-run/back-run the deterministic result using ordering alone
  • Reveal withholding: commit-reveal without slashing lets a loser never reveal
  • Reveal front-running: the last revealer steers the combined seed
  • Address grinding: seed from msg.sender grinds throwaway/CREATE2 addresses to win
  • Weak PRNG expansion: one public seed plus a counter unrolls every "random" draw

🔴 Attack Flow

1. Identify the seed expression (block/msg variables)
↓
2. Reproduce the exact math in an attacker contract
↓
3. Compute the outcome in the same block
↓
4. IF win → call target; ELSE → revert (pay only gas)
↓
5. IMPACT: Retry each block until the prize pool is drained!

❌ Vulnerable Code

// Bad: lottery winner from public block data contract LotteryVulnerable { address[] public players; function drawWinner() external { // Every input is public or proposer-controlled this block uint256 rand = uint256(keccak256(abi.encodePacked( block.timestamp, ← proposer-set within tolerance blockhash(block.number - 1), ← fixed & public before tx block.prevrandao ← known to proposer, biasable ))); uint256 winnerIndex = rand % players.length; ← predictable payable(players[winnerIndex]).transfer(address(this).balance); } } // Bad: coin flip from keccak256 of block variables function flip(bool guess) external returns (bool won) { bool outcome = uint256(keccak256(abi.encodePacked( block.timestamp, blockhash(block.number - 1) ))) % 2 == 0; won = (guess == outcome); ← attacker precomputes, always wins if (won) payable(msg.sender).transfer(1 ether); } // Bad: NFT rarity + identity seed, derivable at mint time function mint() external payable returns (uint256 id) { id = nextId++; uint256 roll = uint256(keccak256(abi.encodePacked( block.timestamp, block.prevrandao, msg.sender, id ← msg.sender is attacker-grindable ))) % 1000; _assignRarity(id, roll); ← bot mints only rare rolls, reverts rest }

✅ Secure Code

// Good: Chainlink VRF - randomness arrives in a LATER tx with an on-chain proof import {VRFConsumerBaseV2} from "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol"; import {VRFCoordinatorV2Interface} from "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol"; contract LotterySecure is VRFConsumerBaseV2 { VRFCoordinatorV2Interface immutable COORDINATOR; uint64 immutable subId; bytes32 immutable keyHash; uint16 constant CONFIRMATIONS = 3; // blunt reorg manipulation uint32 constant CALLBACK_GAS = 200000; uint32 constant NUM_WORDS = 1; address[] public players; mapping(uint256 => bool) public pending; // requestId -> awaiting fulfil // 1) REQUEST: no usable randomness exists in THIS transaction function drawWinner() external returns (uint256 requestId) { requestId = COORDINATOR.requestRandomWords( keyHash, subId, CONFIRMATIONS, CALLBACK_GAS, NUM_WORDS); pending[requestId] = true; } // 2) FULFIL: coordinator calls back ONLY after the VRF proof verifies function fulfillRandomWords(uint256 requestId, uint256[] memory words) internal override { require(pending[requestId], "unknown request"); pending[requestId] = false; uint256 winnerIndex = words[0] % players.length; ← proven random payable(players[winnerIndex]).transfer(address(this).balance); delete players; } } // Alternative (no oracle): commit-reveal WITH deposit, slashing, timeout // COMMIT locks keccak256(secret, salt) + a slashable deposit BEFORE outcome data exists. function reveal(uint256 secret, bytes32 salt) external { require(keccak256(abi.encodePacked(secret, salt)) == e.commitment, "bad reveal"); seedAccumulator = keccak256(abi.encodePacked(seedAccumulator, secret)); // mix EVERY secret payable(msg.sender).transfer(e.deposit); // returned only on honest reveal } // Unrevealed deposits are FORFEIT after the deadline → withholding costs more than it saves.

✓ Prevention Checklist

  • Never seed value-bearing randomness from block or message variables
  • Use a verifiable randomness oracle (Chainlink VRF) with an on-chain proof
  • Separate the randomness request from its consumption across two transactions
  • Ensure the outcome does not exist in the tx that acts on it (kills compute-then-revert)
  • Require enough block confirmations to resist reorgs on high-value draws
  • If on-chain only, use commit-reveal with deposits, slashing, and timeouts
  • Salt commitments as keccak256(secret, salt) to stop brute-forcing
  • Bind and mix every revealed secret so no last actor can steer the seed
  • Close the entry set before randomness is requested
  • Charge a non-refundable entry cost to remove free retries
  • Assume an adversary who is also a block proposer and can revert on a loss
  • Do not rely on msg.sender == tx.origin as the primary control

🔍 Detection & Tools

Analysis Tools:

Slither Mythril Echidna Foundry Manticore

Secure Randomness & Libraries:

Chainlink VRF OpenZeppelin

How to Test:

  • Run Slither: it flags weak-prng / block-timestamp / block-value usage
  • Grep for banned sources: block.timestamp, blockhash, prevrandao, difficulty, coinbase, gasleft
  • Write an adversarial test that reproduces the contract's own RNG expression
  • Add a compute-then-revert attacker contract and assert it cannot force a win
  • Review specifically for proposer bias and reveal withholding, not just "looks random"
  • Confirm the randomness is delivered in a later tx than the one requesting it

🌍 Real-World Incident Classes

  • Predictable lottery / raffle draws: the winner index is a keccak256 of block variables (timestamp, block hash, prevrandao) read at draw time. Because those values are readable at execution, an entrant computes the winning ticket first and enters only in blocks where it wins-draining the pool over many rounds.
  • On-chain games of chance (dice / coin-flip / slots): the bet is placed and resolved in the same transaction from a block value. A wrapper contract computes the roll first and only bets on a guaranteed win, using free reverts to retry until the house edge inverts.
  • NFT rarity / mint-order sniping: trait rarity is derived at mint from block variables or the minter's own inputs. Bots precompute which mint yields a rare token, mint only the valuable tiers (reverting on common results), and capture the collection's tail of value.
  • Proposer-biased draws: when the prize exceeds a block reward, the block producer nudges block.timestamp within tolerance or skips a slot to influence prevrandao-bending even "future block" designs toward the adversary who builds that block.
  • Commit-reveal games gamed by withholding: without a slashable deposit or timeout, a participant who dislikes the pending outcome simply never reveals, forcing a re-draw or stalling settlement at no cost.

Note: these are recurring classes of exploit, not claims about a single named victim. Exact loss totals differ between reports and many exploited contracts are never publicly attributed; the durable lesson is that on-chain randomness derived from block or message data is predictable, cheap to exploit, and repeatedly abused.

📌 Quick Tips

  • DO NOT seed randomness from block.timestamp or blockhash
  • DO NOT trust prevrandao/difficulty for high-value payouts
  • DO NOT resolve a bet or draw in the same transaction that places it
  • DO use a verifiable oracle (Chainlink VRF) with an on-chain proof
  • DO split the randomness request from its consumption
  • DO harden commit-reveal with deposits, slashing, and timeouts
  • DO require confirmations to resist reorg manipulation