Migrate from Ronin VRF
Ronin VRF is the on-chain verifiable random function used by Ronin games: a consumer calls requestRandomSeed on a coordinator and receives a proven seed in rawFulfillRandomSeed. Native Ronin VRF is the Sky Mavis coordinator. Randoo is a separate drop-in with the same consumer interface.
Your consumer already works with Randoo. It is the same IRoninVRFCoordinatorForConsumers interface (requestRandomSeed, estimateRequestRandomFee, rawFulfillRandomSeed), so the migration is one address:
// before: native Ronin VRF
new LootBox(0x16A62a921e7fEC5Bf867fF5c805b662Db757B778); // mainnet
new LootBox(0xA60c1e07fa030E4B49Eb54950ADb298Ab94dD312); // Saigon
// after: Randoo (same address on mainnet AND Saigon)
new LootBox(0xC66eB2e7EE91145875000Ad46B55600000000001);No code changes, no re-audit of your consumer, no new SDK. Deploy your existing VRFConsumer with the Randoo coordinator address and requests flow the same way, for half the service fee ($0.005 vs $0.01), quoted by the same estimateRequestRandomFee call.
Consumer · unchanged
The published sample is TemplateConsumer; its base RandooConsumerRonin ships with the sample and is inlined first. It has the same shape as the native VRFConsumer base, so an existing consumer keeps its code and only changes the coordinator address at deploy.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IRoninVRFCoordinatorForConsumers} from "./interfaces/IRoninVRFCoordinatorForConsumers.sol";
/// @notice Ronin VRFConsumer-shaped base. Swap coordinator address to migrate.
abstract contract RandooConsumerRonin {
error OnlyCoordinatorCanFulfill();
address public vrfCoordinator;
constructor(address vrfCoordinator_) {
vrfCoordinator = vrfCoordinator_;
}
function rawFulfillRandomSeed(bytes32 reqHash, uint256 randomSeed) external {
if (msg.sender != vrfCoordinator) revert OnlyCoordinatorCanFulfill();
_fulfillRandomSeed(reqHash, randomSeed);
}
function _fulfillRandomSeed(bytes32 reqHash, uint256 randomSeed) internal virtual;
function _requestRandomness(
uint256 value,
uint256 callbackGasLimit,
uint256 gasPriceToFulfill,
address refundAddr
) internal returns (bytes32 reqHash) {
reqHash = IRoninVRFCoordinatorForConsumers(vrfCoordinator).requestRandomSeed{value: value}(
callbackGasLimit, gasPriceToFulfill, address(this), refundAddr
);
}
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
// ---------------------------------------------------------------------------
// Randoo consumer template: copy-pasteable as a unit with
// RandooConsumerTemplate.t.sol and MockRandooCoordinator.sol (this folder), plus
// src/RandooConsumerRonin.sol and src/interfaces/IRoninVRFCoordinatorForConsumers.sol.
// Imports resolve in a fresh `forge init` with only forge-std installed.
// ---------------------------------------------------------------------------
import {RandooConsumerRonin} from "../../src/RandooConsumerRonin.sol";
/// @notice The canonical Randoo consumer. Copy it, rename it, put your game
/// logic in `_fulfillRandomSeed`.
///
/// Flow: a caller pays `estimateRequestRandomFee(callbackGasLimit, gasPrice)`
/// into `roll`, which forwards the request to the coordinator and returns the
/// `reqHash`. Later the coordinator calls `rawFulfillRandomSeed` (inherited,
/// coordinator-only) which lands in `_fulfillRandomSeed` with the seed.
contract TemplateConsumer is RandooConsumerRonin {
/// @notice Last seed delivered by the coordinator.
uint256 public lastSeed;
/// @notice Request hash of the most recent `roll`, then of the most recent fulfil.
bytes32 public lastReqHash;
constructor(address vrfCoordinator_) RandooConsumerRonin(vrfCoordinator_) {}
/// @param callbackGasLimit Gas the coordinator will give `_fulfillRandomSeed` (50k..2.5M).
/// @param gasPrice Gas price the fulfiller may spend; must be >= the coordinator floor.
/// @param refundAddr Receives unspent fulfilment gas; must be non-zero.
function roll(uint256 callbackGasLimit, uint256 gasPrice, address refundAddr)
external
payable
returns (bytes32 reqHash)
{
reqHash = _requestRandomness(msg.value, callbackGasLimit, gasPrice, refundAddr);
lastReqHash = reqHash;
}
/// @dev Your randomness lands here. Keep it within `callbackGasLimit`.
function _fulfillRandomSeed(bytes32 reqHash, uint256 randomSeed) internal override {
lastReqHash = reqHash;
lastSeed = randomSeed;
}
}What stays the same
- The consumer base contract and callback signature.
- Pay-per-request in RON with msg.value; leftover refunded.
- Billing at your declared gasPrice (plus a 10% buffer applied at request time), never the fulfiller's actual tx fee. Recommended:
20e9 + block.basefee * 2. - Proofs verified on-chain by the same audited Chainlink VRF verifier.
What is different under the hood
- Half the service fee: $0.005 per request instead of $0.01.
- One coordinator address on both networks, so configs and scripts need no per-chain address table.
- Different seed derivation (bound to the request blockhash): seeds are not comparable across the two coordinators, only their guarantees are.
- If your consumer is not payable, set
refundAddressto an EOA.
The full caveat list lives in the migration playbook: /docs/migrate-from-ronin-vrf.md.
Last verified 2026-08-25.