Skip to main content

Use Ronin VRF service

Overview

This guide shows how to write a smart contract that requests random data from the Ronin VRF (Verifiable Random Function) service.

The Ronin VRF service is a random number generator that is used by smart contracts to request random values without compromising security or usability. In response to each request, the Ronin VRF generates a random value and cryptographic proof of how that value was determined. The proof is published and verified on-chain before it can be used by any consuming apps.

Ronin VRF contracts

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.

Here's a breakdown of the 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
  2. 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).

Step 1. Extend the VRFConsumer contract

The VRFConsumer contract provides a set of functions that allow a smart contract to request random data from Ronin VRF.

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. Write a method to request random data

Write a method that requests randomness from the VRF 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 be equal to or larger than 20 GWEI.

The code then returns the request hash reqHash that you should store for fulfillment.

Step 3. Override the _fulfillRandomSeed function

Override the _fulfillRandomSeed function provided by the VRFConsumer contract. This function is called by the VRF coordinator contract 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

In the following example, the code implements a consumer smart contract that requests random numbers from a VRF contract. The purpose of the smart contract is to mint an ERC721 token with a randomly generated token ID for the user who requests it.

We start by extending the VRFConsumer contact:

contract MockNFT is ERC721Enumerable, VRFConsumer {
constructor(address _vrfCoordinator) payable
VRFConsumer(_vrfCoordinator)
ERC721("MockNFT", "MNFT") {}
}

After that, we write the methods to request minting NFTs randomly:

/// @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;
}

Code explanation

Let's walk through the example code line by line.

  1. The first line declares a public mapping that associates an address with a boolean value indicating whether the address has already requested a random number.

    mapping(address => bool) public isUserRequested;
  2. The second line declares a public mapping that associates a request hash with a request address that has requested a random number.

    mapping(bytes32 => address) public getUserByReqHash;
  3. The requestMintRandom function is the entry point for users to request a random number and mint a new ERC721 token. The function checks whether the user has already made a request by checking the value of the isUserRequested mapping for the sender's address. If the user has already requested a random number, the function reverts the transaction with an error message. If the user has not yet made a request, the function calls the internal function _requestMintRandom with the sender's address.

    function requestMintRandom() external payable {
    require(!isUserRequested[msg.sender], "MockNFT: already requested");
    _requestMintRandom(msg.sender);
    }
  4. The internal function _requestMintRandom makes the actual request for a random number from the VRF contract. First, the function calls the function _requestRandomness with several arguments including the contract's current balance, the gas amount, the gas price, and the sender address. After that, the function sets the isUserRequested mapping for the user's address to true, indicating that the user has made a request. Finally, it associates the request hash returned by _requestRandomness with the user's address in the getUserByReqHash mapping, so that the callback function can later identify the user who made the request.

    function _requestMintRandom(address user) internal {
    bytes32 reqHash = _requestRandomness(
    address(this).balance,
    callbackGaslimit(),
    gasPrice(),
    user
    );
    isUserRequested[user] = true;
    getUserByReqHash[reqHash] = user;
    }
  5. The callbackGaslimit and gasPrice methods assume that the rawFulfillRandomSeed costs 500k gas unit, and the oracle must call back with the gas price of 20 GWEI.

    function callbackGaslimit() public pure returns (uint256) {
    return 500000;
    }

    function gasPrice() public pure returns (uint256) {
    return 20e9;
    }
  6. The following code overrides the _fulfillRandomSeed function to mint the token with an ID equal to a random seed, or request to a new one if the ID is claimed.

    function _fulfillRandomSeed(
    bytes32 reqHash,
    uint256 randomSeed
    )
    internal
    override
    {
    uint256 tokenId = randomSeed;
    address user = getUserByReqHash[reqHash];
    if (_exists(tokenId)) {
    _requestMintRandom(user);
    } else {
    _mint(user, tokenId);
    }
    }

You can view the full version of the MockNFT contract in the Reference section.

Reference

This section contains the example contracts used in this guide.

VRFConsumer contract

VRFConsumer
// 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
);
}
}

MockNFT contract

MockNFT
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;
}
}
Was this page helpful?
Happy React is loading...