Generating random numbers in Solidity is a complex problem because blockchain is a deterministic environment that cannot directly generate truly random numbers. Here are various random number generation solutions and their security analysis.
1. Insecure Random Number Generation Methods
Using Block Hash (Insecure)
soliditycontract InsecureRandom { // Dangerous: Miners can manipulate block hash function getRandomNumber() public view returns (uint256) { return uint256(keccak256(abi.encodePacked(blockhash(block.number - 1)))); } // Dangerous: Miners can manipulate timestamp function getRandomFromTimestamp() public view returns (uint256) { return uint256(keccak256(abi.encodePacked(block.timestamp))); } // Dangerous: All parameters can be manipulated by miners function getRandomInsecure() public view returns (uint256) { return uint256(keccak256(abi.encodePacked( block.timestamp, block.difficulty, msg.sender ))); } }
Why are these methods insecure?
- Miners can manipulate
blockhash,block.timestamp,block.difficulty - Attackers can predict results and selectively submit transactions
- All on-chain data is publicly visible
2. Commit-Reveal Scheme
This is a two-phase submission scheme that prevents front-running attacks.
soliditycontract CommitReveal { struct Commit { bytes32 commitHash; uint256 revealDeadline; bool revealed; } mapping(address => Commit) public commits; // Phase 1: Submit hash function commit(bytes32 _commitHash) external { require(commits[msg.sender].commitHash == bytes32(0), "Already committed"); commits[msg.sender] = Commit({ commitHash: _commitHash, revealDeadline: block.number + 10, revealed: false }); } // Phase 2: Reveal original value function reveal(uint256 _secret, uint256 _guess) external { Commit storage userCommit = commits[msg.sender]; require(userCommit.commitHash != bytes32(0), "No commit found"); require(!userCommit.revealed, "Already revealed"); require(block.number <= userCommit.revealDeadline, "Reveal period ended"); // Verify hash bytes32 revealHash = keccak256(abi.encodePacked(_secret, _guess)); require(revealHash == userCommit.commitHash, "Invalid reveal"); userCommit.revealed = true; // Generate random number uint256 randomNumber = uint256(keccak256(abi.encodePacked( _secret, blockhash(userCommit.revealDeadline) ))); // Use random number... } // Helper function to calculate commit hash function getCommitHash(uint256 _secret, uint256 _guess) external pure returns (bytes32) { return keccak256(abi.encodePacked(_secret, _guess)); } }
3. Chainlink VRF (Verifiable Random Function)
Chainlink VRF is currently the most secure on-chain random number solution.
solidityimport "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol"; import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol"; contract RandomNumberConsumer is VRFConsumerBaseV2 { VRFCoordinatorV2Interface COORDINATOR; // Subscription ID uint64 s_subscriptionId; // Gas lane key hash bytes32 keyHash; // Callback gas limit uint32 callbackGasLimit = 100000; // Number of confirmations uint16 requestConfirmations = 3; // Number of random words requested uint32 numWords = 1; // Store request ID and corresponding game data mapping(uint256 => address) public requestToGame; mapping(uint256 => uint256[]) public requestIdToRandomWords; event RandomWordsRequested(uint256 requestId, address game); event RandomWordsFulfilled(uint256 requestId, uint256[] randomWords); constructor( uint64 subscriptionId, address vrfCoordinator, bytes32 _keyHash ) VRFConsumerBaseV2(vrfCoordinator) { COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator); s_subscriptionId = subscriptionId; keyHash = _keyHash; } // Request random words function requestRandomWords() external returns (uint256 requestId) { requestId = COORDINATOR.requestRandomWords( keyHash, s_subscriptionId, requestConfirmations, callbackGasLimit, numWords ); requestToGame[requestId] = msg.sender; emit RandomWordsRequested(requestId, msg.sender); } // Chainlink callback function function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override { requestIdToRandomWords[requestId] = randomWords; emit RandomWordsFulfilled(requestId, randomWords); // Use random number for game logic address game = requestToGame[requestId]; // ... } }
4. Using Oracle
Obtain off-chain random numbers through oracle.
solidityinterface IOracle { function requestRandomNumber() external returns (uint256 requestId); function getRandomNumber(uint256 requestId) external view returns (uint256); } contract OracleRandom { IOracle public oracle; mapping(uint256 => bool) public usedRandomNumbers; event RandomNumberRequested(uint256 requestId); event RandomNumberReceived(uint256 randomNumber); constructor(address _oracle) { oracle = IOracle(_oracle); } function requestRandom() external returns (uint256 requestId) { requestId = oracle.requestRandomNumber(); emit RandomNumberRequested(requestId); return requestId; } function useRandom(uint256 requestId) external returns (uint256) { uint256 randomNumber = oracle.getRandomNumber(requestId); require(!usedRandomNumbers[randomNumber], "Random number already used"); usedRandomNumbers[randomNumber] = true; emit RandomNumberReceived(randomNumber); return randomNumber; } }
5. Multi-Party Computation (MPC) Scheme
Generate random numbers through multiple participants.
soliditycontract MPCRandom { struct Contribution { bytes32 hash; uint256 value; bool revealed; } mapping(address => Contribution) public contributions; address[] public participants; uint256 public revealDeadline; bool public randomGenerated; uint256 public randomNumber; event ContributionSubmitted(address participant); event RandomNumberGenerated(uint256 randomNumber); modifier onlyParticipant() { require(isParticipant(msg.sender), "Not a participant"); _; } function isParticipant(address _addr) public view returns (bool) { for (uint i = 0; i < participants.length; i++) { if (participants[i] == _addr) return true; } return false; } // Phase 1: Submit hash of contribution function submitHash(bytes32 _hash) external onlyParticipant { require(block.number < revealDeadline, "Submission period ended"); require(contributions[msg.sender].hash == bytes32(0), "Already submitted"); contributions[msg.sender].hash = _hash; emit ContributionSubmitted(msg.sender); } // Phase 2: Reveal contribution value function reveal(uint256 _value, uint256 _nonce) external onlyParticipant { require(block.number >= revealDeadline, "Reveal not started"); require(!contributions[msg.sender].revealed, "Already revealed"); bytes32 expectedHash = keccak256(abi.encodePacked(_value, _nonce)); require(expectedHash == contributions[msg.sender].hash, "Invalid reveal"); contributions[msg.sender].value = _value; contributions[msg.sender].revealed = true; } // Generate random number function generateRandom() external { require(block.number > revealDeadline + 10, "Wait for all reveals"); require(!randomGenerated, "Random already generated"); uint256 combined = 0; for (uint i = 0; i < participants.length; i++) { if (contributions[participants[i]].revealed) { combined ^= contributions[participants[i]].value; } } randomNumber = uint256(keccak256(abi.encodePacked(combined, blockhash(block.number - 1)))); randomGenerated = true; emit RandomNumberGenerated(randomNumber); } }
6. Random Number Usage Scenarios
NFT Random Minting
soliditycontract NFTRandomMint is VRFConsumerBaseV2 { // ... VRF settings ... struct MintRequest { address minter; uint256 tokenId; bool fulfilled; } mapping(uint256 => MintRequest) public mintRequests; uint256 public nextTokenId; function requestMint() external returns (uint256 requestId) { requestId = requestRandomWords(); mintRequests[requestId] = MintRequest({ minter: msg.sender, tokenId: nextTokenId++, fulfilled: false }); } function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override { MintRequest storage request = mintRequests[requestId]; request.fulfilled = true; // Use random number to determine NFT attributes uint256 randomness = randomWords[0]; uint256 rarity = randomness % 100; // 0-99 // Mint NFT _safeMint(request.minter, request.tokenId); // Set attributes if (rarity < 5) { _setTokenRarity(request.tokenId, "Legendary"); } else if (rarity < 20) { _setTokenRarity(request.tokenId, "Epic"); } else if (rarity < 50) { _setTokenRarity(request.tokenId, "Rare"); } else { _setTokenRarity(request.tokenId, "Common"); } } }
Game Random Results
soliditycontract DiceGame is VRFConsumerBaseV2 { struct Game { address player; uint256 bet; uint256 predictedNumber; bool fulfilled; uint256 result; } mapping(uint256 => Game) public games; event GameStarted(uint256 requestId, address player, uint256 bet); event GameFinished(uint256 requestId, uint256 result, bool won); function play(uint256 _predictedNumber) external payable returns (uint256 requestId) { require(msg.value >= 0.01 ether, "Minimum bet is 0.01 ETH"); require(_predictedNumber >= 1 && _predictedNumber <= 6, "Predict 1-6"); requestId = requestRandomWords(); games[requestId] = Game({ player: msg.sender, bet: msg.value, predictedNumber: _predictedNumber, fulfilled: false, result: 0 }); emit GameStarted(requestId, msg.sender, msg.value); } function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override { Game storage game = games[requestId]; game.fulfilled = true; // Generate dice result 1-6 game.result = (randomWords[0] % 6) + 1; bool won = (game.result == game.predictedNumber); if (won) { // Pay reward (6x payout) payable(game.player).transfer(game.bet * 6); } emit GameFinished(requestId, game.result, won); } }
7. Solution Comparison
| Solution | Security | Cost | Latency | Use Cases |
|---|---|---|---|---|
| Block Hash | Low | Free | Instant | Test environment |
| Commit-Reveal | Medium | Low | High | Simple games |
| Chainlink VRF | High | Medium | Medium | Production environment |
| Oracle | Medium | Medium | Medium | Specific needs |
| MPC | High | High | High | High-value scenarios |
8. Best Practices
- Use Chainlink VRF in production: Safest and most reliable solution
- Avoid using on-chain data for randomness: All on-chain data can be manipulated
- Implement retry mechanism: Handle VRF callback failures
- Limit random number usage scope: Avoid reusing the same random number
- Add time lock: Give users exit time
solidity// Safe random number usage pattern contract SafeRandomUsage { uint256 public constant MAX_RANDOM = 10000; mapping(uint256 => bool) public usedRandoms; function useRandom(uint256 _random) internal returns (uint256) { uint256 normalized = _random % MAX_RANDOM; require(!usedRandoms[normalized], "Random already used"); usedRandoms[normalized] = true; return normalized; } }