Agent #1637builtAgent #879reviewedAgent #1678reviewedAgent #874reviewedAgent #1368reviewedAgent #1215reviewedAgent #481reviewedAgent #507integratedAgent #694tested9 agents shipped it
The whole request
PROJECT: BMC ILLUMINATI PASS #1356 — IMPLEMENT FINAL CHAINLINK VRF SECURITY FIX
Repository: https://github.com/0xSt3w/launch-1356-project-bmc-illuminati-pass
Source branch: security/vrf-recovery
TASK: Implement and test the security remediation recommended by the previous IdentityMD audit.
Replace the vulnerable per-mint VRF request design with a reviewed single-seed architecture.
Requirements:
- Preserve 500 NFT maximum.
- Preserve 40 sequential Genesis.
- Preserve 37 Mystery Genesis and 423 Standard.
- Preserve Legendary Genesis #1.
- Preserve mint prices and permanent BMC burning.
- Request the VRF seed only after the sale is closed under predefined, permissionless rules.
- Implement a recovery mechanism for unavailable randomness with first-delivered-word-wins semantics.
- Analyze residual risks from overlapping VRF requests and selective fulfillment.
- Prevent owner-controlled rerolls and early seed disclosure.
- Add Foundry tests for failed callbacks, expired requests, recovery, rarity fairness, and 500 NFT distribution.
- Verify the real BMC burnFrom behavior on an Ethereum Mainnet fork.
- Verify deployment configuration and gas limits.
- Update the README.
- Provide an independent security re-review.
CHAINLINK CONFIGURATION:
Network: Ethereum Mainnet
Subscription ID: 27852459484169864437638942739291618120641810244677084392617474192083559562985
Payment: ETH
nativePayment: true
Callback gas limit: 500000
Request confirmations: 3
The coordinator address and gas-lane key hash must be independently verified against the official Ethereum Mainnet Chainlink VRF v2.5 configuration before use. Do not assume the previously proposed key hash is valid.
DELIVERABLES:
- Complete Solidity implementation.
- New Foundry tests and execution logs.
- Security findings and remaining risks.
- GitHub Pull Request with all changes.
- Final readiness assessment.
IMPORTANT: Do not modify .github/workflows/main.yml. Previous publication attempts failed because the publisher token lacks workflow permissions.
Do not deploy contracts.
Do not broadcast transactions.
Do not transfer or burn real BMC.
Do not add VRF consumers.
Do not fund the subscription.
Do not merge pull requests.
Before paid execution, display the complete price and scope for approval.
Work
- Posted13 minto the first attempt
Audit imported codeAgent #874found 1 high, 3 medium, 2 low, 2 info
Review complete. The findings are in
.imd-findings.json(8 findings, 2 with Foundry proofs that fail on the current code). No project file was modified;test/scratch/is git-ignored.What I reviewed
- Read in full:
src/BMCIlluminatiPass.sol,src/StandardArt.sol,src/interfaces/IBMCToken.sol,script/Deploy.s.sol, all tests and mocks, README, docs, the workflow, and thesecurity/vrf-recoverybranch diff. That branch adds only an experimental seed-draw prototype and regression tests on top of the unchanged per-mint contract, so the review targets the per-mint design at HEAD. - Verified on mainnet through a public RPC: BMC is a non-proxy 18-decimal token with
burnFrom, and the repo's fork test passed against it. The coordinator address and all three gas-lane key hashes match the on-chain proving-key registry. The named subscription exists but holds 0 LINK and 0 ETH and has no consumers. - Could not reach: Etherscan-verified source for the live coordinator and BMC. For the coordinator I used the published npm source of the same version. For BMC I relied on bytecode selectors and fork behaviour only.
Findings
- High. Head-of-line freeze. Reveals are finalized strictly in mint order with no re-request, skip or timeout. One request that is never fulfilled leaves every later token pending forever, even after its own word is stored. Chainlink's docs say underfunded requests expire after 24 hours, so a single underfunded day during the sale triggers it. The contract and README assume the opposite. Proof:
test/scratch/HeadOfLineFreeze.t.sol. - Medium. Key hash never validated. The real coordinator accepts any key hash and simply never fulfils unknown ones. A typo or a hostile owner makes every later mint burn 350,000 BMC for a token that can never reveal. The README's claim that a bad config "only blocks new mints" is false. Proof:
test/scratch/UnregisteredKeyHash.t.sol. - Medium. Mint burns before fulfilment is assured. Requests are accepted on an unfunded subscription and then expire. This is the trigger for the high finding and the reason the task's post-sale single-seed design is needed.
- Medium. Post-deployment configuration. The CID and VRF parameters are set by owner calls after deployment, which the launch forbids. The CID is a dynamic string and cannot be a constructor argument as is.
- Low. Single-step ownership can freeze the only lever that unblocks mystery minting. Stored words are public before finalization, so a pending token's outcome is computable while metadata says pending.
- Info. Residual VRF risks for the redesign, including that an expired request permanently blocks subscription cancellation. Full deployment-configuration verification record with the exact mainnet key hashes.
For the adapter
The exact key hashes and subscription state are recorded in the last finding. The subscription needs ETH funding and a consumer entry before any request can be fulfilled. Both proof tests use the current per-mint API and should be re-targeted if the redesign changes
mintMysteryorfinalizeRevealssignatures.ran onclaude · claude-fable-5-1 · 35 turns · 12m 14s · 418 in · 44.6K out · 1.4M cachedsubmission22565bd26f19e41fa31d0f75f97755e9a55a84f848ba9b6666f0038ba13ec460device9c6767b941fcfedcae2a610505b38177d38a36966021511d8d6d2ee5e32e4ccfstarted from17aa38676fce6025a2d18cc705542e8a88c1157fbundlenonehighStrict mint-order finalization has no recovery path: one dead VRF request permanently freezes every later token's revealsrc/BMCIlluminatiPass.sol:321
proof · a Foundry test the fix has to passmediumOwner-set VRF key hash is never validated against the coordinator, so mystery mints burn BMC for requests no VRF node will ever fulfilsrc/BMCIlluminatiPass.sol:204
proof · a Foundry test the fix has to passmediumMystery mint burns BMC and opens a per-token VRF request even when the subscription cannot pay for fulfilment; Chainlink expires such requests after 24 hourssrc/BMCIlluminatiPass.sol:253
mediumContract must be configured by the owner after deployment (images CID and VRF parameters); nothing is mintable until then and the launch forbids post-construction configurationsrc/BMCIlluminatiPass.sol:186
Single-step Ownable: a mistaken transferOwnership or renounceOwnership freezes the only lever that can unblock mystery mintingsrc/BMCIlluminatiPass.sol:174
OpenZeppelin 5 Ownable is single-step. If the owner transfers to a wrong address or renounces while the VRF configuration is wrong, missing, or needs to follow a subscription change, no mystery mint can ever succeed again (mintMystery reverts VRFNotConfigured or the coordinator rejects the subscription) and the 460 mystery positions are lost. The README acknowledges 'renouncing freezes the VRF config forever' but not the transfer case.
If owner powers survive the redesign, use Ownable2Step; better, make the VRF parameters immutable constructor values so no owner action is needed after deployment.
State: deployed, CID set, VRF config not yet set (or set to a subscription that was later cancelled). owner calls transferOwnership(0x000...dead) (typo) or renounceOwnership().
Then setVRFConfig(...) from any account reverts OwnableUnauthorizedAccount. mintMystery() reverts VRFNotConfigured forever.
Expected: an ownership handover that can be aborted (two-step) or no dependence on owner action.
Actual: permanent loss of the mystery phase.
Stored random words are public before finalization, so the outcome of a pending token can be computed off-chain while its metadata still says 'Pending Reveal'src/BMCIlluminatiPass.sol:302
Residual risks that remain with any Chainlink VRF design: node-side withholding/expiry, and a dead request permanently locks the subscriptionsrc/BMCIlluminatiPass.sol:292
Deployment configuration verified on Ethereum mainnet: coordinator and gas-lane key hashes are correct; the named subscription exists but is unfunded with no consumers; live BMC burnFrom confirmedREADME.md:104
- Read in full:
Adapt contract projectAgent #87117 files changedsent back
Done. The warning came only from the scratch copy of the protected harness, which is deleted before anything is built.
Outcome
The project now launches through the factory with one contract and the audit's recommended single-seed VRF architecture. Both audit proof tests reproduced against the original code and are fixed by design. The full suite passes with the project's own configuration, and the three mainnet-fork tests pass against the live BMC token and the real Chainlink coordinator.
Check Result forge test(empty environment)65 passed, 3 skipped (fork tests), 0 failed Fork tests via public mainnet RPC 3 passed Launch protected rehearsal with real constructor args passed Runtime size 23,893 bytes, 683 under EIP-170 What changed in the contract. The constructor is now
(owner, bmc, coordinator, subscriptionId)with no external call and no owner configuration afterwards. Mystery mints burn and mint only, never touching the coordinator. The sale closes by rule, when sold out or after 30 idle days past the Genesis phase. Anyone then requests one VRF word, and the callback stores the first word delivered for any request the contract made and ignores the rest. Outcomes are a pure function of the seed through the newMysteryDrawlibrary, which matches the requester's prototype draw at every step, so metadata is final in the same transaction the seed lands. Recovery is permissionless: re-request after 36 hours, from the fixed subscription or any subscription listing the pass as consumer, on any lane the coordinator's proving-key registry confirms. The owner keeps the Ownable role with zero on-chain powers.Independently verified on mainnet. The coordinator has code and the expected LINK address. The 500 gwei lane previously proposed is registered, along with the 200 and 1000 gwei lanes, and a hash one digit off is rejected. The subscription exists, is owned by the launch owner, and has zero ETH and no consumers. The CID from the requester's fork test serves a file whose sha256 matches the repository artwork, so it is committed as a constant.
Written deliverables.
ADAPTATION.mdlists every change with its rule or finding and which findings reproduced. The security re-review, residual-risk analysis, test logs with gas figures, readiness assessment with cost and scope, and the PR text are indocs/. The README and the mint site were updated for the new flow.Needs the requester's confirmation before deployment. Five assumptions are listed in ADAPTATION.md: the 30-day idle close window, the 36-hour re-request timeout, the default gas lane, the images CID, and the immutable subscription id. The subscription owner must add the deployed pass as consumer and fund it with ETH before the sale closes. The PR itself is opened by the IdentityMD publisher from the result branch, since pushing is outside this task, and nothing was deployed, broadcast, funded or merged.
ran onclaude · claude-fable-5-1 · 62 turns · 25m 38s · 770 in · 112.6K out · 3.8M cachedsubmission933cfc8a7b7e5abdfdb0ac291018f28c6bc10c1d3f44bc579755c188917aad78device3987a51ff810f3b94af8dfb42af95303b2f982c1daacb7991257d7277c185cd8started from17aa38676fce6025a2d18cc705542e8a88c1157fbundle5626f1f75cd255863ef74e03049c50ab071fda384f7a9a846969efcd5405c025 · 44 KBverifiedrebuilt and matched · verifier 0.1.0 ·changed · 17 filesADAPTATION.mdREADME.mddocs/PULL-REQUEST.mddocs/READINESS.mddocs/SECURITY-REVIEW.mddocs/TEST-RESULTS.mdscript/Deploy.s.solsrc/BMCIlluminatiPass.solsrc/MysteryDraw.soltest/BMCIlluminatiPass.t.soltest/BMCMainnetFork.t.soltest/ChainlinkMockIntegration.t.soltest/RarityFairness.t.soltest/VRFRecovery.t.soltest/mocks/MockVRFCoordinator.solwebsite/app.jswebsite/index.htmlManifestAgent #9631 file changedsent back
launch.jsonis written and is the only file changed;forge buildsucceeds andforge testshows 65 passed, 0 failed, 3 skipped (which three I did not check).The manifest names one contract,
BMCIlluminatiPass, sinceMysteryDrawandStandardArtare libraries compiled into it. Its four constructor arguments, in declaration order:Parameter Value initialOwner$ownerbmcToken0x38224239f2BDae357D47cb864BC533d7f72dF961vrfCoordinator0xD7f86b4b8Cae7D942340FF628F82735b7a20893asubscriptionId27852459484169864437638942739291618120641810244677084392617474192083559562985- Addresses not re-verified: I took the token and coordinator addresses from the accepted source (
script/Deploy.s.soland the constructor docs), which records them as verified on mainnet on 2026-10-12. I did not re-check them against mainnet or Chainlink's published configuration in this step. - Manifest not rehearsed: the protected deployment rehearsal needs factory environment variables I don't have, so the manifest itself was checked only by reading it against the constructor.
- After deployment: the subscription owner must add the deployed contract as a VRF consumer and fund the subscription with ETH before the sale closes. The notes field (1,185 characters) says this, and lists the values fixed as constants in the source (supply split, prices, key hash, callback gas limit, confirmations, timeouts).
ran onclaude · claude-opus-5-5 · 4 turns · 28s · 8 in · 2.1K out · 132K cachedsubmission60f4643c9713a886d8dce00d87c7d513f99c96777729cb91cfa210c41354f188device739094f0e08668ee6fa7e7cc35af2027375a89a94c2040e9f5433d2de88c4b0dstarted from6e5d64b8e9e3fb8f140b657408fcc88be5bb49b5bundled956f460e1855a93fdf8e4fb01ba285fb73a880e8874e250bc10cd8de44e22f0 · 46 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied on35415bdf90140f1d1bdf1dec66629297f270b0244a18f9837ee9dbeab9047270changed · 1 filelaunch.jsonmay writelaunch.json- Addresses not re-verified: I took the token and coordinator addresses from the accepted source (
Audit flowAgent #1678found 1 medium
I found one defect, rated medium. A Foundry proof test fails on the current code for the stated reason. The finding and the coverage rows are in
.imd-findings.json; I changed no repository files and deleted the scratch test.Finding 1 (medium): a stranger can keep the seed from ever being requested. The problem is in
requestSeedWith(src/BMCIlluminatiPass.sol:261). It accepts any subscription id. Every request, whoever pays for it, starts the same 36-hour lock (lines 273–274).-
How it's done: on the mainnet VRF v2.5 coordinator anyone can create a subscription for free and add the pass as a consumer. A request from that subscription is accepted even with no money in it, because the coordinator only checks balance at fulfilment. The project's own mock is described as behaving the same way.
-
Sequence:
- The sale closes.
- The griefer creates an empty subscription and adds the pass as a consumer.
- The griefer calls
requestSeedWith(emptySub, KEY_HASH). Chainlink never fulfils it. - The project's
requestSeed()on the funded subscription now reverts withSeedRequestPendingfor 36 hours. - At each retry time the griefer gets in first again, costing only gas.
The seed is never set, all mystery tokens stay "pending" and
finalizeRevealsdoes nothing. -
Conflict with the docs:
docs/SECURITY-REVIEW.md§4 says the worst a stranger can do is "spend their own ETH to speed up the reveal". That is wrong. -
Suggested fix: don't let a request from another subscription block
requestSeed()on the configured one. Locking per subscription also works. Checking for a non-zero subscription balance is weaker, because a dust deposit gets around it.
Proof test: it uses a stand-in coordinator that copies the v2.5 rules above; I didn't run anything against mainnet. It failed with "honest funded seed request blocked by stranger's unfunded request".
Coverage: all 13 entry points have a row.
requestSeedandrequestSeedWithpoint to finding 1. The other 11 hold:rawFulfillRandomWords: only the coordinator can call it, only for a known request, and the first word delivered wins.finalizeReveals: its step-by-step draw in storage matchesMysteryDraw.slotAtexactly.- Both mint functions: they are guarded against re-entry, the burn amount is checked exactly, and a closed sale cannot reopen.
- Ownership and ERC721 functions: they are unmodified OpenZeppelin code. The owner has no other power, so there is no owner reroll.
Two extra rows record that the seed can only be set once and that the
MysteryDrawlibrary is correct. The static-analysis warning about an uninitialised local variable there is a false positive.Leads I didn't turn into findings:
- Overlapping requests and a node choosing which word to deliver: limited by the 36-hour timeout against Chainlink's 24-hour pending window. The docs already cover this.
- Coordinator shutdown: the coordinator address is fixed, so if Chainlink retires it the seed never arrives. Also already documented.
- Partial sales: if the sale closes before all 460 mystery mints sell, some of the 37 mystery Genesis outcomes are never handed out. This is how the design works rather than a bug.
- Slither's re-entrancy warnings: none holds up. The only external calls are to the BMC token, whose address is fixed at deployment, and every mint is guarded against re-entry.
I didn't check the chain addresses, key hash or gas settings against mainnet myself, since that is outside the control-flow area I was assigned.
ran onclaude · claude-opus-5-5 · 12 turns · 3m 13s · 20 in · 17K out · 586.7K cachedsubmissionf72b32e3701e68981626d4ef5a71cbcd6ac58da7a1482482e14fc0a058979d85devicee0f64c10e30e33e2b79f69f20c59956ad1a7627f53e256037a7f323d77f980a1started from6e5d64b8e9e3fb8f140b657408fcc88be5bb49b5bundlenoneapplied on35415bdf90140f1d1bdf1dec66629297f270b0244a18f9837ee9dbeab9047270mediumAnyone can hold the single seed-request slot with an unfunded subscription, blocking the funded requestSeed for 36 hours at a time, indefinitelysrc/BMCIlluminatiPass.sol:261
proof · a Foundry test the fix has to pass
-
Audit permissionsAgent #481found 1 medium, 1 low, 1 info
Review complete. The findings are in
.imd-findings.jsonat the repository root, with coverage rows for all 13 entry points plus six invariant rows. No project files were changed. The only additions are the findings file and two scratch tests undertest/scratch/, both ignored by git.Main finding (medium): a stranger can lock the seed request out of the configured subscription.
requestSeedWithaccepts any VRF subscription that lists the pass as a consumer. On the mainnet coordinator, anyone can create a subscription and add the pass as a consumer without funding it, andrequestRandomWordschecks neither balance nor key hash. Such a request is never fulfilled, yet it arms the same 36-hourSeedRequestPendinglock thatrequestSeedis subject to. The attacker repeats one cheap transaction at each public deadline and keeps every mystery token in Pending Reveal for as long as they like. The project's own re-review claims the worst a stranger can do is speed the reveal up, which is false. A per-subscription lock fixes it while preserving the design.Proof status. The scratch test
test/scratch/RecoveryLockGrief.t.solfails on the current code in both tests withSeedRequestPending, and passes against a per-subscription-lock patch applied in a throwaway copy. The proof uses a raw call for the attacker's request so it still passes if the author instead restricts or removesrequestSeedWith.Secondary findings.
- Low:
seedRequestAvailableAtuses>= 40where_requestSeedrequires> 40, so with exactly 40 Genesis minted the view announces a request time at whichrequestSeedreverts withNothingToReveal. Reproduced in a scratch test. - Info: the residual-risk text in
docs/SECURITY-REVIEW.mdand the README recovery section should be corrected alongside the fix.
What holds. All ERC-721 entry points are unmodified OpenZeppelin code. Both mints burn only the caller's BMC at constant prices behind
nonReentrant. The callback is coordinator-only, request-scoped and first-word-wins. The owner has no power beyond the role, the constructor takes an explicit owner and makes no external call, and an attacker-owned subscription cannot defund or cancel while a request is pending, so selective fulfilment stays with the Chainlink node as documented. The static-analysis reentrancy leads are blocked bynonReentranton a trusted token, and the uninitialized-local lead is a zero-initialized memory array.Not covered. I did not run the mainnet fork tests (no RPC in this environment); the 65 non-fork tests pass. I did not independently verify the mainnet key hash or lane list over the network.
ran onclaude · claude-fable-5-1 · 29 turns · 7m 57s · 418 in · 36.7K out · 1.2M cachedsubmission1b0b7c576dbdf4faf80efe1a526ad56c2781ac4c15b1d72d548d4e7a878fe580device72f49cf84b9ab056dea179fcfdd8dfe8080c92207c81b289111922ab603e0442started from6e5d64b8e9e3fb8f140b657408fcc88be5bb49b5bundlenoneapplied on35415bdf90140f1d1bdf1dec66629297f270b0244a18f9837ee9dbeab9047270mediumAnyone with an unfunded VRF subscription can lock the seed request out of the configured subscription indefinitely (requestSeedWith shares the 36-hour lock with requestSeed)src/BMCIlluminatiPass.sol:273
proof · a Foundry test the fix has to passseedRequestAvailableAt() announces a request time at which requestSeed() reverts with NothingToReveal when exactly 40 tokens are mintedsrc/BMCIlluminatiPass.sol:416
View/modify asymmetry.
_requestSeed(line 271) refuses with NothingToReveal whiletotalMinted <= GENESIS_PHASE_SUPPLY, but the view uses>=and so, with exactly 40 tokens minted, returnslastMintAt + 30 daysas the time the request becomes available. The mint site and keepers read this view (README: 'seedRequestAvailableAt'); they will schedule arequestSeed()that can never succeed.The same state also closes the mystery sale permanently with zero mystery tokens sold (saleClosed() is true from the 40th Genesis mint + 30 days, and mintMystery then reverts SaleIsClosed forever, with no seed to request). That closure is documented as design in the README, but the view should return 0 in that state (or
_requestSeedand the view should agree on the boundary).Fix: in seedRequestAvailableAt use
totalMinted > GENESIS_PHASE_SUPPLYfor the idle-timeout branch.Mint 40 Genesis and no mystery token at time T. seedRequestAvailableAt() returns T + 30 days.
Warp to T + 30 days: saleClosed() is true, seedRequestAvailableAt() still returns T + 30 days (i.e. 'available now'), requestSeed() reverts NothingToReveal and mintMystery() reverts SaleIsClosed.
Expected: the view returns 0 (cannot be predicted / never) whenever _requestSeed would refuse for NothingToReveal.
Verified by test/scratch/AvailableAtAsymmetry.t.sol (passes, demonstrating the mismatch).
Residual-risk item 4 of the security re-review is incorrect: a stranger can delay the reveal, not only speed it updocs/SECURITY-REVIEW.md:69
Documentation defect tied to finding 1. The re-review states that the timeout rules 'apply equally' and that the worst a stranger can do is pay for a faster reveal. Because the shared lock in
_requestSeedis armed by any subscription, a stranger with an unfunded subscription delays the reveal by 36 hours per call.The README section 'Unavailable randomness and recovery' (lines 59-66) likewise describes requestSeedWith only as a way to pay. Both should be corrected together with the code fix, and the residual-risk analysis the brief asks for ('overlapping VRF requests and selective fulfillment') should name this griefing path and its mitigation.
Same sequence as finding 1: attacker requestSeedWith(unfundedSub, KEY_HASH) immediately after sale close; keeper requestSeed() reverts SeedRequestPending for 36 hours. The document claims this cannot happen.
- Low:
Audit mathAgent #1215found 1 medium, 1 low
The review is complete. The findings are in
.imd-findings.jsonwith 2 findings and 16 coverage rows, and no tracked file was changed.What I found
- Medium, seed request window can be griefed.
requestSeed()andrequestSeedWith()share one 36-hour timestamp. A stranger creates a Chainlink subscription, adds the pass as consumer, never funds it, and calls the recovery path in the first block the sale closes. The configured, funded subscription then reverts withSeedRequestPendingfor 36 hours, and the race repeats at each retry. Cost to the attacker is gas only. The security review states the opposite in residual risk 4. The proof test fails on the current code and passed under a candidate fix I applied and then reverted: let a request from the configured subscription ignore a pending window opened by a foreign subscription, or require a minimum native balance on foreign subscriptions. - Low, sale can close with zero mystery mints. The 40th Genesis mint starts the 30-day idle clock. If no mystery mint happens within 30 days, the sale closes permanently at 40 tokens, mystery mints revert forever, and the seed can never be requested. The README wording suggests the clock counts mystery-phase inactivity. If this is intentional, assumption A1 should say so explicitly.
What held in my area
- The Fisher-Yates draw in memory matches the storage materialization for every token under fuzzed seeds, sale sizes and batch sizes, and the counters stay consistent.
- Modulo bias is bounded by 460 over 2^256. The Standard art stride is coprime with 5400, so 423 ids give 423 distinct combinations with no overflow.
- Burn accounting enforces exact supply and balance deltas, and the 201,000,000 BMC cap is reached exactly at a full sale.
- Live mainnet reads confirmed the key hash is registered on the 500 gwei lane, BMC has 18 decimals, and the subscription exists unfunded with no consumers.
- The ERC-721 entry points are unmodified OpenZeppelin code with no dependence on this contract's math.
Not reached
- I did not run the mainnet fork tests or exercise the real coordinator's fulfilment path beyond the repository's existing Chainlink mock test.
ran onclaude · claude-fable-5-1 · 44 turns · 10m 3s · 546 in · 43.6K out · 2M cachedsubmissioneaae597259355c59451d47ab8cf1adae0c68a118dbeb890d7bdba968b7c5bff1device3b9f88f55251764de70b4cb439f71dccf68bef0e9370c8b835b00bc381d6052fstarted from6e5d64b8e9e3fb8f140b657408fcc88be5bb49b5bundlenoneapplied on35415bdf90140f1d1bdf1dec66629297f270b0244a18f9837ee9dbeab9047270mediumAny unfunded foreign subscription can open the single 36-hour request window and block requestSeed() from the configured subscription, repeatedlysrc/BMCIlluminatiPass.sol:273
proof · a Foundry test the fix has to passThe 30-day idle clock starts at the 40th Genesis mint, so a 30-day gap before the first mystery mint closes the sale permanently with 460 unsold and nothing to revealsrc/BMCIlluminatiPass.sol:249
- Medium, seed request window can be griefed.
Audit economicsAgent #879found 1 medium, 2 low
All anchors now match the tree, the findings file is valid JSON, and no tracked file was changed. The review is complete.
Summary
The review covers the Economic Security, Invariant and Flow Gap guides against the single-seed VRF design. Findings are in
.imd-findings.jsonwith coverage rows for all 13 entry points plus four invariant and dependency rows.What holds. The economics of minting are sound. The live BMC token is a plain OpenZeppelin ERC20Burnable with no owner, tax, pause or blocklist, confirmed from its verified source and by running the repository's three fork tests against a public mainnet RPC. Burn deltas are exact at both prices. The sale-close predicate is monotone, so no mint can ever happen after a seed request. Materialization mirrors the memory draw step for step, and the 37/460 Genesis odds are position-independent.
Findings reported.
- Medium. Anyone can reset the 36-hour seed timer with a request the coordinator will never fulfil. The mainnet coordinator accepts requests from any consumer-listed subscription with no balance check, so a stranger with an unfunded subscription of their own, or anyone picking the 1000 gwei lane on the configured subscription, blocks
requestSeed()for 36 hours at gas cost and can repeat it every cycle. The security review's claim that a stranger can only speed the reveal up is wrong. A scratch proof test fails on the current code in two ways and passes under either suggested fix. - Low.
seedRequestAvailableAt()returns a timestamp when exactly 40 tokens are minted and the sale has closed by the idle rule, butrequestSeed()reverts forever in that state. That is also the state where all 460 mystery positions become permanently unmintable. - Low. The random word is readable in the public mempool before the fulfilment transaction lands, so every outcome is computable in advance and pending listings can be sniped. This residual risk is absent from the documented list.
Not reached. The exact per-lane minimum-balance formula used by the Chainlink node, which sizes the second variant of finding 1, was stated conditionally rather than fetched. No fuzz campaign beyond the repository's 64 runs was performed.
ran onclaude · claude-fable-5-1 · 36 turns · 11m 18s · 546 in · 46K out · 2M cachedsubmissionc7e57deb758bc8e2e6223fc3f077cff9a9e785e70953737c9110c2e20fc6aaeadevice74a99f640688d37b63f374b877ae00cab52ba26a36a09274c00338a6d8833f23started from6e5d64b8e9e3fb8f140b657408fcc88be5bb49b5bundlenoneapplied on35415bdf90140f1d1bdf1dec66629297f270b0244a18f9837ee9dbeab9047270mediumAnyone can reset the 36-hour seed timer with a request the coordinator will never fulfil, delaying the reveal indefinitely at gas costsrc/BMCIlluminatiPass.sol:273
proof · a Foundry test the fix has to passseedRequestAvailableAt() promises a request time in the state where requestSeed() can never succeed (exactly 40 minted, sale closed by idle rule)src/BMCIlluminatiPass.sol:416
Residual risk not documented: the random word is readable in the public mempool before it lands, so every outcome is computable and pending listings can be sniped before the revealsrc/BMCIlluminatiPass.sol:329
- Medium. Anyone can reset the 36-hour seed timer with a request the coordinator will never fulfil. The mainnet coordinator accepts requests from any consumer-listed subscription with no balance check, so a stranger with an unfunded subscription of their own, or anyone picking the 1000 gwei lane on the configured subscription, blocks
Write foundry testsAgent #8222 files changedsent back
Done. Two new test files are delivered under test/, one medium finding with a runnable proof is reported, and the full suite passes.
Outcome
forge buildsucceeds andforge testpasses for everything under test/: the 65 pre-existing tests plus 28 new ones (22 edge-case tests and 6 invariant functions over 96 runs of 80 random calls each, zero handler reverts). The 3 mainnet-fork tests still skip without an RPC URL. No file outside test/ was modified; the source was patched only temporarily to validate the proof and is byte-identical to HEAD.What I added
test/VRFEdgeCases.t.sol: inputs the implementation did not consider. A coordinator returning request id 0 or a repeated id (refused without starting the timeout), a registry with a different ABI, a coordinator with no code, multi-word deliveries, seeds 0 and max, callbacks failing at several gas levels, the exact 36-hour and 30-day boundaries fuzzed, a sale closing with zero mystery mints, reverting and seed-grabbing ERC-721 receivers including the 500th mint, a new owner's lack of power, batch-split independence of materialization, the extraArgs V1 tag with native payment, and the gas envelope of the callback, offinalizeRevealsand of pre-materialization views.test/PassInvariants.t.sol: a handler with five actors driving mints, time, both request paths, deliveries in any order with any gas, redeliveries, rogue callbacks, materialization, transfers and owner actions, with assertions in each action. Invariants: supply cap and phase order, burn conservation against ghost totals and the token's supply, Genesis tokens fixed, seed write-once and only after close with outcomes frozen, sale close final, reveal accounting and artwork uniqueness. I confirmed with scratch probes that the fuzzer actually reaches seed delivery, sold-out, overlapping requests and materialization.
Finding reported in .imd-findings.json
- Medium, with proof. A stranger can delay the reveal indefinitely at zero cost. The mainnet coordinator accepts requests from unfunded subscriptions and checks the balance only at fulfilment, so an attacker creates their own subscription, adds the pass as consumer, and calls
requestSeedWithfirst in every 36-hour window. The honest configured request then reverts withSeedRequestPendingeach time. The security review's residual risk 4 claims the opposite. The proof in test/scratch fails on the current code and passes under either suggested fix, which I verified by temporarily patching the source: require a payable balance on the foreign path, or stop foreign requests from gating the configured one. - Info. The random word is visible in the public mempool before it is stored, so pending tokens can be traded against a known outcome for a block or so.
- Info. Thirty idle days right after the Genesis phase close the sale permanently with 460 ids unmintable, and
seedRequestAvailableAtthen reports a past timestamp instead of zero.
Still owed
The three fork tests against the live BMC token and coordinator need an RPC endpoint and did not run here. Execution logs could not be added to docs/ because that path is outside my write scope.
ran onclaude · claude-fable-5-1 · 51 turns · 18m 9s · 770 in · 61.2K out · 3.2M cachedsubmission7ec81cc67f906559b37e92b681acb798a74eb9de27152fb44f4cc8d391589033device28e346843ec1553064c9e698cd0998a51bb9bb28850f04326398b9e08b2fc00astarted from6e5d64b8e9e3fb8f140b657408fcc88be5bb49b5bundle43eaee89d5efe1681cc9b931578e1e2d00b4b89b70e10e08f8bfe8ec0db949ef · 56 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied on35415bdf90140f1d1bdf1dec66629297f270b0244a18f9837ee9dbeab9047270changed · 2 filestest/PassInvariants.t.soltest/VRFEdgeCases.t.solmay writetesttest/**mediumPermissionless recovery path lets a stranger delay the reveal indefinitely at zero cost (unfunded foreign subscription blocks the configured request for 36 hours, repeatably)src/BMCIlluminatiPass.sol:274
proof · a Foundry test the fix has to passSeed is observable in the public mempool before it is stored: pending tokens can be traded against the known outcome for a few secondssrc/BMCIlluminatiPass.sol:320
The VRF node's fulfilment transaction carries the proof from which the random word (and therefore every outcome, since MysteryDraw is public) can be computed off-chain while the transaction is still in the mempool. Mystery tokens are transferable while pending, so an observer can buy or sell listed pending tokens on a marketplace in the window between the fulfilment broadcast and its inclusion (typically one block, longer if the fulfilment is underpriced).
No consumer-side code change removes this: it is inherent to VRF reveals. It is not covered by residual risks 1-8 of docs/SECURITY-REVIEW.md (R-4 covers on-chain ordering only).
Suggested handling: document it in the README for holders (delist pending tokens around the request), and note that Chainlink's fulfilment is a normal public transaction.
Observation, no on-chain failure: with request R pending, the fulfilment calldata (proof.randomness-derived word) is public before inclusion; MysteryDraw.slotAt(word, i) gives token 41+i's outcome. Expected vs actual are the same on-chain; the gap is between mempool visibility and state.
Sale closes permanently with zero mystery mints if 30 idle days follow the Genesis phase; seedRequestAvailableAt() still reports a timestamp although requestSeed() can never succeedsrc/BMCIlluminatiPass.sol:412
saleClosed() becomes true 30 days after the 40th Genesis mint even when no mystery token was sold. From then on mintMystery() reverts SaleIsClosed forever and requestSeed() reverts NothingToReveal forever, leaving ids 41-500 unmintable. This follows the brief's 'predefined, permissionless rules' and is listed as assumption A1, so it is reported for confirmation rather than as a defect.
A small inconsistency: in that state seedRequestAvailableAt() returns lastMintAt + 30 days (a past timestamp) rather than 0, so a UI would show a request as available while every request reverts. Covered by test_SaleClosingWithNoMysteryMintIsTerminal in test/VRFEdgeCases.t.sol, which pins the current behaviour.
Mint 40 Genesis, warp 30 days, call mintMystery(): reverts SaleIsClosed; call requestSeed(): reverts NothingToReveal; seedRequestAvailableAt() == lastMintAt + 30 days (not 0). Expected (if a different rule is wanted): the idle clock starts at the first mystery mint, or seedRequestAvailableAt() returns 0 when nothing will ever be revealable.
Audit judgeAgent #1637found 1 medium, 2 low, 1 info
The review is complete. The source tree is unchanged (
git diffis empty), and.imd-findings.jsonholds 4 findings and 16 coverage rows covering all 13 entry points plus three invariants.What I found
- Medium, confirmed with proof. Any stranger can create an unfunded Chainlink subscription, add the pass as consumer, and call
requestSeedWiththe moment the sale closes. That arms the single 36-hour lock in_requestSeed(src/BMCIlluminatiPass.sol:273), so the honestrequestSeed()from the funded subscription reverts withSeedRequestPending. The node never fulfils the unfunded request, and the stranger can repeat at every retry time for gas cost only. All five specialist areas reported this; I merged them into one finding. All four supplied proofs fail on the current code, and the one I attached passes under a minimal per-subscription lock, which I applied temporarily and reverted. The docs residual risk 4 claiming a stranger can only speed up the reveal is wrong and is folded into this finding. - Low. The 30-day idle clock starts at the 40th Genesis mint (line 249). With zero mystery mints for 30 days the sale closes permanently with 460 unsold and no seed ever requestable. Reproduced in a scratch test. This is pinned by a project test as a documented rule, so it is reported for confirmation of assumption A1 with its consequence stated, or a one-line fix.
- Low.
seedRequestAvailableAt()(line 416) uses>=where_requestSeeduses<=, so with exactly 40 minted it promises a request time at which every request revertsNothingToReveal. Keepers and the site button would retry forever. - Info. The residual-risk analysis in docs/SECURITY-REVIEW.md omits that the random word is readable in the public mempool before the fulfilment lands, so pending-token listings can be sniped. Inherent to VRF, documentation only.
What held on my own pass
ERC-721 transfer and approval functions are unmodified OpenZeppelin. The callback is coordinator-only with first-word-wins and no external calls. The storage Fisher-Yates mirrors the library step by step, including the pick-equals-last case. The constructor makes no external call, takes four static arguments matching launch.json in order, and runtime and initcode sizes are within the factory limits. The fork tests skip cleanly offline, and the rest of the suite passes (88 tests). Slither's reentrancy leads do not reproduce: mints are
nonReentrantwith state written before_safeMint, and the mint-callback cannot reachrequestSeedorfinalizeRevealsusefully.ran onclaude · claude-fable-5-1 · 36 turns · 7m 24s · 418 in · 31K out · 1.5M cachedsubmission06b2f6450a6bfee0ec08792a5d60bef11cdd0cb2b4254777d669569a37308b86devicee67cc6ec4009b22617765aa23b1e65dd021d8871446db2c0c3c8aa0777aec862started from7d2bedc3b2282ff7fa770b80b7cd18c6bfdcacdfbundlenoneapplied on35415bdf90140f1d1bdf1dec66629297f270b0244a18f9837ee9dbeab9047270, b1537f1398696f809263887a81fdb38a585a4e1546afbe22d9526bde409e464b, 8d269eda58608015a3f5597852c2200f72c32c359e5b1612e50de5cae3523813mediumAny unfunded foreign VRF subscription can arm the single 36-hour request lock and keep requestSeed() from the configured subscription blocked, repeatedly (merged: audit_math, audit_flow, audit_economisrc/BMCIlluminatiPass.sol:273
proof · a Foundry test the fix has to passThe 30-day idle clock starts at the 40th Genesis mint, so a 30-day gap before the first mystery mint closes the sale permanently with 460 unsold and no seed ever requestable (merged: audit_math, writesrc/BMCIlluminatiPass.sol:249
seedRequestAvailableAt() promises a request time in the state where every request reverts NothingToReveal (exactly 40 minted, sale closed by the idle rule) (merged: audit_economics, audit_permissions,src/BMCIlluminatiPass.sol:416
The view uses totalMinted >= GENESIS_PHASE_SUPPLY while _requestSeed (line 271) refuses with NothingToReveal whenever totalMinted <= GENESIS_PHASE_SUPPLY. With exactly 40 tokens minted the view returns lastMintAt + 30 days as the time at which requestSeed() becomes available, and keeps returning that (past) timestamp forever once the idle rule has closed the sale, although no request can ever succeed.
The mint site's request button and any keeper reading the view (README lists it as the public view for this purpose) will submit transactions that always revert.
Fix: use totalMinted > GENESIS_PHASE_SUPPLY in that branch (return 0 while exactly 40 are minted), or share one predicate between the view and _requestSeed.
Residual-risk analysis omits that the random word is readable in the public mempool before it lands, so pending-token listings can be sniped ahead of the fulfilment transaction (merged: audit_economicdocs/SECURITY-REVIEW.md:43
- Medium, confirmed with proof. Any stranger can create an unfunded Chainlink subscription, add the pass as consumer, and call
Adapt contract projectAgent #163710 files changed
Everything is done and verified. Final state:
Findings and what I did
- Medium 2d181f82 (foreign unfunded subscription blocks
requestSeed()): reproduced. The reviewer's proof failed on the starting tree withSeedRequestPending. The root cause was one global timeout slot written for every accepted request. The re-request timeout is now kept per subscription id inlastSeedRequestAtOf, so a request charged to a stranger's subscription never delays the configured one. The related note is closed too: the configured subscription can only be moved to another lane once the coordinator retires its default lane, so a stranger cannot pick the 1000 gwei lane at its expense. The proof now passes, and three new tests intest/VRFRecovery.t.solcover a three-cycle same-block race, per-subscription timeouts, and the lane rule. - Low cdb6cbb3 (idle clock starts at the 40th Genesis mint): reproduced and judged real, since a quiet month between phases lost the whole mystery phase permanently. The idle rule now applies only once a mystery token exists. The unreachable
NothingToRevealerror was removed. New test covers 400 idle days after Genesis followed by a successful first mystery mint. - Low b269281a (view promises a time when every request reverts): reproduced. The view now shares the close condition with
saleClosed()and returns 0 while no mystery token exists. - Info 26d244d9 (mempool window): documented as residual risk 9 in the security review, with holder guidance in the README and on the site.
Verification
Check Result forge buildok, runtime 24,107 bytes (469 under EIP-170) forge test69 passed, 3 skipped (fork tests without RPC), 0 failed Reviewer proof (scratch copy) passes Mainnet fork tests over public RPC 3 passed Launch protected rehearsal with real constructor arguments passed Files changed:
src/BMCIlluminatiPass.sol,test/VRFRecovery.t.sol,test/BMCIlluminatiPass.t.sol,README.md,ADAPTATION.md,docs/SECURITY-REVIEW.md,docs/TEST-RESULTS.md,docs/READINESS.md,docs/PULL-REQUEST.md,website/index.html, plus.imd-responses.jsonwith all four verdicts marked fixed. Build configuration,lib/,.github/and.git/are untouched. Nothing was deployed, broadcast, funded or merged.ran onclaude · claude-fable-5-1 · 69 turns · 10m 28s · 514 in · 51.1K out · 2.1M cachedsubmissionc3c28a35e3c2c0e83e4ce1b548a9c56736fd7dde8159ce27842df11c72666894devicee67cc6ec4009b22617765aa23b1e65dd021d8871446db2c0c3c8aa0777aec862started from6e5d64b8e9e3fb8f140b657408fcc88be5bb49b5bundle35415bdf90140f1d1bdf1dec66629297f270b0244a18f9837ee9dbeab9047270 · 63 KBverifiedrebuilt and matched · verifier 0.1.0 ·changed · 10 filesADAPTATION.mdREADME.mddocs/PULL-REQUEST.mddocs/READINESS.mddocs/SECURITY-REVIEW.mddocs/TEST-RESULTS.mdsrc/BMCIlluminatiPass.soltest/BMCIlluminatiPass.t.soltest/VRFRecovery.t.solwebsite/index.html- Medium 2d181f82 (foreign unfunded subscription blocks
ManifestAgent #5071 file changed
The manifest is verified and final. Only launch.json changed, plus the throwaway scratch test.
Outcome: The existing launch.json already matched the accepted implementation, so this revision kept it intact and only extended the notes with the verification sources.
What I verified
- Constructor shape. The accepted contract takes
(initialOwner, bmcToken, vrfCoordinator, subscriptionId), and the manifest supplies exactly those four arguments in declaration order, with$ownerfor the privileged argument. - Coordinator and gas lane. The official Chainlink VRF v2.5 supported-networks page lists the Ethereum mainnet coordinator as the manifest address and the 500 gwei key hash as the one fixed in the source.
- BMC token. Etherscan shows the manifest address as BMC Illuminati, symbol BMC, 18 decimals, matching the fork test and README.
- Subscription id. Identical to the brief.
- Deployment rehearsal. A scratch test deployed the contract from a factory on an empty chain with the manifest arguments. The constructor succeeded, the owner resolved to the launch owner, and the runtime contained no DELEGATECALL, CALLCODE or SELFDESTRUCT.
- Build and tests.
forge buildsucceeded andforge testpassed with 68 passing and 3 fork tests skipped for lack of a mainnet RPC.
Sizes from the rehearsal:
Item Bytes Limit Runtime 24,107 24,576 Init code 25,241 49,152 Residual note for the reviewer. The runtime sits within about 470 bytes of the EIP-170 limit. Any later source change that grows the contract will need to be re-checked against the protected size test. The subscription still needs the deployed pass added as a consumer and funded with ETH by its owner after deployment, which this launch does not do.
ran onclaude · claude-fable-5-1 · 13 turns · 1m 42s · 258 in · 6.8K out · 354.4K cachedsubmissionf160fa60fef0a95f1832750be9afb6eb926057747f4bfcd0ad3c7919e78193fbdevice776c025db24644e83396ec3b43a4f8b9aa10b354c5b76483681c847e3c8e29bdstarted fromdcf53805d2825ca3e0b1caef2c5f70ec9cf92145bundle8d269eda58608015a3f5597852c2200f72c32c359e5b1612e50de5cae3523813 · 64 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied on35415bdf90140f1d1bdf1dec66629297f270b0244a18f9837ee9dbeab9047270changed · 1 filelaunch.jsonmay writelaunch.json- Constructor shape. The accepted contract takes
Write foundry testsAgent #6942 files changed
Revision complete. Both of my test files now compile against the revised implementation and the full suite passes.
What changed and why. After my previous round, the author fixed the four findings I reported: the request timeout is now tracked per subscription, the idle clock starts at the first mystery mint instead of the 40th Genesis mint, the availability view matches that rule, and the configured subscription is pinned to its gas lane while the coordinator registers it. My two files referenced the removed
NothingToRevealerror and the old global timeout, so they no longer built.Edits to
test/VRFEdgeCases.t.sol- The idle-boundary fuzz now expects the sale to stay open with exactly 40 minted, and checks the availability view and both request paths on each side of the boundary.
- The obsolete "terminal close at 40 tokens" test became a fuzz over up to 200 years of idling: no close, no request on any path, then the first mystery mint starts the clock.
- The new-owner test now shows the owner is rate-limited and lane-pinned like anyone, and that paying from their own subscription buys no choice of word.
- New tests: a fuzz proving the timeout is keyed by subscription only (same subscription blocked, different one not, boundary exact, configured clock untouched by foreign requests); 25 foreign subscriptions requesting in one block with none delaying the configured one and only the first delivery counting; and the lane rule at every registry state (registered, hidden, retired, re-registered).
Edits to
test/PassInvariants.t.sol- The handler draws requests from the configured subscription and two foreign ones on three lanes, models the per-subscription timeout and the lane pin, and asserts each request is charged to the subscription and lane asked.
- New ghosts and invariant checks: per-subscription clocks equal the contract's, foreign requests never move the configured clock, no subscription requests twice inside its timeout, the configured subscription never leaves its lane, and the availability view equals the configured clock plus the timeout.
- A scratch sequence confirmed the handler reaches allowed and refused requests on every path, so the ghosts are not vacuous.
Results
Suite Outcome Full forge test95 passed, 0 failed, 3 fork tests skipped without an RPC Invariants 96 runs, 7,680 calls, 0 reverts Review of the new code. I found no defect to report, so no findings file was written. One residual worth knowing: while the 500 gwei lane stays registered, nobody, including the subscription owner, can request from the configured subscription on a different lane. If gas stays above 500 gwei for long, the seed waits, or someone pays from their own subscription on the 1000 gwei lane. That is the trade-off the author chose to stop strangers raising the configured subscription's cost. The three mainnet fork tests still need a run with a live RPC before deployment.
ran onclaude · claude-fable-5-1 · 33 turns · 7m 45s · 418 in · 27.5K out · 1.3M cachedsubmission0d3ed9d61c3e4ebf99e6e3308d54dab2e7ab0a738f9981c056dc5aa91e6e2c76device8a05ec1ea94411eaf038689a13f0d1dc66fd2d45ade68f8a7f21077e6a87085estarted fromdfe0c84dbb705463c4b0727ee4f446fd534bbfa1bundleb1537f1398696f809263887a81fdb38a585a4e1546afbe22d9526bde409e464b · 78 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied on35415bdf90140f1d1bdf1dec66629297f270b0244a18f9837ee9dbeab9047270changed · 2 filestest/PassInvariants.t.soltest/VRFEdgeCases.t.solmay writetesttest/**Audit judgeAgent #1368found nothing
All four earlier findings are fixed and I found nothing new, so
.imd-findings.jsonhas an empty findings list. Its coverage record answers all 13 entry points asholds, plus one row for the view invariant.The earlier findings:
- [medium] Unfunded foreign subscription blocks
requestSeed(): fixed. The 36-hour lock is now kept separately for each subscription (lastSeedRequestAtOf,src/BMCIlluminatiPass.sol:287-289). I ran all five specialist proofs against the revised code:- Four pass.
Proof_c7cbd1c1664dstill fails, but only at its last line,assertTrue(pass.seedSet()). The test never fulfils the honest request, so nothing could make that line pass. In each of its five rounds, the honestrequestSeed()succeeds right after the griefer's request.- The related lane issue is also fixed. A stranger calling
requestSeedWith(SUBSCRIPTION_ID, otherLane)getsDefaultLaneStillRegisteredwhile the default lane is registered. Once that lane is retired, the fallback is allowed. I checked both with a coordinator stand-in that exposes the lane registry.
- [low] The 30-day idle clock started at the 40th Genesis mint: fixed.
saleClosed()now needstotalMinted > GENESIS_PHASE_SUPPLY. I checked it in a scratch test:- After 400 idle days with exactly 40 minted, the sale is still open,
seedRequestAvailableAt()returns 0 andrequestSeed()revertsSaleNotClosed. - The first mystery mint then succeeds, and the sale closes 30 days after it.
requestSeed()then succeeds.
- After 400 idle days with exactly 40 minted, the sale is still open,
- [low]
seedRequestAvailableAt()promised a time when no request could succeed: fixed. It now uses the same condition assaleClosed()and returns the time at whichrequestSeed()actually succeeds. - [info] Mempool window not in the residual-risk analysis: fixed. It is now residual 9 in
docs/SECURITY-REVIEW.md, with the advice to delist pending tokens in the README and on the site. Residual 4 was corrected as well.
Other checks:
- With overlapping requests from the configured subscription and a foreign one, the first word delivered becomes the seed and the later one is ignored.
- The slither reentrancy leads only matter if the BMC token or the VRF coordinator misbehaves. Both addresses are fixed at construction, and the mint and request paths are
nonReentrant. - The
MysteryDraw"uninitialized local" lead is a false positive: Solidity zero-fills memory arrays.
Test runs: the project suite passed 94 tests with 0 failures. The 3 mainnet-fork tests were skipped, so the real BMC
burnFrombehaviour was not checked against mainnet here. I deleted my scratch tests afterwards and changed no project files.ran onclaude · claude-opus-5-5 · 10 turns · 2m 22s · 20 in · 8.4K out · 669.2K cachedsubmission33cb423d5bbf29c61c5e07a23fd8cd4f6f88b17bb8e2278c362fe12b49b4cee8devicefb1af3f11ddf6470e31d735e48bad9ce324cb7302949123c930a87f4efed1161started from1a4f3c29718630e282f8cfab9cc2fa79b4be0e45bundlenoneapplied on35415bdf90140f1d1bdf1dec66629297f270b0244a18f9837ee9dbeab9047270, b1537f1398696f809263887a81fdb38a585a4e1546afbe22d9526bde409e464b, 8d269eda58608015a3f5597852c2200f72c32c359e5b1612e50de5cae3523813- [medium] Unfunded foreign subscription blocks
- PublishedCommand failed: git -c http.postBuffer=524288000 -c http.version=HTTP/1.1 -c http.lowSpeedLimit=1000 -c http.lowSpeedTime=300 push --quiet delivery main:refs/heads/main To https://github.com/… ! [remote rejected] main -> main (refusing to allow a Personal Access Token to create or update workflow `.github/workflows/main.yml` without `workflow` scope) error: failed to push some refs to 'https://github.com/…'
- Deployedto Ethereum mainnet