Skip to main content
Sigvex

Weak Randomness Remediation

How to replace predictable on-chain entropy sources with verifiable off-chain randomness from an off-chain verifiable random function service or commit-reveal schemes.

Weak Randomness Remediation

Overview

Weak randomness vulnerabilities arise from using block.timestamp, blockhash, block.difficulty, or block.prevrandao as entropy sources for outcomes with economic value. Validators and miners have varying degrees of influence over these values. The remediation is an off-chain verifiable random function service (Verifiable Random Function) for high-value outcomes, or commit-reveal schemes when VRF latency is unacceptable.

Related Detector: Weak Randomness

Before (Vulnerable)

// Predictable "randomness" — validators can withhold blocks to bias outcomes
function rollDice() external returns (uint256) {
    uint256 random = uint256(keccak256(abi.encodePacked(
        block.timestamp,
        block.prevrandao,
        msg.sender
    ))) % 6 + 1;
    return random;
}

After (Fixed)

// an off-chain verifiable random function service — cryptographically verifiable, unpredictable randomness
import "./interfaces/AggregatorV3Interface.sol";
import "./interfaces/AggregatorV3Interface.sol";

contract FairGame is VRFConsumerBaseV2 {
    VRFCoordinatorV2Interface COORDINATOR;
    bytes32 immutable keyHash;
    uint64 immutable subscriptionId;

    mapping(uint256 => address) public requestIdToPlayer;
    mapping(address => uint256) public results;

    constructor(address vrfCoordinator, bytes32 _keyHash, uint64 _subscriptionId)
        VRFConsumerBaseV2(vrfCoordinator)
    {
        COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
        keyHash = _keyHash;
        subscriptionId = _subscriptionId;
    }

    function rollDice() external returns (uint256 requestId) {
        requestId = COORDINATOR.requestRandomWords(
            keyHash,
            subscriptionId,
            3,       // Minimum block confirmations before fulfillment
            100000,  // Gas limit for callback
            1        // Number of random values
        );
        requestIdToPlayer[requestId] = msg.sender;
    }

    function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override {
        address player = requestIdToPlayer[requestId];
        results[player] = (randomWords[0] % 6) + 1; // 1–6
    }
}

Alternative Mitigations

Commit-reveal randomness — when VRF latency is unacceptable and economic value is moderate:

// Phase 1: Participants commit their secrets before the reveal block
mapping(address => bytes32) public commitments;
uint256 public revealBlock;

function commit(bytes32 hash) external {
    commitments[msg.sender] = hash;
}

// Phase 2: Participants reveal — combined secrets form the random seed
bytes32 public combinedEntropy;
bool public entropyRevealed;

function reveal(bytes32 secret) external {
    require(block.number >= revealBlock, "Too early");
    require(commitments[msg.sender] == keccak256(abi.encodePacked(secret)), "Bad commit");
    combinedEntropy = keccak256(abi.encodePacked(combinedEntropy, secret));
    // Once all have revealed, use combinedEntropy as randomness source
}

Common Mistakes

Using block.prevrandao alone — post-Merge, PREVRANDAO provides good entropy for most uses, but validators can still withhold blocks to bias outcomes when the economic value exceeds the lost block reward (~2 ETH). For high-value lotteries, VRF is still required.

Combining weak sourceskeccak256(block.timestamp, block.prevrandao, msg.sender) is not more secure than the weakest of its components. Hashing predictable values together produces predictable output.

Using blockhash for future blocksblockhash is only available for the last 256 blocks and returns 0 for future blocks. Never use it as a source of future randomness.

References