> ## Documentation Index
> Fetch the complete documentation index at: https://docs.goldsky.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a VRF system

> Build an on-chain verifiable random function (VRF) system using Compose and drand

This guide walks you through building a randomness delivery system using Compose and [drand](https://drand.love), a distributed randomness beacon. The system listens for on-chain randomness requests and fulfills them with drand values that anyone can verify off-chain against drand's BLS public key.

<Note>
  This example stores the drand signature on-chain but does not verify the BLS signature inside the Solidity contract (on-chain BLS12-381 verification is expensive and out of scope for this guide). Consumers who need trustless randomness should either verify the signature off-chain before using the result, or add on-chain BLS verification to the contract.
</Note>

## How it works

```mermaid theme={null}
flowchart LR
    A[Source Contract] -->|"emit event"| B[Compose Task]
    B -->|"fetch randomness"| C[drand API]
    C -->|"round, randomness, signature"| B
    B -->|"fulfillRandomness"| D[Target Contract]
```

1. **Source contract** emits a `RandomnessRequested` event
2. **Compose task** is triggered by the on-chain event
3. **drand API** provides verifiable randomness with BLS signatures
4. **Target contract** records the randomness along with the drand round and signature so consumers can verify it off-chain

## Prerequisites

* [Goldsky CLI installed](/installation)
* [Foundry](https://book.getfoundry.sh/getting-started/installation) for contract deployment
* A funded wallet on Base Sepolia

## Project structure

```text theme={null}
VRF/
├── compose.yaml                    # Compose configuration
├── contracts/
│   └── RandomnessConsumer.sol      # Example contract
├── src/
│   ├── contracts/
│   │   └── RandomnessConsumer.json # Contract ABI (for codegen)
│   ├── lib/
│   │   └── drand.ts                # drand API utilities
│   └── tasks/
│       ├── generate-wallet.ts      # Wallet generation utility
│       ├── request-randomness.ts   # HTTP endpoint for requests
│       └── fulfill-randomness.ts   # Main fulfillment task
```

## Step 1: Set up the project

Clone the example repository:

```bash theme={null}
git clone https://github.com/goldsky-io/documentation-examples.git
cd documentation-examples/compose/VRF
```

## Step 2: Generate contract types

The project includes a `RandomnessConsumer.json` ABI in `src/contracts/`. Generate the typed contract class:

```bash theme={null}
goldsky compose codegen
```

This creates a typed `RandomnessConsumer` class that provides type-safe contract interaction instead of raw function signature strings.

## Step 3: Generate your Compose wallet

Start Compose locally in one terminal:

```bash theme={null}
goldsky compose start --fork-chains
```

`--fork-chains` lets you run a smart wallet locally by forking supported chains with TEVM. You can also use a private key wallet for local development — see [Secrets](/compose/secrets) for how to store the key.

In another terminal, get your wallet address:

```bash theme={null}
goldsky compose callTask generate_wallet '{}'
```

Save the wallet address — this will be the authorized fulfiller for your contract.

## Step 4: Deploy the smart contract

The `RandomnessConsumer.sol` contract handles randomness requests and fulfillment:

```solidity theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract RandomnessConsumer {
    struct RandomnessRequest {
        address requester;
        bool fulfilled;
        bytes32 randomness;
        uint64 round;
        bytes signature;
    }

    address public fulfiller;
    uint256 public nextRequestId;
    mapping(uint256 => RandomnessRequest) public requests;

    event RandomnessRequested(uint256 indexed requestId, address indexed requester);
    event RandomnessFulfilled(uint256 indexed requestId, bytes32 randomness, uint64 round, bytes signature);

    constructor(address _fulfiller) {
        fulfiller = _fulfiller;
    }

    function requestRandomness() external returns (uint256 requestId) {
        requestId = nextRequestId++;
        requests[requestId] = RandomnessRequest({
            requester: msg.sender,
            fulfilled: false,
            randomness: bytes32(0),
            round: 0,
            signature: ""
        });
        emit RandomnessRequested(requestId, msg.sender);
    }

    function fulfillRandomness(
        uint256 requestId,
        bytes32 randomness,
        uint64 round,
        bytes calldata signature
    ) external {
        require(msg.sender == fulfiller, "OnlyFulfiller");
        RandomnessRequest storage request = requests[requestId];
        require(request.requester != address(0), "RequestNotFound");
        require(!request.fulfilled, "AlreadyFulfilled");

        request.fulfilled = true;
        request.randomness = randomness;
        request.round = round;
        request.signature = signature;

        emit RandomnessFulfilled(requestId, randomness, round, signature);
    }
}
```

Deploy to Base Sepolia:

```bash theme={null}
forge create contracts/RandomnessConsumer.sol:RandomnessConsumer \
  --rpc-url https://sepolia.base.org \
  --private-key $PRIVATE_KEY \
  --broadcast \
  --constructor-args 0xYOUR_COMPOSE_WALLET_ADDRESS
```

Save the deployed contract address.

## Step 5: Configure the Compose app

Update `compose.yaml` with your contract address:

```yaml theme={null}
name: "compose-vrf"
api_version: "stable"

tasks:
  - path: "./src/tasks/generate-wallet.ts"
    name: "generate_wallet"
    triggers:
      - type: "http"
        authentication: "auth_token"

  - path: "./src/tasks/request-randomness.ts"
    name: "request_randomness"
    triggers:
      - type: "http"
        authentication: "auth_token"

  - path: "./src/tasks/fulfill-randomness.ts"
    name: "fulfill_randomness"
    triggers:
      - type: "onchain_event"
        network: "base_sepolia"
        contract: "0xYOUR_DEPLOYED_CONTRACT_ADDRESS"
        events:
          - "RandomnessRequested(uint256,address)"
    retry_config:
      max_attempts: 3
      initial_interval_ms: 1000
      backoff_factor: 2
```

Update the contract address in the task files:

* `src/tasks/fulfill-randomness.ts` — `TARGET_CONTRACT`
* `src/tasks/request-randomness.ts` — `CONTRACT_ADDRESS`

## Step 6: Understand the fulfillment task

The `fulfill-randomness.ts` task handles the core logic:

```typescript theme={null}
import { TaskContext, OnchainEvent } from "compose";
import {
  fetchLatestRandomness,
  toBytes32,
  toBytes,
  DRAND_CHAIN_INFO,
} from "../lib/drand.ts";

const CONTRACT_ADDRESS = "0xYOUR_DEPLOYED_CONTRACT_ADDRESS";

export async function main(context: TaskContext, event?: OnchainEvent) {
  const { fetch, evm } = context;

  // Extract request ID from the event
  const requestId = event?.topics[1] ? BigInt(event.topics[1]) : 0n;

  // Fetch randomness from drand
  const drandResponse = await fetchLatestRandomness(fetch);

  console.log(`Fetched drand round ${drandResponse.round}`);

  // Get wallet and instantiate typed contract (generated from src/contracts/RandomnessConsumer.json)
  const wallet = await evm.wallet({
    name: "randomness-fulfiller",
  });

  const contract = new evm.contracts.RandomnessConsumer(
    CONTRACT_ADDRESS,
    evm.chains.baseSepolia,
    wallet
  );

  // Fulfill the randomness request on-chain
  const { hash } = await contract.fulfillRandomness(
    requestId.toString(),
    toBytes32(drandResponse.randomness),
    drandResponse.round,
    toBytes(drandResponse.signature)
  );

  console.log(`Fulfilled request ${requestId} in tx ${hash}`);

  return {
    success: true,
    requestId: requestId.toString(),
    transactionHash: hash,
    drand: {
      round: String(drandResponse.round),
      randomness: toBytes32(drandResponse.randomness),
      chainHash: DRAND_CHAIN_INFO.hash,
    },
  };
}
```

Key points:

* **Contract codegen** — `evm.contracts.RandomnessConsumer` is generated from the ABI JSON, providing type-safe method calls like `contract.fulfillRandomness(...)` instead of raw function signature strings
* Uses `evm.chains.baseSepolia` — a built-in chain, no custom configuration needed
* Gas is sponsored by default for smart wallets on supported chains. On chains where sponsorship isn't available, fund the wallet directly or pass `sponsorGas: false` and cover gas from the wallet
* `fulfillRandomness` does seven or more SSTOREs (the signature is a 96-byte `bytes` field). Gas estimation handles this on Base Sepolia, but if you port this to a chain where you set the gas limit manually, budget \~250k. On Monad, where the full gas limit is charged regardless of gas used, keep the limit as tight as possible

## Step 7: Understand the drand integration

The `drand.ts` library fetches verifiable randomness:

```typescript theme={null}
export type DrandResponse = {
  round: number;
  randomness: string; // hex - sha256(signature)
  signature: string;  // hex - BLS12-381 signature (96 bytes)
  previous_signature: string;
};

export const DRAND_CHAIN_INFO = {
  hash: "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971",
  publicKey: "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a",
  genesisTime: 1692803367,
  period: 3, // seconds between rounds
};

export async function fetchLatestRandomness(
  fetchFn: <T>(url: string) => Promise<T | undefined>
): Promise<DrandResponse> {
  const response = await fetchFn<DrandResponse>(
    `https://api.drand.sh/${DRAND_CHAIN_INFO.hash}/public/latest`
  );
  if (!response) {
    throw new Error("Failed to fetch randomness from drand");
  }
  return response;
}
```

The randomness is verifiable off-chain using drand's BLS12-381 signatures. Anyone can check that the signature is valid against drand's public key, and that `sha256(signature) == randomness`, using a drand client library.

## Step 8: Run locally

Start the Compose app:

```bash theme={null}
goldsky compose start --fork-chains
```

## Step 9: Test the system

Request randomness by calling the contract:

```bash theme={null}
cast send 0xYOUR_CONTRACT_ADDRESS "requestRandomness()" \
  --rpc-url https://sepolia.base.org \
  --private-key $PRIVATE_KEY
```

Watch the Compose logs. You should see:

1. The `RandomnessRequested` event being detected
2. Randomness fetched from drand
3. The fulfillment transaction submitted

## Step 10: Deploy to Goldsky

Once tested locally, deploy to Goldsky's cloud:

```bash theme={null}
goldsky compose deploy
```

## Customization

### Different chains

Use any [supported chain](/compose/context/evm/chains) — no custom configuration needed:

```typescript theme={null}
const wallet = await evm.wallet({ name: "my-wallet" });
const result = await wallet.writeContract(
  evm.chains.arbitrumSepolia,  // or baseSepolia, polygonAmoy, etc.
  CONTRACT_ADDRESS,
  "myFunction(uint256)",
  [arg1]
);
```

### Different events

Modify the `compose.yaml` to listen for different events:

```yaml theme={null}
triggers:
  - type: "onchain_event"
    network: "base_sepolia"
    contract: "0xYOUR_CONTRACT"
    events:
      - "YourCustomEvent(uint256,address,bytes32)"
```

## Resources

* [drand documentation](https://docs.drand.love)
* [Compose introduction](/compose/introduction)
* [Task triggers](/compose/task-triggers)
* [EVM wallets](/compose/context/evm/wallets)
* [GitHub repository](https://github.com/goldsky-io/documentation-examples/tree/main/compose/VRF)
