Skip to main content

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.

GitHub repository

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 the proof 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:

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

  1. 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
  1. 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.