AvaCloud VRF Quickstart Guide

Congratulations! You have just completed the AvaCloud VRF onboarding flow. This quickstart guide assumes you have completed the VRF setup process on your L1 and have access to an active VRF Proxy dashboard.

AvaCloud VRF is a service that provides on-chain random values to Avalanche L1s deployed via AvaCloud. This service leverages Chainlink VRF on the Avalanche C-Chain to fetch the random values and pass back to the L1. For more details on the design and architecture, please see the contracts.

Quick Start

Prerequisites

This guide uses Foundry for smart contract development. If you do not have Foundry, run the following command:

curl -L https://foundry.paradigm.xyz | bash

To ensure proper installation, run:

forge --version
cast --version

Integration Steps

Once the VRF Proxy is deployed, the steps to integrate with a solidity smart contract are simple.

  1. Create a new forge project. This will create a new repository. After running, you should see the following files in the directory.

    forge init avacloud-vrf
    cd avacloud-vrf
    ls
    README.md foundry.toml lib script src test
  2. Install dependencies.

    forge install NIMA-Enterprises/subnet-vrf
    forge install smartcontractkit/chainlink

    Add the following to your foundry.toml file under the [profile.default] header

     remappings = ["@chainlink/=lib/chainlink/", "@vrf/=lib/subnet-vrf/src"]
  3. Create a new smart contract file in /src and import the required dependencies. Create a default constructor to accept the vrfProxyAddress found in the AvaCloud portal.

    // SPDX-License-Identifier: UNLICENSED

    pragma solidity 0.8.25;

    import {VRFConsumerBaseV2} from "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";
    import {IVRFProxy} from "@vrf/interfaces/IVRFProxy.sol";

    contract AvaCloudVRFConsumer is VRFConsumerBaseV2 {
    IVRFProxy private proxy;

    constructor(address vrfProxyAddress) VRFConsumerBaseV2(vrfProxyAddress) {
    proxy = IVRFProxy(vrfProxyAddress);
    }
    }
  4. Implement contract. By inheriting VRFConsumerBaseV2, you must implement fulfillRandomWords() as a callback once a VRF request is returned.

    Our implementation allows users to guess a random number between 1 - 10, and rewards them if they guess correctly.

    Constants

    struct RandomNumberGuess {
    address requestor;
    uint256 guess;
    bool isResolved;
    }

    IVRFProxy private proxy;
    mapping(uint256 vrfRequestId => RandomNumberGuess guess) public guesses;
    // if requesting more than one random number, value of mapping should be array
    mapping(uint256 vrfRequestId => uint256 randomNum) public returnedNumber;
    mapping(address guesser => uint256 amountToClaim) public rewards;

    uint256 public constant REWARD_AMOUNT = 0.05 ether;

    event Payout(address indexed guesser, uint256 reward);
    event Fund(address indexed funder, uint256 total);
    event RandomNumberGuessed(
    address indexed guesser,
    uint256 guess,
    uint256 indexed vrfRequestID
    );
    event RewardsClaimed(address owner, uint256 reward);

    error InvalidDataLength();
    error InvalidRequestID();
    error RequestAlreadyResolved();
    error NoRewardsToClaim();

    Helper Functions

    receive() external payable {
    emit Fund(msg.sender, msg.value);
    }

    function proxyAddress() public view returns (address) {
    return address(proxy);
    }

    function claimRewards() external {
    uint256 reward = rewards[msg.sender];
    if (reward <= 0) revert NoRewardsToClaim();

    rewards[msg.sender] = 0;
    (bool success, ) = msg.sender.call{value: reward}("");
    require(success, "Transfer failed");

    emit RewardsClaimed(msg.sender, reward);
    }

    Guess Function: to fetch a random number, call proxy.requestRandomWords()

    function guessRandomNumber(
    uint256 guess
    ) public returns (uint256 _requestId) {
    // blockDelay and callbackGasLimit are recommended values
    uint16 blockDelay = 1;
    uint32 callbackGasLimit = 200_000;
    uint32 numberOfRandomValues = 1;

    // get the current nonce and increment
    uint256 requestId = proxy.requestNonce() + 1;
    guesses[requestId] = RandomNumberGuess(msg.sender, guess, false);
    proxy.requestRandomWords(
    blockDelay,
    callbackGasLimit,
    numberOfRandomValues
    );
    emit RandomNumberGuessed(msg.sender, guess, requestId);
    return requestId;
    }

    Callback Function

    function fulfillRandomWords(
    uint256 _requestId,
    uint256[] memory _randomWords
    ) internal override {
    if (_randomWords.length != 1) revert InvalidDataLength();
    RandomNumberGuess storage guess = guesses[_requestId];
    if (guess.requestor == address(0)) revert InvalidRequestID();
    if (guess.isResolved) revert RequestAlreadyResolved();
    guess.isResolved = true;

    // Set the range of responses to be 1 - 10
    uint256 result = (_randomWords[0] % 10) + 1;
    returnedNumber[_requestId] = result;
    if (result == guess.guess) {
    rewards[guess.requestor] += REWARD_AMOUNT;
    }
    }
  5. Deploy contract: take note of the contract address

    forge create --rpc-url RPC_URL \
    --private-key PRIVATE_KEY \
    src/AvaCloudVRFConsumer.sol:AvaCloudVRFConsumer \
    --constructor-args {ADDRESS}
  6. Add the contract address to the Consumer Allowlist in the AvaCloud Portal VRF module

    Add new consumer contract

  7. Test the contract. The last value is the guess, for this demo we are guessing 5.

    cast send --rpc-url RPC_URL \
    --private-key PRIVATE_KEY \
    CONTRACT_ADDRESS "guessRandomNumber(uint256)" 5

    To get the requestID, look in the event logs for your smart contract address (ours is 0xc615f4614056bc15744db453d119134ee48e1cde). Since the last value in the topics list is 0x0...02, the requestID is 2

    "address":"0xc615f4614056bc15744db453d119134ee48e1cde",
    "topics":[
    "0x1d9111435f4b854f073bc412802d9927efaba88c5d4f981bb66464a1ea11f045",
    "0x000000000000000000000000b4ca6c121d6287af7ac7cb62ae33d2b054b9fc44",
    "0x0000000000000000000000000000000000000000000000000000000000000002" # REQUEST ID IN HEX
    ],
    "data":"0x0000000000000000000000000000000000000000000000000000000000000005"

    Then, query the guesses mapping to see if your request was successfully resolved.

    cast call SMART_CONTRACT_ADDRESS "guesses(uint256)(address,uint256,bool)" REQUEST_ID --rpc-url RPC_URL
    0xB4cA6C121D6287af7ac7cb62Ae33d2b054b9FC44
    5
    true

    Since the last value is true, we know our request was resolved. Finally, we can check to see what random valued was returned and if we guessed correctly.

    cast call SMART_CONTRACT_ADDRESS "returnedNumber(uint256)(uint256)" 2 --rpc-url RPC_URL      
    7

    Unfortunately, we did not guess correctly, but we now know our contract successfully returns a random value from C-Chain!

Congrats, we have now successfully created and deployed a contract to use random values! Now you can extend this functionality in your own smart contracts.

Full Contract Implementation

// SPDX-License-Identifier: UNLICENSED

pragma solidity 0.8.25;

import {VRFConsumerBaseV2} from "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";
import {IVRFProxy} from "@vrf/interfaces/IVRFProxy.sol";

contract AvaCloudVRFConsumer is VRFConsumerBaseV2 {
struct RandomNumberGuess {
address requestor;
uint256 guess;
bool isResolved;
}

IVRFProxy private proxy;
mapping(uint256 vrfRequestId => RandomNumberGuess guess) public guesses;
// if requesting more than one random number, value of mapping should be array
mapping(uint256 vrfRequestId => uint256 randomNum) public returnedNumber;
mapping(address guesser => uint256 amountToClaim) public rewards;

uint256 public constant REWARD_AMOUNT = 0.05 ether;

event Payout(address indexed guesser, uint256 reward);
event Fund(address indexed funder, uint256 total);
event RandomNumberGuessed(
address indexed guesser,
uint256 guess,
uint256 indexed vrfRequestID
);
event RewardsClaimed(address owner, uint256 reward);

error InvalidDataLength();
error InvalidRequestID();
error RequestAlreadyResolved();
error NoRewardsToClaim();

constructor(address vrfProxyAddress) VRFConsumerBaseV2(vrfProxyAddress) {
proxy = IVRFProxy(vrfProxyAddress);
}

receive() external payable {
emit Fund(msg.sender, msg.value);
}

function proxyAddress() public view returns (address) {
return address(proxy);
}

function claimRewards() external {
uint256 reward = rewards[msg.sender];
if (reward <= 0) revert NoRewardsToClaim();

rewards[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: reward}("");
require(success, "Transfer failed");

emit RewardsClaimed(msg.sender, reward);
}

function guessRandomNumber(
uint256 guess
) public returns (uint256 _requestId) {
// blockDelay and callbackGasLimit are recommended values
uint16 blockDelay = 1;
uint32 callbackGasLimit = 200_000;
uint32 numberOfRandomValues = 1;

// get the current nonce and increment
uint256 requestId = proxy.requestNonce() + 1;
guesses[requestId] = RandomNumberGuess(msg.sender, guess, false);
proxy.requestRandomWords(
blockDelay,
callbackGasLimit,
numberOfRandomValues
);
emit RandomNumberGuessed(msg.sender, guess, requestId);
return requestId;
}

function fulfillRandomWords(
uint256 _requestId,
uint256[] memory _randomWords
) internal override {
if (_randomWords.length != 1) revert InvalidDataLength();
RandomNumberGuess storage guess = guesses[_requestId];
if (guess.requestor == address(0)) revert InvalidRequestID();
if (guess.isResolved) revert RequestAlreadyResolved();
guess.isResolved = true;

// Set the range of responses to be 1 - 10
uint256 result = (_randomWords[0] % 10) + 1;
returnedNumber[_requestId] = result;
if (result == guess.guess) {
rewards[guess.requestor] += REWARD_AMOUNT;
}
}
}
 

For any additional questions, please view our other knowledge base articles or contact a support team member via the chat button. Examples are for illustrative purposes only.