Skip to main content
Sigvex

Weak Randomness

Detects use of predictable on-chain values — blockhash, block.timestamp, block.prevrandao — as sources of randomness, which miners or validators can manipulate.

Weak Randomness

Overview

Remediation Guide: How to Fix Weak Randomness

The weak randomness detector flags functions that use predictable block properties — block.timestamp, blockhash, block.difficulty, or (post-Merge) block.prevrandao — as the sole or primary source of entropy for outcomes that have economic value. Miners (pre-Merge) and validators (post-Merge) have influence over these values and can withhold unfavorable blocks to bias outcomes.

Sigvex detects this by scanning for the relevant EVM opcodes (TIMESTAMP, BLOCKHASH, DIFFICULTY, PREVRANDAO) in functions that also contain conditional branching or value transfers whose outcome depends on a comparison against a hashed or manipulated form of those values. The detector also recognizes patterns where keccak256 is applied to block properties in an attempt to create randomness that is still predictable.

Why This Is an Issue

On-chain randomness has no truly unpredictable source without a commit-reveal scheme or a verifiable random function (VRF). A validator producing a block knows block.timestamp and block.prevrandao before committing. If the gamble result is unfavorable, they can simply withhold the block and try again with a different timestamp. The cost is one missed block reward; the gain can be much larger in high-stakes contracts.

Notable exploits include: FOMO3D (2018, miners selectively withheld blocks to win the jackpot), Lottery.sol (multiple incidents), and various NFT minting contracts where blockhash determined mint rarity.

How to Resolve

// Before: Vulnerable — randomness from block properties
function rollDice() external returns (uint256) {
    uint256 randomNumber = uint256(
        keccak256(abi.encodePacked(block.timestamp, block.difficulty, msg.sender))
    ) % 6; // PREDICTABLE: validator knows this before block is finalized
    return randomNumber;
}

// After: Use the off-chain aggregator VRF for verifiable randomness
import "@audited/VRFConsumerBaseV2.sol";
import "@audited/interfaces/VRFCoordinatorV2Interface.sol";

contract SecureDice is VRFConsumerBaseV2 {
    VRFCoordinatorV2Interface private vrfCoordinator;
    bytes32 private keyHash;
    uint64 private subscriptionId;

    mapping(uint256 => address) private requestToPlayer;

    function rollDice() external returns (uint256 requestId) {
        requestId = vrfCoordinator.requestRandomWords(
            keyHash, subscriptionId, 3, 100000, 1
        );
        requestToPlayer[requestId] = msg.sender;
    }

    function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords)
        internal override
    {
        uint256 result = (randomWords[0] % 6) + 1;
        // Use result for the player stored in requestToPlayer[requestId]
    }
}

Examples

Vulnerable Code

contract VulnerableLottery {
    address[] public participants;
    uint256 public jackpot;

    function enterLottery() external payable {
        require(msg.value == 0.1 ether);
        participants.push(msg.sender);
        jackpot += msg.value;
    }

    function pickWinner() external {
        require(participants.length > 0);
        // VULNERABLE: block.timestamp and blockhash are known to validators
        uint256 seed = uint256(
            keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))
        );
        uint256 winnerIdx = seed % participants.length;
        address winner = participants[winnerIdx];
        delete participants;
        payable(winner).transfer(jackpot);
        jackpot = 0;
    }
}

Fixed Code

// Off-chain VRF or commit-reveal scheme
contract SecureLottery {
    // Commit-reveal pattern
    mapping(address => bytes32) public commitments;
    uint256 public revealDeadline;

    function commit(bytes32 commitment) external payable {
        require(msg.value == 0.1 ether);
        commitments[msg.sender] = commitment;
    }

    function reveal(uint256 secret) external {
        require(block.timestamp <= revealDeadline);
        bytes32 commitment = keccak256(abi.encodePacked(secret, msg.sender));
        require(commitments[msg.sender] == commitment, "Invalid reveal");
        // Combine all revealed secrets for final randomness
    }
}

Sample Sigvex Output

{
  "detector_id": "weak-randomness",
  "severity": "high",
  "confidence": 0.91,
  "description": "Function pickWinner() uses block.timestamp and blockhash as randomness sources. Validators can manipulate these values to bias lottery outcomes.",
  "location": { "function": "pickWinner()", "offset": 156 }
}

Detection Methodology

  1. Entropy source detection: Identifies uses of TIMESTAMP, BLOCKHASH, DIFFICULTY, PREVRANDAO opcodes.
  2. Randomness derivation tracking: Checks whether these values flow through keccak256 or other hash operations intended to produce randomness.
  3. Economic outcome detection: Determines whether the derived value influences a MODULO operation, JUMPI branching, or token/ether transfer that has financial impact.
  4. VRF recognition: Identifies calls to the off-chain aggregator VRF coordinator contracts and suppresses findings for those functions.

Limitations

False positives:

  • Timestamp usage for non-randomness purposes (deadlines, cooldowns) may sometimes be caught if the pattern resembles randomness derivation.
  • blockhash used for linking blocks or merkle proofs — not as randomness — can trigger false positives.

False negatives:

  • Commit-reveal schemes that are not sufficiently random (e.g., low-entropy user inputs) are not detected.
  • Multi-party computation schemes that appear as simple hash operations may be missed.

References