How to generate a random number in Solidity
In this tutorial, we are going to learn multiple ways to generate random numbers in Solidity and evaluate which would be the best one. We are going to use pure Solidity and the ChainLink oracle.
In this tutorial, we are going to learn multiple ways to generate random numbers in Solidity and evaluate which would be the best one.w We are going to use pure Solidity and the ChainLink oracle.
Generate a random number using block data
The first solution is to use data like the block timestamp, the block number, the block hash or the block difficulty to generate a hash and then a number out of it.
Here is an example using the block difficulty, the block timestamp and the address of the caller of the function:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract GenerateRandomNumberWithBlockData {
function generateRandomNumber(uint256 max)
external
view
returns (uint256)
{
return
uint256(
keccak256(
abi.encodePacked(
block.timestamp,
block.difficulty,
msg.sender
)
)
) % max;
}
}
In the code above, we use the abi.encodePacked
function to concatenate and convert to hex the block timestamp and difficulty and the address that called the function.
We then pass the result of that function to the keccak256
function. That function hashes the data that we pass to it and returns the Keccak-256 hash that was generated.
Lastly, we convert that hash into a number and return that number modulo the max random number we want.
Using that code you'll get an integer between 0 and the max value passed in the parameters of the generateRandomNumber
function.
The advantage of that method is that it doesn't cost any gas and it's pretty fast.
But the problem with that method is that you can sort of predict what the generated number will be. The block timestamp, the block number and the hash are all predictable with more or less accuracy, but still!
So it's more pseudo-random than random.
The biggest problem is that a validator on the blockchain can choose to publish or not the block that they validated.
So they can just keep mining blocks until the random number that is generated is a number that they like. By doing so, the number generated is not random anymore!
To fix that security issue, we need to generate the random numbers off-chain using a decentralised oracle. The oracle we are going to use (which is the most popular) is ChainLink.
Generate a random number using ChainLink VRF
As said before, we need to generate the random numbers off-chain for better security.
The problem now is that when nodes validate the transaction, they need to be able to verify that the function did the right thing. If the number is generated off-chain, by the time the node validates the transaction, that number will be different and the node won't be able to verify the transaction.
The second problem is that the off-chain source that generates the number needs to be decentralised, otherwise that service can just send you a non-random number that they like.
Fortunately, oracles like ChainLink are decentralised and use verifiable random functions (VRF) that allow anyone to verify that a number was generated randomly and to verify the source of randomness.
That's the go-to solution to generate random numbers in smart contracts.
Here is an example in which I generate a random number using ChainLink VRF V2:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@chainlink/contracts/src/v0.8/VRFV2WrapperConsumerBase.sol";
contract GenerateRandomNumberWithChainLink is VRFV2WrapperConsumerBase {
struct ChainLinkRandomNumber {
address caller;
uint256 number;
bool fulfilled;
}
event RandomNumberGenerated(
uint256 indexed requestId,
address indexed caller,
uint256 number
);
// request ID => random number
mapping(uint256 => ChainLinkRandomNumber) public randomNumbers;
// fee given to the oracle for fulfilling the request (LINK tokens)
uint256 internal fee;
// Test and adjust this limit.
// It depends on the network, the size of the request,
// and the complexity in the fulfillRandomWords() function.
uint32 callbackGasLimit = 100000;
// The number of confirmations to wait for
uint16 confirmations = 3;
// The Goerli testnet contract addresses
address linkToken = 0x326C977E6efc84E512bB9C30f76E30c160eD06FB;
address vrfWrapper = 0x708701a1DfF4f478de54383E49a627eD4852C816;
constructor() VRFV2WrapperConsumerBase(linkToken, vrfWrapper) {
// 0.25 LINK is the flat fee in the docs
fee = 0.25 * 10**18;
}
// Requests a random number from ChainLink
function generateRandomNumber() public returns (uint256) {
require(
LINK.balanceOf(address(this)) > fee,
"Not enough LINK - fill contract with faucet"
);
// Request a random number
uint256 requestId = requestRandomness(
callbackGasLimit,
confirmations,
1
);
randomNumbers[requestId] = ChainLinkRandomNumber(msg.sender, 0, false);
return requestId;
}
// Callback function called by the VRF Coordinator after the random numbers are generated
function fulfillRandomWords(
uint256 _requestId,
uint256[] memory _randomWords
) internal override {
uint256 number = _randomWords[0];
randomNumbers[_requestId].number = number;
randomNumbers[_requestId].fulfilled = true;
emit RandomNumberGenerated(
_requestId,
randomNumbers[_requestId].caller,
number
);
}
}
Here are the steps to follow to get a random number from ChainLink:
1. Install ChainLink to get the contracts we need
First, we need to install the ChainLink contracts using this command:
// With NPM:
npm install @chainlink/contracts
// Or with Yarn:
yarn add @chainlink/contracts
2. Inherit from the VRFV2WrapperConsumerBase contract
Next, we need to import the VRFV2WrapperConsumerBase
contract from ChainLink and have our smart contract inherit from it:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@chainlink/contracts/src/v0.8/VRFV2WrapperConsumerBase.sol";
contract GenerateRandomNumber is VRFV2WrapperConsumerBase {
}
3. Use the VRFV2WrapperConsumerBase constructor
Now, the constructor of our smart contract needs to call the VRFV2WrapperConsumerBase
constructor which takes these in these 2 parameters:
- The address of the LINK token
- The VRF Wrapper contract address
Depending on the network you are using, these values will be different. Check out this page to see all the supported networks and the values you can use for each network.
In the example above, we are on the Goerli testnet so the values we can pass are here and are the following:
- The LINK token address:
0x326C977E6efc84E512bB9C30f76E30c160eD06FB
- The VRF Wrapper contract address:
0x708701a1DfF4f478de54383E49a627eD4852C816
So our constructor looks like this:
address linkToken = 0x326C977E6efc84E512bB9C30f76E30c160eD06FB;
address vrfWrapper = 0x708701a1DfF4f478de54383E49a627eD4852C816;
constructor() VRFV2WrapperConsumerBase(linkToken, vrfWrapper) {
// ...
}
4. Prepare the parameters of the requestRandomness function
Before requesting a random number from ChainLink, we need to prepare a few variables. We need 3 variables:
- The callback's gas limit. That's the maximum amount of gas that the callback function can use (the
fulfillRandomness
function). We'll see that function more in details in the next section. It must not exceedmaxGasLimit - wrapperGasOverhead
. You can find themaxGasLimit
for your network here and thewrapperGasOverhead
here. - The number of confirmations to wait for when getting the random number. It must be more than the minimum amount of confirmations for your network here and the maximum here.
- The number of random numbers to generate. The minimum is one and the maximum for your network can be found here.
For the first value, I usually set 100000
and it's enough but depending on the complexity of your callback function, it might be more. I recommend you create a property for that value instead of hardcoding it and create a method to update that value so you can adjust it if it's not enough.
So in our smart contract, we create 3 properties:
uint32 callbackGasLimit = 100000;
uint16 confirmations = 3;
uint16 numbersToGenerate = 1;
This step is not mandatory but you need to know that you'll need these 3 values.
5. Funding the contract with LINK tokens
Every time you request something from ChainLink, you need to pay fees in LINK tokens. The fee you will pay depends on the request you make. In our case, we pay a flat fee no matter what parameters we pass to requestRandomness
.
You can get more information about the fee applied to your network here. On the Goerli network and on the Mainnet, the fee is 0.25 LINK per request.
So, when you deploy your smart contract, you need to send some LINK tokens to it and these tokens will be used to pay for the fees.
For that, you can either transfer tokens from your wallet to the smart contract if you have tokens in that wallet (that's probably what you will need to do on the Mainnet) or, if you are on a testnet, you can use this faucet to get free tokens: https://faucets.chain.link/
Let's store the flat fee value in a property in our smart contract so we can re-use it later:
uint256 internal fee;
constructor() VRFV2WrapperConsumerBase(linkToken, vrfWrapper) {
fee = 0.25 * 10**18; // 0.25 LINK
}
6. Requesting random numbers
Everything is now set up properly for us to request random numbers from ChainLink. For that, we need to call the requestRandomness
function from the VRFV2WrapperConsumerBase
.
In the parameters of that function we pass the variables we prepared above in the same order:
- The callback gas limit
- The number of confirmations (3 on the Goerli testnet and on the Mainnet)
- The number of random numbers to generate
Now, we can create a function that will call the requestRandomness
function:
// Requests a random number from ChainLink using a user-provided seed
function generateRandomNumberWithChainLink() public returns (uint256) {
require(
LINK.balanceOf(address(this)) > fee,
"Not enough LINK - fill contract with faucet"
);
// Request a random number
uint256 requestId = requestRandomness(
callbackGasLimit,
confirmations,
numbersToGenerate
);
return requestId;
}
Before requesting a random number, we need to make sure that the contract has enough tokens to pay for the fees, otherwise it will fail. So the first thing we can do in that function is require the contract to have enough LINK tokens.
Next, we call the requestRandomness
function which returns the request ID which is a unique uint256
ID that allows you to identify that request.
7. Implementing the callback and getting the random numbers
Since the random number generation is done off-chain, that process is asynchronous. First, we asked the coordinator to generate a random number for us.
Once that transaction is done and the coordinator generated a random number, it calls a callback function in our smart contract called fulfillRandomWords
which is defined in the VRFV2WrapperConsumerBase
contract and that we can override.
Here is how to override it:
function fulfillRandomWords(
uint256 _requestId,
uint256[] memory _randomWords
) internal override {
uint256 number = _randomWords[0];
emit RandomNumberGenerated(_requestId, number);
}
That function takes 2 parameters:
- The ID of the request that was fulfilled
- An array containing the random numbers that were generated.
It contains as many numbers as you requested
In the example above, I just emit an event and pass the request ID and the number that was generated but you're free to do whatever you want in that function.
An important thing to know here is that the random numbers generated are extremely large, since they are uint256
numbers.
If you want that number to have a maximum value, you can use the modulo operator. That way, you'll get an integer between 0 (included) and the modulo value (excluded).
For example, if you want a number between 0 and 99 (included) you can do this in the fulfillRandomWords
function:
function fulfillRandomWords(
uint256 _requestId,
uint256[] memory _randomWords
) internal override {
uint256 number = _randomWords[0] % 100;
emit RandomNumberGenerated(_requestId, number);
}
9. Using subscriptions
If you are going to request random numbers frequently, you might want to use ChainLink's VRF subscriptions instead of direct funding like we've done.
To use ChainLink subscriptions, you'll need to create an account here.
Then, in the Subscription Manager, you can pre-pay for the VRF V2 requests. That way, your smart contract doesn't need to have LINK tokens and pay for fees every time it requests random numbers.
That method also has the advantage of reducing the total gas cost to request random numbers and provides a simple way to fund your use of Chainlink products from a single location.
You can follow this guide in the documentation to get a random number using subscriptions. The main difference is how you call the requestRandomness
:
And you can get more information about subscriptions in general here:
The main disadvantage of using ChainLink or any other oracle is that it has a cost. But that's the price to pay for your smart contract to have security.
And that's it 🎉
Thank you for reading this article