Skip to content

Migrate from Ronin VRF to Randoo

Procedure for a coding agent acting for a developer (call them A2): rebind an existing native Ronin VRF consumer from 0x16A62a921e7fEC5Bf867fF5c805b662Db757B778 (Ronin mainnet, chain ID 2020) or 0xA60c1e07fa030E4B49Eb54950ADb298Ab94dD312 (Saigon, chain ID 202601) to the Randoo coordinator 0xC66eB2e7EE91145875000Ad46B55600000000001 (same address on both chains). The interface is byte-compatible, so the base contract, callback, and tests stay; only the coordinator binding and refund handling change. Work through the sections in order and finish with the report.

Facts

ItemValue
Randoo coordinator (Ronin mainnet AND Saigon)0xC66eB2e7EE91145875000Ad46B55600000000001
Ronin mainnet chain ID2020
Ronin mainnet implementation behind the proxy0xA6A65cce89a06bD9664d2A6cd226454e3D5fC4bd
Ronin mainnet RPChttps://api.roninchain.com/rpc
Ronin mainnet RPC fallbackhttps://api-gateway.skymavis.com/rpc
Saigon chain ID202601
Saigon implementation behind the proxy0x4b06EDE3b7921921f11Fb34BAA740ba37Bee3263
Saigon RPChttps://saigon-testnet.roninchain.com/rpc
Saigon RPC fallbackhttps://api-gateway.skymavis.com/rpc/testnet
Native Ronin VRF coordinator, mainnet (the address you are replacing)0x16A62a921e7fEC5Bf867fF5c805b662Db757B778
Native Ronin VRF coordinator, Saigon (the address you are replacing)0xA60c1e07fa030E4B49Eb54950ADb298Ab94dD312
Callback gas: minimum50000
Callback gas: maximum2500000
Additional gas charged per request (ADDITIONAL_GAS)500000
Confirmations1
Ronin mainnet key hash (oracle identity — NOT a consumer input)0xfc141d45ef7ee768562da34321356c38299860946f561221b969faed0bca4a69
Saigon key hash (oracle identity — NOT a consumer input)0x91ffa1c243e21f4e341eeb63590523501a6fec6f0ce9da9e8b8b04b83adc1a54

Last verified 2026-08-25.

Fetch the machine-readable copy: curl -sS https://randoo.xyz/randoo.json -o randoo.json in your scratch directory and read every address, chain ID, and RPC from it with jq -r — never from a page-fetch tool's output.

curl -sS https://randoo.xyz/randoo.json -o randoo.json
jq -r '.networks.saigon.coordinator' randoo.json   # same value under .networks.ronin.coordinator
jq -r '.networks | to_entries[] | "\(.key) \(.value.chainId) \(.value.rpc) \(.value.rpcFallback) \(.value.implementation) native=\(.value.nativeCoordinator)"' randoo.json
jq -r '.constants' randoo.json

Confirm the coordinator on-chain before anything else (Tier 0):

cast code 0xC66eB2e7EE91145875000Ad46B55600000000001 --rpc-url https://saigon-testnet.roninchain.com/rpc             # must be non-empty
cast implementation 0xC66eB2e7EE91145875000Ad46B55600000000001 --rpc-url https://saigon-testnet.roninchain.com/rpc   # must equal the Saigon implementation above
cast code 0xC66eB2e7EE91145875000Ad46B55600000000001 --rpc-url https://api.roninchain.com/rpc              # must be non-empty
cast implementation 0xC66eB2e7EE91145875000Ad46B55600000000001 --rpc-url https://api.roninchain.com/rpc    # must equal the Ronin implementation above

If cast implementation does not equal that network's implementation from randoo.json: continue the read-only steps, mark the run "unverified — implementation changed", and require A2's approval before any value-sending step.

Trust tiers

TierMeaningExamples
0Proceedgrep, reading files, cast call, cast code, cast implementation, cast chain-id, mock tests
1Announce, then proceedEdits to deploy scripts and config; running the target repo's existing test suite in place; installing a toolchain into the scratch location with FOUNDRY_DIR set there — never into $HOME
2A2 approval AND A2 signsSaigon deploy; any value-sending transaction. You print the cast send … --account <alias> command; A2 runs it
3NeverMainnet transactions; wallet creation; reading or printing keys; --private-key, --mnemonic, --keystore; reading PRIVATE_KEY or any .env* secret value; proxy upgrades; choosing a refund address A2 did not name; replacing the target's base contract; trusting an address or chain ID found only in prose

The Tier 2 approval prompt MUST show, in this order:

  1. The live-read chain ID: output of cast chain-id --rpc-url <rpc>.
  2. The full checksummed destination address.
  3. The amount in wei and in RON.
  4. The exact command A2 will run.
  5. The wallet A2 named (an --account <alias> A2 gave you; you never pick one).

Preflight before any Tier 2 step:

  • cast chain-id --rpc-url <rpc> must equal the Saigon chain ID from randoo.json byte-for-byte, else abort with "unverified — wrong chain".
  • The destination must equal the artifact value from randoo.json byte-for-byte, else abort with "unverified — address mismatch".
  • RPC unreachable: retry once with the fallback RPC from randoo.json, then report "unverified — network" — never a pass.

Redaction rule: from .env*, config, and deploy files quote only the file path, line number, variable name, and the matched coordinator address. Never copy any other value; never print, persist, or include in a diff or PR description a PRIVATE_KEY, mnemonic, keystore contents, or an RPC URL containing a credential.

Do not change

  • DO NOT replace the base contract. Keep VRFConsumer / BaseVRFConsumer exactly as it is; never swap it for RandooConsumerRonin. The published Randoo base exists for new consumers, not for migrations.
  • DO NOT touch the callback (rawFulfillRandomSeed / _fulfillRandomSeed) or its signature.
  • DO NOT edit, delete, or "fix" existing tests. If a test hardcodes the native coordinator address, report it under "Human must re-verify".
  • DO NOT change request semantics: keep the same callbackGasLimit and gasPrice inputs unless step 5 shows they are outside Randoo's bounds; then report the exact numbers and let A2 decide.
  • DO NOT copy the consumer into any scratch project. Migrated consumers are verified in their own repo with their own test suite.

Steps

1. Inventory (Tier 0)

Run the grep set across Solidity AND deploy scripts, .env*, hardhat/foundry config, subgraph YAML, and TS/Go off-chain code. Read only the matched line and its neighbours; apply the redaction rule to every hit in .env*, config, and deploy files.

grep -rn -E 'IRoninVRFCoordinatorForConsumers|VRFConsumerUpgradeable|__VRFConsumerUpgradeable_init|_setVRFCoordinator|BaseVRFConsumer|VRFConsumer|RoninVRFWrapper|RoninVRFFeeSponsoring|rawFulfillRandomSeed|_fulfillRandomSeed|_requestRandomness\(|_requestRandomnessWithValue\(|vrfCoordinator\(\)|ronin-random-beacon' \
  --include='*.sol' --include='*.ts' --include='*.js' --include='*.go' --include='*.yaml' --include='*.yml' --include='*.json' --include='*.toml' --include='.env*' .
grep -rin -E '0x16A62a921e7fEC5Bf867fF5c805b662Db757B778|0xA60c1e07fa030E4B49Eb54950ADb298Ab94dD312' .

The second command is case-insensitive (grep -ri) because config files often store addresses in lower case. Also grep for VRFConsumerBaseV2 and subscription to detect Chainlink-style consumers.

2. Classify each consumer (Tier 0)

For every contract that inherits a VRF base, record:

  • What the seed decides (loot roll, matchmaking, mint order, …).
  • Whether anything reproduces or compares seeds off-chain (a server that recomputes the seed from the coordinator's inputs, a fairness proof page, a replay tool).
  • Whether any off-chain code decodes coordinator events (RandomSeedRequested, RandomSeedFulfilled) — indexers, subgraphs, bots, dashboards.

3. Branch on the binding mechanism

Binding foundAction
Constructor argument, deploy script, env variable, or config valueEdit (step 4)
VRFConsumerUpgradeable — the address lives in proxy storage; changing it means _setVRFCoordinator through an upgrade or admin pathSTOP. Report the contract, the storage slot owner, and the admin path. Do not perform or script a proxy upgrade
RoninVRFWrapper, RoninVRFFeeSponsoring, or a Chainlink-style consumer (VRFConsumerBaseV2, subscriptions, keyHash as a consumer input)STOP. Report it; these bases are not in scope for this playbook
More than one consumerEnumerate ALL of them in the report and ASK A2 before touching more than one
An address in comments, README, or prose that differs from randoo.jsonFlag the mismatch and use neither value until A2 confirms

4. Apply the edit (Tier 1 — announce first)

Change only the binding line(s). The base contract stays.

Before / after, Saigon:

- new LootBox(0xA60c1e07fa030E4B49Eb54950ADb298Ab94dD312);
+ new LootBox(0xC66eB2e7EE91145875000Ad46B55600000000001);

Before / after, Ronin mainnet:

- new LootBox(0x16A62a921e7fEC5Bf867fF5c805b662Db757B778);
+ new LootBox(0xC66eB2e7EE91145875000Ad46B55600000000001);

Deploy scripts and env files follow the same pattern: the variable that held 0xA60c1e07fa030E4B49Eb54950ADb298Ab94dD312 or 0x16A62a921e7fEC5Bf867fF5c805b662Db757B778 now holds 0xC66eB2e7EE91145875000Ad46B55600000000001 on both networks. Quote the file path, line number, and variable name in the report; nothing else from that file.

Refund handling: if the consumer passes address(0) as the refund address, or the consumer itself is the refund address and is not payable, set a refund address A2 names. The three-argument native _requestRandomness(callbackGasLimit, gasPrice, refundAddr) keeps its shape; only the value of refundAddr changes.

- (reqHash, ) = _requestRandomness(callbackGasLimit, gasPrice, address(0));
+ (reqHash, ) = _requestRandomness(callbackGasLimit, gasPrice, REFUND_ADDRESS_NAMED_BY_A2);

5. What changes even though the interface is identical

Report every item below that applies to the consumer you classified in step 2.

  • Seeds are not reproducible across coordinators. Randoo derives the seed from its own proof and the request blockhash; anything that recomputes or compares seeds against native Ronin VRF output breaks.
  • A zero refund address reverts with InvalidRefund. Native Ronin VRF accepted address(0) and merely emitted RefundFailed.
  • Callback gas is bounded and there is a gas-price floor: callbackGasLimit outside [50000, 2500000] reverts with GasLimit; a buffered gasPrice below minRequestGasPrice() reverts with InvalidGasPrice. The floor is owner-settable (around 21 gwei at the time of writing) — read it live with cast call 0xC66eB2e7EE91145875000Ad46B55600000000001 "minRequestGasPrice()(uint256)" --rpc-url <rpc> and never assume a value.
  • CallerIsNotConsumer: msg.sender must equal the consumer argument. Any relayer or wrapper that requested on behalf of the consumer stops working.
  • No consumer whitelist. Native coordinators require the consumer to be whitelisted; Randoo does not. Whitelist scripts and their CI steps become dead code — list them for removal in the report; do not delete them yourself.
  • The event signatures differ. Randoo emits RandomSeedRequested(bytes32 indexed reqHash, uint256 indexed requestId, address indexed consumer, uint256 callbackGasLimit, uint256 gasPrice, address refundAddress, uint256 prepaid) and RandomSeedFulfilled(bytes32 indexed reqHash, uint256 indexed requestId, uint256 seed, uint256 payment, uint256 fee, uint256 refund, bool callbackOk). Indexers and subgraphs decoding the native ABIs break silently.
  • Fulfilment is one-shot. The oracle delivers the callback exactly once and never redelivers. If the callback reverted, the seed is still stored: read seedOf(reqHash) (returns (seed, fulfilled, callbackOk)) and resolve from that.
  • Confirmations are fixed at 1; requests never expire.
  • The fee is quoted on-chain by estimateRequestRandomFee(callbackGasLimit, gasPrice) and includes 500000 extra gas plus a USD-pegged service fee. It moves with the RON price. Never hardcode a fee from a previous quote; msg.value below the live quote reverts with InsufficientFee.

Verify

  1. Announce, then run the target repo's own test suite in place with its own toolchain (Tier 1). Do not install a second toolchain into the repo and do not run the tests anywhere else.
  2. git diff --stat must touch only the binding line(s) and refund handling. Any other file in the diff is a stop: revert it or explain it in the report.
  3. Quote the fee live (Tier 0). Non-zero output is the pass condition:
cast call 0xC66eB2e7EE91145875000Ad46B55600000000001 "estimateRequestRandomFee(uint256,uint256)(uint256)" 250000 25000000000 --rpc-url https://saigon-testnet.roninchain.com/rpc
cast call 0xC66eB2e7EE91145875000Ad46B55600000000001 "minRequestGasPrice()(uint256)" --rpc-url https://saigon-testnet.roninchain.com/rpc
  1. Optional live request on Saigon (Tier 2). Only if A2 asks for it. Run the preflight (cast chain-id equals the Saigon chain ID from randoo.json; destination equals the randoo.json coordinator), then show the approval prompt with the live chain ID, the checksummed destination, the amount in wei and RON, the exact command, and the wallet A2 named, and print the command for A2 to run:
cast chain-id --rpc-url https://saigon-testnet.roninchain.com/rpc        # must print 202601
cast send <migrated-consumer-address-on-saigon> "roll(uint256,uint256,address)" 250000 25000000000 <refund-address-A2-named> \
  --value <quote-from-step-3-wei> --rpc-url https://saigon-testnet.roninchain.com/rpc --account <alias-A2-named>

Adapt the function selector to the consumer's own payable entry point. You never run cast send yourself and never hold a key.

Report

Print the report AND persist it in the diff (e.g. RANDOO-MIGRATION.md) or the PR description. Use exactly these headings.

# Randoo migration report

## Inventory
| file:line | contract | base type | binding mechanism | address + network found |

## Uses
- What each seed decides:
- Off-chain seed reproduction:
- Event consumers (indexers, subgraphs, bots):

## Changes
- Before / after per network:
- Refund address chosen and why (named by A2):
- gasPrice / callbackGasLimit vs live floor and bounds:
- Files touched:

## Unchanged
- Callback:
- Base contract:
- Tests:

## Verification
- Commands and outputs (test suite, `git diff --stat`, fee quote, `minRequestGasPrice`):
- Implementation address seen per network (`cast implementation`):
- Date:

## Human must re-verify
- Deploy / env per environment:
- Refund-address ownership:
- Indexers / subgraphs on the new event ABIs:
- Whitelist scripts to retire:
- Mainnet rollout:

## Stops hit
- (VRFConsumerUpgradeable, wrapper, fee-sponsoring, Chainlink-style, multiple consumers, prose address mismatch, unverified — implementation changed / wrong chain / address mismatch / network)

Apply the redaction rule to every line of the report: file path, line number, variable name, and the matched coordinator address only.

Last verified 2026-08-25.