Migrate from Chainlink VRF to Chainlink CRE
For new randomness use cases, use the Chainlink Runtime Environment (CRE). Where Chainlink VRF delivers a random number and nothing else, CRE generates randomness as one step inside a workflow that can also read onchain state, call APIs, run your business logic, and write results to one or more chains — all in a single deployment.
This guide maps a VRF integration to a CRE workflow: how CRE generates randomness, how that randomness is secured, and how to move the request/fulfill contract flow into a workflow.
How VRF and CRE randomness differ
In VRF, your consumer contract inherits VRFConsumerBaseV2Plus and calls requestRandomWords() on the VRF Coordinator. A Chainlink node generates a random value off-chain together with an ECVRF proof, submits it to the Coordinator, and the Coordinator verifies the proof onchain before calling your fulfillRandomWords() callback with the verified random words. Randomness is the entire product; any surrounding logic lives in your contract or an offchain service.
In CRE, the unit of execution is a workflow — a TypeScript (or Go) project compiled to WebAssembly and run across a Decentralized Oracle Network (DON). A trigger (cron, HTTP, or onchain log) starts the workflow, the handler calls the runtime's randomness function, and the result is written onchain as a DON-signed report to any contract implementing IReceiver. Randomness is one capability among many the handler can call.
How CRE randomness works and how it is secured
CRE randomness is consensus-derived. Every node in the DON runs the same workflow binary and, for a given execution, derives the same per-execution seed. Calling the runtime's random function produces the same sequence of values on every node, so the DON can reach consensus on a single result. See Using Randomness in Workflows for the full behavior.
What that gives you, and how each part is checkable:
- No single node chooses the number. The value is fixed by the shared seed and reproduced independently by every node. A result is only produced if a Byzantine-fault-tolerant supermajority of the DON agrees on it, so no individual node operator can select or bias the outcome.
- The result reaches the chain as a DON-signed report. CRE writes the result through the Keystone Forwarder; a consumer implementing
IReceiveronly accepts reports carrying a valid signature from the authorized DON. This produces a tamper-evident, onchain trail of every delivered value. - The randomness code is open source and auditable. The seed derivation and the generator are part of the public SDK and runtime. Anyone can read exactly how a value is produced rather than trusting a description of it.
- Every result is bound to an identified workflow build. The
workflowID— a hash of the compiled workflow binary plus its config — is registered onchain and included in every signed report. This ties each delivered value to a specific, published workflow, so a result cannot be attributed to code that was never registered. See Verifying Workflows for how to check a workflow's identity and reproduce its build. - Executions are inspectable by your team. The CRE dashboard shows each execution's handler logs, per-step timings, consensus outcome, and the resulting onchain transaction — for your own operational audit and incident review.
Chain support
CRE runs on a broad set of networks. For the canonical, up-to-date list, see Supported Networks.
Terminology
| Chainlink VRF | Chainlink CRE |
|---|---|
VRFConsumerBaseV2Plus consumer contract | IReceiver consumer contract (use ReceiverTemplate) |
requestRandomWords() from a consumer | A trigger (EVM Log, Cron, or HTTP) starts the workflow |
| VRF Coordinator | Workflow DON + Keystone Forwarder |
| ECVRF proof verified onchain by the Coordinator | DON consensus + DON-signed report accepted by IReceiver |
fulfillRandomWords(requestId, randomWords) | _onReport(metadata, payload) on the receiver |
| Off-chain proof generation by a single node | Math.random() inside the CRE WASM runtime, agreed by the DON |
keyHash / gas lane | Chain selector + forwarder address per chain |
| LINK/native subscription or direct funding | CRE service model — no per-request subscription to fund |
Triggers
VRF has one entry path: a contract calling requestRandomWords(). CRE makes the trigger first-class, which also determines how predictable the per-execution seed is.
Onchain events
The direct replacement for the VRF request path. Instead of your contract calling a coordinator, the DON watches for an event your contract emits and fires the handler with the decoded event data.
const evmClient = new cre.evm.EVMClient(CHAIN_SELECTOR)
const trigger = evmClient.logTrigger({
addresses: [
/* base64-encoded contract address */
],
topics: [
/* base64-encoded event signature hash */
],
})
Time-based
Use a cron trigger for scheduled draws (periodic raffles, sampling) with no onchain request needed.
const trigger = cre.capabilities.cron.trigger({ schedule: "*/5 * * * *" }) // every 5 min
HTTP triggers
For draws requested by an offchain application. The workflow exposes an HTTPS endpoint; callers POST a payload signed with an authorized EVM key.
const trigger = cre.capabilities.http.trigger({
authorizedKeys: [{ type: "EVM", key: "0xYourAuthorizedSigner" }],
})
Generating the random number
In VRF the number arrives in fulfillRandomWords(). In CRE you generate it inside the handler with Math.random(), which the CRE WASM runtime overrides with a consensus-safe, DON-seeded generator. See Using Randomness in Workflows for the full reference.
// Math.random() returns a value in [0, 1)
const randomFloat = Math.random()
// Integer in [0, 100)
const randomInt = Math.floor(Math.random() * 100)
// bigint for a Solidity uint256 range [0, max)
const max = 1000000000000000000n // 1 ETH in wei
const randomBigInt = BigInt(Math.floor(Number(max) * Math.random()))
Onchain writes
This is the biggest structural change. In VRF the Coordinator calls your fulfillRandomWords() callback. In CRE the workflow ABI-encodes a payload, produces a signed report with runtime.report(), and submits it with evmClient.writeReport() to a consumer that implements IReceiver (typically by extending ReceiverTemplate), whose handler is _onReport(bytes metadata, bytes payload).
import { cre } from "@chainlink/cre-sdk"
import { encodeAbiParameters } from "viem"
export async function main() {
const evmClient = new cre.evm.EVMClient(CHAIN_SELECTOR)
// EVM Log trigger fires on each RandomnessRequested event
const trigger = evmClient.logTrigger({
addresses: [
/* consumer address */
],
topics: [
/* RandomnessRequested signature hash */
],
})
cre.handler(trigger, async (runtime, event) => {
const { requestId } = decodeEventLog({
abi: consumerAbi,
eventName: "RandomnessRequested",
data: event.data,
topics: event.topics,
})
// Consensus-safe randomness (DON-seeded)
const randomWord = BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER))
const payload = encodeAbiParameters([{ type: "uint256" }, { type: "uint256" }], [requestId, randomWord])
const report = await runtime.report(payload).result()
await evmClient.writeReport({ receiver: RECEIVER_ADDRESS, report, gasLimit: "300000" }).result()
})
return cre.workflow()
}
Receiver contract (replaces the VRFConsumerBaseV2Plus consumer):
import {ReceiverTemplate} from "@chainlink/cre-contracts/ReceiverTemplate.sol";
contract RandomnessConsumer is ReceiverTemplate {
uint256 private nextRequestId;
mapping(uint256 => bool) public pendingRequests;
mapping(uint256 => uint256) public fulfilledWords;
event RandomnessRequested(uint256 indexed requestId, address requester);
constructor(address forwarder) ReceiverTemplate(forwarder) {}
// Emit event -> CRE workflow fires
function requestRandomness() external returns (uint256 requestId) {
requestId = ++nextRequestId;
pendingRequests[requestId] = true;
emit RandomnessRequested(requestId, msg.sender);
}
// DON writes back via the Keystone Forwarder
function _onReport(bytes calldata /* metadata */, bytes calldata payload) internal override {
(uint256 reqId, uint256 word) = abi.decode(payload, (uint256, uint256));
require(pendingRequests[reqId], "unknown request");
delete pendingRequests[reqId]; // mark fulfilled first
fulfilledWords[reqId] = word;
}
}
Chain selectors and forwarder addresses are network-specific; see the Forwarder Directory and Supported Networks. Use MockKeystoneForwarder for local simulation. See Building Consumer Contracts for how to permission your IReceiver.
Security patterns that carry over
These are standard defenses whenever an onchain outcome depends on a value that is only known after a request is made. They apply to CRE randomness exactly as they did to VRF.
Do:
- Commit before reveal. Lock in every user choice (bet side, purchase, entry) in one transaction before the random result is known; reveal only after it arrives.
- Separate request from consumption. The random value must come from a future state that is unknowable at request time. Never request and consume randomness in the same transaction.
- Track request IDs. Map each request ID to its pending state and verify in
_onReportthat the ID is known and still pending before acting. - Mark fulfilled before downstream logic. Set the fulfilled flag atomically before invoking anything else, closing re-entrancy and replay windows.
- One value per independent outcome. Deriving several outcomes from one number (
word % 6andword % 10) correlates them. Generate a separate value per independent outcome. - Avoid modulo bias. Use rejection sampling or big-integer arithmetic when mapping a large number into a small range.
Avoid:
- Block variables as entropy (
block.timestamp,blockhash,block.prevrandao) — influenceable by validators. - Caller-controlled seeds for high-value draws — see the trigger caution above.
- Revert-and-retry ("rerolling") — if a write can revert and be replayed, the caller gets multiple attempts. Mark fulfilled atomically and guard against acting again after acceptance.
Where VRF still fits
VRF remains available and is the right choice when a contract, regulator, or counterparty must verify a per-request cryptographic proof onchain without trusting any operator's execution — for example high-stakes onchain lotteries or casino payouts where each draw needs its own independently checkable proof. CRE secures randomness through DON consensus and the onchain signed-report trail rather than a per-draw proof, which fits the large majority of randomness use cases, especially those already embedded in a broader automated flow.
Get started
To begin, follow the CRE Getting Started guide to install the CLI and initialize your first project.
Reference template
The randomness starter template is a runnable, end-to-end version of this guide — the request/fulfill flow, a ReceiverTemplate-based consumer, generated bindings, tests, and a simulation command:
starter-templates/randomness/randomness-ts(TypeScript)starter-templates/randomness/randomness-go(Go)
It demonstrates each piece of the migration:
- An EVM Log trigger on a
RandomnessRequestedevent — replaces therequestRandomWords()entry path. - A consensus-safe draw with
Math.random()inside the runtime. runtime.report()+evmClient.writeReport()writing a signed(requestId, randomWord)report to aReceiverTemplate-based consumer, whose_processReport()records the result — replaces thefulfillRandomWords()callback.- Request-ID tracking — the consumer marks each request fulfilled before storing the result, closing reroll/replay windows.
Mapping your own integration
- Use
cre workflow init(or clone the template above) to scaffold your project. - Replace
requestRandomWords()with an appropriate trigger — usually an EVM Log trigger emitted by your consumer. - Generate the value with
Math.random()in the handler; see Using Randomness in Workflows. - Deploy a
ReceiverTemplate-based consumer that records the signed report in_processReport(), replacing thefulfillRandomWords()callback. - Test locally with
cre workflow simulatebeforecre workflow deploy.