npm package: @paintswap/vrf
Description: Paintswap VRF
Latest version: 0.0.10

# Paintswap VRF

[![npm version](https://badge.fury.io/js/%40paintswap%2Fvrf.svg)](https://badge.fury.io/js/%40paintswap%2Fvrf)

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](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. Typical round trip response time to fulfillment is 1-2 seconds.

The only fee required is the fulfillment gas payment, based on the callback gas limit and the current gas prices seen on the network. To use the service implement the `PaintswapVRFConsumer` contract or `IPaintswapVRFConsumer` interface, price the request, then submit the fulfillment gas payment either from the user or supplied at request time by the consumer contract. Our VRF Oracle will do the rest.

If the callback to the consumer contract reverts or runs out of gas, the request is considered fulfilled and **_will not be retried_**. Please ensure that your callback functions can handle this case.

Unused gas for the fulfillment callback is refunded to the `refundee` address specified in the callback. This could be anything from `tx.origin`, `msg.sender`, `address(this)`, or use `address(0)` to leave excess gas as a tip for the service 🙏.

> _Note: there is a 50k gas threshold for refunds as well as a 10% Sonic network penalty on unused gas._

### Features

- ✅ **Verifiable Randomness**: Uses cryptographic proofs to ensure randomness cannot be manipulated
- ✅ **Gas Efficient**: Optimized for low-cost operations on Sonic
- ✅ **Solidity Support**: Consumer contracts, interfaces, and mocks included
- ✅ **TypeScript Support**: Full type definitions included
- ✅ **Blazing Fast**: Leverages the speed of the Sonic network

## Installation

```bash
npm install @paintswap/vrf
```

## Network Support

| Network       | Chain ID | VRF Coordinator                              |
| ------------- | -------- | -------------------------------------------- |
| Sonic Mainnet | 146      | `0x6E3efcB244e74Cb898A7961061fAA43C3cf79691` |
| Blaze Testnet | 57054    | `0x6E3efcB244e74Cb898A7961061fAA43C3cf79691` |

## Quick Start

### Using the Consumer Contract

```solidity
// 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 gasPayment = _calculateRequestPriceNative(CALLBACK_GAS_LIMIT);
        require(msg.value >= gasPayment, InsufficientPayment());

        // Request one random number
        uint256 numberOfWords = 1;
        address refundee = msg.sender;
        requestId = _requestRandomnessPayInNative(CALLBACK_GAS_LIMIT, numberOfWords, refundee, gasPayment);

        // 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];

        // Process your random words here
        emit RandomnessReceived(requestId, randomWords);
    }
}
```

### Using the TypeScript SDK

```typescript
import { ethers } from "ethers";
import { PaintswapVRFCoordinator__factory } from "@paintswap/vrf";

const vrfAddress = "0x6E3efcB244e74Cb898A7961061fAA43C3cf79691";

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

```typescript
// 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)` and `getRequestResult(requestId)`
- **Statistics**: Monitor requests and fulfillments with `getFulfillmentStats()`
- **Debug Events**: Detailed `DebugFulfillment` events for callback failure analysis
- **Request ID Prediction**: Use `calculateNextRequestId(consumer)` to predict request IDs

#### Advanced Testing Functions

```typescript
// 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.

```solidity
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
// TypeScript types and factories
import { PaintswapVRFConsumer__factory } from "@paintswap/vrf";
import { MockPaintswapVRFCoordinator__factory } from "@paintswap/vrf";
```

```solidity
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

```solidity
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](docs/interfaces/IPaintswapVRFCoordinator.md)

### IPaintswapVRFConsumer

```solidity
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](docs/interfaces/IPaintswapVRFConsumer.md)

### PaintswapVRFConsumer

```solidity
abstract contract PaintswapVRFConsumer {
    // Calculate the cost of a request
    function _calculateRequestPriceNative(uint256 callbackGasLimit)
        internal view returns (uint256 requestPrice);

    // Request randomness with native payment
    function _requestRandomnessPayInNative(
        uint256 callbackGasLimit, // gas limit for callback
        uint256 numWords,         // number of random words
        uint256 refundee,         // gas refundee or address(0)
        uint256 gasPayment        // fulfillment gas fee
    ) internal returns (uint256 requestId);

    // 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](docs/PaintswapVRFConsumer.md)

## Events

### RandomWordsRequested

```solidity
 event RandomWordsRequested(
  uint256 indexed requestId, // unique request id
  uint256 callbackGasLimit,  // gas limit for consumer callback
  uint256 numWords,          // random words requested
  address indexed origin,    // tx.origin of request
  address indexed consumer,  // msg.sender of request
  uint256 nonce,             // request nonce for consumer
  address refundee,          // gas refunds or address(0)
  uint256 gasPricePaid,      // fulfillment fee gass price
  uint256 requestedAt        // block.timestamp of request
);
```

### RandomWordsFulfilled

```solidity
event RandomWordsFulfilled(
    uint256 indexed requestId, // unique request id
    uint256[] randomWords,     // random words generated
    address indexed oracle,    // fulfillment worker
    bool callSuccess,          // consumer callback result
    uint256 fulfilledAt        // block.timestamp of fulfillment
);
```

### ConsumerCallbackFailed

```solidity
event ConsumerCallbackFailed(
    uint256 indexed requestId, // unique request id
    uint8 indexed reason,      // 1 = NotEnoughGas,
                               // 2 = NoCodeAtAddress,
                               // 3 = RevertedOrOutOfGas
    address indexed target,    // callback consumer contract
    uint256 gasLeft            // gas remaining after callback
);
```

## Error Handling

```solidity
// 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

1. **Request Phase**: Your contract calls `_requestRandomnessPayInNative()` which creates a unique commitment hash and emits a `RandomWordsRequested` event that oracles monitor.

2. **Oracle Processing**: Oracles detect the event and calculate the VRF proof off-chain using cryptographic algorithms.

3. **Fulfillment Phase**: The oracle calls `fulfillRandomWords()` on the coordinator which verifies the proof, generates random words, and calls your consumer's `rawFulfillRandomWords` function.

### 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

```typescript
// 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

```typescript
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

```typescript
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](https://github.com/paintswap/paintswap-vrf/issues)
- 💬 **Discord**: [Paintswap Community](https://discord.gg/paintswap)
- 🐦 **Twitter**: [@paint_swap](https://twitter.com/paint_swap)

---

Built with ❤️ by the Paintswap team for the Sonic ecosystem.