Skip to content

VRF Quickstart

Build a dice game with verifiable randomness and real-time updates in under 15 minutes. This tutorial demonstrates VRF integration with instant result notifications.

What We'll Build

A dice game that:

  • Requests verifiable random numbers
  • Tracks player streaks for rolling sixes
  • Updates UI in real-time via WebSocket
  • Handles VRF callbacks securely

Prerequisites

  • Node.js 16+ installed
  • Basic Solidity knowledge
  • MetaMask or similar wallet
  • RISE testnet tokens (available from faucet)

Smart Contract Setup

Create the Project

mkdir vrf-dice-game
cd vrf-dice-game
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npx hardhat init

Write the Dice Game Contract

Create contracts/DiceGame.sol:

contracts/DiceGame.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
 
interface IVRFCoordinator {
    function requestRandomNumbers(uint32 numNumbers, uint256 seed) external returns (uint256);
}
 
interface IVRFConsumer {
    function rawFulfillRandomNumbers(
        uint256 requestId,
        uint256[] memory randomNumbers
    ) external;
}
 
contract DiceGame is IVRFConsumer {
    IVRFCoordinator public coordinator;
    
    mapping(address => bool) public hasPendingRoll;
    mapping(address => uint256) public currentStreak;
    mapping(address => uint256) public topStreak;
    mapping(uint256 => address) public requestOwners;
    
    uint256 public requestCount = 0;
    uint8 private constant DICE_SIDES = 6;
    
    event DiceRollRequested(address indexed player, uint256 indexed requestId);
    event DiceRollCompleted(
        address indexed player,
        uint256 indexed requestId,
        uint256 result,
        uint256 currentStreak,
        uint256 topStreak
    );
    event NewTopScore(address indexed player, uint256 newTopStreak);
    
    constructor(address _coordinator) {
        coordinator = IVRFCoordinator(_coordinator);
    }
    
    function rollDice() external returns (uint256 requestId) {
        require(!hasPendingRoll[msg.sender], "Already has a pending roll");
        
        hasPendingRoll[msg.sender] = true;
        uint256 seed = requestCount++;
        requestId = coordinator.requestRandomNumbers(1, seed);
        requestOwners[requestId] = msg.sender;
        
        emit DiceRollRequested(msg.sender, requestId);
        return requestId;
    }
    
    function rawFulfillRandomNumbers(
        uint256 requestId,
        uint256[] memory randomNumbers
    ) external {
        require(msg.sender == address(coordinator), "Only coordinator can fulfill");
        require(randomNumbers.length > 0, "No random numbers provided");
        
        address player = requestOwners[requestId];
        require(player != address(0), "Unknown request ID");
        require(hasPendingRoll[player], "No pending roll");
        
        uint256 diceRoll = (randomNumbers[0] % DICE_SIDES) + 1;
        
        if (diceRoll == 6) {
            currentStreak[player] += 1;
            if (currentStreak[player] > topStreak[player]) {
                topStreak[player] = currentStreak[player];
                emit NewTopScore(player, topStreak[player]);
            }
        } else {
            currentStreak[player] = 0;
        }
        
        hasPendingRoll[player] = false;
        
        emit DiceRollCompleted(
            player,
            requestId,
            diceRoll,
            currentStreak[player],
            topStreak[player]
        );
        
        delete requestOwners[requestId];
    }
}

Deploy to RISE Testnet

Create scripts/deploy.js:

scripts/deploy.js
const hre = require("hardhat");
 
async function main() {
  const VRF_COORDINATOR = "0x9d57aB4517ba97349551C876a01a7580B1338909";
  
  const DiceGame = await hre.ethers.getContractFactory("DiceGame");
  const diceGame = await DiceGame.deploy(VRF_COORDINATOR);
  
  await diceGame.waitForDeployment();
  
  console.log("DiceGame deployed to:", await diceGame.getAddress());
}
 
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Configure hardhat.config.js:

hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
 
module.exports = {
  solidity: "0.8.19",
  networks: {
    riseTestnet: {
      url: "https://testnet.riselabs.xyz",
      chainId: 11155931,
      accounts: [process.env.PRIVATE_KEY]
    }
  }
};

Deploy:

npx hardhat run scripts/deploy.js --network riseTestnet

Frontend Integration

Setup Frontend

npm install shreds viem ethers@6

Create the UI

Create index.html:

index.html
<!DOCTYPE html>
<html>
<head>
    <title>RISE VRF Dice Game</title>
    <style>
        body { font-family: monospace; padding: 20px; }
        .dice { font-size: 72px; margin: 20px 0; }
        .stats { margin: 20px 0; }
        button { padding: 10px 20px; font-size: 16px; }
        .pending { opacity: 0.5; }
        .log { margin-top: 20px; padding: 10px; background: #f0f0f0; }
    </style>
</head>
<body>
    <h1>🎲 RISE VRF Dice Game</h1>
    <div id="dice" class="dice">?</div>
    <div class="stats">
        <p>Current Streak: <span id="streak">0</span></p>
        <p>Top Streak: <span id="topStreak">0</span></p>
    </div>
    <button id="rollBtn" onclick="rollDice()">Roll Dice</button>
    <div id="log" class="log"></div>
    
    <script type="module" src="app.js"></script>
</body>
</html>

Monitor VRF Events

Create monitor.ts to track dice roll events:

monitor.ts
import { ethers } from 'ethers';
 
const wsUrl = 'wss://testnet.riselabs.xyz/ws';
const diceGameAddress = '0x...'; // Your deployed contract
 
// Event signatures
const DICE_ROLL_COMPLETED = ethers.id('DiceRollCompleted(address,uint256,uint256,uint256,uint256)');
const DICE_ROLL_REQUESTED = ethers.id('DiceRollRequested(address,uint256)');
 
const ws = new WebSocket(wsUrl);
 
ws.onopen = () => {
  // Subscribe to contract events
  ws.send(JSON.stringify({
    jsonrpc: '2.0',
    id: 1,
    method: 'eth_subscribe',
    params: ['logs', { address: diceGameAddress }]
  }));
};
 
ws.onmessage = (event) => {
  const response = JSON.parse(event.data);
  
  if (response.method === 'eth_subscription') {
    const log = response.params.result;
    const eventTopic = log.topics[0];
    
    if (eventTopic === DICE_ROLL_COMPLETED) {
      // Decode event data
      const playerAddress = '0x' + log.topics[1].slice(26);
      const requestId = BigInt(log.topics[2]).toString();
      
      const [result, currentStreak, topStreak] = ethers.AbiCoder.defaultAbiCoder().decode(
        ['uint256', 'uint256', 'uint256'],
        log.data
      );
      
      console.log(`Player ${playerAddress} rolled ${result}`);
      console.log(`Streak: ${currentStreak}, Top: ${topStreak}`);
    }
  }
};

Testing the Game

  1. Open the Application: Load index.html in your browser
  2. Connect Wallet: Ensure you have RISE testnet tokens
  3. Roll the Dice: Click the button and watch the instant result
  4. Track Streaks: Try to roll consecutive sixes!

How It Works

  1. User initiates: Click roll button in the DApp
  2. Request randomness: DApp calls rollDice() on the contract
  3. VRF processing: Contract requests random numbers from VRF coordinator
  4. Instant callback: VRF calls rawFulfillRandomNumbers() with result
  5. Event emission: Contract emits DiceRollCompleted event
  6. Real-time update: WebSocket delivers event to DApp in ~4ms
  7. UI update: DApp shows the dice result immediately

Key Takeaways

  • Instant Results: VRF responds in milliseconds via shreds
  • Real-time Updates: WebSocket delivers events instantly
  • Secure Randomness: Cryptographically verifiable numbers
  • Simple Integration: Standard Solidity interfaces

Next Steps

  • Add betting mechanics with token stakes
  • Create multiplayer dice battles
  • Implement leaderboards
  • Add different game modes

Resources