Weak Randomness Remediation
How to replace predictable on-chain entropy sources with verifiable off-chain randomness from Chainlink VRF 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 Chainlink VRF (Verifiable Random Function) for high-value outcomes, or commit-reveal schemes when VRF latency is unacceptable.
Related Detector: Weak Randomness
Recommended Fix
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)
// Chainlink VRF v2 — cryptographically verifiable, unpredictable randomness
import "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";
import "@chainlink/contracts/src/v0.8/vrf/interfaces/VRFCoordinatorV2Interface.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 sources — keccak256(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 blocks — blockhash is only available for the last 256 blocks and returns 0 for future blocks. Never use it as a source of future randomness.