Overview
DogeOS VRF (verifiable random function) gives smart contracts on DogeOS random numbers that nobody can predict or manipulate — including us. Your contract requests randomness and pays a small fee in DOGE. Our oracle derives the random number from your request with its secret key and produces a cryptographic proof. The coordinator contract verifies that proof on-chain against the oracle's registered public key, and only then calls your contract back with the number.
Because every output must pass on-chain verification, the oracle cannot substitute a number it likes better, and the DogeOS sequencer can't influence it the way it could influence a block hash. Proof verification uses Chainlink's MIT-licensed secp256k1 ECVRF verifier, unmodified.
Network & addresses
| Value | |
|---|---|
| Network | DogeOS Chikyū testnet |
| Chain id | 6281971 |
| RPC | https://rpc.testnet.dogeos.com/ |
| Explorer | dogeos-testnet.l2scan.co |
| VRF coordinator | 0x4463AF4EAccFFB4AC14e87e9dac09B59e9E3614c |
| Gas token | DOGE (native, 18 decimals) |
Quickstart: Solidity
A complete consumer contract: players pay the VRF fee to roll a die, and the result is written when the coordinator calls back. This exact file is in the repo at contracts/src/examples/DiceRoller.sol and is covered by an end-to-end test that verifies real proofs.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
// Example DogeOS VRF consumer, shown verbatim in the developer docs
// (/docs#quickstart-solidity). Tested in test/DiceRoller.t.sol against the
// real coordinator with real ECVRF proofs.
interface IDogeVRFCoordinator {
function requestFee() external view returns (uint256);
function requestRandomness(bytes32 userSeed) external payable returns (uint256 requestId);
}
interface IVRFConsumer {
function rawFulfillRandomness(uint256 requestId, uint256 randomness) external;
}
contract DiceRoller is IVRFConsumer {
IDogeVRFCoordinator public immutable coordinator;
mapping(uint256 => address) public rollerOf; // requestId => player
mapping(address => uint256) public lastRoll; // player => 1..6
event RollRequested(uint256 indexed requestId, address indexed player);
event Rolled(uint256 indexed requestId, address indexed player, uint256 result);
constructor(address coordinator_) {
coordinator = IDogeVRFCoordinator(coordinator_);
}
/// Player pays the VRF fee; the result arrives in a later transaction.
function roll() external payable returns (uint256 requestId) {
uint256 fee = coordinator.requestFee();
require(msg.value == fee, "send exactly the VRF fee");
requestId = coordinator.requestRandomness{value: fee}(
keccak256(abi.encode(msg.sender, block.number))
);
rollerOf[requestId] = msg.sender;
emit RollRequested(requestId, msg.sender);
}
/// Called by the coordinator once the oracle's proof has been verified.
function rawFulfillRandomness(uint256 requestId, uint256 randomness) external {
require(msg.sender == address(coordinator), "only coordinator");
address player = rollerOf[requestId];
require(player != address(0), "unknown request");
delete rollerOf[requestId];
uint256 result = (randomness % 6) + 1;
lastRoll[player] = result;
emit Rolled(requestId, player, result);
}
}
- Deploy it with the coordinator address:
new DiceRoller(0x4463AF4E…). - Read the current fee with
requestFee()on the coordinator, then callroll()sending exactly that amount. - Listen for
Rolled— or readlastRoll(player)— once the request is fulfilled.
Quickstart: JavaScript
You can also request randomness straight from a wallet or backend (no contract needed): the result is stored on the coordinator for you to read. This script was run end-to-end against the testnet; it's in the repo at bot/examples/request-randomness.mjs.
// Request a verifiable random number from DogeOS VRF and wait for it.
//
// PRIVATE_KEY=0x... COORDINATOR=0x... node examples/request-randomness.mjs
//
// Shown verbatim in the developer docs (/docs#quickstart-js).
import {
createPublicClient,
createWalletClient,
defineChain,
http,
keccak256,
parseAbi,
parseEventLogs,
toHex,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
const dogeos = defineChain({
id: 6281971,
name: "DogeOS Chikyu Testnet",
nativeCurrency: { name: "Dogecoin", symbol: "DOGE", decimals: 18 },
rpcUrls: { default: { http: ["https://rpc.testnet.dogeos.com/"] } },
});
const coordinator = {
address: process.env.COORDINATOR,
abi: parseAbi([
"function requestFee() view returns (uint256)",
"function requestRandomness(bytes32 userSeed) payable returns (uint256 requestId)",
"function pendingRequest(uint256 requestId) view returns (address consumer, uint64 requestedAt, bool fulfilled, uint256 seed)",
"function responses(uint256 requestId) view returns (uint256)",
"event RandomnessRequested(uint256 indexed requestId, address indexed consumer, uint256 seed)",
]),
};
const account = privateKeyToAccount(process.env.PRIVATE_KEY);
const pub = createPublicClient({ chain: dogeos, transport: http() });
const wallet = createWalletClient({ account, chain: dogeos, transport: http() });
// 1. Pay the current fee and request randomness
const fee = await pub.readContract({ ...coordinator, functionName: "requestFee" });
const hash = await wallet.writeContract({
...coordinator,
functionName: "requestRandomness",
args: [keccak256(toHex(`my-app:${Date.now()}`))],
value: fee,
});
const receipt = await pub.waitForTransactionReceipt({ hash });
const [requested] = parseEventLogs({
abi: coordinator.abi,
logs: receipt.logs,
eventName: "RandomnessRequested",
});
const { requestId } = requested.args;
console.log(`request ${requestId} sent (${hash})`);
// 2. Wait for the oracle's proof to be verified on-chain
for (;;) {
const [, , fulfilled] = await pub.readContract({
...coordinator,
functionName: "pendingRequest",
args: [requestId],
});
if (fulfilled) break;
await new Promise((r) => setTimeout(r, 3000));
}
const randomness = await pub.readContract({
...coordinator,
functionName: "responses",
args: [requestId],
});
console.log(`random number: ${randomness}`);
console.log(`dice roll (1-6): ${(randomness % 6n) + 1n}`);
Request lifecycle
- Request. You call
requestRandomness(userSeed)with the fee. The coordinator assigns a request id and derives the VRF seed askeccak256(abi.encode(chainId, coordinator, requestId, msg.sender, userSeed))— unique per request, so two requests can never share an output — and emitsRandomnessRequested(requestId, consumer, seed). - Prove. The oracle computes the VRF output and proof for that seed with its secret key.
- Verify. Anyone may submit the proof with
fulfill(proof, requestId)— it is self-authenticating. The coordinator checks it against the registered key and seed, stores the output inresponses(requestId), and emitsRandomnessFulfilled(requestId, randomness, callbackSuccess). - Callback. The coordinator calls
rawFulfillRandomness(requestId, randomness)on the requester with a gas limit ofcallbackGasLimit()(500,000 by default). If your callback reverts or runs out of gas, the number is still stored and you can read it fromresponses.
Fees
Each request costs a flat fee in native DOGE — currently 2 DOGE — sent as msg.value. The amount must match exactly; read the live value from requestFee() rather than hard-coding it, since it can be adjusted. The fee covers the oracle's gas for delivering your result; there's nothing else to pay and no subscription or token.
Contract reference
Functions
| Function | Description |
|---|---|
requestRandomness(bytes32 userSeed) payable → uint256 requestId | Request a random number. Send exactly requestFee(). |
requestFee() → uint256 | Current fee per request, in wei of DOGE. |
pendingRequest(uint256 id) → (consumer, requestedAt, fulfilled, seed) | Status of a request. |
responses(uint256 id) → uint256 | The verified random number, once fulfilled (0 before). |
lastRequestId() → uint256 | Total requests made. |
callbackGasLimit() → uint32 | Gas forwarded to your callback. |
oraclePublicKey(uint256) → uint256 | The registered oracle key (x at index 0, y at index 1). |
fulfill(Proof proof, uint256 id) | Submit a proof. Called by the oracle; permissionless. |
Your callback
| Function | Notes |
|---|---|
rawFulfillRandomness(uint256 requestId, uint256 randomness) | Implement this. Must check msg.sender is the coordinator. |
Events & errors
| Name | When |
|---|---|
RandomnessRequested(id, consumer, seed) | A request was made. |
RandomnessFulfilled(id, randomness, callbackSuccess) | A proof was verified and delivered. |
WrongFee(expected, sent) | msg.value didn't match requestFee(). |
UnknownRequest / AlreadyFulfilled | Fulfilling a request that doesn't exist or is done. |
WrongKey / WrongSeed | Proof made with another key or for another request. |
Security & best practices
- Always check the caller. In
rawFulfillRandomness, requiremsg.sender == coordinator. Otherwise anyone can feed you a number. - Lock the inputs before you request. Close the bet, entry list or game state first, then request. Never let users act after seeing a result that affects them.
- One request per outcome. Don't reuse a random number for several independent decisions without deriving separate values, e.g.
keccak256(abi.encode(randomness, i)). - Keep the callback small. Stay well under the 500,000 gas limit and don't revert on valid input. If you need heavy work, store the number and process it in a separate transaction.
- Plan for a slow response. The oracle can't forge an output, but like any oracle it could fail to respond. For anything with money at stake, add your own timeout fallback, as Doge Raffle does.
- Mind reorgs. DogeOS can reorg up to 17 blocks. For high-value outcomes, wait for that depth before treating a result as final off-chain.
Verifying a result
A result only exists if its fulfill transaction succeeded — and it only succeeds if the proof passed on-chain verification against the oracle's registered key and your request's seed. To re-check independently, take the proof from that transaction's input data and run it through the same verifier (contracts/src/vrf/vendor/VRF.sol), for example in a Foundry test. The random number is uint256(keccak256(abi.encode(3, gamma))), where gamma is part of the proof.
Testnet status
Questions or want to integrate? Open an issue on GitHub.