Verifiable Randomness on Somnia using Protofire Chainlink VRF v2.5
Protofire Chainlink’s Verifiable Random Function (VRF) allows developers to securely request random numbers in a tamper-proof and auditable way. It is ideal for gaming, NFT mints, and lotteries. This tutorial walks you through integrating Protofire's Chainlink VRF v2.5 on Somnia Network, using native STT (Somnia Token) as the payment currency.
Understanding VRF and Why It Matters
Randomness is essential for many blockchain applications, such as Games, Lotteries, Raffles, and NFT drops, but blockchains are deterministic by nature. This means every node must produce the same output given the same inputs. If you try to use on-chain data like block.timestamp
or blockhash
as a random source, miners/validators can manipulate these values to influence the outcome. This is where VRF comes in.
What is VRF?
A Verifiable Random Function (VRF) is a cryptographic method of generating random numbers along with a proof that the result was not tampered with. When using Protofire Chainlink VRF:
You request a random number from the VRF service.
Protofire Chainlink’s decentralized oracle network generates a random value off-chain along with a cryptographic proof.
The proof is verified on-chain before the value is returned to your contract.
This ensures tamper-proof randomness and publicly verifiable results. Where the outcomes are fair.
Why is VRF important on blockchain?
Without VRF, randomness in blockchain apps can be gamed. With VRF:
No single party can manipulate the results
Users can independently verify the randomness
Applications gain trust from players, participants, and investors
Requesting VRF Data using Protofire Chainlink services relies on two methods: Subscription and Direct Funding
In the Subscription method, Chainlink VRF requests receive funding from subscription accounts. The Subscription Manager lets you create an account and pre-pay for your use of Chainlink VRF requests. You can learn more about the subscription method by referencing the Chainlink documentation. The Direct Funding method doesn't require a subscription and is optimal for one-off requests for randomness. This method also works best for applications where your end-users must pay the fees for VRF because the cost of the request is determined at request time. Learn more.
In this guide, we will build a Smart Contract called RandomNumberConsumer
that:
Inherits the Protofire Chainlink VRF Wrapper.
Requests 3 secure random numbers
Pays for randomness using native STT (no LINK subscription required)
Emits events and exposes functions to retrieve the randomness
Handles overpayments and pending request checks
Prerequisites
Before getting started:
You are familiar with Solidity (v0.8+)
You have the VRF Wrapper address for Protofire ChainLink VRF Wrapper
TL;DR
Owner calls
requestRandomNumber()
and sends enough STT(msg.value)
to cover the fee.Contract uses VRF Wrapper to request 3 random words (in native STT).
When VRF is ready, the wrapper calls
fulfillRandomWords
, the contract:verifies the request,
stores the 3 words,
toggles
fulfilled = true
,emits RandomNumberFulfilled.
User Interfaces and Scripts can read
getLatestRandomWord()
or pollgetRequestStatus()
.
Code Breakdown
contract RandomNumberConsumer
is VRFV2PlusWrapperConsumerBase, ConfirmedOwner
VRFV2PlusWrapperConsumerBase
gives you the glue code for requesting randomness from the VRF Wrapper and receiving the callback (fulfillRandomWords)
. It also exposes the wrapper instance i_vrfV2PlusWrapper
.
ConfirmedOwner
is a lightweight ownership module; it lets you restrict actions to the contract owner via onlyOwner.
State Variables
uint256 public latestRequestId;
uint256[] public latestRandomWord;
bool public fulfilled;
latestRequestId
tracks the most recent VRF request ID. Used to make sure the fulfillment we receive matches the last request.
latestRandomWord
stores the three random words returned by VRF for the latest request.
fulfilled
marks whether the latest request has finished (prevents overlapping requests and makes UI/state checks easy).
VRF Request Parameters (constants)
uint32 public constant CALLBACK_GAS_LIMIT = 2_100_000;
uint16 public constant REQUEST_CONFIRMATIONS = 3;
uint32 public constant NUM_WORDS = 3;
CALLBACK_GAS_LIMIT
is the max gas VRF can use when calling your fulfillRandomWords
. Must be large enough for your logic. This example uses headroom for 3 words.
REQUEST_CONFIRMATIONS
is how many blocks to wait before fulfillment (trade-off between speed and reorg safety).
NUM_WORDS
This is how many random numbers you want per request. Here, it’s 3.
Events
event RandomNumberRequested(uint256 indexed requestId, address indexed requester, uint256 paid);
event RandomNumberFulfilled(uint256 indexed requestId, uint256[] randomWord);
RandomNumberRequested
is emitted right after submitting a VRF request. Includes the paid cost in native STT.
RandomNumberFulfilled
is emitted when VRF returns the result, with the three random words.
Events make it easy to monitor behavior from explorers, indexers, or frontends.
Custom errors
error InsufficientPayment(uint256 required, uint256 sent);
error RequestAlreadyPending();
InsufficientPayment
is thrown if msg.value doesn’t cover the VRF native fee at request time.
RequestAlreadyPending
is thrown if you try to request again while the previous request hasn’t been fulfilled.
Errors are cheaper than require("string") and clearer to reason about.
Constructor
constructor(address wrapper) ConfirmedOwner(msg.sender) VRFV2PlusWrapperConsumerBase(wrapper) {}
Takes the VRF V2+ Wrapper address (the on-chain contract that mediates VRF requests) and initializes ownership to the deployer.
Function Requesting Randomness
function requestRandomNumber() external payable onlyOwner {
// 1) block overlapping requests
if (latestRequestId != 0 && !fulfilled) revert RequestAlreadyPending();
// 2) compute required fee and validate payment
uint256 requestPrice = getRequestPrice();
if (msg.value < requestPrice) revert InsufficientPayment(requestPrice, msg.value);
// 3) signal native payment to the wrapper
bytes memory args = VRFV2PlusClient._argsToBytes(
VRFV2PlusClient.ExtraArgsV1({ nativePayment: true })
);
// 4) submit request (uses native STT)
(uint256 requestId, uint256 paid) = requestRandomnessPayInNative(
CALLBACK_GAS_LIMIT,
REQUEST_CONFIRMATIONS,
NUM_WORDS,
args
);
latestRequestId = requestId;
fulfilled = false;
emit RandomNumberRequested(requestId, msg.sender, paid);
// 5) refund any excess back to caller
if (msg.value > paid) {
(bool ok, ) = msg.sender.call{ value: msg.value - paid }("");
require(ok, "Refund failed");
}
}
The requestRandomNumber()
function implements safeguards and processes to ensure reliable VRF operation. First, it enforces safety by preventing spam or overlapping requests, which ensures a predictable user experience and maintains simpler state management. The function then calculates the exact payment required by calling getRequestPrice()
to determine how much STT the wrapper currently needs, rejecting any transaction with insufficient payment. To specify the payment method, it encodes nativePayment: true
in the request parameters, instructing the wrapper to charge in native STT tokens rather than LINK.
Once validated, the function submits the request through requestRandomnessPayInNative()
, which initiates the VRF request to Chainlink while storing the returned requestId and marking the fulfilled status as false to track the pending request.
Finally, the function implements automatic refund logic that returns any excess funds to the user if they overpaid, ensuring users never lose funds due to price variations.
READ OPERATIONS
VRF callback (fulfillment)
function fulfillRandomWords(
uint256 requestId,
uint256[] memory randomWords
) internal override {
require(randomWords.length > 0, "No random word returned");
require(requestId == latestRequestId, "Unexpected request ID");
latestRandomWord = randomWords; // stores 3 words
fulfilled = true;
emit RandomNumberFulfilled(requestId, randomWords);
}
Called by the VRF Wrapper (not by you) and validates that we actually received words, and the requestId
matches the latest request (guards against stale/foreign callbacks). It then stores the 3 words and flips fulfilled = true
and emits a completion event. If you need game logic, derive from these random words inside this function or store and consume later.
getRequestStatus()
function getRequestStatus()
external
view
returns (uint256 requestId, bool isPending, bool isFulfilled)
{
return (latestRequestId, latestRequestId != 0 && !fulfilled, fulfilled);
}
For frontends/monitoring: see the last request ID
, whether it’s still pending
, and whether it was fulfilled
.
getLatestRandomWord()
function getLatestRandomWord() external view returns (uint256[] memory) {
require(fulfilled, "No fulfilled request yet");
return latestRandomWord;
}
Returns the three words from the most recent fulfilled request, which is actually a string on numbers for example: 93869141573160465677701763703933181905260360385351294458479680637737009096153
Pricing Helper
function getRequestPrice() public view returns (uint256) {
return i_vrfV2PlusWrapper.calculateRequestPriceNative(CALLBACK_GAS_LIMIT, NUM_WORDS);
}
Asks the wrapper how much STT (in wei) you need right now for a request with your chosen CALLBACK_GAS_LIMIT
and NUM_WORDS
. Use this in your UI or scripts to fill msg.value
.
Conclusion
You've successfully built a secure random number generator on Somnia using Chainlink VRF v2.5. Your RandomNumberConsumer
Smart Contract provides tamper proof randomness with native STT payment, automatic refunds, and proper request management, everything needed for production use.
Real-World VRF Use Cases
VRF powers a wide range of blockchain applications where fairness is critical. In gaming, it enables trustworthy dice rolls, loot drops, critical hit calculations, and procedurally generated maps. For NFT collections, VRF ensures unbiased trait assignment during minting, metadata reveals, and rarity distribution. This is crucial when traits can be worth thousands.
Lotteries and raffles benefit from transparent winner selection, whether for small community giveaways or million dollar prize pools. DeFi protocols use VRF for random liquidator selection, fair distribution, and variable reward mechanisms, while DAO governance applications include jury selection for disputes, randomized proposal ordering, and representative sampling for surveys.
With VRF integrated, you're ready to build applications where fairness is cryptographically guaranteed, not just promised. Whether for games, NFTs, or DeFi protocols, your users can verify that randomness is truly random and build trust through mathematics, not faith.
Last updated