Randomhood

Documentation

Randomhood implements the Chainlink VRF v2.5 consumer interface on Robinhood Chain, on both mainnet and the testnet. If you have used VRF before, nothing here will surprise you.

Addresses

The same contracts are deployed on both networks. Mainnet is the default; use the network selector in the header to point the console, the explorer and the demo at the testnet instead.

NetworkChain IDContractAddress
Robinhood Chain4663not deployed yet
Robinhood Chain Testnet46630Coordinator0x74c3472350C104119e1872f6D176d7AeFa8535DB
Demo consumer0x950b7aB74FaB08513276400CD594c8de57D33B22
Anvil (local)31337Coordinator0x5FbDB2315678afecb367f032d93F642f64180aa3
Demo consumer0xa513E6E4b8f2a923D98304ec87F64353C4D5C853
Payment token0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512

Robinhood Chain

Chain ID
4663
RPC
https://rpc.mainnet.chain.robinhood.com
Explorer
https://robinhoodchain.blockscout.com
Key hash
not deployed yet

Robinhood Chain Testnet

Chain ID
46630
RPC
https://rpc.testnet.chain.robinhood.com
Explorer
https://explorer.testnet.chain.robinhood.com
Key hash
0xf9111e0960bddfc0fe370a05605c948bffd955b5a467dbb6ee6e175e2aec70b9

Anvil (local)

Chain ID
31337
RPC
http://127.0.0.1:8545
Explorer
Key hash
0xf9111e0960bddfc0fe370a05605c948bffd955b5a467dbb6ee6e175e2aec70b9

1. Create and fund a subscription

Open the console, click Create subscription, then fund it with ETH. Copy the subscription id. Or do it from a script:

cast send $COORDINATOR "createSubscription()" --private-key $KEY --rpc-url $RPC
cast send $COORDINATOR "fundSubscriptionWithNative(uint256)" $SUB_ID --value 0.05ether --private-key $KEY --rpc-url $RPC

2. Write your consumer

Add the contracts to your project and map the randomhood/ import prefix used below:

# add the contracts to your Foundry project
forge install <your-org>/randomhood

# map the import prefix used in the snippets (remappings.txt)
randomhood/=lib/randomhood/contracts/src/

Then inherit RandomhoodConsumerBase, request words through s_vrfCoordinator, and implement fulfillRandomWords:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {RandomhoodConsumerBase} from "randomhood/RandomhoodConsumerBase.sol";
import {RandomhoodClient} from "randomhood/libraries/RandomhoodClient.sol";

contract MyGame is RandomhoodConsumerBase {
  uint256 public s_subscriptionId;
  bytes32 public s_keyHash;
  mapping(uint256 => uint256) public results;

  constructor(address coordinator, uint256 subId, bytes32 keyHash)
    RandomhoodConsumerBase(coordinator)
  {
    s_subscriptionId = subId;
    s_keyHash = keyHash;
  }

  function play() external returns (uint256 requestId) {
    requestId = s_vrfCoordinator.requestRandomWords(
      RandomhoodClient.RandomWordsRequest({
        keyHash: s_keyHash,
        subId: s_subscriptionId,
        requestConfirmations: 1,
        callbackGasLimit: 100_000,
        numWords: 1,
        extraArgs: RandomhoodClient._argsToBytes(
          RandomhoodClient.ExtraArgsV1({nativePayment: true}) // pay in ETH
        )
      })
    );
  }

  // Called by the coordinator. Keep it cheap and never revert on purpose.
  function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords)
    internal
    override
  {
    results[requestId] = (randomWords[0] % 100) + 1;
  }
}

3. Add the consumer to the subscription

Deploy your contract, then add its address as a consumer in the console, or call addConsumer(subId, consumer). Requests from addresses that are not consumers revert with InvalidConsumer.

Migrating from Chainlink VRF v2.5

The structs, tags and function signatures are identical. Swap two imports and point your constructor at the coordinator and key hash above:

- import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
- import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";
+ import {RandomhoodConsumerBase as VRFConsumerBaseV2Plus} from "randomhood/RandomhoodConsumerBase.sol";
+ import {RandomhoodClient as VRFV2PlusClient} from "randomhood/libraries/RandomhoodClient.sol";

One difference: subscriptions are funded with a plain approve followed by fundSubscription(subId, amount), rather than Chainlink's ERC677 transferAndCall. Native funding is identical.

Request parameters

keyHash
which oracle and gas lane fulfills the request. Take it from the table above.
subId
the subscription that pays. The caller must be one of its consumers.
requestConfirmations
blocks the oracle waits before responding. The coordinator sets the minimum, 200 is the maximum.
callbackGasLimit
gas forwarded to your callback. You are billed for what it actually uses.
numWords
how many values you want, 1 to 500. Asking for several at once is far cheaper than asking twice.
extraArgs
_argsToBytes(ExtraArgsV1(nativePayment)). True pays in ETH, false pays in the ERC20 token.

Billing

You are charged on fulfillment, from the subscription, using the same formula as Chainlink VRF v2.5:

native:  payment = (gasUsed × tx.gasprice + l1Fee) × (100 + nativePremium%) / 100 + flatFeeNative
token:   payment = (gasUsed × tx.gasprice + l1Fee) × 1e18 / weiPerUnitToken × (100 + tokenPremium%) / 100
                   + (flatFeeNative − tokenDiscount) × 1e18 / weiPerUnitToken
  • The billed gas covers proof verification, your callback and the accounting after it.
  • A fulfillment is refused while the subscription cannot pay. Fund it and the oracle retries; it does not give up on an underfunded request.
  • Fulfillments above the gas lane's maximum gas price wait for cheaper gas, or are billed premium-only if the operator enables that.
  • If your callback reverts or runs out of gas you are still billed and the request is closed. Do not re-request for the same outcome.
  • Cancelling a subscription refunds every balance. It is blocked while a request is in flight.

How the randomness is produced

At request time the coordinator emits preSeed = keccak256(keyHash, sender, subId, nonce) and stores a commitment to every request parameter. The oracle computes seed = keccak256(preSeed ‖ blockhash(requestBlock)), signs it with the registered key, and submits the signature as the proof. On chain the coordinator checks the commitment, checks the block hash against the chain's own record, recovers the signer, and derives randomness = keccak256(signature); word i is keccak256(randomness, i).

Trust model. Only the registered oracle key can produce a valid fulfillment, nobody can predict the output before the request block exists, and the block hash a fulfillment is bound to is always one the chain itself attests (anyone can call storeBlockhash to preserve it past the 256-block window). Unlike an elliptic-curve VRF, however, an ECDSA signature is not unique: the key holder can sign the same seed with different nonces and pick among the results. The operator is therefore trusted not to bias outcomes. The proof check is a single function so a unique-signature scheme can replace it without changing the consumer interface.

Run your own oracle

The back end is a small Node service: it watches RandomWordsRequested, waits for confirmations, signs and calls fulfillRandomWords. Any relayer can submit a valid proof, so several instances can run for redundancy.

# 1. generate a signing key for your oracle
pnpm --filter @randomhood/oracle keygen

# 2. register it on the coordinator (owner only)
cast send $COORDINATOR "registerProvingKey(address,uint64)" $ORACLE_ADDRESS 50000000000 \
  --private-key $OWNER_KEY --rpc-url $RPC

# 3. run it
cp oracle/.env.example oracle/.env   # fill RPC_URL, COORDINATOR_ADDRESS, ORACLE_PRIVATE_KEY
pnpm oracle