Agent playbook: rebind a native VRFConsumer
For a coding agent working on behalf of a developer (A2). Goal: rebind an existing native Ronin VRF consumer from 0x16A62a921e7fEC5Bf867fF5c805b662Db757B778 (Ronin mainnet, chain 2020) or 0xA60c1e07fa030E4B49Eb54950ADb298Ab94dD312 (Saigon, chain 202601) to the Randoo coordinator 0xC66eB2e7EE91145875000Ad46B55600000000001 (same address on both chains). The interface is byte-compatible: the base contract, callback, and tests stay; only the coordinator binding and refund handling change. Finish with the report.
Facts
| Ronin mainnet | Saigon | |
|---|---|---|
| Randoo coordinator | 0xC66eB2e7EE91145875000Ad46B55600000000001 | 0xC66eB2e7EE91145875000Ad46B55600000000001 |
| Chain ID | 2020 | 202601 |
| Default public RPC | https://api.roninchain.com/rpc | https://saigon-testnet.roninchain.com/rpc |
| Other RPC providers | https://docs.roninchain.com/developers/tools/node-providers | same list |
| Native Ronin VRF (the address you replace) | 0x16A62a921e7fEC5Bf867fF5c805b662Db757B778 | 0xA60c1e07fa030E4B49Eb54950ADb298Ab94dD312 |
Callback gas 50000-2500000 · ADDITIONAL_GAS 500000 · confirmations 1. Last verified 2026-08-25.
Read every address, chain ID, and RPC from the machine-readable artifact, never from prose:
curl -sS https://randoo.xyz/randoo.json -o randoo.json
jq -r '.networks.saigon | .coordinator, .chainId, .rpc, .implementation, .nativeCoordinator' randoo.jsonConfirm the coordinator before anything else (Tier 0): cast code 0xC66eB2e7EE91145875000Ad46B55600000000001 --rpc-url <rpc> must be non-empty and cast implementation 0xC66eB2e7EE91145875000Ad46B55600000000001 --rpc-url <rpc> must equal networks.<network>.implementation in randoo.json (the proxy is the only address consumers use; this check only confirms it has not been swapped). If it does not, continue read-only, mark the run "unverified: implementation changed", and get A2's approval before any value-sending step.
Trust tiers
- Tier 0, proceed: reading files,
cast call/cast code/cast implementation/cast chain-id, mock tests. - Tier 1, announce, then proceed: edits to deploy scripts and config; running the target repo's own test suite in place.
- Tier 2, A2 approves and signs: a Saigon deploy, any value-sending transaction. You print the
cast send … --account <alias>command; A2 runs it. - Tier 3, never: mainnet transactions; creating wallets; reading or printing keys,
--private-key,--mnemonic,--keystore,PRIVATE_KEY, or any.env*secret; proxy upgrades; picking a refund address A2 did not name; replacing the base contract; trusting an address or chain ID found only in prose.
Before a Tier 2 step: cast chain-id --rpc-url <rpc> must equal the Saigon chain ID from randoo.json (else "unverified: wrong chain"); the destination must equal the randoo.json coordinator (else "unverified: address mismatch"); an unreachable default RPC is reported as "unverified: network", never a pass; A2 can supply an alternative endpoint from the Ronin node-providers list in randoo.json (nodeProviders), you never pick one. The approval prompt shows, in order: the live chain ID, the checksummed destination, the amount in wei and RON, the exact command, and the wallet A2 named.
Redaction: from .env*, config, and deploy files quote only the file path, line number, variable name, and the matched coordinator address; never print, persist, or include in a diff or PR a PRIVATE_KEY, mnemonic, keystore, or credentialed RPC URL.
Do not change
- The base contract. Keep
VRFConsumer/BaseVRFConsumer; never swap it forRandooConsumerRonin, which exists for new consumers. - The callback (
rawFulfillRandomSeed/_fulfillRandomSeed) or its signature. - Existing tests. Never edit, delete, or "fix" them; a test that hardcodes the native address goes under "Human must re-verify".
- Request semantics. Same
callbackGasLimitandgasPriceunless step 5 shows they are out of Randoo's bounds; report the numbers and let A2 decide. - The consumer's home. A migrated consumer is verified in its own repo with its own test suite, never copied into a scratch project.
Steps
1. Inventory (Tier 0)
Grep Solidity, deploy scripts, .env*, hardhat/foundry config, subgraph YAML, and off-chain TS/Go. Read only the matched line and its neighbours; apply the redaction rule to every hit outside Solidity.
grep -rn -E 'IRoninVRFCoordinatorForConsumers|VRFConsumerUpgradeable|__VRFConsumerUpgradeable_init|_setVRFCoordinator|BaseVRFConsumer|VRFConsumer|RoninVRFWrapper|RoninVRFFeeSponsoring|rawFulfillRandomSeed|_fulfillRandomSeed|_requestRandomness\(|_requestRandomnessWithValue\(|vrfCoordinator\(\)|ronin-random-beacon|VRFConsumerBaseV2|subscription' \
--include='*.sol' --include='*.ts' --include='*.js' --include='*.go' --include='*.yaml' --include='*.yml' --include='*.json' --include='*.toml' --include='.env*' .
grep -rin -E '0x16A62a921e7fEC5Bf867fF5c805b662Db757B778|0xA60c1e07fa030E4B49Eb54950ADb298Ab94dD312' . # -i: config files often lower-case addresses2. Classify each consumer (Tier 0)
For every contract that inherits a VRF base, record what the seed decides, whether anything reproduces or compares seeds off-chain (a server recomputing seeds, a fairness page, a replay tool), and whether any off-chain code decodes coordinator events (indexers, subgraphs, bots, dashboards).
3. Branch on the binding
| Binding found | Action |
|---|---|
| Constructor argument, deploy script, env variable, or config value | Edit (step 4) |
VRFConsumerUpgradeable: address in proxy storage, changed via _setVRFCoordinator through an upgrade or admin path | STOP. Report the contract, storage owner, and admin path. Never perform or script a proxy upgrade |
RoninVRFWrapper, RoninVRFFeeSponsoring, or a Chainlink-style consumer (VRFConsumerBaseV2, subscriptions, keyHash as a consumer input) | STOP. Out of scope; report it |
| More than one consumer | List all of them and ASK A2 before touching more than one |
An address in comments, README, or prose that differs from randoo.json | Flag it 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. The variable that held 0xA60c1e07fa030E4B49Eb54950ADb298Ab94dD312 or 0x16A62a921e7fEC5Bf867fF5c805b662Db757B778 now holds 0xC66eB2e7EE91145875000Ad46B55600000000001 on both networks.
- new LootBox(0xA60c1e07fa030E4B49Eb54950ADb298Ab94dD312); // or 0x16A62a921e7fEC5Bf867fF5c805b662Db757B778 on mainnet
+ new LootBox(0xC66eB2e7EE91145875000Ad46B55600000000001);Refund handling: if the consumer passes address(0), or is its own refund address without being payable, set a refund address A2 names. The native three-argument _requestRandomness(callbackGasLimit, gasPrice, refundAddr) keeps its shape.
- (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 that applies to the consumer from step 2.
- Seeds are not reproducible across coordinators: anything that recomputes or compares seeds against native output breaks.
- A zero refund address reverts with
InvalidRefund(native only emittedRefundFailed). callbackGasLimitoutside [50000, 2500000] reverts withGasLimit; a bufferedgasPricebelowminRequestGasPrice()reverts withInvalidGasPrice. The floor is owner-settable; read it live withcast call 0xC66eB2e7EE91145875000Ad46B55600000000001 "minRequestGasPrice()(uint256)" --rpc-url <rpc>.CallerIsNotConsumer:msg.sendermust be the consumer. Relayers and wrappers requesting on its behalf stop working.- No consumer whitelist. Whitelist scripts and their CI steps become dead code; list them for removal, do not delete them yourself.
- The event signatures differ:
RandomSeedRequested(bytes32 indexed reqHash, uint256 indexed requestId, address indexed consumer, uint256 callbackGasLimit, uint256 gasPrice, address refundAddress, uint256 prepaid)andRandomSeedFulfilled(bytes32 indexed reqHash, uint256 indexed requestId, uint256 seed, uint256 payment, uint256 fee, uint256 refund, bool callbackOk). Indexers decoding the native events break silently. - Fulfilment is one-shot. If the callback reverted, the seed is still stored: read
seedOf(reqHash)→(seed, fulfilled, callbackOk)and resolve from that. - Confirmations are fixed at 1; requests never expire.
- The fee is
estimateRequestRandomFee(callbackGasLimit, gasPrice):500000extra gas plus a USD-pegged service fee that moves with the RON price. Never hardcode a previous quote;msg.valuebelow the live quote reverts withInsufficientFee.
Verify
- Announce, then run the target repo's own test suite in place with its own toolchain (Tier 1).
git diff --statmust touch only the binding line(s) and refund handling. Anything else is a stop: revert it or explain it in the report.- Quote the fee live (Tier 0); non-zero output passes:
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- Optional live request on Saigon, only if A2 asks (Tier 2). Preflight, approval prompt, then print for A2 (adapt the selector to the consumer's own payable entry point):
cast chain-id --rpc-url https://saigon-testnet.roninchain.com/rpc # must print 202601
cast send <migrated-consumer> "roll(uint256,uint256,address)" 250000 25000000000 <refund-named-by-A2> --value <quote-wei> --rpc-url https://saigon-testnet.roninchain.com/rpc --account <alias>Report
Print it and persist it (RANDOO-MIGRATION.md or the PR description) with exactly these headings; apply the redaction rule to every line.
# Randoo migration report
## Inventory file:line | contract | base | binding | address + network found
## Uses what each seed decides; off-chain seed reproduction; event consumers
## Changes before/after per network; refund address named by A2; gasPrice / callbackGasLimit vs live floor and bounds; files touched
## Unchanged callback; base contract; tests
## Verification test suite, git diff --stat, fee quote, minRequestGasPrice, cast implementation per network, date
## Human must re-verify deploy/env per environment; refund-address ownership; indexers on the new event signatures; 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 / networkLast verified 2026-08-25.