Skip to content

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

  1. Always validate the VRF coordinator address
  2. Track request state to prevent double processing
  3. Handle failures gracefully with fallback mechanisms
  4. Use events for off-chain monitoring
  5. Test thoroughly with mock VRF before mainnet
  6. Consider gas costs when requesting multiple numbers
  7. Implement timeouts for unfulfilled requests