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.
-
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 -
Install dependencies.
forge install NIMA-Enterprises/subnet-vrf
forge install smartcontractkit/chainlinkAdd the following to your
foundry.toml
file under the[profile.default]
headerremappings = ["@chainlink/=lib/chainlink/", "@vrf/=lib/subnet-vrf/src"]
-
Create a new smart contract file in
/src
and import the required dependencies. Create a default constructor to accept thevrfProxyAddress
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);
}
} -
Implement contract. By inheriting
VRFConsumerBaseV2
, you must implementfulfillRandomWords()
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;
}
} -
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} -
Add the contract address to the Consumer Allowlist in the AvaCloud Portal VRF module
-
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)" 5To get the requestID, look in the event logs for your smart contract address (ours is
0xc615f4614056bc15744db453d119134ee48e1cde
). Since the last value in the topics list is0x0...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
trueSince 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
7Unfortunately, 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.