Skip to content

Harden randomizer request lifecycle#65

Merged
punk6529 merged 3 commits into
mainfrom
codex/randomizer-callback-hardening
Jun 10, 2026
Merged

Harden randomizer request lifecycle#65
punk6529 merged 3 commits into
mainfrom
codex/randomizer-callback-hardening

Conversation

@punk6529

@punk6529 punk6529 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #37. Refs #14, #38, #39, #41, #43.

This PR implements the first ADR 0005 randomizer callback hardening slice for the current async adapters:

  • adds shared StreamRandomizerLifecycle request records, states, views, custom errors, and request/fulfillment/stale events
  • adds collection randomizer epochs in StreamCore and exposes the live randomizer provider/epoch through IStreamCore
  • routes VRF and arRNG callbacks through lifecycle validation before setTokenHash
  • validates request ID, token, core token-to-collection binding, provider, randomizer epoch, non-empty output, duplicate fulfillment, zero arRNG request IDs, and stale provider/epoch conditions
  • guards the arRNG request-submission window where the provider request ID is returned from an external payable call, with a reentrant-controller regression test
  • disables RandomizerNXT as a production-configurable randomizer under ADR 0005
  • updates status docs, ADR 0005, roadmap/test traceability, and the Slither baseline

Review Follow-Up

CodeRabbit comment 4671138033 reported no blockers and suggested low-priority hardening. Commit d6b3788 addresses that follow-up by:

  • documenting the deliberate one-randomness-per-token lifecycle invariant
  • documenting the defense-in-depth provider check
  • clarifying the arRNG direct-call precondition around tokenIdToCollection
  • adding testArrngZeroRequestIdFailsBeforeRecordingLifecycle
  • updating test, roadmap, Slither, and run-state traceability

Remaining Randomness Work

This PR intentionally does not finish every ADR 0005 child ticket. Remaining work stays tracked for deterministic post-processing retry, callback-after-burn behavior, richer metadata state integration, raw-output hash storage policy, provider config/runbooks, request-level reserve accounting, and final XRandoms scoping/removal.

Local Validation

  • forge test --match-contract "StreamRandomizerLifecycleTest" -vvv -> 10 passed
  • make check -> 152 passed
  • powershell -ExecutionPolicy Bypass -File scripts\check.ps1 -> 152 passed
  • forge fmt --check on touched Solidity/test files -> passed
  • git diff --check -> passed
  • Markdown heading/traceability greps -> passed
  • Slither: exits nonzero for the tracked baseline; final JSON reports 686 total findings with unchanged 9 High / 29 Medium, two existing weak-prng rows, no NextGenRandomizerRNG.requestRandomWords reentrancy-eth row, and no lifecycle-mock locked-ether / arbitrary-send-eth rows

Notes For Reviewers

NextGenRandomizerRNG.requestRandomWords has a scoped Slither suppression for reentrancy-eth and write-after-write. The arRNG controller returns the provider request ID from the external payable call, so request state must be recorded after the call. The adapter now uses a local in-progress guard, fulfillment rejects during that window, and testArrngControllerCannotReenterFulfillmentDuringRequest plus testArrngZeroRequestIdFailsBeforeRecordingLifecycle cover the critical request-submission edges.

Summary by CodeRabbit

  • New Features

    • End-to-end randomness request lifecycle tracking with derived-seed recording, per-collection randomizer epochs, admin-mark-stale, and new view helpers.
  • Bug Fixes

    • Guards against stale or reentrant fulfillments and enforces request/provider/epoch validation.
  • Tests

    • New comprehensive lifecycle test suite covering VRF/ARRNG/NXT, reentrancy, zero-request-id, stale/invalid callbacks.
  • Documentation

    • Updated ADRs, roadmap, status, blockers, ops, and test docs to reflect progress and remaining work.
  • Updates

    • NXT randomizer adapter marked non-production.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Code review skipped — your organization's overage spend limit has been reached.

Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.

Once credits are available, push a new commit or reopen this pull request to trigger a review.

Copy link
Copy Markdown
Contributor Author

@claude please review this PR.

Focus areas:

  • randomizer lifecycle validation: request ID, token, core token-to-collection binding, provider, epoch, duplicate/stale handling
  • arRNG request-submission reentrancy guard and the scoped Slither suppression rationale
  • whether RandomizerNXT.isRandomizerContract() == false is sufficient for this PR's production-disablement slice while XRandoms remains tracked separately
  • any regression risk from adding viewCollectionRandomizerContract / viewRandomizerEpoch to IStreamCore and StreamCore

Local validation is in the PR body; full make check and Windows scripts/check.ps1 both pass with 151 tests.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8bce3b0e-c3ad-4f3a-9a39-5e0f5ff1c7dd

📥 Commits

Reviewing files that changed from the base of the PR and between 02950ba and d6b3788.

📒 Files selected for processing (7)
  • ops/AUTONOMOUS_RUN.md
  • ops/ROADMAP.md
  • ops/SLITHER_BASELINE.md
  • smart-contracts/RandomizerRNG.sol
  • smart-contracts/StreamRandomizerLifecycle.sol
  • test/README.md
  • test/StreamRandomizerLifecycle.t.sol
✅ Files skipped from review due to trivial changes (2)
  • test/README.md
  • ops/SLITHER_BASELINE.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • smart-contracts/RandomizerRNG.sol
  • smart-contracts/StreamRandomizerLifecycle.sol
  • ops/AUTONOMOUS_RUN.md
  • test/StreamRandomizerLifecycle.t.sol
  • ops/ROADMAP.md

📝 Walkthrough

Walkthrough

Centralizes randomness request lifecycle (StreamRandomizerLifecycle), exposes per-collection epochs in StreamCore, refactors VRF/RNG adapters to use lifecycle validation (and add reentrancy guard for ARRNG), marks NXT non-production, adds mocks and tests, and updates docs/ops baselines.

Changes

Randomizer Lifecycle and Request Validation

Layer / File(s) Summary
StreamRandomizerLifecycle abstraction and shared validation
smart-contracts/StreamRandomizerLifecycle.sol
New abstract contract storing RandomnessRequest, RandomnessRequestState, requestToToken mapping, events, and internal functions _recordRandomnessRequest, _fulfillRandomnessRequest, _markRandomnessRequestStale with validation and derived-seed computation.
Per-collection randomizer epoch tracking in StreamCore
smart-contracts/StreamCore.sol
Adds collectionRandomizerEpoch, updates addRandomizer to increment epoch and emit CollectionRandomizerUpdated, and exposes viewCollectionRandomizerContract and viewRandomizerEpoch.
IStreamCore interface updates
smart-contracts/IStreamCore.sol
Adds viewCollectionRandomizerContract(uint256) and viewRandomizerEpoch(uint256); reformats signatures for readability.
NextGenRandomizerVRF refactored to use lifecycle hooks
smart-contracts/RandomizerVRF.sol
VRF adapter now inherits StreamRandomizerLifecycle, records requests via _recordRandomnessRequest, resolves via _fulfillRandomnessRequest and forwards derivedSeed to gencoreContract.setTokenHash; removes local token/request mappings; adds admin markStaleRequest.
NextGenRandomizerRNG refactored with reentrancy guarding and lifecycle hooks
smart-contracts/RandomizerRNG.sol
RNG adapter inherits StreamRandomizerLifecycle, adds randomnessRequestInProgress guard and RandomizerRequestReentrancy error, sets/clears guard around external arrngController call, records via _recordRandomnessRequest, fulfills via _fulfillRandomnessRequest, and adds admin markStaleRequest.
RandomizerNXT marked non-production-eligible
smart-contracts/RandomizerNXT.sol
isRandomizerContract() now returns false and includes an ADR 0005 comment preventing production configuration.
Test mock infrastructure: MockRandomizerCore
test/mocks/MockRandomizerCore.sol
Adds MockRandomizerCore with setters and view helpers for token→collection, collection→randomizer, and collection→epoch to support tests.
Comprehensive StreamRandomizerLifecycle test suite
test/StreamRandomizerLifecycle.t.sol
Adds tests covering VRF/ARRNG/NXT flows: request lifecycle, fulfillment, unknown/empty/stale/wrong binding rejection, mark-stale observability, pause behavior, ARRNG reentrancy and zero-request-id failure, and NXT restriction; includes provider mocks.
Updated test fixtures for emergency-withdraw and pause-control regression tests
test/StreamEmergencyWithdraw.t.sol, test/StreamPauseControls.t.sol
Tests now deploy/use MockRandomizerCore, pass its address to NextGenRandomizerRNG, configure via setRandomizer, update vm.prank callers, and add core to RandomizerSetup.
Documentation and ops baseline updates
docs/adr/0005-randomness.md, docs/known-blockers.md, docs/status.md, ops/AUTONOMOUS_RUN.md, ops/ROADMAP.md, ops/SLITHER_BASELINE.md, test/README.md
Adds ADR “Current implementation status”, refines blockers/status/ROADMAP P0-RAND-001 language, updates AUTONOMOUS_RUN worklog and queue, refreshes SLITHER baseline counts and documents a scoped suppression, and documents test coverage in test README.

Sequence Diagram

sequenceDiagram
  participant VrfCoord as MockVrfCoordinator
  participant RandomizerVRF as NextGenRandomizerVRF
  participant Lifecycle as StreamRandomizerLifecycle
  participant StreamCore as StreamCore
  VrfCoord->>RandomizerVRF: requestRandomWords(tokenId) -> requestId
  RandomizerVRF->>Lifecycle: _recordRandomnessRequest(requestId, collectionId, tokenId, epoch)
  VrfCoord->>RandomizerVRF: fulfillRandomWords(requestId, randomWords[])
  RandomizerVRF->>Lifecycle: _fulfillRandomnessRequest(requestId, randomWords[])
  Lifecycle->>StreamCore: viewRandomizerEpoch(collectionId) (staleness check)
  Lifecycle->>StreamCore: (on success) emit RandomnessFulfilled
  RandomizerVRF->>StreamCore: setTokenHash(tokenId, derivedSeed)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 I hop through logs and epochs bright,
I guard each seed through day and night,
No reentry slips, no stale reply,
Each request recorded, hash held high,
A little rabbit cheers the hardened sky.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Harden randomizer request lifecycle' accurately and concisely captures the main change: implementing request lifecycle validation and state tracking for randomizer callbacks, which is the primary objective of PR #65.
Linked Issues check ✅ Passed The PR implements all core requirements from #37: StreamRandomizerLifecycle for request state tracking, epoch validation via collection randomizer epochs, provider/token/collection binding validation, duplicate/stale/unknown rejection, events for request states, ADR 0005 alignment, and comprehensive test coverage including lifecycle states, wrong-collection binding, reentrancy protection, and NXT production-disablement.
Out of Scope Changes check ✅ Passed All changes are directly scoped to #37 objectives: StreamRandomizerLifecycle contract, IStreamCore/StreamCore epoch tracking, adapter refactors (VRF/RNG), RandomizerNXT disablement, mock infrastructure, test suites, and documentation/status updates. No unrelated changes detected outside randomizer request lifecycle hardening scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/randomizer-callback-hardening

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Please review the latest head for randomizer request lifecycle/callback hardening, especially the scoped Slither suppression around NextGenRandomizerRNG.requestRandomWords and the associated reentrant-controller regression test.

Copy link
Copy Markdown
Contributor Author

@claude please review the latest head 02950ba after the state-only run-log update.

The implementation commit remains 6413d45; the follow-up only records PR #65 and bot-review state in ops/AUTONOMOUS_RUN.md.

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Please review latest head 02950ba. The newest commit is state-only; implementation remains in 6413d45.

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Please start a fresh review for latest head 02950ba1e5c5f5f109a669697cc93968749a0189. The prior CodeRabbit attempt failed because the head moved while it was reviewing; CI for this head is green.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

@punk6529 I’ll review the latest head 02950ba, treating the newest commit as state-only and focusing on the implementation from 6413d45, especially the randomizer lifecycle/callback hardening, scoped Slither suppression around NextGenRandomizerRNG.requestRandomWords, and the reentrant-controller regression coverage.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get PR details and file list
gh pr view 65 --repo 6529-Collections/6529Stream --json title,body,files,commits,headRefName 2>/dev/null | jq '{title,headRefName,files: [.files[].path], commits: [.commits[] | {oid:.oid, message:.messageHeadline}]}'

Length of output: 1299


🏁 Script executed:

#!/bin/bash
# Get the diff for the PR
gh pr diff 65 --repo 6529-Collections/6529Stream 2>/dev/null | head -3000

Length of output: 80946


🏁 Script executed:

#!/bin/bash
# Get remaining diff
gh pr diff 65 --repo 6529-Collections/6529Stream 2>/dev/null | tail -n +3001 | head -3000

Length of output: 365


🏁 Script executed:

#!/bin/bash
cat smart-contracts/RandomizerRNG.sol

Length of output: 5154


🏁 Script executed:

#!/bin/bash
cat smart-contracts/RandomizerVRF.sol

Length of output: 4950


🏁 Script executed:

#!/bin/bash
cat smart-contracts/StreamRandomizerLifecycle.sol

Length of output: 7123


🏁 Script executed:

#!/bin/bash
cat test/StreamRandomizerLifecycle.t.sol

Length of output: 11838



@punk6529: Reviewed head 02950ba. Below is my finding-by-finding analysis across your focus areas.


Overall

The implementation is solid. The shared StreamRandomizerLifecycle base provides clean, reusable lifecycle validation, and the adapter refactors follow it correctly. No critical issues found. Several low/informational items and one architectural note are called out below.


1. Randomizer Lifecycle Validation (StreamRandomizerLifecycle.sol)

_recordRandomnessRequest – tokenIdToCollection initialization is correct
Both calculateTokenHash overrides set tokenIdToCollection[_mintIndex] = _collectionID before calling requestRandomWords, so the read inside requestRandomWords is correctly populated. No bug here; the ordering is safe in both adapters.

tokenToRequest is never cleared after fulfillment/stale marking
Once a token's request completes or goes stale, tokenToRequest[tokenId] retains the old request ID forever. A burn-and-remint of the same tokenId would revert with TokenRandomnessRequestAlreadyExists. This appears intentional (one token → one hash semantics) and is tracked as future P0-RAND-004 work. Worth adding a code comment to make the deliberate choice explicit:

// tokenToRequest is intentionally not cleared on fulfillment or stale marking:
// a token may only receive randomness once. Burn-and-remint policy is tracked
// under P0-RAND-004.

WrongRandomnessProvider check is practically unreachable
_recordRandomnessRequest always sets provider: address(this), and _fulfillRandomnessRequest checks request.provider != address(this). Since each adapter has isolated storage and both operations run inside the same contract, these will always match. It is valid defense-in-depth, but a comment would prevent future confusion about when this fires:

// Defense-in-depth: provider is always address(this) at record time;
// this check guards against any future cross-adapter storage sharing.

Zero-seed guard
The ZeroDerivedSeed revert is practically unreachable via keccak256 but is correct to include.


2. arRNG Reentrancy Guard and Scoped Slither Suppression

The guard pattern is sound:

randomnessRequestInProgress = true;
uint256 collectionId = tokenIdToCollection[tokenid];   // read before external call (already set by calculateTokenHash)
uint256 requestId = arrngController.requestRandomWords{value: _ethRequired}(1, address(this));
randomnessRequestInProgress = false;
_recordRandomnessRequest(requestId, collectionId, tokenid, ...);

The write-after-call constraint is forced by arRNG's API (the provider request ID is the return value). Setting randomnessRequestInProgress = false before _recordRandomnessRequest is also correct: if _recordRandomnessRequest reverts (e.g., duplicate provider ID), the in-progress flag does not remain set, which avoids a permanent DOS on the contract.

The ReentrantArrngLifecycleController regression test covers the critical path (controller tries to re-enter receiveRandomness during the request window, which is rejected by the fulfillRandomWords guard). The scoped suppression at function level with an adjacent rationale comment meets the triage rules added to SLITHER_BASELINE.md.

One minor note: write-after-write is suppressed alongside reentrancy-eth. If Slither's write-after-write finding solely refers to randomnessRequestInProgress = truefalse, the suppression is correctly scoped; just confirming that is the full extent of the write-after-write detection.


3. RandomizerNXT.isRandomizerContract() == false – Sufficient for This Slice

StreamCore.addRandomizer gates on:

IRandomizer(_randomizerContract).isRandomizerContract() == true

isRandomizerContract() returning false is a clean, unambiguous gate: attempting to configure NXT as a production collection randomizer reverts before any storage is written. testNxtRandomizerCannotBeConfiguredForProductionCollections asserts both the return value and the rejection. For this PR's scope, this is the right mechanism. XRandoms remaining tracked separately under P0-RAND-ADR is correctly deferred.


4. viewCollectionRandomizerContract / viewRandomizerEpoch Addition to IStreamCore / StreamCore

Regression risk: low. The additions are append-only view functions. StreamCore implements both. No existing callers could have been relying on these selectors to be absent.

Interface completeness: MockRandomizerCore does not declare IStreamCore — it is used directly in _fulfillRandomnessRequest calls which only need the three methods it implements (viewColIDforTokenID, viewCollectionRandomizerContract, viewRandomizerEpoch). No interface-break concern.

One minor note: requestRandomWords in NextGenRandomizerRNG is public and requires msg.sender == gencore. If it is called directly (bypassing calculateTokenHash), tokenIdToCollection[tokenid] may be stale or zero for a new token. Access is correctly gated to gencore, but a @dev comment clarifying that callers must set tokenIdToCollection first (or should use calculateTokenHash) would harden the call path against future refactors:

// Callers must ensure tokenIdToCollection[tokenid] is set before calling;
// calculateTokenHash is the canonical entry point.

5. Minor: Double Pause Check in NextGenRandomizerRNG

calculateTokenHash checks isPaused(RANDOMNESS_REQUEST) and then calls requestRandomWords which checks it again. This is harmless but costs a redundant external read on each request. Since requestRandomWords is public and can be called directly (with msg.sender == gencore), the check in requestRandomWords is the authoritative one; the check in calculateTokenHash is redundant. Not a blocker.


6. Test Coverage Assessment

Path Covered
VRF pending → fulfilled → duplicate revert testVrfRequestRecordsLifecycleAndFulfillmentSetsHashOnce
Unknown request ID testVrfUnknownAndEmptyFulfillmentsFailClosed
Empty random words ✅ same
Stale epoch / replaced provider testVrfStaleEpochOrProviderFulfillmentFails
Admin-marked stale request testMarkedStaleRequestIsObservableAndCannotFulfill
Pause does not block fulfillment testRandomnessRequestPauseDoesNotBlockVrfFulfillment
arRNG full lifecycle testArrngRequestAndFulfillmentUseSameLifecycle
Reentrant arRNG controller testArrngControllerCannotReenterFulfillmentDuringRequest
NXT production disablement testNxtRandomizerCannotBeConfiguredForProductionCollections
Wrong core token-to-collection binding testVrfWrongCollectionBindingFailsClosed
requestId == 0 from arRNG controller ⬜ Not explicitly tested (malicious controller returning 0 causes _recordRandomnessRequest to revert with UnknownRandomnessRequest(0); the ETH is already sent. This is the inherent write-after-call tradeoff, acceptable for the scoped suppression).

The requestId == 0 path is the only untested edge; it is a low-priority addition since the guard reverts before writing any state.


Summary

No blocking issues. The lifecycle validation is complete for VRF and arRNG adapters, the reentrancy guard rationale is correct, NXT disablement is sufficient for this PR slice, and the IStreamCore additions carry no regression risk within this codebase. The items above are low-priority hardening and documentation suggestions for follow-on work.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor Author

@claude please review latest head d6b3788bbe820a1dcb4cd8b091110eb067d9ba24.

This follow-up addresses CodeRabbit comment 4671138033 with lifecycle invariant comments, arRNG direct-call precondition commentary, zero request ID regression coverage, and updated traceability. Local validation is now StreamRandomizerLifecycleTest 10/10 and full make check / Windows wrapper 152/152.

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Please review latest head d6b3788bbe820a1dcb4cd8b091110eb067d9ba24. This follow-up addresses your comment 4671138033 with the requested lifecycle comments and explicit arRNG zero request ID coverage. Full local make check, Windows wrapper, formatting, diff hygiene, and Slither baseline checks are green as summarized in the PR body.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the diff for the latest head commit specifically
gh pr diff 65 --repo 6529-Collections/6529Stream 2>/dev/null | head -2000

# Also get the specific commit details
gh api repos/6529-Collections/6529Stream/commits/d6b3788bbe820a1dcb4cd8b091110eb067d9ba24 2>/dev/null | jq '{sha, message: .commit.message, files: [.files[].filename]}'

Length of output: 84281


🏁 Script executed:

#!/bin/bash
# Get the specific files changed in the follow-up commit
gh api repos/6529-Collections/6529Stream/commits/d6b3788bbe820a1dcb4cd8b091110eb067d9ba24 2>/dev/null | jq '.files[] | {filename, additions, deletions, patch}'

Length of output: 17521



@punk6529: Reviewed head d6b3788. All four items from comment 4671138033 are addressed — no new concerns.

Verification summary:

Prior suggestion Resolution
tokenToRequest non-clearing invariant comment Added in StreamRandomizerLifecycle._recordRandomnessRequest: "a token may only receive randomness once. Burn/remint redraw policy is tracked separately."
WrongRandomnessProvider defense-in-depth comment Added in _fulfillRandomnessRequest: "Defense-in-depth for future shared-storage or adapter migration paths."
arRNG direct-call precondition comment Added in RandomizerRNG.requestRandomWords: "calculateTokenHash is the canonical entry point and initializes this binding before gencore calls requestRandomWords directly."
Explicit requestId == 0 regression test testArrngZeroRequestIdFailsBeforeRecordingLifecycle with ZeroRequestIdArrngLifecycleController; asserts revert with UnknownRandomnessRequest(0), then verifies state remains None, tokenToRequest == 0, and token hash remains bytes32(0).

The Slither suppression entry, roadmap test matrix, and test/README.md are all updated to reference the new test. LGTM.

[approve]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P0-RAND-001] Harden randomizer requests and callbacks

1 participant