From 7ca44605bb3e587337aa2391f06a3d9717f4268a Mon Sep 17 00:00:00 2001 From: Claudio Condor Date: Sun, 10 May 2026 09:09:21 -0400 Subject: [PATCH 1/7] feat(cadence-lang): add randomness reference --- plugins/flow-dev/skills/cadence-lang/SKILL.md | 3 +- .../cadence-lang/references/randomness.md | 212 ++++++++++++++++++ 2 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 plugins/flow-dev/skills/cadence-lang/references/randomness.md diff --git a/plugins/flow-dev/skills/cadence-lang/SKILL.md b/plugins/flow-dev/skills/cadence-lang/SKILL.md index cb1edf4..7af0d4a 100644 --- a/plugins/flow-dev/skills/cadence-lang/SKILL.md +++ b/plugins/flow-dev/skills/cadence-lang/SKILL.md @@ -2,7 +2,7 @@ name: cadence-lang description: | Comprehensive guide for writing correct, secure, and idiomatic Cadence smart contract code on the Flow blockchain. Covers language fundamentals (resources, contracts, transactions, interfaces, accounts, references, imports), access control and entitlements, capabilities, pre/post conditions, security best practices, anti-patterns to avoid, and proven design patterns. - TRIGGER when: writing or debugging Cadence code, asking about Cadence syntax, access(self), access(all), entitlements, resources, move operator (<-), capabilities, references, pre/post conditions, storage paths, "how do I write cadence", "cadence error", "compile error in .cdc", "what does access(self) mean", "how do resources work", "capability-based security". + TRIGGER when: writing or debugging Cadence code, asking about Cadence syntax, access(self), access(all), entitlements, resources, move operator (<-), capabilities, references, pre/post conditions, storage paths, "how do I write cadence", "cadence error", "compile error in .cdc", "what does access(self) mean", "how do resources work", "capability-based security", randomness, revertibleRandom, RandomBeaconHistory, RandomConsumer, Xorshift128plus, commit-reveal, "random number in cadence", "how to generate random in flow". DO NOT TRIGGER when: building NFT/FT token contracts (use cadence-tokens), setting up flow.json or FCL (use flow-project-setup), reviewing existing code for vulnerabilities (use cadence-audit), generating new contracts from scratch (use cadence-scaffold). --- @@ -39,6 +39,7 @@ Read the relevant reference file based on your task: | Security best practices | [security-best-practices.md](references/security-best-practices.md) | | Anti-patterns to avoid | [anti-patterns.md](references/anti-patterns.md) | | Design patterns | [design-patterns.md](references/design-patterns.md) | +| Randomness APIs (revertibleRandom, beacon, RandomConsumer, commit-reveal) | [randomness.md](references/randomness.md) | For security-sensitive tasks, also read `security-best-practices.md` and `anti-patterns.md`. diff --git a/plugins/flow-dev/skills/cadence-lang/references/randomness.md b/plugins/flow-dev/skills/cadence-lang/references/randomness.md new file mode 100644 index 0000000..2d7609c --- /dev/null +++ b/plugins/flow-dev/skills/cadence-lang/references/randomness.md @@ -0,0 +1,212 @@ +# Randomness in Cadence + +Flow exposes three randomness primitives. Pick the right one for your threat model — they trade off latency against the abort-on-bad-roll attack. + +## Decision matrix + +| API | Latency | Safe against abort-on-bad-roll? | Use when | +|---|---|---|---| +| `revertibleRandom(modulo:)` | same tx | ❌ NO | result is independent of caller incentives (cosmetic, fair-coin-flip with no payoff revert) | +| `RandomBeaconHistory.sourceOfRandomness(atBlockHeight:)` | 1 block | ✅ YES (committed beacon) | reading randomness derived from a specific past block | +| `RandomConsumer` + `Xorshift128plus` (commit-reveal) | 2 txs | ✅ YES | any user-facing action with economic outcomes — gambling, loot, raffles, NFT trait reveals | +| `useFlowRevertibleRandom` (frontend) | same query | ❌ NO | UI-only seeds (animations, demo dice). Never economic. | + +**Default to `RandomConsumer`** for anything that pays out value. Only use the native function when you can prove the caller has no incentive to revert on a bad outcome. + +## A. `revertibleRandom()` + +Built-in. Returns `T` for any unsigned integer type. Accepts an optional `modulo:` to avoid modulo bias. + +```cadence +let a: UInt64 = revertibleRandom() +let b: UInt8 = revertibleRandom() +let c: UInt256 = revertibleRandom() + +// Uniform in [0, 100). The built-in modulo is bias-free; do NOT use `r % 100`. +let d: UInt64 = revertibleRandom(modulo: 100) +``` + +### ❌ Cannot be called from a `view` function + +```cadence +access(all) view fun roll(): UInt64 { + return revertibleRandom() // compile error: impure operation in view context +} +``` + +### ❌ Modulo bias if you reduce manually + +```cadence +let biased = revertibleRandom() % 100 // skewed distribution +``` + +### ✅ Use the `modulo:` parameter + +```cadence +let fair = revertibleRandom(modulo: 100) +``` + +### Scripts repeat within the same block + +A `flow scripts execute` call returns the same value every time until a transaction advances the block height. Three back-to-back script calls in the same block all returned `[13564773934808714830, 60, 3]`. **Don't write tests as scripts** — the freeze hides bugs. + +### Multiple calls in one transaction DO advance state + +```cadence +transaction { + execute { + let a = revertibleRandom() + let b = revertibleRandom() // different from a + let c = revertibleRandom() // different from a and b + } +} +``` + +## B. `RandomBeaconHistory` + +Verifiable beacon randomness recorded per block. Read past blocks only. + +| Network | Address | +|---|---| +| Mainnet | `0xe467b9dd11fa00df` | +| Testnet | `0x8c5303eaa26202d6` | +| Emulator | `0xf8d6e0586b0a20c7` | + +```cadence +import "RandomBeaconHistory" + +access(all) fun beaconAt(height: UInt64): [UInt8] { + return RandomBeaconHistory.sourceOfRandomness(atBlockHeight: height).value +} + +access(all) view fun lowest(): UInt64 { + return RandomBeaconHistory.getLowestHeight() +} +``` + +### Constraints + +- Reading the **current** block panics: `Source of randomness not yet recorded for block height N`. The source for block N exists only at block N+1. +- `getLowestHeight()` returns the earliest available block. On a fresh emulator it is 1; on mainnet/testnet it grows over time as old beacon entries are pruned. +- Pre-deployed on the emulator service account — do NOT try to deploy `RandomBeaconHistory` yourself. + +### Use case + +Resolving a result that depends on a past block — the user already committed at block N (recorded in storage), and now you read `sourceOfRandomness(atBlockHeight: N)` to derive their outcome deterministically. + +## C. `RandomConsumer` + `Xorshift128plus` (commit-reveal) + +Standard library for safe randomness. The `Consumer` resource issues a `Request` (commit) that can only be fulfilled at block N+1 or later (reveal). Because the source for block N is undetermined when the commit lands, the user cannot inspect it mid-tx and revert. + +| Network | Address | +|---|---| +| Mainnet | `0x45caec600164c9e6` | +| Testnet | `0xed24dbe901028c5c` | + +Install via: + +```bash +flow dependencies install mainnet://45caec600164c9e6.RandomConsumer +``` + +This transitively pulls `Burner`, `RandomBeaconHistory`, `Xorshift128plus`. Do NOT use `flow dependencies install RandomConsumer` (bare name) — fails with `invalid dependency format`. + +### Setup (one-time) + +```cadence +import "RandomConsumer" + +transaction { + prepare(signer: auth(SaveValue, BorrowValue) &Account) { + let path: StoragePath = /storage/MyConsumer + if signer.storage.borrow<&RandomConsumer.Consumer>(from: path) == nil { + signer.storage.save(<-RandomConsumer.createConsumer(), to: path) + } + } +} +``` + +### Commit (block N) + +```cadence +import "RandomConsumer" + +transaction { + prepare(signer: auth(BorrowValue, SaveValue) &Account) { + let consumer = signer.storage + .borrow(from: /storage/MyConsumer) + ?? panic("Consumer not initialized") + let req <- consumer.requestRandomness() + signer.storage.save(<-req, to: /storage/MyRequest) + } +} +``` + +### Reveal (block N+1 or later) + +```cadence +import "RandomConsumer" + +transaction { + prepare(signer: auth(BorrowValue, LoadValue) &Account) { + let consumer = signer.storage + .borrow(from: /storage/MyConsumer) + ?? panic("Consumer not initialized") + let req <- signer.storage.load<@RandomConsumer.Request>(from: /storage/MyRequest) + ?? panic("No pending request") + let result = consumer.fulfillRandomInRange(request: <-req, min: 1, max: 100) + log("revealed: ".concat(result.toString())) + } +} +``` + +### Entitlements + +`Commit` and `Reveal` are distinct entitlements. Borrow with the narrowest one for the operation. **Never publish a public capability to `&Consumer`** — it would let anyone request and fulfill on your behalf. + +### Fulfillment timing + +- `fulfillRandomInRange(min:max:)` panics if called in the same block as the commit: `Cannot fulfill random request before the eligible block height of N`. +- The `Request` stores the commit block; reveal is allowed once `getCurrentBlock().height > request.block`. + +### PRG for multi-draw + +`Xorshift128plus.PRG(sourceOfRandomness:salt:)` builds a deterministic PRG from a beacon source. Use when one beacon read should produce multiple correlated values (e.g., shuffle 100 cards from one source). + +```cadence +import "Xorshift128plus" + +access(all) fun draw5(seed: [UInt8], salt: [UInt8]): [UInt64] { + let prg = Xorshift128plus.PRG(sourceOfRandomness: seed, salt: salt) + let out: [UInt64] = [] + var i = 0 + while i < 5 { + out.append(prg.nextUInt64()) + i = i + 1 + } + return out +} +``` + +## flow.json deploy order gotcha + +The deployments array is NOT topo-sorted. `Xorshift128plus` must appear before `RandomConsumer`: + +```json +"deployments": { + "emulator": { + "emulator-account": [ + "Xorshift128plus", + "RandomConsumer" + ] + } +} +``` + +Reverse order fails at startup: `deployment contains nonexisting contract Xorshift128plus`. + +## See also + +- `cadence-audit/references/randomness-vulns.md` — abort-on-bad-roll, modulo bias, reveal-too-early, missing entitlement +- `cadence-scaffold/references/secure-randomness.md` — copy-paste templates +- `flow-react-sdk/references/use-flow-revertible-random.md` — frontend hook From 07da0508c351bad420fc8e7f79605fcfe67b4db1 Mon Sep 17 00:00:00 2001 From: Claudio Condor Date: Sun, 10 May 2026 09:10:29 -0400 Subject: [PATCH 2/7] feat(cadence-audit): add randomness vulnerabilities reference --- .../flow-dev/skills/cadence-audit/SKILL.md | 3 +- .../references/randomness-vulns.md | 135 ++++++++++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 plugins/flow-dev/skills/cadence-audit/references/randomness-vulns.md diff --git a/plugins/flow-dev/skills/cadence-audit/SKILL.md b/plugins/flow-dev/skills/cadence-audit/SKILL.md index d896307..d1b8b5d 100644 --- a/plugins/flow-dev/skills/cadence-audit/SKILL.md +++ b/plugins/flow-dev/skills/cadence-audit/SKILL.md @@ -2,7 +2,7 @@ name: cadence-audit description: | Comprehensive audit and review skill for Cadence smart contracts on the Flow blockchain. Identifies security vulnerabilities, bugs, code quality issues, and optimization opportunities. Produces severity-rated findings (Critical/High/Medium/Low) with actionable fixes. - TRIGGER when: auditing, reviewing, or improving Cadence code, checking for security issues, performing code review on .cdc files, looking for anti-patterns or vulnerabilities, optimizing smart contract code, "review cadence", "audit cadence", "check cadence security", "validate cadence contract", "review my .cdc file", "security review", "code review", "find vulnerabilities", "check this contract", "is this code secure", "audit my project". + TRIGGER when: auditing, reviewing, or improving Cadence code, checking for security issues, performing code review on .cdc files, looking for anti-patterns or vulnerabilities, optimizing smart contract code, "review cadence", "audit cadence", "check cadence security", "validate cadence contract", "review my .cdc file", "security review", "code review", "find vulnerabilities", "check this contract", "is this code secure", "audit my project", randomness vulnerabilities, abort-on-bad-roll, modulo bias. DO NOT TRIGGER when: writing new contracts from scratch (use cadence-scaffold), asking about Cadence syntax or patterns (use cadence-lang), building token contracts (use cadence-tokens). --- @@ -44,6 +44,7 @@ When auditing a full project: |-----------|---------| | [audit-checklist.md](references/audit-checklist.md) | Full security, bugs, quality, DeFi, optimization checklists | | [review-format.md](references/review-format.md) | Structured output format, severity levels, verdict criteria | +| [randomness-vulns.md](references/randomness-vulns.md) | Abort-on-bad-roll, modulo bias, public Consumer cap, reveal-too-early, single-Request misuse | ## Companion Skills diff --git a/plugins/flow-dev/skills/cadence-audit/references/randomness-vulns.md b/plugins/flow-dev/skills/cadence-audit/references/randomness-vulns.md new file mode 100644 index 0000000..2546363 --- /dev/null +++ b/plugins/flow-dev/skills/cadence-audit/references/randomness-vulns.md @@ -0,0 +1,135 @@ +# Randomness Vulnerabilities + +Six classes of bugs specific to randomness on Flow. Severity ratings reflect what auditors observed in real contracts. + +## V1 — Abort-on-bad-roll (Critical) + +**The pattern:** a transaction calls `revertibleRandom()` (or any same-tx randomness), evaluates the result, and reverts via `panic` or `assert` if the result is unfavorable. The user pays only gas; they keep every win and discard every loss. + +### Vulnerable + +```cadence +transaction(targetMin: UInt64) { + execute { + let roll = MyContract.rollDie(sides: 6) + log("rolled: ".concat(roll.toString())) + assert(roll >= targetMin, message: "bad roll") + } +} +``` + +Reproduced on emulator with `targetMin: 6`: 10 attempts, 2 commits, 8 reverts. Attacker keeps a perfect 6 every time without paying for the misses. + +### Fix + +Use the commit-reveal pattern via `RandomConsumer`. The reveal transaction's outcome is fully determined by `(beacon[N], request.uuid)` — re-running the reveal yields the same number, so reverting is pointless. Lock all wagers/state changes at commit time, not reveal time. + +### Detection + +Grep heuristic: any `.cdc` file that contains `revertibleRandom` AND (`panic(` OR `assert(`) in the same scope, AND has state mutation in the same `execute` block. Manual review required — not every same-tx use of `revertibleRandom` is exploitable, only those where the caller has incentive to revert. + +## V2 — Modulo bias (Medium) + +**The pattern:** reducing a uniform integer to a smaller range with `%` produces a non-uniform distribution unless the range divides the integer's modulus. + +### Vulnerable + +```cadence +let r = revertibleRandom() % 100 // values 0..15 are slightly more likely +``` + +### Fix + +Use the built-in `modulo:` parameter: + +```cadence +let r = revertibleRandom(modulo: 100) +``` + +Or for `RandomConsumer`, use `getNumberInRange(min:max:)` / `fulfillRandomInRange(request:min:max:)` — both handle bias correctly. + +### Severity + +Medium for most uses (skew is ~0.0001%). Critical for cryptographic schemes or large-scale games where small biases compound. + +## V3 — Reveal too early (High) + +**The pattern:** calling `Request._fulfill()` or `fulfillRandomInRange()` in the same block as the commit. The Consumer enforces this and panics, but a bad implementation might catch the error and retry, opening a same-block reveal vector. + +### Symptom + +Emulator panic: `Cannot fulfill random request before the eligible block height of N`. + +### Fix + +Always reveal in a separate transaction submitted at block height > commit block. Do not wrap fulfillment in `try`/recover patterns — let the panic propagate. + +## V4 — Public Consumer capability (Critical) + +**The pattern:** publishing a public capability to `&RandomConsumer.Consumer`. Anyone can call `requestRandomness()` and `_fulfill()` against the user's Consumer, draining their state or front-running their reveals. + +### Vulnerable + +```cadence +let cap = signer.capabilities.storage.issue<&RandomConsumer.Consumer>(/storage/MyConsumer) +signer.capabilities.publish(cap, at: /public/MyConsumer) // ❌ +``` + +### Fix + +`Consumer` is a private resource. Borrow it directly from `signer.storage` in transactions; never expose via public capability. If you need third-party access for fulfillment, use a controlled wrapper resource that only exposes the specific operations you trust. + +### Entitlement narrowing + +Borrow with the minimum entitlement set required: +- Commit transaction: `auth(RandomConsumer.Commit) &Consumer` +- Reveal transaction: `auth(RandomConsumer.Reveal) &Consumer` + +Never borrow `auth(RandomConsumer.Commit, RandomConsumer.Reveal) &Consumer` unless the same transaction must do both (rare and usually wrong). + +## V5 — Single-Request misuse (Medium) + +**The pattern:** treating a `RandomConsumer.Request` as a multi-use source. Each Request fulfills exactly once. To get multiple correlated values, use the PRG path: + +```cadence +let prg = Xorshift128plus.PRG(sourceOfRandomness: source, salt: salt) +let v1 = prg.nextUInt64() +let v2 = prg.nextUInt64() // correlated to v1, derived from same seed +``` + +### Vulnerable + +Calling `fulfillRandomInRange()` on the same Request twice. The second call panics or returns a stale value depending on Consumer version. + +### Fix + +For one outcome: one Request, one fulfill. For N correlated outcomes from a single commit: read the source via Request, build a `Xorshift128plus.PRG`, and call `nextUInt64()` N times. + +## V6 — Script-based randomness in tests (Low) + +**The pattern:** writing tests that read `revertibleRandom` via `flow scripts execute` and asserting different values across calls. Scripts within the same block return identical values — the test will silently freeze on a single roll. + +### Symptom + +Three back-to-back script invocations all return `[13564773934808714830, 60, 3]`. Test passes accidentally because every assertion sees the same value. + +### Fix + +Test randomness via transactions, not scripts. Each transaction advances the block and produces a fresh value. For deterministic test values, use `RandomBeaconHistory.sourceOfRandomness(atBlockHeight:)` against a known past block. + +## Audit checklist additions + +When auditing a contract that uses randomness, add these to the checklist: + +- [ ] Every `revertibleRandom` call traced to its caller — caller has no incentive to revert? +- [ ] Modulo reductions use `revertibleRandom(modulo:)` or `getNumberInRange(min:max:)`, not `%`? +- [ ] No same-block fulfillment paths (no `try`/recover wrapping `_fulfill`)? +- [ ] `RandomConsumer.Consumer` is private — no public capability published? +- [ ] Borrow entitlements narrowed to `Commit` xor `Reveal`? +- [ ] State changes (wagers, locks, escrows) happen at commit, not reveal? +- [ ] Tests exercise randomness via transactions, not scripts? + +## See also + +- `cadence-lang/references/randomness.md` — full API reference +- `cadence-scaffold/references/secure-randomness.md` — safe templates From 4efc817ac2bd3bb561242f877bea36f30395c229 Mon Sep 17 00:00:00 2001 From: Claudio Condor Date: Sun, 10 May 2026 09:11:36 -0400 Subject: [PATCH 3/7] feat(cadence-scaffold): add secure randomness templates --- .../flow-dev/skills/cadence-scaffold/SKILL.md | 3 +- .../references/secure-randomness.md | 196 ++++++++++++++++++ 2 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 plugins/flow-dev/skills/cadence-scaffold/references/secure-randomness.md diff --git a/plugins/flow-dev/skills/cadence-scaffold/SKILL.md b/plugins/flow-dev/skills/cadence-scaffold/SKILL.md index 2f8d607..e875389 100644 --- a/plugins/flow-dev/skills/cadence-scaffold/SKILL.md +++ b/plugins/flow-dev/skills/cadence-scaffold/SKILL.md @@ -2,7 +2,7 @@ name: cadence-scaffold description: | Generate production-ready Cadence smart contracts, transactions, and DeFi transactions from scratch following security-first standards. Scaffolds secure code with proper access control, entitlements, events, storage paths, and phase discipline. - TRIGGER when: creating new Cadence contracts, generating transactions, scaffolding DeFi transactions, building smart contracts from scratch, "generate contract", "create transaction", "scaffold", "new contract", "new transaction", "build a contract for", "write me a contract", "help me create a", "template for", "boilerplate", "starter contract", "create an NFT contract", "generate a transfer transaction". + TRIGGER when: creating new Cadence contracts, generating transactions, scaffolding DeFi transactions, building smart contracts from scratch, "generate contract", "create transaction", "scaffold", "new contract", "new transaction", "build a contract for", "write me a contract", "help me create a", "template for", "boilerplate", "starter contract", "create an NFT contract", "generate a transfer transaction", "scaffold randomness", "commit-reveal template", "RandomConsumer setup". DO NOT TRIGGER when: reviewing or auditing existing code (use cadence-audit), asking about Cadence syntax or language rules (use cadence-lang), configuring flow.json (use flow-project-setup). --- @@ -19,6 +19,7 @@ Determine what the user needs and follow the appropriate reference: | Smart contract (NFT, FT, general, admin) | [scaffold-contract.md](references/scaffold-contract.md) | | Transaction (transfer, setup, mint, multi-sig) | [scaffold-transaction.md](references/scaffold-transaction.md) | | DeFi Actions transaction (restake, swap, auto-balance) | [scaffold-defi.md](references/scaffold-defi.md) | +| Secure randomness (native roll, beacon, commit-reveal, PRG) | [secure-randomness.md](references/secure-randomness.md) | ## General Principles diff --git a/plugins/flow-dev/skills/cadence-scaffold/references/secure-randomness.md b/plugins/flow-dev/skills/cadence-scaffold/references/secure-randomness.md new file mode 100644 index 0000000..bdfbe31 --- /dev/null +++ b/plugins/flow-dev/skills/cadence-scaffold/references/secure-randomness.md @@ -0,0 +1,196 @@ +# Secure Randomness Templates + +Copy-paste templates for the four common randomness patterns. All examples are Cadence 1.0 and verified on the Flow emulator. + +## Decision: which template? + +| Use case | Template | +|---|---| +| Cosmetic / non-economic | T1 — Native roll | +| Past-block lookup | T2 — Beacon read | +| Game payout, raffle, NFT mint reveal | T3+T4 — Commit-reveal pair | +| Many correlated draws from one commit | T5 — PRG multi-draw | + +Anything with economic outcomes uses T3+T4. **Never use T1 for payouts.** + +## flow.json setup + +Run once before deploying anything that uses `RandomConsumer`: + +```bash +flow dependencies install mainnet://45caec600164c9e6.RandomConsumer +``` + +Pulls `Burner`, `RandomBeaconHistory`, `Xorshift128plus`, `RandomConsumer` transitively. The bare-name form `flow dependencies install RandomConsumer` fails with `invalid dependency format`. + +In `flow.json`, ensure the deployments list keeps `Xorshift128plus` BEFORE `RandomConsumer` (the array is not topo-sorted): + +```json +"deployments": { + "emulator": { + "emulator-account": [ + "Xorshift128plus", + "RandomConsumer" + ] + } +} +``` + +## T1 — Native roll (cosmetic only) + +```cadence +access(all) contract MyGame { + access(all) event Rolled(value: UInt64) + + /// Cosmetic die roll. NOT safe if the caller can revert on a bad outcome. + access(all) fun roll(sides: UInt64): UInt64 { + let r = revertibleRandom(modulo: sides) + 1 + emit Rolled(value: r) + return r + } +} +``` + +Use this for visual effects, randomized UI, demo dice — never for outcomes that pay value. + +## T2 — Beacon read (past block) + +```cadence +import "RandomBeaconHistory" + +access(all) contract Lottery { + /// Resolve a draw whose seed was determined at `commitBlock`. + /// Caller must save commitBlock at request time and pass it here. + access(all) fun resolve(commitBlock: UInt64): [UInt8] { + return RandomBeaconHistory.sourceOfRandomness(atBlockHeight: commitBlock).value + } +} +``` + +Panics if `commitBlock` is the current or future block. Always read past blocks only. + +## T3 — Commit transaction (request) + +```cadence +import "RandomConsumer" + +transaction { + prepare(signer: auth(BorrowValue, SaveValue, LoadValue) &Account) { + let consumerPath: StoragePath = /storage/MyConsumer + let requestPath: StoragePath = /storage/MyRequest + + let consumer = signer.storage + .borrow(from: consumerPath) + ?? panic("Consumer not initialized — run setup_consumer first") + + // Discard any stale request + if let stale <- signer.storage.load<@RandomConsumer.Request>(from: requestPath) { + destroy stale + } + + let req <- consumer.requestRandomness() + signer.storage.save(<-req, to: requestPath) + + // LOCK ALL WAGERS / STATE CHANGES HERE — at commit, not reveal. + // e.g. transfer the bet vault into escrow now. + } +} +``` + +## T4 — Reveal transaction (fulfill) + +```cadence +import "RandomConsumer" + +transaction { + prepare(signer: auth(BorrowValue, LoadValue) &Account) { + let consumerPath: StoragePath = /storage/MyConsumer + let requestPath: StoragePath = /storage/MyRequest + + let consumer = signer.storage + .borrow(from: consumerPath) + ?? panic("Consumer not initialized") + + let req <- signer.storage.load<@RandomConsumer.Request>(from: requestPath) + ?? panic("No pending request — run commit first") + + // Range-bounded reveal (handles modulo bias internally) + let result = consumer.fulfillRandomInRange(request: <-req, min: 1, max: 100) + + // Settle outcome here. State was already locked at commit; this only PAYS OUT. + log("revealed: ".concat(result.toString())) + } +} +``` + +Must run at block height > commit block. The Consumer enforces this. + +## T5 — Setup transaction (one-time) + +```cadence +import "RandomConsumer" + +transaction { + prepare(signer: auth(SaveValue, BorrowValue) &Account) { + let path: StoragePath = /storage/MyConsumer + if signer.storage.borrow<&RandomConsumer.Consumer>(from: path) == nil { + signer.storage.save(<-RandomConsumer.createConsumer(), to: path) + } + } +} +``` + +## T6 — PRG multi-draw + +When a single commit needs to produce N correlated values (shuffle a deck, generate traits for one mint): + +```cadence +import "Xorshift128plus" + +access(all) contract DeckShuffle { + /// Build a PRG from a beacon-sourced seed and crank it N times. + access(all) fun shuffle(seed: [UInt8], salt: [UInt8], count: Int): [UInt64] { + let prg = Xorshift128plus.PRG(sourceOfRandomness: seed, salt: salt) + let out: [UInt64] = [] + var i = 0 + while i < count { + out.append(prg.nextUInt64()) + i = i + 1 + } + return out + } +} +``` + +Pair with T2 (read the seed from a past block via `RandomBeaconHistory`) or T4 (extract the source from a fulfilled `Request`). + +## Anti-template — DO NOT USE + +```cadence +// ❌ ABORT-ON-BAD-ROLL — vulnerable +transaction(targetMin: UInt64) { + execute { + let roll = MyGame.roll(sides: 6) + // Pays out value on win, reverts on loss. + // Attacker keeps every win for free. + assert(roll >= targetMin, message: "bad roll, retry") + MyGame.payout(amount: 100.0) + } +} +``` + +The fix is structural, not a tweak: replace with T3 + T4. Lock the wager at commit, settle at reveal. + +## Storage path conventions + +| Path | Purpose | +|---|---| +| `/storage/Consumer` | The user's `RandomConsumer.Consumer` resource | +| `/storage/Request` | The pending `RandomConsumer.Request` between commit and reveal | + +Do not publish public capabilities to either. Both are private. + +## See also + +- `cadence-lang/references/randomness.md` — API reference and decision matrix +- `cadence-audit/references/randomness-vulns.md` — what the audit will look for From 89348b7e3641eb4df76443207a900c1b41365dc5 Mon Sep 17 00:00:00 2001 From: Claudio Condor Date: Sun, 10 May 2026 09:12:25 -0400 Subject: [PATCH 4/7] feat(flow-react-sdk): document useFlowRevertibleRandom hook --- .../flow-dev/skills/flow-react-sdk/SKILL.md | 3 +- .../references/use-flow-revertible-random.md | 95 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 plugins/flow-dev/skills/flow-react-sdk/references/use-flow-revertible-random.md diff --git a/plugins/flow-dev/skills/flow-react-sdk/SKILL.md b/plugins/flow-dev/skills/flow-react-sdk/SKILL.md index f00c382..d2df570 100644 --- a/plugins/flow-dev/skills/flow-react-sdk/SKILL.md +++ b/plugins/flow-dev/skills/flow-react-sdk/SKILL.md @@ -2,7 +2,7 @@ name: flow-react-sdk description: | Guide for building React applications on the Flow blockchain using @onflow/react-sdk. Covers FlowProvider setup, Cadence hooks (useFlowQuery, useFlowMutate, useFlowCurrentUser, useFlowEvents), Cross-VM hooks for EVM bridging and batch transactions, and UI components (Connect, TransactionButton, TransactionDialog, NftCard). Built on TanStack Query with TypeScript support. - TRIGGER when: building React apps on Flow, using @onflow/react-sdk, setting up FlowProvider, using Flow React hooks, "useFlowQuery", "useFlowMutate", "useFlowCurrentUser", "Connect component", "TransactionButton", "wallet connect react", "react flow app", "FCL React", "flow react hooks", "bridge tokens react", "cross-vm react", "NftCard", "useFlowEvents", "how to query Flow from React", "authenticate user in React". + TRIGGER when: building React apps on Flow, using @onflow/react-sdk, setting up FlowProvider, using Flow React hooks, "useFlowQuery", "useFlowMutate", "useFlowCurrentUser", "Connect component", "TransactionButton", "wallet connect react", "react flow app", "FCL React", "flow react hooks", "bridge tokens react", "cross-vm react", "NftCard", "useFlowEvents", "useFlowRevertibleRandom", "how to query Flow from React", "authenticate user in React", "random number in react flow". DO NOT TRIGGER when: writing Cadence contracts (use cadence-lang), configuring flow.json without React (use flow-project-setup), building non-React frontends or backends, auditing code (use cadence-audit). --- @@ -37,6 +37,7 @@ function App() { | Cadence hooks: query, mutate, auth, events, accounts, blocks, NFT metadata | [hooks.md](references/hooks.md) | | Cross-VM hooks: EVM batch tx, token/NFT bridging, cross-chain balances | [cross-vm.md](references/cross-vm.md) | | UI components: Connect, TransactionButton, TransactionDialog, NftCard | [components.md](references/components.md) | +| Randomness hook (cosmetic UI only — never economic) | [use-flow-revertible-random.md](references/use-flow-revertible-random.md) | ## Companion Skills diff --git a/plugins/flow-dev/skills/flow-react-sdk/references/use-flow-revertible-random.md b/plugins/flow-dev/skills/flow-react-sdk/references/use-flow-revertible-random.md new file mode 100644 index 0000000..9def0ad --- /dev/null +++ b/plugins/flow-dev/skills/flow-react-sdk/references/use-flow-revertible-random.md @@ -0,0 +1,95 @@ +# useFlowRevertibleRandom + +React hook for fetching `revertibleRandom` values from Flow via a Cadence script. Re-exported from `@onflow/react-sdk`; lives in `onflow/fcl-js` at `packages/react-core/src/hooks/useFlowRevertibleRandom.ts`. + +## When to use + +| Use case | Use this hook? | +|---|---| +| Animations, loading skeletons, splash variations | ✅ Yes | +| Demo dice / coin-flip with no payoff | ✅ Yes | +| Lottery, raffle, NFT mint reveal, game payout | ❌ No — use commit-reveal on chain | +| Anything tied to value or user economic outcome | ❌ No | + +The hook reads on-chain randomness via a script. **Scripts can be re-executed for free until the user gets a result they like** — a frontend that ties value to a script-read random is exploitable. For economic outcomes, use the `RandomConsumer` commit-reveal flow on the contract side and surface the fulfilled value to React via `useFlowQuery` or `useFlowEvents`. + +## Signature + +```ts +const { data, isLoading, error, refetch } = useFlowRevertibleRandom({ + min, // optional UInt256 (decimal string) — default 0 + max, // required UInt256 (decimal string) — exclusive upper bound + count, // optional Int — number of values to fetch (default 1) + query, // optional TanStack query options (staleTime, enabled, etc.) +}); +``` + +`data` is `Array<{ blockHeight: string; value: string }>` where `value` is a UInt256 in decimal string form. The hook returns `count` values, all sourced from the same query block. + +## Quick example + +```tsx +import { useFlowRevertibleRandom } from '@onflow/react-sdk'; + +function FlavorEmoji() { + const { data } = useFlowRevertibleRandom({ + max: '4', + count: 1, + }); + + if (!data?.[0]) return null; + const idx = Number(BigInt(data[0].value)) % 4; + return {['🌟', '🎉', '🎯', '🚀'][idx]}; +} +``` + +## Refetch to advance + +Within a single block, repeated queries return the same value. To get a fresh value, either: + +1. Wait for a new block to be sealed (typically ~1s on Flow), then call `refetch()`. +2. Set `query: { staleTime: 0 }` and trigger a refetch via user action. + +```tsx +const { data, refetch } = useFlowRevertibleRandom({ max: '100', count: 1 }); +return ; +``` + +The block height in `data[0].blockHeight` tells you which block the value came from — useful if you want to display it or detect when the value actually changed. + +## TanStack caching + +Inherits all TanStack Query behavior. By default the result is cached and re-used until stale. Override via `query`: + +```tsx +useFlowRevertibleRandom({ + max: '100', + query: { staleTime: 0, refetchInterval: 1000 }, +}); +``` + +## Working with UInt256 strings + +Values come back as decimal strings to avoid JavaScript precision loss. Convert with `BigInt`: + +```tsx +const v = BigInt(data[0].value); +const inRange = Number(v % 100n); // 0..99 +``` + +Don't use `parseInt` or `Number(value)` directly on UInt256 — overflow loses precision past 2^53. + +## Companion patterns + +For value-bearing flows, mix this hook with the contract-side commit-reveal: + +1. User clicks "play" → React calls a `useFlowMutate` that sends a commit transaction (locks the wager, requests randomness). +2. React subscribes via `useFlowEvents` to the `RandomnessFulfilled` event from `RandomConsumer`. +3. When the event arrives, render the result. Optionally trigger a follow-up reveal mutation. +4. Use `useFlowRevertibleRandom` ONLY for cosmetic UI variations during the wait. + +## See also + +- `hooks.md` — full hook catalog, including `useFlowMutate` and `useFlowEvents` +- `cadence-lang/references/randomness.md` — when to use which on-chain API +- `cadence-audit/references/randomness-vulns.md` — why script-read randomness is unsafe for value From df901fd87f86c31ced7019ce506af252faabf3f6 Mon Sep 17 00:00:00 2001 From: Claudio Condor Date: Sun, 10 May 2026 09:12:38 -0400 Subject: [PATCH 5/7] docs(agents): route randomness questions across four skills --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index a6d5ee6..c010bd3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,6 +82,7 @@ When a developer asks for help, use this table to determine which skill(s) to ac | Design token economics for a Flow protocol | `flow-tokenomics` | `flow-defi`, `cadence-tokens` | | Write unit tests for Cadence contracts | `cadence-testing` | `cadence-lang` | | Debug failing Cadence tests / add coverage | `cadence-testing` | `cadence-lang`, `cadence-audit` | +| Generate random numbers / commit-reveal / VRF on Flow | `cadence-lang` | `cadence-audit`, `cadence-scaffold`, `flow-react-sdk` | ## Install and Validate Commands From 37fc3bcbe36f5d089bb84892da0dee78fd24cc7a Mon Sep 17 00:00:00 2001 From: Claudio Condor Date: Sun, 10 May 2026 21:54:50 -0400 Subject: [PATCH 6/7] docs: cross-link existing hooks.md and flow-defi VRF section to new randomness refs --- .../skills/flow-defi/references/protocol-architecture.md | 2 ++ plugins/flow-dev/skills/flow-react-sdk/references/hooks.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/plugins/flow-dev/skills/flow-defi/references/protocol-architecture.md b/plugins/flow-dev/skills/flow-defi/references/protocol-architecture.md index 690da36..a8e19f5 100644 --- a/plugins/flow-dev/skills/flow-defi/references/protocol-architecture.md +++ b/plugins/flow-dev/skills/flow-defi/references/protocol-architecture.md @@ -121,4 +121,6 @@ access(all) fun getRandomSeed(blockHeight: UInt64): [UInt8] { **DeFi applications:** Fair lottery/raffle contracts, randomized NFT drops, prediction market resolution. +> For any user-facing payout, do NOT use `revertibleRandom()` in the same transaction — the caller can revert on a bad roll. Use the commit-reveal flow via `RandomConsumer` + `Xorshift128plus`. Full API reference, decision matrix, and templates: [`cadence-lang/references/randomness.md`](../../cadence-lang/references/randomness.md), [`cadence-scaffold/references/secure-randomness.md`](../../cadence-scaffold/references/secure-randomness.md). + > **See also:** `defi-primitives.md` for building blocks (lending models, AMM selection). diff --git a/plugins/flow-dev/skills/flow-react-sdk/references/hooks.md b/plugins/flow-dev/skills/flow-react-sdk/references/hooks.md index a3b8b23..43b7409 100644 --- a/plugins/flow-dev/skills/flow-react-sdk/references/hooks.md +++ b/plugins/flow-dev/skills/flow-react-sdk/references/hooks.md @@ -208,3 +208,5 @@ const { data: randoms } = useFlowRevertibleRandom({ ``` Values are deterministic per block — same call in same block returns same values. For unpredictable randomness, use commit-reveal scheme in transactions. + +> **Do NOT use for economic outcomes.** A frontend can re-query a script for free until it sees a favorable value. Restrict this hook to cosmetic UI (animations, demo dice). For payouts, raffles, NFT trait reveals, or anything tied to user value, use the on-chain commit-reveal pattern (see [use-flow-revertible-random.md](./use-flow-revertible-random.md) and the `cadence-lang` randomness reference). From 8700c850ee010b2352fa24dfc4403da6dd19e7ea Mon Sep 17 00:00:00 2001 From: Claudio Condor Date: Sun, 10 May 2026 22:03:58 -0400 Subject: [PATCH 7/7] docs(randomness): add upfront answer to caller-revert question; tighten AGENTS routing --- AGENTS.md | 2 +- .../skills/cadence-lang/references/randomness.md | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index c010bd3..a166be6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,7 +82,7 @@ When a developer asks for help, use this table to determine which skill(s) to ac | Design token economics for a Flow protocol | `flow-tokenomics` | `flow-defi`, `cadence-tokens` | | Write unit tests for Cadence contracts | `cadence-testing` | `cadence-lang` | | Debug failing Cadence tests / add coverage | `cadence-testing` | `cadence-lang`, `cadence-audit` | -| Generate random numbers / commit-reveal / VRF on Flow | `cadence-lang` | `cadence-audit`, `cadence-scaffold`, `flow-react-sdk` | +| Generate random numbers on Flow (revertibleRandom, commit-reveal, VRF) | `cadence-lang` | `cadence-audit`, `cadence-scaffold`, `flow-react-sdk` | ## Install and Validate Commands diff --git a/plugins/flow-dev/skills/cadence-lang/references/randomness.md b/plugins/flow-dev/skills/cadence-lang/references/randomness.md index 2d7609c..11f75c4 100644 --- a/plugins/flow-dev/skills/cadence-lang/references/randomness.md +++ b/plugins/flow-dev/skills/cadence-lang/references/randomness.md @@ -2,6 +2,19 @@ Flow exposes three randomness primitives. Pick the right one for your threat model — they trade off latency against the abort-on-bad-roll attack. +## Can you get randomness the caller cannot revert on? + +Yes, via the commit-reveal pattern with `RandomConsumer`. The caller commits +at block N (locking their wager) before the beacon for that block exists. +The reveal at block N+1 reads the now-committed beacon and produces a +deterministic result. Re-running the reveal yields the same number, so +reverting it is pointless. Anyone can submit the reveal, so the caller +cannot skip it either. + +For anything tied to user value, use this pattern. `revertibleRandom()` +alone is safe only when the caller has no incentive to revert on a bad +roll. + ## Decision matrix | API | Latency | Safe against abort-on-bad-roll? | Use when |