Request randomness using Ronin VRF
Overview
This guide demonstrates how to request random data from the Ronin VRF (Verifiable Random Function) service in your smart contracts. The Ronin VRF service is a random number generator that provides random values that are both random and verifiable. Use cases for randomness include generating random token IDs, selecting random winners for contests, and more.
Find additional VRF-related code in the ronin-chain/ronin-random-beacon GitHub repository.
How VRF proof is generated
- The proof is generated from the oracle's secret key, based on requested information that was emitted.
- The contract uses the proof with the original request information struct to recover the oracle's public key hash. This allows verification that the proof was generated by a valid oracle.
- The random seed is created based on the
gamma
field value of theproof
struct, which is a point on the elliptic curve of the oracle's secret key. It isn't influenced by the requested info data. As long as the secret key is also generated randomly, you can treat the random seed as genuine randomness.
Note: It still depends on your implementation to ensure the randomness is preserved and that it's not manipulated by users.
VRF contract addresses
The Ronin VRF Coordinator Proxy contract is deployed at the following addresses:
- Ronin mainnet: 0x16a62a921e7fec5bf867ff5c805b662db757b778
- Saigon testnet: 0xa60c1e07fa030e4b49eb54950adb298ab94dd312
Estimate gas costs
For every randomness request, Ronin VRF charges the consuming contract a service charge of 0.01 USD (in RON) plus an oracle gas fee. Make sure to maintain a sufficient amount of RON so that VRF can fulfill your requests.
Fee structure
- A consumer contract requests randomness with specified gas amount and gas callback in the arguments. This step in our contract takes an amount of RON called
estimate_random_fee
:
estimate_random_fee = service_charge + fulfillment_gas_fee
Where:
service_charge
is pegged to 0.01 USD (paid in RON).fulfillment_gas_fee
is the gas fee for the Oracle service. The amount depends on the requested arguments and includes the 500000 of gas in addition to verifying proofs and generating random seed:
fulfillment_gas_fee = gas_price * (gas_callback + additional_gas)
additional_gas = 500_000
For example, if a request is sent with a callback gas amount of 250000 and the gas price of 20 GWEI, the total amount the contract must pay is as follows:
fulfillment_gas_fee = 20 GWEI * (250_000 + 500_000) = 0.015 RON
service_charge = 0.01 USD (equivalent to 0.0025 RON at an example exchange rate of $4 per RON)
estimate_random_fee = service_charge + fulfillment_gas_fee = 0.0175 RON
- The oracle fulfills the random seed and refunds the remaining gas. The refund amount is less than
fulfillment_gas_fee
(0.015 RON in the preceding example).
Steps
Step 1. Extend VRFConsumer contract
The VRFConsumer
contract provides a set of functions that allow a smart contract to request random data from VRF.
View VRFConsumer
contract
// File contracts/coordinators/IRoninVRFCoordinatorForConsumers.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IRoninVRFCoordinatorForConsumers {
/**
* @dev Request random seed to the coordinator contract. Returns the request hash.
* Consider using the method `estimateRequestRandomFee` to estimate the random fee.
*
* @param _callbackGasLimit The callback gas amount.
* @param _gasPrice The gas price that oracle must send transaction to fulfill.
* @param _consumer The consumer address to callback.
* @param _refundAddress Refund address if there is RON left after paying gas fee to oracle.
*/
function requestRandomSeed(
uint256 _callbackGasLimit,
uint256 _gasPrice,
address _consumer,
address _refundAddress
) external payable returns (bytes32 _reqHash);
/**
* @dev Estimates the request random fee in RON.
*
* @notice It should be larger than the real cost and the contract will refund if any.
*/
function estimateRequestRandomFee(uint256 _callbackGasLimit, uint256 _gasPrice) external view returns (uint256);
}
// File contracts/consumer/VRFConsumer.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
abstract contract VRFConsumer {
error OnlyCoordinatorCanFulfill();
address public vrfCoordinator;
constructor(address _vrfCoordinator) {
vrfCoordinator = _vrfCoordinator;
}
/**
* @dev Raw fulfills random seed.
*
* Requirements:
* - The method caller is VRF coordinator `vrfCoordinator`.
*
* @notice The function `rawFulfillRandomSeed` is called by VRFCoordinator when it receives a valid VRF
* proof. It then calls `_fulfillRandomSeed`, after validating the origin of the call.
*
*/
function rawFulfillRandomSeed(bytes32 _reqHash, uint256 _randomSeed) external {
if (msg.sender != vrfCoordinator) revert OnlyCoordinatorCanFulfill();
_fulfillRandomSeed(_reqHash, _randomSeed);
}
/**
* @dev Fulfills random seed `_randomSeed` based on the request hash `_reqHash`
*/
function _fulfillRandomSeed(bytes32 _reqHash, uint256 _randomSeed) internal virtual;
/**
* @dev Request random seed to the coordinator contract. Returns the request hash.
* Consider using the method `IRoninVRFCoordinatorForConsumers.estimateRequestRandomFee` to estimate the random fee.
*
* @param _value Amount of RON to cover gas fee for oracle, will be refunded to `_refundAddr`.
* @param _callbackGasLimit The callback gas amount, which should cover enough gas used for the method `_fulfillRandomSeed`.
* @param _gasPriceToFulFill The gas price that orale must send transaction to fulfill.
* @param _refundAddr Refund address if there is RON left after paying gas fee to oracle.
*/
function _requestRandomness(
uint256 _value,
uint256 _callbackGasLimit,
uint256 _gasPriceToFulFill,
address _refundAddr
) internal virtual returns (bytes32 _reqHash) {
return
IRoninVRFCoordinatorForConsumers(vrfCoordinator).requestRandomSeed{ value: _value }(
_callbackGasLimit,
_gasPriceToFulFill,
address(this),
_refundAddr
);
}
}
To extend the VRFConsumer
contract, add the following code to your contract:
import "./VRFConsumer.sol";
contract MyContract is VRFConsumer {
constructor(address _vrfCoordinator) VRFConsumer(_vrfCoordinator) {}
// your code here
}
Step 2. Request randomness
Write a method that requests randomness from the VRF coordinator contract. This method should call the _requestRandomness
function provided by the VRFConsumer
contract and pass in the necessary parameters.
Here's an example:
function requestRandomNumber(
uint256 callbackGasLimit,
uint256 gasPriceToFulFill
)
public
payable
returns (bytes32 reqHash)
{
reqHash = _requestRandomness(
msg.value,
callbackGasLimit,
gasPriceToFulFill,
msg.sender
);
}
This code requests a random number by calling the requestRandomSeed
function in the VRF coordinator contract by using the method _requestRandomness
. It requires the following:
- The contract must have enough RON to pay the gas fee for oracle calling back. You can estimate the fee by calling
VRFCoordinator.estimateRequestRandomFee(callbackGasLimit, gasPrice)
. - The gas amount must be determined to call back to the method
rawFulfillRandomSeed
. The amount of gas depends on the method_fulfillRandomSeed
in the next step. - The gas price must greater than or equal to 20 GWEI.
The code then returns the request hash reqHash
that you should store for fulfillment.
Step 3. Fulfill randomness
Override the _fulfillRandomSeed
function provided by the VRFConsumer
contract. The VRF coordinator contract calls this function when it generates a random number in response to a request. The function takes the request hash and the random seed as arguments and performs any necessary actions based on the random number.
function _fulfillRandomSeed(
bytes32 reqHash,
uint256 randomSeed
)
internal
override
{
// use the random number to do something
}
You can use the resultant random number to generate a random token ID or select a random winner for a contest, for example.
Example. Mint an NFT with a random token ID
In the following example, the code implements a consumer smart contract that requests random numbers from the Ronin VRF service to mint an NFT (non-fungible token) with a randomly generated token ID.
Step 1. Extend the VRFConsumer contract
In an example contract, MockNFT
, extend the VRFConsumer
contract to request random numbers from the VRF service.
contract MockNFT is ERC721Enumerable, VRFConsumer {
constructor(address _vrfCoordinator) payable
VRFConsumer(_vrfCoordinator)
ERC721("MockNFT", "MNFT") {}
}
Step 2. Implement the
Write a method that requests randomness from the VRF coordinator contract. This method should call the _requestRandomness
function provided by the VRFConsumer
contract and pass in the necessary parameters.
/// @dev Mapping from user address => flag indicating whether user is requested
mapping(address => bool) public isUserRequested;
/// @dev Mapping from request hash => user address
mapping(bytes32 => address) public getUserByReqHash;
function requestMintRandom() external payable {
require(!isUserRequested[msg.sender], "MockNFT: already requested");
_requestMintRandom(msg.sender);
}
function callbackGaslimit() public pure returns (uint256) {
return 500_000;
}
function gasPrice() public pure returns (uint256) {
return 20e9;
}
function _requestMintRandom(address user) internal {
bytes32 reqHash = _requestRandomness(address(this).balance, callbackGaslimit(), gasPrice(), user);
isUserRequested[user] = true;
getUserByReqHash[reqHash] = user;
}
This code does the following:
- The mappings store whether a user has already requested a random number and map the request hash to the user who made the request.
- The
requestMintRandom
function allows a user to request the minting of an NFT. - The
_fulfillRandomSeed
method is overridden to mint the NFT with a randomly generated token ID.
View MockNFT
contract
contract MockNFT is ERC721Enumerable, VRFConsumer {
/// @dev Mapping from user address => flag indicating whether user is requested
mapping(address => bool) public isUserRequested;
/// @dev Mapping from request hash => user address
mapping(bytes32 => address) public getUserByReqHash;
constructor(address _vrfCoordinator) payable VRFConsumer(_vrfCoordinator) ERC721("MockNFT", "MNFT") {}
receive() external payable {}
function requestMintRandom() external payable {
require(!isUserRequested[msg.sender], "MockNFT: already requested");
_requestMintRandom(msg.sender);
}
function _fulfillRandomSeed(bytes32 reqHash, uint256 randomSeed) internal override {
uint256 tokenId = randomSeed;
address user = getUserByReqHash[reqHash];
if (_exists(tokenId)) {
_requestMintRandom(user);
} else {
_mint(user, tokenId);
}
}
function callbackGaslimit() public pure returns (uint256) {
return 500_000;
}
function gasPrice() public pure returns (uint256) {
return 20e9;
}
function _requestMintRandom(address user) internal {
bytes32 reqHash = _requestRandomness(address(this).balance, callbackGaslimit(), gasPrice(), user);
isUserRequested[user] = true;
getUserByReqHash[reqHash] = user;
}
}
Step 3. Deploy the contract
Deploy the contract to the Ronin mainnet or Saigon testnet. Make sure to fund the contract with enough RON to cover the gas fees for the VRF service. For deployment instructions, see the Deploy a smart contract guide.