RISE Logo-Light

Quickstart

Build a dice game with verifiable randomness in 15 minutes

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

Smart Contract Setup

Create the Project

mkdir vrf-dice-game
cd vrf-dice-game
forge init

Write the Dice Game Contract

Create src/DiceGame.sol:

src/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

Configure foundry.toml:

foundry.toml
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc_version = "0.8.19"

[rpc_endpoints]
rise_testnet = "https://testnet.riselabs.xyz"

[etherscan]
rise_testnet = { key = "", url = "https://explorer.testnet.riselabs.xyz/api" }

Deploy:

forge create --rpc-url rise_testnet \
  --private-key $PRIVATE_KEY \
  --constructor-args 0x9d57aB4517ba97349551C876a01a7580B1338909 \
  src/DiceGame.sol:DiceGame

Verify on Blockscout:

forge verify-contract <DEPLOYED_ADDRESS> \
  src/DiceGame.sol:DiceGame \
  --chain 11155931 \
  --verifier blockscout \
  --verifier-url https://explorer.testnet.riselabs.xyz/api \
  --constructor-args $(cast abi-encode "constructor(address)" 0x9d57aB4517ba97349551C876a01a7580B1338909)

Frontend Integration

Setup Frontend

npm install viem
pnpm add viem
yarn add viem
bun add viem

Monitor VRF Events

Create monitor.ts to track dice roll events:

monitor.ts
import { createPublicClient, webSocket } from 'viem';
import { riseTestnet } from 'viem/chains';

const wsUrl = 'wss://testnet.riselabs.xyz/ws';
const diceGameAddress = '0x...'; // Your deployed contract

const vrfAbi = [
  {
    type: 'event',
    name: 'DiceRollCompleted',
    inputs: [
      { name: 'player', type: 'address', indexed: true },
      { name: 'requestId', type: 'uint256', indexed: true },
      { name: 'result', type: 'uint256' },
      { name: 'currentStreak', type: 'uint256' },
      { name: 'topStreak', type: 'uint256' }
    ]
  }
] as const;

const client = createPublicClient({
  chain: riseTestnet,
  transport: webSocket(wsUrl)
});

const unwatch = client.watchContractEvent({
  address: diceGameAddress,
  abi: vrfAbi,
  eventName: 'DiceRollCompleted',
  onLogs: (logs) => {
    logs.forEach((log) => {
      console.log(`Player rolled: ${log.args.result}`);
      console.log(`Streak: ${log.args.currentStreak}`);
    });
  }
});

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