@paintswap/vrf v0.0.7
Paintswap VRF
A decentralized Verifiable Random Function (VRF) service for the Sonic ecosystem, providing secure and verifiable on-chain randomness for smart contracts.
Paintswap VRF Dashboard: https://vrf.paintswap.io
Overview
Paintswap VRF is a comprehensive solution for generating verifiable random numbers on-chain. It consists of a coordinator contract that manages randomness requests and oracle fulfillments, along with consumer contracts that can request and receive random numbers.
Features
- ✅ Verifiable Randomness: Uses cryptographic proofs to ensure randomness cannot be manipulated
- ✅ Oracle Network: Distributed oracle system for reliable fulfillment
- ✅ Gas Efficient: Optimized for low-cost operations on Sonic
- ✅ TypeScript Support: Full type definitions included
- ✅ Development Tools: Mock contracts and examples for testing
- ✅ Battle Tested: Comprehensive test suite with full coverage
Installation
npm install @paintswap/vrfNetwork Support
| Network | Chain ID | VRF Coordinator |
|---|---|---|
| Sonic Mainnet | 146 | Coming soon |
| Blaze Testnet | 57054 | 0xcCD87C20Dc14ED79c1F827800b5a9b8Ef2E43eC5 |
Quick Start
Using the Consumer Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@paintswap/vrf/contracts/PaintswapVRFConsumer.sol";
contract MyContract is PaintswapVRFConsumer {
uint256 public constant CALLBACK_GAS_LIMIT = 100_000;
mapping(uint256 => address) public requestToUser;
event RandomnessRequested(uint256 indexed requestId, address indexed user);
event RandomnessReceived(uint256 indexed requestId, uint256[] randomWords);
error InsufficientPayment();
error InvalidRequest(uint256 requestId);
constructor(address vrfCoordinator) PaintswapVRFConsumer(vrfCoordinator) {}
function requestRandomness() external payable returns (uint256 requestId) {
// Calculate the required payment for the VRF request
uint256 requestPrice = _calculateRequestPriceNative(CALLBACK_GAS_LIMIT);
require(msg.value >= requestPrice, InsufficientPayment());
// Request one random number
uint256 numberOfWords = 1;
requestId = _requestRandomnessPayInNative(CALLBACK_GAS_LIMIT, numberOfWords, requestPrice);
// Store the user for this request
requestToUser[requestId] = msg.sender;
emit RandomnessRequested(requestId, msg.sender);
return requestId;
}
function _fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) internal override {
address user = requestToUser[requestId];
require(user != address(0), InvalidRequest(requestId));
// Process your random words here
emit RandomnessReceived(requestId, randomWords);
}
}Using the TypeScript SDK
import { ethers } from "ethers";
import { PaintswapVRFCoordinator__factory } from "@paintswap/vrf";
const vrfAddress = "0xcCD87C20Dc14ED79c1F827800b5a9b8Ef2E43eC5";
// Connect to the VRF Coordinator
const provider = new ethers.JsonRpcProvider("https://rpc.soniclabs.com");
const coordinator = PaintswapVRFCoordinator__factory.connect(
vrfAddress,
provider,
);
// Listen for fulfillments
coordinator.on(
coordinator.filters.RandomWordsFulfilled,
(requestId, randomWords, oracle, callSuccess, fulfilledAt) => {
const fulfilledAtMs = Number(fulfilledAt) * 1000;
console.log(
`Request ${requestId} fulfilled at ${fulfilledAtMs}:`,
randomWords,
);
},
);
// Calculate request price
const callbackGasLimit = 100000;
const requestPrice =
await coordinator.calculateRequestPriceNative(callbackGasLimit);
// Request randomness
const numberOfWords = 2;
const tx = await coordinator.requestRandomnessPayInNative(
callbackGasLimit,
numberOfWords,
{ value: requestPrice },
);
// Wait for the transaction to be mined
const receipt = await tx.wait();
// Extract the request ID from the RandomWordsRequested event
const requestedEvent = receipt.logs.find(
(log) =>
log.topics[0] ===
coordinator.interface.getEvent("RandomWordsRequested").topicHash,
);
if (requestedEvent) {
const decodedEvent = coordinator.interface.parseLog(requestedEvent);
const requestId = decodedEvent.args.requestId;
console.log(`Request ID: ${requestId}`);
} else {
console.error("RandomWordsRequested event not found");
}Development & Testing
MockVRFCoordinator
For development and testing, use the MockVRFCoordinator which simulates the VRF coordinator without requiring cryptographic proofs or oracle networks. Important: The mock coordinator should only be used in your test files, not in your production consumer contracts.
Basic Testing Setup
// test/MyContract.test.ts
import { expect } from "chai";
import { ethers } from "hardhat";
import { MockVRFCoordinator } from "@paintswap/vrf";
import { MyContract } from "../typechain-types";
describe("MyContract", function () {
let mockCoordinator: MockVRFCoordinator;
let myContract: MyContract;
beforeEach(async function () {
// Deploy mock coordinator in your test
const MockCoordinator =
await ethers.getContractFactory("MockVRFCoordinator");
mockCoordinator = await MockCoordinator.deploy();
// Deploy your consumer contract with the mock coordinator address
const MyContract = await ethers.getContractFactory("MyContract");
myContract = await MyContract.deploy(await mockCoordinator.getAddress());
});
it("should handle randomness request and fulfillment", async function () {
// Calculate fee and make request
const fee = await mockCoordinator.calculateRequestPriceNative(100_000);
const tx = await myContract.requestRandomness({ value: fee });
const receipt = await tx.wait();
// Extract request ID from event
const event = receipt.logs.find(
(log) =>
log.topics[0] ===
myContract.interface.getEvent("RandomnessRequested").topicHash,
);
const requestId = myContract.interface.parseLog(event).args.requestId;
// Manually fulfill the request in the test
await mockCoordinator.fulfillRequestMock(requestId, [123n]);
// Verify that your contract processed the randomness
});
});Mock Coordinator Features
- Manual Fulfillment: Use
fulfillRequestMock(requestId, randomWords)to manually fulfill requests - Auto-Random Fulfillment: Use
fulfillRequestMockWithRandomWords(requestId)to fulfill with pseudo-random data - Request Tracking: Get request details with
getRequest(requestId)andgetRequestResult(requestId) - Statistics: Monitor requests and fulfillments with
getFulfillmentStats() - Debug Events: Detailed
DebugFulfillmentevents for callback failure analysis - Request ID Prediction: Use
calculateNextRequestId(consumer)to predict request IDs
Advanced Testing Functions
// Get detailed request information
const [consumer, gasLimit, words, payment, fulfilled] =
await mockCoordinator.getRequest(requestId);
// Check request results with detailed status
const [wasSuccess, wasFulfilled] =
await mockCoordinator.getRequestResult(requestId);
// Get comprehensive fulfillment statistics
const [total, pending, successes, failures, totalWordsRequested] =
await mockCoordinator.getFulfillmentStats();
// Predict request IDs for testing
const predictedRequestId =
await mockCoordinator.calculateNextRequestId(consumerAddress);ExampleVRFConsumer
The ExampleVRFConsumer demonstrates best practices for implementing VRF functionality with dual payment methods, request management, and utility functions.
import "@paintswap/vrf/contracts/examples/ExampleVRFConsumer.sol";
// Deploy the example consumer
ExampleVRFConsumer consumer = new ExampleVRFConsumer(coordinatorAddress);
// Fund the contract for requests
consumer.fundVRF{value: 1 ether}();
// Request randomness with direct payment
uint256 fee = consumer.getRequestPrice(3);
uint256 requestId = consumer.requestRandomWords{value: fee}(3);
// Or request using contract funds
uint256 requestId2 = consumer.requestRandomWordsFromContract(2);
// Check request status
(bool exists, bool fulfilled, address requester, uint256 numWords, uint256 requestedAt, uint256[] memory randomWords) =
consumer.getRequestStatus(requestId);Contract Imports
// TypeScript types and factories
import { PaintswapVRFConsumer__factory } from "@paintswap/vrf";
import { MockPaintswapVRFCoordinator__factory } from "@paintswap/vrf";pragma solidity ^0.8.20;
// Production contracts
import "@paintswap/vrf/contracts/PaintswapVRFConsumer.sol";
import "@paintswap/vrf/contracts/interfaces/IPaintswapVRFCoordinator.sol";
import "@paintswap/vrf/contracts/interfaces/IPaintswapVRFConsumer.sol";
// Development and testing (only import in test contracts)
import "@paintswap/vrf/contracts/mocks/MockVRFCoordinator.sol";
import "@paintswap/vrf/contracts/examples/ExampleVRFConsumer.sol";API Reference
IPaintswapVRFCoordinator
interface IPaintswapVRFCoordinator {
// Calculate the cost of a request
function calculateRequestPriceNative(uint256 callbackGasLimit)
external view returns (uint256 payment);
// Request random words with native payment
function requestRandomnessPayInNative(uint256 callbackGasLimit, uint256 numWords)
external payable returns (uint256 requestId);
// Check if a request is still pending
function isRequestPending(uint256 requestId)
external view returns (bool isPending);
}IPaintswapVRFCoordinator Docs: docs/interfaces/IPaintswapVRFCoordinator.md
IPaintswapVRFConsumer
interface IPaintswapVRFConsumer {
// Handle VRF response - must be implemented by consumer contracts
function rawFulfillRandomWords(
uint256 requestId,
uint256[] calldata randomWords
) external;
}IPaintswapVRFConsumer Docs: docs/interfaces/IPaintswapVRFConsumer.md
PaintswapVRFConsumer
abstract contract PaintswapVRFConsumer {
// Request randomness with native payment
function _requestRandomnessPayInNative(
uint256 callbackGasLimit,
uint256 numWords,
uint256 value
) internal returns (uint256 requestId);
// Calculate the cost of a request
function _calculateRequestPriceNative(uint256 callbackGasLimit)
internal view returns (uint256 requestPrice);
// Override this function to handle random words
function _fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords)
internal virtual;
// Callback from coordinator (only callable by coordinator)
function rawFulfillRandomWords(uint256 requestId, uint256[] calldata randomWords)
external virtual override;
}PaintswapVRFConsumer Docs: docs/PaintswapVRFConsumer.md
Events
RandomWordsRequested
event RandomWordsRequested(
uint256 indexed requestId,
uint256 callbackGasLimit,
uint256 numWords,
address indexed origin,
address indexed consumer,
uint256 nonce,
uint256 requestedAt
);RandomWordsFulfilled
event RandomWordsFulfilled(
uint256 indexed requestId,
uint256[] randomWords,
address indexed oracle,
bool callSuccess,
uint256 fulfilledAt
);ConsumerCallbackFailed
event ConsumerCallbackFailed(
uint256 indexed requestId,
uint8 indexed reason, // 1 = NotEnoughGas, 2 = NoCodeAtAddress, 3 = RevertedOrOutOfGas
address indexed target,
uint256 gasLeft
);Error Handling
// Consumer errors
error OnlyVRFCoordinator(address sender, address coordinator);
// Coordinator errors
error ZeroAddress();
error NotOracle(address invalid);
error InsufficientGasLimit(uint256 sent, uint256 required);
error InsufficientGasPayment(uint256 sent, uint256 required);
error InvalidNumWords(uint256 numWords, uint256 max);
error CommitmentMismatch(uint256 requestId);
error InvalidProof(uint256 requestId);
error InvalidPublicKey(uint256 requestId, address proofSigner, address vrfSigner);
error OverConsumerGasLimit(uint256 sent, uint256 max);Gas Considerations
| Operation | Estimated Gas | Notes |
|---|---|---|
| Request | ~120,000 - 140,000 | Includes request tracking and state updates |
| Mock Fulfillment | ~100,000 + callback | Simplified verification + your callback |
| Real VRF Fulfillment | ~300,000 + callback | Full cryptographic verification + callback |
How It Works
Request Phase: Your contract calls
_requestRandomnessPayInNative()which creates a unique commitment hash and emits aRandomWordsRequestedevent that oracles monitor.Oracle Processing: Oracles detect the event and calculate the VRF proof off-chain using cryptographic algorithms.
Fulfillment Phase: The oracle calls
fulfillRandomWords()on the coordinator which verifies the proof, generates random words, and calls your consumer'srawFulfillRandomWordsfunction.
Security Guarantees
- Each request has a unique commitment hash preventing replay attacks
- VRF proofs are mathematically verifiable and cannot be forged
- Only registered oracles with valid cryptographic signatures can fulfill requests
- Failed consumer callbacks don't affect the randomness generation or oracle payment
Testing Best Practices
Test Fixtures Setup
// test/fixtures.ts
import { ethers } from "hardhat";
import { MockVRFCoordinator, ExampleVRFConsumer } from "@paintswap/vrf";
export async function deployVRFFixture() {
const [owner, user1, user2] = await ethers.getSigners();
// Deploy mock coordinator
const MockCoordinator = await ethers.getContractFactory("MockVRFCoordinator");
const mockCoordinator = await MockCoordinator.deploy();
// Deploy example consumer
const Consumer = await ethers.getContractFactory("ExampleVRFConsumer");
const consumer = await Consumer.deploy(await mockCoordinator.getAddress());
return {
mockCoordinator,
consumer,
owner,
user1,
user2,
};
}
export async function deployVRFWithRequestFixture() {
const fixture = await deployVRFFixture();
const { mockCoordinator, consumer, user1 } = fixture;
// Make a request for testing
const fee = await consumer.getRequestPrice(2);
const tx = await consumer
.connect(user1)
.requestRandomWords(2, { value: fee });
const receipt = await tx.wait();
// Extract request ID from event
const event = receipt.logs.find(
(log) =>
log.topics[0] ===
consumer.interface.getEvent("RandomnessRequested").topicHash,
);
const requestId = consumer.interface.parseLog(event).args.requestId;
return {
...fixture,
requestId,
fee,
};
}Unit Testing with Fixtures
import { expect } from "chai";
import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers";
import { deployVRFFixture, deployVRFWithRequestFixture } from "./fixtures";
describe("VRF Consumer", function () {
describe("Request and Fulfillment", function () {
it("should handle randomness request and fulfillment", async function () {
const { mockCoordinator, consumer, user1 } =
await loadFixture(deployVRFFixture);
const fee = await consumer.getRequestPrice(2);
const tx = await consumer
.connect(user1)
.requestRandomWords(2, { value: fee });
const receipt = await tx.wait();
// Extract request ID from event
const event = receipt.logs.find(
(log) =>
log.topics[0] ===
consumer.interface.getEvent("RandomnessRequested").topicHash,
);
const requestId = consumer.interface.parseLog(event).args.requestId;
// Fulfill with mock coordinator
await mockCoordinator.fulfillRequestMock(requestId, [123n, 456n]);
// Verify fulfillment
const [exists, fulfilled, , , , randomWords] =
await consumer.getRequestStatus(requestId);
expect(exists).to.be.true;
expect(fulfilled).to.be.true;
expect(randomWords).to.deep.equal([123n, 456n]);
});
it("should handle multiple requests", async function () {
const { mockCoordinator, consumer, user1, user2 } =
await loadFixture(deployVRFFixture);
// Fund contract for requests
await consumer.fundVRF({ value: ethers.parseEther("1") });
// Make multiple requests
const tx1 = await consumer
.connect(user1)
.requestRandomWordsFromContract(3);
const tx2 = await consumer
.connect(user2)
.requestRandomWordsFromContract(5);
const receipt1 = await tx1.wait();
const receipt2 = await tx2.wait();
// Extract request IDs
const event1 = receipt1.logs.find(
(log) =>
log.topics[0] ===
consumer.interface.getEvent("RandomnessRequested").topicHash,
);
const event2 = receipt2.logs.find(
(log) =>
log.topics[0] ===
consumer.interface.getEvent("RandomnessRequested").topicHash,
);
const requestId1 = consumer.interface.parseLog(event1).args.requestId;
const requestId2 = consumer.interface.parseLog(event2).args.requestId;
// Fulfill both requests
await mockCoordinator.fulfillRequestMockWithRandomWords(requestId1);
await mockCoordinator.fulfillRequestMockWithRandomWords(requestId2);
// Verify statistics
const [total, fulfilled, pending] = await consumer.getStats();
expect(total).to.equal(2);
expect(fulfilled).to.equal(2);
expect(pending).to.equal(0);
});
});
describe("Request Management", function () {
it("should track requests by requester", async function () {
const { consumer, user1, user2 } = await loadFixture(deployVRFFixture);
// Fund contract
await consumer.fundVRF({ value: ethers.parseEther("1") });
// Make requests from different users
await consumer.connect(user1).requestRandomWordsFromContract(1);
await consumer.connect(user1).requestRandomWordsFromContract(2);
await consumer.connect(user2).requestRandomWordsFromContract(3);
// Check requests per user
const user1Requests = await consumer.getRequestsByRequester(
user1.address,
);
const user2Requests = await consumer.getRequestsByRequester(
user2.address,
);
expect(user1Requests).to.have.length(2);
expect(user2Requests).to.have.length(1);
});
});
describe("Error Handling", function () {
it("should revert with insufficient payment", async function () {
const { consumer, user1 } = await loadFixture(deployVRFFixture);
const fee = await consumer.getRequestPrice(1);
await expect(
consumer.connect(user1).requestRandomWords(1, { value: fee - 1n }),
).to.be.revertedWithCustomError(consumer, "InsufficientPayment");
});
it("should handle callback failures gracefully", async function () {
const { mockCoordinator, requestId } = await loadFixture(
deployVRFWithRequestFixture,
);
// This should emit DebugFulfillment event for debugging callback failures
await expect(
mockCoordinator.fulfillRequestMock(requestId, [123n, 456n]),
).to.emit(mockCoordinator, "RandomWordsFulfilled");
});
});
describe("Advanced Features", function () {
it("should predict request IDs", async function () {
const { mockCoordinator, consumer, user1 } =
await loadFixture(deployVRFFixture);
// Predict the next request ID
const predictedId = await mockCoordinator.calculateNextRequestId(
await consumer.getAddress(),
);
// Make the actual request
const fee = await consumer.getRequestPrice(1);
const tx = await consumer
.connect(user1)
.requestRandomWords(1, { value: fee });
const receipt = await tx.wait();
// Extract actual request ID
const event = receipt.logs.find(
(log) =>
log.topics[0] ===
consumer.interface.getEvent("RandomnessRequested").topicHash,
);
const actualId = consumer.interface.parseLog(event).args.requestId;
expect(actualId).to.equal(predictedId);
});
it("should provide detailed request information", async function () {
const { mockCoordinator, requestId } = await loadFixture(
deployVRFWithRequestFixture,
);
// Get request details
const [consumerAddr, gasLimit, numWords, payment, fulfilled] =
await mockCoordinator.getRequest(requestId);
expect(consumerAddr).to.not.equal(ethers.ZeroAddress);
expect(gasLimit).to.be.gt(0);
expect(numWords).to.equal(2);
expect(payment).to.be.gt(0);
expect(fulfilled).to.be.false;
// Fulfill and check again
await mockCoordinator.fulfillRequestMock(requestId, [123n, 456n]);
const [, , , , fulfilledAfter] =
await mockCoordinator.getRequest(requestId);
expect(fulfilledAfter).to.be.true;
});
});
});Integration Testing
describe("Integration Tests", function () {
it("should handle complete request lifecycle", async function () {
const { mockCoordinator, consumer, user1 } =
await loadFixture(deployVRFFixture);
// Step 1: Make request
const fee = await consumer.getRequestPrice(3);
const tx = await consumer
.connect(user1)
.requestRandomWords(3, { value: fee });
const receipt = await tx.wait();
// Step 2: Verify request is pending
const event = receipt.logs.find(
(log) =>
log.topics[0] ===
consumer.interface.getEvent("RandomnessRequested").topicHash,
);
const requestId = consumer.interface.parseLog(event).args.requestId;
expect(await mockCoordinator.isRequestPending(requestId)).to.be.true;
// Step 3: Fulfill request
await mockCoordinator.fulfillRequestMock(requestId, [111n, 222n, 333n]);
// Step 4: Verify fulfillment
expect(await mockCoordinator.isRequestPending(requestId)).to.be.false;
const [exists, fulfilled, requester, numWords, , randomWords] =
await consumer.getRequestStatus(requestId);
expect(exists).to.be.true;
expect(fulfilled).to.be.true;
expect(requester).to.equal(user1.address);
expect(numWords).to.equal(3);
expect(randomWords).to.deep.equal([111n, 222n, 333n]);
});
});Support
For questions and support:
- 🐛 GitHub Issues: paintswap/paintswap-vrf
- 💬 Discord: Paintswap Community
- 🐦 Twitter: @paint_swap
Built with ❤️ by the Paintswap team for the Sonic ecosystem.