Smart Contract Integration
Learn how to integrate Fast VRF into your smart contracts for secure, verifiable randomness.
VRF Coordinator
The VRF Coordinator manages randomness requests and fulfillment on RISE Chain.
// RISE Testnet VRF Coordinator
address constant VRF_COORDINATOR = 0x9d57aB4517ba97349551C876a01a7580B1338909;
Required Interfaces
Your contract must implement these interfaces to interact with VRF:
// Interface for requesting random numbers
interface IVRFCoordinator {
function requestRandomNumbers(
uint32 numNumbers, // How many random numbers you need
uint256 seed // Seed for randomness generation
) external returns (uint256 requestId);
}
// Interface your contract must implement
interface IVRFConsumer {
function rawFulfillRandomNumbers(
uint256 requestId,
uint256[] memory randomNumbers
) external;
}
Basic Implementation
Minimal VRF Consumer
contract BasicVRFConsumer is IVRFConsumer {
IVRFCoordinator public coordinator;
mapping(uint256 => uint256) public results;
event RandomnessRequested(uint256 indexed requestId);
event RandomnessFulfilled(uint256 indexed requestId, uint256 randomNumber);
constructor(address _coordinator) {
coordinator = IVRFCoordinator(_coordinator);
}
function requestRandom() external returns (uint256) {
uint256 requestId = coordinator.requestRandomNumbers(1, block.timestamp);
emit RandomnessRequested(requestId);
return requestId;
}
function rawFulfillRandomNumbers(
uint256 requestId,
uint256[] memory randomNumbers
) external override {
require(msg.sender == address(coordinator), "Only VRF coordinator");
require(randomNumbers.length > 0, "No random numbers");
results[requestId] = randomNumbers[0];
emit RandomnessFulfilled(requestId, randomNumbers[0]);
}
}
Advanced Patterns
Request Tracking
Track who made each request and handle state properly:
contract TrackedVRFConsumer is IVRFConsumer {
IVRFCoordinator public coordinator;
struct Request {
address requester;
uint256 timestamp;
bool fulfilled;
uint256 result;
}
mapping(uint256 => Request) public requests;
mapping(address => uint256[]) public userRequests;
function requestRandomForUser() external returns (uint256) {
uint256 requestId = coordinator.requestRandomNumbers(
1,
uint256(keccak256(abi.encode(msg.sender, block.timestamp)))
);
requests[requestId] = Request({
requester: msg.sender,
timestamp: block.timestamp,
fulfilled: false,
result: 0
});
userRequests[msg.sender].push(requestId);
return requestId;
}
function rawFulfillRandomNumbers(
uint256 requestId,
uint256[] memory randomNumbers
) external override {
require(msg.sender == address(coordinator), "Only VRF coordinator");
Request storage request = requests[requestId];
require(request.requester != address(0), "Invalid request");
require(!request.fulfilled, "Already fulfilled");
request.fulfilled = true;
request.result = randomNumbers[0];
// Process the result for the user
_processRandomness(request.requester, randomNumbers[0]);
}
function _processRandomness(address user, uint256 randomNumber) internal {
// Your custom logic here
}
}
Multiple Random Numbers
Request and handle multiple random values in one call:
contract MultiRandomConsumer is IVRFConsumer {
IVRFCoordinator public coordinator;
event LotteryDrawn(uint256[] winningNumbers);
function drawLottery() external returns (uint256) {
// Request 6 random numbers for lottery
return coordinator.requestRandomNumbers(6, block.timestamp);
}
function rawFulfillRandomNumbers(
uint256 requestId,
uint256[] memory randomNumbers
) external override {
require(msg.sender == address(coordinator), "Only VRF coordinator");
require(randomNumbers.length == 6, "Expected 6 numbers");
uint256[] memory lotteryNumbers = new uint256[](6);
for (uint i = 0; i < 6; i++) {
// Generate lottery numbers 1-49
lotteryNumbers[i] = (randomNumbers[i] % 49) + 1;
}
emit LotteryDrawn(lotteryNumbers);
}
}
Common Use Cases
Fair NFT Minting
contract FairNFTMint is IVRFConsumer, ERC721 {
IVRFCoordinator public coordinator;
uint256 public constant MAX_SUPPLY = 10000;
uint256 public totalMinted = 0;
mapping(uint256 => address) public mintRequests;
mapping(uint256 => uint256) public tokenTraits;
function requestMint() external payable {
require(msg.value >= 0.1 ether, "Insufficient payment");
require(totalMinted < MAX_SUPPLY, "Sold out");
uint256 requestId = coordinator.requestRandomNumbers(1, totalMinted);
mintRequests[requestId] = msg.sender;
}
function rawFulfillRandomNumbers(
uint256 requestId,
uint256[] memory randomNumbers
) external override {
require(msg.sender == address(coordinator), "Only VRF coordinator");
address minter = mintRequests[requestId];
require(minter != address(0), "Invalid request");
uint256 tokenId = totalMinted++;
uint256 traits = randomNumbers[0];
tokenTraits[tokenId] = traits;
_mint(minter, tokenId);
delete mintRequests[requestId];
}
}
Random Rewards Distribution
contract RandomRewards is IVRFConsumer {
IVRFCoordinator public coordinator;
IERC20 public rewardToken;
address[] public participants;
mapping(uint256 => uint256) public pendingRewards;
function distributeRewards(uint256 totalReward) external {
require(participants.length > 0, "No participants");
uint256 requestId = coordinator.requestRandomNumbers(
uint32(participants.length),
block.timestamp
);
pendingRewards[requestId] = totalReward;
}
function rawFulfillRandomNumbers(
uint256 requestId,
uint256[] memory randomNumbers
) external override {
require(msg.sender == address(coordinator), "Only VRF coordinator");
uint256 totalReward = pendingRewards[requestId];
require(totalReward > 0, "No pending reward");
// Distribute proportionally based on random weights
uint256 totalWeight = 0;
for (uint i = 0; i < randomNumbers.length; i++) {
totalWeight += randomNumbers[i] % 1000;
}
for (uint i = 0; i < participants.length; i++) {
uint256 share = (totalReward * (randomNumbers[i] % 1000)) / totalWeight;
rewardToken.transfer(participants[i], share);
}
delete pendingRewards[requestId];
}
}
Security Considerations
Access Control
Always verify the caller is the VRF coordinator:
modifier onlyVRFCoordinator() {
require(msg.sender == address(coordinator), "Only VRF coordinator");
_;
}
function rawFulfillRandomNumbers(
uint256 requestId,
uint256[] memory randomNumbers
) external override onlyVRFCoordinator {
// Your logic here
}
Request Validation
Validate requests before processing:
function rawFulfillRandomNumbers(
uint256 requestId,
uint256[] memory randomNumbers
) external override onlyVRFCoordinator {
// Check request exists
require(requests[requestId].requester != address(0), "Unknown request");
// Check not already fulfilled
require(!requests[requestId].fulfilled, "Already fulfilled");
// Check expected number count
require(randomNumbers.length == expectedCount[requestId], "Wrong count");
// Process...
}
Reentrancy Protection
Use checks-effects-interactions pattern:
function rawFulfillRandomNumbers(
uint256 requestId,
uint256[] memory randomNumbers
) external override onlyVRFCoordinator nonReentrant {
// 1. Checks
require(requests[requestId].valid, "Invalid request");
// 2. Effects
requests[requestId].fulfilled = true;
requests[requestId].result = randomNumbers[0];
// 3. Interactions
if (callbacks[requestId] != address(0)) {
ICallback(callbacks[requestId]).onRandomness(randomNumbers[0]);
}
}
Gas Optimization
Batch Processing
Process multiple requests efficiently:
contract BatchVRFProcessor is IVRFConsumer {
struct BatchRequest {
address[] users;
uint256 processedCount;
}
mapping(uint256 => BatchRequest) public batches;
function requestBatchRandom(address[] calldata users) external {
uint256 requestId = coordinator.requestRandomNumbers(
uint32(users.length),
block.timestamp
);
batches[requestId] = BatchRequest({
users: users,
processedCount: 0
});
}
function rawFulfillRandomNumbers(
uint256 requestId,
uint256[] memory randomNumbers
) external override onlyVRFCoordinator {
BatchRequest storage batch = batches[requestId];
// Process all in one transaction
for (uint i = 0; i < batch.users.length; i++) {
_processUserRandom(batch.users[i], randomNumbers[i]);
}
delete batches[requestId];
}
}
Storage Optimization
Minimize storage operations:
contract OptimizedVRF is IVRFConsumer {
// Pack struct to save gas
struct Request {
address requester; // 20 bytes
uint96 timestamp; // 12 bytes - enough for timestamps
bool fulfilled; // 1 byte
// Total: 32 bytes (1 storage slot)
}
mapping(uint256 => Request) public requests;
mapping(uint256 => uint256) public results;
function rawFulfillRandomNumbers(
uint256 requestId,
uint256[] memory randomNumbers
) external override onlyVRFCoordinator {
Request memory req = requests[requestId]; // Load once
require(req.requester != address(0) && !req.fulfilled, "Invalid");
requests[requestId].fulfilled = true; // Single SSTORE
results[requestId] = randomNumbers[0];
// Process in memory
_process(req.requester, randomNumbers[0]);
}
}
Testing
Mock VRF Coordinator
For local testing:
contract MockVRFCoordinator {
uint256 private nonce;
function requestRandomNumbers(uint32 numNumbers, uint256 seed)
external
returns (uint256)
{
return ++nonce;
}
function fulfillRandomness(
address consumer,
uint256 requestId,
uint256[] calldata randomNumbers
) external {
IVRFConsumer(consumer).rawFulfillRandomNumbers(requestId, randomNumbers);
}
}
Best Practices
- Always validate the VRF coordinator address
- Track request state to prevent double processing
- Handle failures gracefully with fallback mechanisms
- Use events for off-chain monitoring
- Test thoroughly with mock VRF before mainnet
- Consider gas costs when requesting multiple numbers
- Implement timeouts for unfulfilled requests