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.
| Network | Chain ID | Contract | Address |
|---|---|---|---|
| Robinhood Chain | 4663 | not deployed yet | |
| Robinhood Chain Testnet | 46630 | Coordinator | 0x74c3472350C104119e1872f6D176d7AeFa8535DB |
| Demo consumer | 0x950b7aB74FaB08513276400CD594c8de57D33B22 | ||
| Anvil (local) | 31337 | Coordinator | 0x5FbDB2315678afecb367f032d93F642f64180aa3 |
| Demo consumer | 0xa513E6E4b8f2a923D98304ec87F64353C4D5C853 | ||
| Payment token | 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 | ||
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).
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