WTF Solidity 39. On-chain Randomness
I am currently re-learning Solidity to sharpen my skills and writing a "WTF Solidity Tutorial" guide for beginners to use (advanced programmers can look for other tutorials). I will update 1-3 lectures every week.
Twitter: @0xAA_Science
Discord: WTF Academy
All code and tutorials are open source on GitHub: github.com/AmazingAng/WTFSolidity
Many Ethereum applications require the use of random numbers, such as NFT random tokenId selection, blind box drawing, and randomly determining the winner in gamefi battles. However, since all data on Ethereum is public and deterministic, it cannot provide developers with a method of generating random numbers like other programming languages. In this tutorial, we will introduce two methods of on-chain (hash function) and off-chain (Chainlink oracle) random number generation, and use them to create a tokenId random minting NFT.
On-chain Random Number Generation
We can use some on-chain global variables as seeds and use the keccak256()
hash function to obtain pseudo-random numbers. This is because the hash function has sensitivity and uniformity, and can generate "apparently" random results. The getRandomOnchain()
function below uses the global variables block.timestamp
, msg.sender
, and blockhash(block.number-1)
as seeds to obtain random numbers:
/**
* Generating pseudo-random numbers on the chain.
* Using keccak256() to pack some on-chain global variables/custom variables.
* Converted to uint256 type when returned.
*/
function getRandomOnchain() public view returns(uint256){
// Generating blockhash in Remix will result in an error.
bytes32 randomBytes = keccak256(abi.encodePacked(block.timestamp, msg.sender, blockhash(block.number-1)));
return uint256(randomBytes);
}
Note: This method is not secure:
- Firstly, variables such as
block.timestamp
,msg.sender
, andblockhash(block.number-1)
are all publicly visible. Users can predict the random number generated by these seeds and select the output they want to execute the smart contract. - Secondly, miners can manipulate
blockhash
andblock.timestamp
to generate a random number that suits their interests.
However, this method is the most convenient on-chain random number generation method, and many project parties rely on it to generate insecure random numbers, including well-known projects such as meebits
and loots
. Of course, all these projects have been attacked: attackers can forge any rare NFT
they want, instead of randomly drawing them.
Off-chain random number generation
We can generate random numbers off-chain and upload them to the chain through oracles. Chainlink provides a VRF (Verifiable Random Function) service, and on-chain developers can pay the LINK token to obtain a random number. Chainlink VRF has two versions. Since the second version requires registration on the official website and prepaid fees, and the usage is similar, only the first version VRF v1 is introduced here.
Steps to use Chainlink VRF
We will use a simple contract to introduce the steps to use Chainlink VRF. The RandomNumberConsumer
contract can request a random number from the VRF and store it in the state variable randomResult
.
1. The user contract inherits from VRFConsumerBase
and transfers the LINK
token
To use VRF to obtain a random number, the contract needs to inherit the VRFConsumerBase
contract and initialize the VRF Coordinator
address, LINK
token address, unique identifier Key Hash
, and usage fee fee
in the constructor.
Note: Different chains correspond to different parameters, please refer to here to find out.
In the tutorial, we use the Rinkeby
testnet. After deploying the contract, users need to transfer some LINK
tokens to the contract. Testnet LINK
tokens can be obtained from the LINK faucet.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";
contract RandomNumberConsumer is VRFConsumerBase {
bytes32 internal keyHash; // VRF唯一标识符
uint256 internal fee; // VRF使用手续费
uint256 public randomResult; // 存储随机数
/**
* 使用chainlink VRF,构造函数需要继承 VRFConsumerBase
* 不同链参数填的不一样
* 网络: Rinkeby测试网
* Chainlink VRF Coordinator 地址: 0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B
* LINK 代币地址: 0x01BE23585060835E02B77ef475b0Cc51aA1e0709
* Key Hash: 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311
*/
constructor()
VRFConsumerBase(
0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B, // VRF Coordinator
0x01BE23585060835E02B77ef475b0Cc51aA1e0709 // LINK Token
)
{
keyHash = 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311;
fee = 0.1 * 10 ** 18; // 0.1 LINK (VRF使用费,Rinkeby测试网)
}
2. User requests random number through contract
Users can call requestRandomness()
inherited from the VRFConsumerBase
contract to request a random number and receive a request identifier requestId
. This request will be passed on to the VRF
contract.
/**
* Request a random number from the VRF contract
*/
function getRandomNumber() public returns (bytes32 requestId) {
// The contract needs to have sufficient LINK
require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK - fill contract with faucet");
return requestRandomness(keyHash, fee);
}
- The
Chainlink
node generates a random number and a digital signature off-chain, and sends them to theVRF
contract. - The
VRF
contract verifies the validity of the signature. - The user contract receives and uses the random number.
After verifying the validity of the signature in the VRF
contract, the fallback function fulfillRandomness()
of the user contract will be automatically called, and the off-chain generated random number will be sent over. The logic of consuming the random number should be implemented in this function.
Note: The requestRandomness()
function called by the user to request a random number and the fallback function fulfillRandomness()
called when the VRF
contract returns the random number are two separate transactions, with the user contract and the VRF
contract being the callers, respectively. The latter will be a few minutes later than the former (with different chain delays).
/**
* VRF合约的回调函数,验证随机数有效之后会自动被调用
* 消耗随机数的逻辑写在这里
*/
function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
randomResult = randomness;
}
tokenId
Randomly Minted NFT
In this section, we will use on-chain and off-chain random numbers to create a tokenId
randomly minted NFT
. The Random
contract inherits from both the ERC721
and VRFConsumerBase
contracts.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "https://github.com/AmazingAng/WTFSolidity/blob/main/34_ERC721/ERC721.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";
contract Random is ERC721, VRFConsumerBase{
State Variables
NFT
relatedtotalSupply
: Total supply ofNFT
.ids
: Array used for calculatingtokenId
s that can beminted
, seepickRandomUniqueId()
function.mintCount
: Number of NFTs that have beenminted
.
Chainlink VRF
relatedkeyHash
: Unique identifier forVRF
.fee
:VRF
fee.requestToSender
: Records the user address that applied forVRF
for minting.
// NFT Related
uint256 public totalSupply = 100; // Total supply
uint256[100] public ids; // Used to calculate the tokenId that can be minted
uint256 public mintCount; // Number of minted tokens
// Chainlink VRF Related
bytes32 internal keyHash; // Key hash for Chainlink VRF
uint256 internal fee; // Fee for Chainlink VRF
// Records the mint address corresponding to the VRF request identifier
mapping(bytes32 => address) public requestToSender;
Constructor
Initialize the relevant variables of the inherited VRFConsumerBase
and ERC721
contracts.
/**
* Using Chainlink VRF, the constructor needs to inherit from VRFConsumerBase
* Different chain parameters are filled differently
* Network: Rinkeby Testnet
* Chainlink VRF Coordinator address: 0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B
* LINK token address: 0x01BE23585060835E02B77ef475b0Cc51aA1e0709
* Key Hash: 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311
**/
constructor()
VRFConsumerBase(
0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B, // VRF Coordinator
0x01BE23585060835E02B77ef475b0Cc51aA1e0709 // LINK Token
)
ERC721("WTF Random", "WTF")
{
keyHash = 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311;
fee = 0.1 * 10 ** 18; // 0.1 LINK (VRF usage fee, Rinkeby test network)
}
Other Functions
In addition to the constructor function, the contract defines 5 other functions:
pickRandomUniqueId()
: takes in a random number and returns atokenId
that can be used for minting.getRandomOnchain()
: returns an on-chain random number (insecure).mintRandomOnchain()
: mints an NFT using an on-chain random number, and callsgetRandomOnchain()
andpickRandomUniqueId()
.mintRandomVRF()
: requests a random number fromChainlink VRF
to mint an NFT. Since the logic for minting with a random number is in the callback functionfulfillRandomness()
, which is called by theVRF
contract, not the user minting the NFT, the function here must use therequestToSender
state variable to record the user address corresponding to theVRF
request identifier.fulfillRandomness()
: the callback function forVRF
, which is automatically called by theVRF
contract after verifying the authenticity of the random number. It uses the returned off-chain random number to mint an NFT.
/**
* Input a uint256 number and return a tokenId that can be mint
* The algorithm process can be understood as: totalSupply empty cups (0-initialized ids) are lined up in a row, and a ball is placed next to each cup, numbered [0, totalSupply - 1].
Every time a ball is randomly taken from the field (the ball may be next to the cup, which is the initial state; it may also be in the cup, indicating that the ball next to the cup has been taken away, then a new ball is placed from the end at this time into the cup)
Then put the last ball (still may be in the cup or next to the cup) into the cup of the removed ball, and loop totalSupply times. Compared with the traditional random arrangement, the gas for initializing ids[] is omitted.
*/
function pickRandomUniqueId(
uint256 random
) private returns (uint256 tokenId) {
// Calculate the subtraction first, then calculate ++, pay attention to the difference between (a++, ++a)
uint256 len = totalSupply - mintCount++; // mint quantity
require(len > 0, "mint close"); // all tokenIds are mint finished
uint256 randomIndex = random % len; // get the random number on the chain
// Take the modulus of the random number to get the tokenId as an array subscript, and record the value as len-1 at the same time. If the value obtained by taking the modulus already exists, then tokenId takes the value of the array subscript
tokenId = ids[randomIndex] != 0 ? ids[randomIndex] : randomIndex; // get tokenId
ids[randomIndex] = ids[len - 1] == 0 ? len - 1 : ids[len - 1]; // update ids list
ids[len - 1] = 0; // delete the last element, can return gas
}
/**
* On-chain pseudo-random number generation
* keccak256(abi.encodePacked() fill in some global variables/custom variables on the chain
* Convert to uint256 type when returning
*/
function getRandomOnchain() public view returns (uint256) {
/*
* In this case, randomness on the chain only depends on block hash, caller address, and block time,
* If you want to improve the randomness, you can add some attributes such as nonce, etc., but it cannot fundamentally solve the security problem
*/
bytes32 randomBytes = keccak256(
abi.encodePacked(
blockhash(block.number - 1),
msg.sender,
block.timestamp
)
);
return uint256(randomBytes);
}
// Use the pseudo-random number on the chain to cast NFT
function mintRandomOnchain() public {
uint256 _tokenId = pickRandomUniqueId(getRandomOnchain()); // Use the random number on the chain to generate tokenId
_mint(msg.sender, _tokenId);
}
/**
* Call VRF to get random number and mintNFT
* To call the requestRandomness() function to obtain, the logic of consuming random numbers is written in the VRF callback function fulfillRandomness()
* Before calling, transfer LINK tokens to this contract
*/
function mintRandomVRF() public returns (bytes32 requestId) {
// Check the LINK balance in the contract
require(
LINK.balanceOf(address(this)) >= fee,
"Not enough LINK - fill contract with faucet"
);
// Call requestRandomness to get a random number
requestId = requestRandomness(keyHash, fee);
requestToSender[requestId] = msg.sender;
return requestId;
}
/**
* VRF callback function, called by VRF Coordinator
* The logic of consuming random numbers is written in this function
*/
function fulfillRandomness(
bytes32 requestId,
uint256 randomness
) internal override {
address sender = requestToSender[requestId]; // Get minter user address from requestToSender
uint256 _tokenId = pickRandomUniqueId(randomness); // Use the random number returned by VRF to generate tokenId
_mint(sender, _tokenId);
}
remix
Verification
1. Deploy the Random
contract on the Rinkeby
testnet
2. Get LINK
and ETH
on the Rinkeby
testnet using Chainlink
faucet
3. Transfer LINK
tokens into the Random
contract
After the contract is deployed, copy the contract address, and transfer LINK
to the contract address just as you would for a normal transfer.
4. Mint NFTs using onchain random numbers
In the remix
interface, click on the orange function mintRandomOnchain
on the left side
Metamask
to start minting the transaction using onchain random numbers.5. Mint NFTs using Chainlink VRF
offchain random numbers
Similarly, in the remix
interface, click on the orange function mintRandomVRF
on the left and click confirm in the pop-up little fox wallet. The transaction of minting an NFT
using Chainlink VRF
off-chain random number has started.
Note: when using VRF
to mint NFT
, initiating the transaction and the success of minting are not in the same block.
6. Verify that the NFT
has been minted
From the above screenshots, it can be seen that in this example, the NFT
with tokenId=87
has been randomly minted on-chain, and the NFT
with tokenId=77
has been minted using VRF
.
Conclusion
Generating a random number in Solidity
is not as straightforward as in other programming languages. In this tutorial, we introduced two methods of generating random numbers on-chain (using hash functions) and off-chain (Chainlink
oracle), and used them to create an NFT
with a randomly assigned tokenId
. Both methods have their own advantages and disadvantages: using on-chain random numbers is efficient but insecure, while generating off-chain random numbers relies on third-party oracle services, which is relatively safe but not as easy and economical. Project teams should choose the appropriate method according to their specific business needs.
Apart from these methods, there are other organizations that are trying new ways of RNG (Random Number Generation), such as randao, which proposes to provide an on-chain and true randomness service in a DAO pattern.