VRF Developer Guide

Supra VRF V2 requires a whitelisted subscription to the service with a customer-controlled wallet address to act as the main reference. Feel free to send us a request via this form to get your wallet registered with Supra.

Once your wallet is registered by the Supra team, you can use it to whitelist any number of VRF requester smart contracts and pre-pay/top up the deposit balance maintained with Supra in order to pay for the gas fees of callback (response) transactions.

  • Please refer to the VRF subscription model page for a better understanding of how it works.

  • Please refer to the Network Addresses page for available networks and Supra contract addresses. The following guide explains the steps a requester contract will have to follow in order to use the VRF service.

Before you dive in..

In EVM ( Ethereum Virtual Machine) based networks, you will be interacting with two Supra smart contracts to connect and consume Supra VRF. Supra Deposit Contract - to whitelist smart contracts under the registered wallet address, pre-pay/top up the callback gas fee deposit maintained with Supra.

Supra Router Contract - to request and receive random numbers.

Step 1: Create the Supra VRF Interface

Add the following code to the requester contract i.e., the contract which uses VRF as a service. You can also add the code in a separate Interface and inherit the interface in the requester contract.

interface ISupraRouterContract {
	function generateRequest(string memory _functionSig, uint8 _rngCount, uint256 _numConfirmations, uint256 _clientSeed, address _clientWalletAddress) external returns(uint256);
	function generateRequest(string memory _functionSig, uint8 _rngCount, uint256 _numConfirmations, address _clientWalletAddress) external returns(uint256);
}

This interface will help the requester contract interact with the Supra Router contract through which the requester contract can use the VRF service.

Step 2: Configure the Supra Router Contract Address

Contracts that need random numbers should utilize the Supra Router Contract. In order to do that, they need to create an interface and bind it to the on-chain address of the Supra router contract.

contract ExampleContract {
    ISupraRouter internal supraRouter;

    constructor(address routerAddress) {
        supraRouter = ISupraRouter(routerAddress);
    }
}

Step 3: Use the VRF Service and Request a Random Number

In this step, we will use the “generateRequest” function of the Supra Router Contract to create a request for random numbers. There are two modes for the "generateRequest" function. The only difference between them is that you can optionally provide a client-side input, which will also be part of the payload being threshold-signed to provide randomness.

  • _functionSig- a string parameter; here, the requester contract will have to pass the function signature, which will receive the callback i.e., a random number from the Supra Router Contract. The function signature should be in the form of the function name following the parameters it accepts. We will see an example later in the document.

  • _rngCount - an integer parameter; this is for the number of random numbers a particular requester wants to generate. Currently, we can generate a maximum of 255 random numbers per request.

  • _numConfirmations - an integer parameter (Minimum: 1 - Maximum :20) that specifies the number of block confirmations needed before Supra VRF can generate the random number.

  • _clientSeed (optional) - an optional integer parameter that could be provided by the client (defaults to 0). This is for additional unpredictability. The source of the seed can be a UUID of 256 bits. This can also be from a centralized source.

  • _clientWalletAddress - an “address” type parameter that takes the client wallet address that is already registered with the Supra Team as input.

Supra's VRF process requires splitting the contract logic into two functions.

  • The request function - the signature of this function is up to the developer.

  • The callback function - the signature must be of the form “uint256 nonce, uint256[] calldata rngList”

function exampleRNG() external {  
     //Function validation and logic
     // requesting 10 random numbers
     uint8 rngCount = 10; 

     // we want to wait for 1 confirmations before the request is considered complete/final. 
     // You can customize this value to meet your needs. Minimum: 1, Maximum: 20.
     uint256 numConfirmations = 1; 
	address _clientWalletAddress = //Add the whitelisted wallet address here
     uint256 generated_nonce = supraRouter.generateRequest(“exampleCallback(uint256,uint256[])”, rngCount, numConfirmations, _clientWalletAddress);

     // store generated_nonce if necessary (eg: in a hashmap)
     // this can be used to track parameters related to the request, such as user address, nft address etc in a lookup table
     // these can be accessed inside the callback since the response from supra will include the nonce
}

Step 4 - Add the Validation in the Callback Function of the Requester Contract

Inside the callback function where the requester contract wants the random number (in this example, the callback function is exampleCallback), the requester contract will have to add the validation such that only the Supra router contract can call the function. The validation is necessary to protect against malicious contracts/users executing the callback with fake data.

For example, if the callback function is pickWinner in the requester contract, the snippet can be as follows:

function exampleCallback(uint256 _nonce ,uint256[] _rngList) external {
    require(msg.sender == address(supraRouter));
    // Following the required logic of the function
 }

Step 5 : Whitelist Your Requester Contract with Supra Deposit Contract and Deposit Funds

It is important to note that your wallet address must be registered with Supra before this step. If that is completed, then you need to whitelist your requester smart contract under your wallet address and deposit funds to be paid for your callback transactions gas fees.

  • The simplest way to interact with the deposit contract will be through Remix IDE.

  • Go to Remix IDE & create a file with the name IDepositContract.sol

  • Paste the following code in the file:

    interface IDepositUserContract {
    	function depositFundClient() external payable;
    	function addContractToWhitelist(address _contractAddress) external;
    	function removeContractFromWhitelist(address _contractAddress) external;
    	function setMinBalanceClient(uint256 _limit) external;
    	function withdrawFundClient(uint256 _amount) external;
    	function checkClientFund(address _clientAddress) external view returns (uint256);
    	function checkEffectiveBalance(address _clientAddress) external view returns (uint256);
    	function checkMinBalanceSupra() external view returns (uint256);
    	function checkMinBalance(address _clientAddress) external view returns(uint256);
    	function countTotalWhitelistedContractByClient(address _clientAddress) external view returns (uint256);
    	function getSubscriptionInfoByClient(address _clientAddress) external view returns (uint256, uint256, bool);
    	function isMinimumBalanceReached(address _clientAddress) external view returns (bool);
    	function listAllWhitelistedContractByClient(address _clientAddress) external view returns (address[] memory);
    	
    }
  • Navigate to the “Navigate & run Transactions” tab in remix and paste the Deposit Contract address into the text box next to the “At Address” button. Then, press the At Address button. You will find the instance for the Deposit Contract created using which a user can interact and use the features provided by the Deposit Contract.

  • The following functions will facilitate whitelisting your requester smart contracts and fund deposits:

    1. addContracttoWhitelist(address)” - The whitelisted users will have to whitelist the contract that they will be using to request the random numbers. The parameter this function takes is the user’s contract address. This function will be called after the user deploys the requester contract post development and makes it ready for interacting with the Supra Contracts.

    2. “depositFundClient()” - This is another mandatory function for a user to use once before the user starts requesting from that contract. This is a function that will deposit funds in the Deposit Contract from the users for the response/callback transaction. The funds for a specific user should remain higher than the minimum amount set by the Supra (ex. 0.1 ETH for Arbitrum testnet) for the new request transactions to be accepted.

  • In essence, the user will have to interact with the Deposit Contract and add funds for their accounts, which will be utilized for the response transaction gas fee. There will be a script from Supra that will monitor the funds and will alert the user if a refill is required.

Additional Functions of the Deposit Contract and its Usage

  • depositFundClient(): This function is for the users to add their funds, which will be used to pay the transaction fees for the response transaction of their request.

  • addContractToWhitelist(address _contractAddress): This function is for the users to whitelist their respective contract address, given that the user’s wallet address which interacts with the function is already whitelisted by the supra team.

    • _contractAddress: The parameter accepts the contract address only and not the EOA(Externally Owned Account) address. The users will have to pass their contract address which will interact with the “SupraRouter” contract and request the random numbers.

  • removeContractFromWhitelist(address _contractAddress): The function helps user’s to remove their contract addresses from the whitelist if that is no longer required.

    • _contractAddress: The parameter, again will only accept the contract address & not the EOA address. Also the contract addresses which are already whitelisted will only be accepted.

  • setMinBalanceClient(uint256 _limit): This function is for the user to set the minimum limit of the fund they will add in the deposit contract. If a user doesn’t set it from their side, there is a limit set by the supra team and that will be considered and if the user sets it, the value which will be higher (the one set by the user or the supra team) will be selected as the limit for the particular user. The limit helps keep track of the balance and notify the user’s if the balance is getting lower and near to the minimum balance. Also the user’s fund added in the client should be greater than the minimum balance to request the random number.

    • _limit: The parameter takes the limit value in wei, for example if a user wants to set the limit as 0.001 ETH, the user will have to pass it as 1000000000000000 in WEI.

  • withdrawFundClient(uint256 _amount): The function helps users to withdraw the amount of funds they have deposited in the deposit contract.

    • _amount: The total amount of funds that the user wants to retrieve from the contract, however the amount should not exceed the deposited amount.

  • checkClientFund(address _clientAddress): The function helps the users to check their total available balance in the deposit contract.

    • _clientAddress: The user’s wallet address has to be passed for which the user wants to check their balance, the contract addresses won’t be accepted by the function and only the EOA address. Also the address has to be whitelisted.

  • checkEffectiveBalance(address _clientAddress): The function helps the user to check the effective balance, that is the balance that is left after subtracting the minimum balance from the total fund that has been deposited by the user.

    • _clientAddress: The function will take the user wallet address as a parameter.

  • checkMinBalanceSupra(): This function checks the minimum balance set by the supra team. As mentioned earlier, the minimum balance is already set by the supra team and the function helps the user to check its value

  • checkMinBalance(address _clientAddress): The function checks the minimum balance for a particular user. As mentioned earlier, the minimum balance can be set by the supra team as well as the user. If the minimum balance is set by both, the minimum balance for a particular user will be the higher value among the two.

    • _clientAddress: The function takes the whitelisted wallet address of the particular user as an input parameter.

  • countTotalWhitelistedContractByClient(address _clientAddress): The function returns the total number of contracts that has been whitelisted by a particular user.

    • _clientAddress: This function also takes the whitelisted wallet address of a particular user as an input parameter.

  • getSubscriptionInfoByClient(address _clientAddress): The function returns the information like when the subscription for a particular user started, when it will end and if the user is a participant of the SNAP programme or not.

    • _clientAddress: The parameter value remains the same as the previous function, a user wallet address which is whitelisted.

  • isMinimumBalanceReached(address _clientAddress): The function returns a boolean value, that is true if the user’s balance is lesser than or equal to the minimum balance set else it returns false.

    • _clientAddress: This is again the user’s wallet address which is whitelisted.

  • listAllWhitelistedContractByClient(address _clientAddress): The function returns the list of addresses, the contract addresses which are whitelisted by a particular user.

    • _clientAddress: The function takes the user’s whitelisted wallet address as an input parameter.

Example Implementation

Please find below an example implementation of Supra VRF.

  • The function getRNGForUser is using the VRF service by calling the generateRequest function of the Supra Router Contract.

  • Then we store the username of the user requesting the random number mapped to the nonce returned by generateRequest.

  • Then the callback function prints the random numbers requested by a specific user, and it has the signature: myCallbackUsername(uint256 nonce, uint256[] calldata rngList).

Once Supra generates the random number and it is verified by the on-chain logic to be authentic, myCallbackUsername is executed by the Supra Router, which completes the second half of the process. The nonce from the first argument is used to look up the username that originated the request.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface ISupraRouter {
   function generateRequest(string memory _functionSig , uint8 _rngCount, uint256 _numConfirmations, uint256 _clientSeed,address _clientWalletAddress) external returns(uint256);
   function generateRequest(string memory _functionSig , uint8 _rngCount, uint256 _numConfirmations,address _clientWalletAddress) external returns(uint256);
}
contract Interaction {
   address supraAddr;
   constructor(address supraSC) {
       supraAddr = supraSC;
   }
   mapping (uint256 => string ) result;
   mapping (string => uint256[] ) rngForUser;
   function getRNGForUser(uint8 rngCount, string memory username) external {
     // You can customize this value to meet your needs. Minimum: 1, Maximum: 20.
     uint256 numConfirmations = 1; 
      uint256 nonce =  ISupraRouter(supraAddr).generateRequest("myCallbackUsername(uint256,uint256[])", rngCount, numConfirmations, 123, msg.sender);
//Can pass "msg.sender" when calling from the whitelisted wallet address
      result[nonce] = username;
   }
   function myCallbackUsername(uint256 nonce, uint256[] calldata rngList) external {
      require(msg.sender == supraAddr, "only supra router can call this function");
      uint8 i = 0;
      uint256[] memory x = new uint256[](rngList.length);
      rngForUser[result[nonce]] = x;
      for(i=0; i<rngList.length;i++){
         rngForUser[result[nonce]][i] = rngList[i] % 100;
      }
   }
   function viewUserName(string memory username) external view returns (uint256[] memory) {
      return rngForUser[username];
   }
   }								

Last updated