{"assessments":[],"deployments":[],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"083fb0b0-099f-4b6e-93f7-1a8db6b3bf03","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"00ff9bf19e261f4acf2fe665cd7cf6b36686edda899e4c613fdfab4aefd0844e","dependsOn":["audit_imported_code"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"2d7d84a1a9972f1497e5bc68a704cbf42294d08c62d2868f102ffe3b909b9b51","skillId":"adapt-contract-project","tools":[]},"key":"adapt_contract_project","kind":"code","role":"implement","skillHash":"2d7d84a1a9972f1497e5bc68a704cbf42294d08c62d2868f102ffe3b909b9b51","skillId":"adapt-contract-project","state":"accepted"},{"acceptedSubmissionHash":"8f19825ef65b610798399bfd3145c39cae798237938a6546121fbdbe334a8931","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"49e4a7ed8e049f09a4342abf6260f3eb56b41870c255b429ee2c4344b921151d","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"87618a78ea917c0dcc0f56b9b5ef8e947d0653f4038ecb0d99d6df7ec2f51b7a","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"9897597976b0e72440db7ca402d225aea20f3d37205ba53e7df2400ff80f29cc","skillId":"audit-imported-code","tools":[]},"key":"audit_imported_code","kind":"code","role":"review","skillHash":"9897597976b0e72440db7ca402d225aea20f3d37205ba53e7df2400ff80f29cc","skillId":"audit-imported-code","state":"accepted"},{"acceptedSubmissionHash":null,"dependsOn":["adapt_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"waiting"},{"acceptedSubmissionHash":"0c8991c6c975ded803c5889510f797338cf6c4947687e2a0ff3ed2f684e0a354","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"c170cef426031290d70105085ba3e095903c53563754ade7f5631b4b075ace8b","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":null,"dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"failed"},{"acceptedSubmissionHash":"87b9bf15fbb2ca03efd35adfee0eb7db2fe24f47e387272f1f8e0b72481ac2d1","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","state":"accepted"}],"objective":"PROJECT: BMC ILLUMINATI PASS #1356 — FINAL VRF ARCHITECTURE SECURITY REVIEW\n\nRepository:\nhttps://github.com/0xSt3w/launch-1356-project-bmc-illuminati-pass\n\nBranch: security/vrf-recovery\n\nPrevious audit:\nhttps://github.com/Identity-md/research/blob/main/jobs/8e2c8de5-58ed-4243-bd09-bfa32e0eafde/files/AUDIT.md\n\nTASK:\n\nPerform an independent security and fairness review of the two Chainlink VRF architectures:\n\nA. Existing 460-request model with strict mint-order finalization.\n\nB. Experimental single-seed deterministic distribution model in src/experimental/BMCPassVRFPrototype.sol.\n\nReview the associated Foundry tests, including VRFRecovery, VRFSafetyProperties and BMCPassVRFChainlinkIntegration.\n\nDetermine whether either architecture can safely handle an expired or permanently unavailable VRF request without introducing exploitable rerolls, owner manipulation, selective fulfillment or rarity bias.\n\nIf neither architecture satisfies these requirements, explicitly report that conclusion and recommend a safer design.\n\nPreserve:\n- 500 NFT maximum\n- 77 Genesis, including Legendary #1\n- 423 Standard\n- 40 sequential initial Genesis\n- 460 Mystery\n- Genesis mint price: 1,000,000 BMC\n- Mystery mint price: 350,000 BMC\n- Permanent BMC burning\n\nDELIVERABLES:\n\n1. Security comparison of both architectures.\n2. Concrete attack scenarios and proof-of-concept tests.\n3. Analysis of randomness availability, liveness and fairness.\n4. Recommended architecture and residual risks.\n5. Required code changes and test coverage.\n6. Mainnet readiness assessment.\n\nSTRICT RESTRICTIONS:\n\nDO NOT deploy on Ethereum Mainnet.\nDO NOT broadcast transactions.\nDO NOT transfer or burn real BMC.\nDO NOT request wallet approvals.\nDO NOT modify production contracts.\nDO NOT merge pull requests.\nDO NOT publish a new website.\n\nPerform the free feasibility check first, disclose any costs and wait for explicit approval before paid work.","parentJobId":null,"planHash":"9526741ea21052d88becd4166d513bae4fe7d87678b40a3b5717c991a51c6866","previousHash":"7ff46ae3c66c38676a109120550b3d209636d4983835d9454cc4c1c83a82a7a6","projectId":"083fb0b0-099f-4b6e-93f7-1a8db6b3bf03","publication":{"commit":null,"deliveredAt":null,"repoUrl":null},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51488","feedbackHash":"4c915981aa7e19a14f1522b6b98e6dfce737a10cfd6ef4b7d8993b40433c0c14","nodeKey":"audit_economics","submissionHash":"8f19825ef65b610798399bfd3145c39cae798237938a6546121fbdbe334a8931","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52193","feedbackHash":"01ccb3d12bd7dc0a7d1c9dd67731e65fc6f79aaa496e4fc4acb2bfe1ea7dc0ab","nodeKey":"audit_flow","submissionHash":"49e4a7ed8e049f09a4342abf6260f3eb56b41870c255b429ee2c4344b921151d","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52254","feedbackHash":"ee835e111cd50249c0deb0d66749c0a1106632c062c147f8a26257d957183551","nodeKey":"audit_math","submissionHash":"0c8991c6c975ded803c5889510f797338cf6c4947687e2a0ff3ed2f684e0a354","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51203","feedbackHash":"fadc6e147f3eacc157749eb9fabb31e2e1228e457fc7cf5164fc70d4cbc91e52","nodeKey":"audit_permissions","submissionHash":"c170cef426031290d70105085ba3e095903c53563754ade7f5631b4b075ace8b","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51020","feedbackHash":"5b0dd882e01e391a2bb02684717afa9e90305fa227ae19023ff7fd97357c15ac","nodeKey":"write_foundry_tests","submissionHash":"87b9bf15fbb2ca03efd35adfee0eb7db2fe24f47e387272f1f8e0b72481ac2d1","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"eac094d7fee4c2bbcb9cd21b904dd32da1e2f38d787d719c60b5c0bc69fb852e","state":"blocked","submissions":[{"artifacts":[],"attempt":1,"bundleHash":"552769ce92f849b6520807f40cdcf35b5de85a243654fe668f3f041e63af5ee2","device":"fbcdfc017217af1f","findings":[],"hash":"00ff9bf19e261f4acf2fe665cd7cf6b36686edda899e4c613fdfab4aefd0844e","nodeId":"b778decd-2656-46cd-a839-c75f51e70867","outcome":"completed","summary":"The work is complete and verified. Build passes, and the suite reports 73 passed, 0 failed, 1 skipped (the mainnet fork test, which needs an RPC).\n\n**Review verdict.** Neither architecture as it arrived handles an expired or lost VRF request. In the production contract a lost request froze that token and every later one forever, and the audit's proof test fails on the original code. The prototype is only a pure function; its sole consumer allowed exactly one request, so one lost request froze all 460 tokens. The full comparison, attack scenarios, liveness and fairness analysis, recommendation and readiness assessment are in `docs/VRF-ARCHITECTURE-REVIEW.md`.\n\n**Recommendation.** Keep architecture A (per-mint requests, strict mint order) with a permissionless timed re-request, which I implemented. Architecture B is fairer on paper but needs a rule-based sale close that the brief does not define, and it would delay every reveal to sell-out. Residual risks are documented: the VRF node could withhold, and the subscription owner can delay but not choose outcomes.\n\n**Code changes in `src/BMCIlluminatiPass.sol`:**\n- New `rerequestRandomness(tokenId)`: anyone may open a replacement request once a token has waited 7 days with no word. Every request ever issued stays valid and the first delivered word wins, so no re-request can discard a word or reroll.\n- Constructor now takes the VRF configuration, so the contract is usable at deployment with no owner call. The owner keeps `setVRFConfig` as a disclosed power.\n- Genesis CID folded into a constant. I verified the CID from the fork test on a public gateway: genesis-01 and genesis-77 match the repository artwork byte for byte.\n- Reveal gas budget raised to cover the measured cost of a cold reveal.\n\n**Tests.** The recovery suite is rewritten so it fails on the freeze and passes on the fix: lost earlier request, failed callback, first-delivered-wins in both orders, rate limiting, owner has no shortcut, five unfunded tokens recovered after funding, minimum callback gas through Chainlink's mock, and a fuzzed delivery order over six pending tokens. The prototype's test consumer now refuses a seed while minting is open and supports a timed re-request, with a proof-of-concept for position targeting. The fork test skips cleanly without an RPC.\n\n**Not changed.** Odds drift with reveal state is inherent to architecture A and is now disclosed in the README rather than removed. The audit's literal proof expects later tokens to reveal with no new randomness for the stuck one, which strict mint order cannot do; the same scenario with the re-request step is covered by a test.\n\n**Open before deployment** (listed in the review): an independent audit of this adaptation, the live BMC burnFrom fork check, a funded subscription whose values go into the constructor, and a gas check under the real coordinator. `ADAPTATION.md` lists all 17 changes with the finding or rule behind each.","treeHash":"ae0daee3b29652de2811161c40278399c764f7df","usage":{"cachedInputTokens":3064946,"inputTokens":706,"model":"claude-fable-5-1","outputTokens":82657,"runtime":"claude","turns":85,"wallClockMs":1038108}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"584bd638e7e022f0","findings":[{"citation":"resolved","description":"Area: Numerical Gap (boundary x invariant) and Boundary. The virtual Fisher-Yates in _finalize is arithmetically correct (uniform permutation, no modulo bias beyond 460/2^256, no over/underflow: remaining is in [1,460] because tokenId = 41 + revealedCount <= 500). The fairness invariant the brief asks about ('no rarity bias') holds only ex ante. Because (a) every token's draw is taken from the pool that remains after all earlier positions and (b) every delivered word is emitted in RandomnessStored and every reveal in Revealed, the conditional Genesis probability of the NEXT mint position is a public quantity that drifts with state and reaches 0 or 1 at the pool boundary, while mintMystery charges the same 350,000 BMC. Concrete numbers: ex-ante 37/460 = 8.04%; at revealedCount=300 with 10 Genesis revealed 27/160 = 16.9%; with 30 revealed 7/160 = 4.4%; at revealedCount=423 with 0 Genesis revealed every one of positions 464..500 is a certain Genesis; at revealedCount=k with 37 Genesis revealed every later position is a certain Standard at full price. Sharper version: a RandomnessStored token whose predecessor is still AwaitingRandomness (lost request) already has a publicly computable outcome. With no reveals yet, token 42 with stored word w42 draws index idx42 = keccak256(abi.encode(w42,42)) % 459 and receives value idx42 unless token 41 draws exactly slot idx42 (probability 1/460), so its tier is known with probability 459/460 before 41 has any randomness, while remainingMysteryGenesis() still reports 37 ('includes outcomes owed to pending tokens'). A buyer deciding whether to mint position 43 therefore knows the effective pool better than the view advertises, and buyers in general can time purchases to favourable states; the project cannot. This is inherent to architecture A (per-mint words are public events, so deferring finalization hides nothing), is disclosed in README 'Odds', and was rated Low by the previous audit (aa16c116). It is reported here because the assignment asks explicitly whether either architecture is free of rarity bias: A is not; only a single seed requested after sale close (architecture B, with the timed re-request from the test consumer) gives flat 37/460 odds for every position. Recommended: keep A only with this disclosure in the mint UI (show the live conditional odds, not 37/460), or adopt B with a rule-based close. No code defect in the arithmetic itself.","line":383,"path":"src/BMCIlluminatiPass.sol","reproduction":"Scratch test test/scratch/GasBoundary.t.sol::test_PoolBoundaryOddsCollapse (passes on this code, demonstrating the state). Deploy BMCIlluminatiPass with MockBMC and MockVRFCoordinator (callback 500k). 40 x mintGenesis. For i in 0..422: mintMystery (tokenId 41+i), fulfil its request with the smallest word w such that keccak256(abi.encode(w, 41+i)) % (460-i) >= 37 (a Standard slot). Drain finalizeReveals(50). State: revealedCount()=423, mysteryGenesisRevealed()=0, remainingMysteryGenesis()=37, remainingMysteryStandard()=0, currentPrice()=350_000e18. Then mintMystery -> tokenId 464, fulfil with any word (e.g. keccak256('whatever')): tierOf(464) == Tier.Genesis. Expected for a bias-free design: every position has 37/460 odds; actual: positions 464..500 are certain Genesis for the same price. Prediction variant (test/scratch/Leak.t.sol::test_StoredWordPredictsOutcomeBeforeFinalization, passes): mint 41 and 42; fulfil 42 with the smallest w42 with keccak256(abi.encode(w42,42)) % 459 < 37; status 42 = RandomnessStored, tierOf(42) = Pending; warp 7 days, rerequestRandomness(41), fulfil with any w41 whose index keccak256(abi.encode(w41,41)) % 460 != idx42; finalizeReveals(10); tierOf(42) == Genesis exactly as predicted from the public word before 41 had randomness.","severity":"low","snippet":"        uint256 remaining = MYSTERY_SUPPLY - revealedCount;\n        uint256 word = _randomWord[tokenId];\n        uint256 r = uint256(keccak256(abi.encode(word, tokenId))) % remaining;","title":"Rarity odds collapse at the pool boundary while the price stays flat: draw-without-replacement in strict mint order makes late positions certain and makes stored words predictive before finalization ("},{"citation":"resolved","description":"Area: Boundary / Numerical Gap (boundary x precision). The first reveal in a callback is most expensive when its outcome is Genesis: mysteryGenesisRevealed goes 0 -> 1 (cold SLOAD 2,100 + SSTORE 20,000) on top of revealedCount 0 -> 1, _tier and _artworkId 0 -> nonzero and the cold _slotSwap write. Measured through finalizeReveals(1) on this tree: Genesis first reveal 151,309 gas, Genesis second reveal 117,109 gas (the project's test_GasPerRevealBudgetCoversMeasuredCost measures 129,539 / 112,439 for its Standard words and asserts < 140,000, so the assertion would fail for a Genesis first word). The constant's own comment ('worst case ... about 134k') is therefore inaccurate by about 17k. Impact check at every batch threshold: batch = (gasAvailable - 30,000) / 140,000 and the inner call receives about 63/64 of gasAvailable, so forwarded gas at the thresholds is about 166k / 304k / 442k / 580k for batches 1..4 while the worst-case cost is about 151k / 268k / 386k / 503k; a scan of callback gas 190k..240k in 2k steps shows the inline Genesis reveal first attempted at 204k and succeeding there (used 188,583), so no window exists where a batch is attempted and reverts, and even a revert would be caught by try/catch with the word kept. Nothing is lost; this is a documentation and test-coverage gap only. Recommended: raise GAS_PER_REVEAL to about 155,000 (or correct the comment to state the real margin source: CALLBACK_GAS_RESERVE plus the 63/64 rule) and add a Genesis-first word to test_GasPerRevealBudgetCoversMeasuredCost.","line":101,"path":"src/BMCIlluminatiPass.sol","reproduction":"test/scratch/GasBoundary.t.sol::test_MeasureGenesisFirstReveal: deploy as in the suite (MockBMC, MockVRFCoordinator, 500k callback), 40 x mintGenesis, mint tokens 41 and 42, pick w41 = smallest w with keccak256(abi.encode(w,41)) % 460 < 37 and w42 = smallest w with keccak256(abi.encode(w,42)) % 459 < 37, deliver both with fulfillWithGas(req, w, 95_000) so only the words are stored, then measure gasleft() around finalizeReveals(1) twice. Observed: 151,309 then 117,109; tierOf(41)=tierOf(42)=Genesis. Expected per the comment at line 101-102: at most about 134k. test_ScanThreshold in the same file: 42 stored first, then deliver 41 (Genesis word) with callback gas g from 190,000 to 240,000: for g <= 202,000 the callback returns early (used about 68k, 41 stays RandomnessStored); from g = 204,000 the inline batch runs and 41 is Revealed (used 188,583). No g produces an attempted-and-failed batch.","severity":"info","snippet":"    /// @dev Gas budget per reveal. Measured worst case for the first reveal in a callback (all storage\n    ///      cold) is about 134k; the budget covers it so an inline batch does not revert.\n    uint256 public constant GAS_PER_REVEAL = 140_000;","title":"GAS_PER_REVEAL budget comment and its test measure a Standard first reveal only; a cold Genesis first reveal costs 151,309 gas (> 140,000). No reachable inline failure, but the documented bound is wro"}],"hash":"0c8991c6c975ded803c5889510f797338cf6c4947687e2a0ff3ed2f684e0a354","nodeId":"01c38cfd-74e3-4868-8dee-55ffeb4bdadb","outcome":"completed","summary":"Review complete. The findings file is at `.imd-findings.json` with 2 findings and 18 coverage rows, and all 13 listed entry points have a verdict.\n\n**What I checked.** My area was Math Precision, Boundary and Numerical Gap. I traced every division, cast, subtraction and modulo in the pass contract and the prototype, the virtual Fisher-Yates swap logic at both edge indices, the price and burn-cap constants, the gas-aware callback batch at all four batch thresholds, the StandardArt bijection, and the odds invariant the brief asks about. Scratch tests under `test/scratch/` reproduced each number below. The full suite passes with 73 tests.\n\n**Findings.**\n- **Low, odds collapse at the pool boundary.** Draw-without-replacement in strict mint order with public words means the next position's Genesis chance is a public, drifting number at a flat price. Reproduced state: after 423 Standard reveals, position 464 is a certain Genesis for 350,000 BMC. A stored-but-unfinalized word also fixes its token's tier with probability 459/460 before the queue head has randomness. This is inherent to architecture A, disclosed in the README, and was rated Low by the prior audit. Only architecture B with a seed requested after a rule-based close gives flat 37/460 odds.\n- **Info, gas budget documentation and test gap.** A cold Genesis first reveal costs 151,309 gas, above the 140,000 constant and its \"about 134k\" comment. The project's budget test only measures a Standard word. A threshold scan showed no reachable inline revert because of the reserve and the 63/64 forwarding rule, and a revert would be caught anyway. Recommend raising the constant to about 155,000 and adding a Genesis-first word to the test.\n\n**Clean areas.** No overflow, underflow, truncating cast, rounding leak or decimals mismatch exists in either contract. The re-request path cannot reroll a delivered word, and a ERC721 receiver re-entering finalizeReveals during a mint gains nothing.\n\n**Not reached.** The real coordinator source is not vendored and both mocks leave pendingRequestExists unimplemented, so I could not reproduce that a never-fulfilled request pins the subscription against cancellation on the real VRF v2.5 coordinator. That is recorded as an unreached coverage row for the subscription owner to weigh.","treeHash":null,"usage":{"cachedInputTokens":1846850,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":49375,"runtime":"claude","turns":40,"wallClockMs":625875}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ce6eaff570c608ab","findings":[],"hash":"222a43e8ec7c4e96287533d17ae517365d7584f6ec5ca7087402809a832a4dce","nodeId":"19fece39-d3d8-4450-b06b-002191e23134","outcome":"failed","summary":"needs input: The accepted BMCIlluminatiPass constructor takes the requester's Chainlink VRF v2.5 subscription id (uint256, rejected when 0), the gas-lane key hash (bytes32, rejected when 0) and the subscription's payment mode (nativePayment bool). None of these is in the repository or the brief: the README lists the VRF subscription as 'Blocker until created' and says the owner chooses the gas lane. A placeholder is impossible because the constructor validates the values and a made-up subscription id would point the contract at a subscription it is not a consumer of. Everything else is fixed: owner $owner, BMC 0x38224239f2BDae357D47cb864BC533d7f72dF961, coordinator 0xD7f86b4b8Cae7D942340FF628F82735b7a20893a, callbackGasLimit 500000 and requestConfirmations 3 (README recommended values). — What are the mainnet Chainlink VRF v2.5 subscription id (the number shown at vrf.chain.link) and the key hash (gas lane) you want BMCIlluminatiPass to use, and is that subscription funded with LINK (nativePayment=","treeHash":null,"usage":{"cachedInputTokens":554723,"inputTokens":322,"model":"claude-fable-5-1","outputTokens":4731,"runtime":"claude","turns":11,"wallClockMs":94528}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"5d667e4b0751bcb5","findings":[{"citation":"resolved","description":"Architecture A recovers a lost request only through `rerequestRandomness`, which is gated by `block.timestamp < dueAt` (src/BMCIlluminatiPass.sol:295-296) with dueAt = lastRequestedAt + 7 days. Because `finalizeReveals` (line 370-379) processes tokens strictly in mint order, one token without a word holds every later token in `RandomnessStored` for the whole window. The window is sized for the case the chain cannot observe (Chainlink dropping an under-funded request after ~24h). But the second loss mode the contract itself documents (lines 38-40), a callback that ran out of gas, is final the moment the coordinator marks the request fulfilled: the request can never deliver, yet the contract still forces a 7-day wait on the affected token and, through strict order, on every token minted after it. In a 460-mint sale that reveals 'minutes after each mint' (README), one such event converts the reveal of every later holder into a >=7-day stall, and a second loss while the queue is stalled compounds it. This is a liveness degradation of a stated guarantee, not a loss of funds or a reroll, so it is reported as low. Recommended: (1) shorten REREQUEST_DELAY to about 48 hours, still 2x Chainlink's 24h drop window, so the probability that two requests are simultaneously live stays negligible while the worst-case stall drops from 7 days to 2; and/or (2) treat a request the coordinator has already consumed as immediately replaceable (the v2.5 coordinator deletes the request commitment on fulfilment; if the deployed coordinator exposes `s_requestCommitments(requestId)`, a zero value proves the old request can never deliver and `rerequestRandomness` can skip the timer for that token without weakening first-delivered-word-wins). Either change preserves all required constants, prices and burning.","line":108,"path":"src/BMCIlluminatiPass.sol","reproduction":"State: Genesis phase complete (40 mints). Alice mints token 41 (request r41), Bob mints 42 (r42) and 43 (r43). The coordinator's callback for r41 fails (test: `MockVRFCoordinator.fulfillWithGas(r41, 1, 20_000)` returns false; a second `fulfill(r41)` reverts 'already fulfilled', so r41 is consumed and lost). r42 and r43 are delivered: both tokens are `RandomnessStored`. Expected: the queue can be unblocked as soon as the loss is final. Actual: at lastRequestedAt(41) + 7 days - 1 second, `finalizeReveals(10)` returns 0 and `rerequestRandomness(41)` reverts `RerequestNotDue(41, dueAt)`; tokens 42 and 43 stay `Pending` with artworkIdOf == 0 for the full week; only at +7 days does `rerequestRandomness(41)` succeed, after which one fulfilment reveals 41, 42, 43 (revealedCount == 3). Reproduced in test/scratch/ReviewProbe.t.sol::test_DeadCallbackStallsWholeQueueSevenDays.","severity":"low","snippet":"    uint256 public constant REREQUEST_DELAY = 7 days;","title":"Strict mint-order plus a fixed 7-day re-request window stalls every later reveal for a full week even when the lost request is already provably dead"},{"citation":"resolved","description":"The contract inherits OpenZeppelin `Ownable` (single-step `transferOwnership(address)` and `renounceOwnership()`; lib/openzeppelin-contracts/contracts/access/Ownable.sol:76-84). The owner's only power is `setVRFConfig` (line 218-226), but that power is the project's sole response to a permanently unavailable VRF path: if Chainlink retires the configured key hash or the subscription becomes unusable, every pending mystery token depends on the owner pointing `setVRFConfig` at a working lane before `rerequestRandomness` can succeed (README 'Unavailable randomness', docs/VRF-ARCHITECTURE-REVIEW.md section 4). `Ownable2Step` (also vendored in lib/openzeppelin-contracts) would require the new owner to accept before control moves, removing the lost-key failure mode at no cost to the agreed design. The README discloses that renouncing freezes the config; the mistaken-transfer case is not disclosed. Reported as informational: it requires an owner mistake and has no unprivileged amplifier, but it sits on the only recovery path for the 'permanently unavailable VRF' question the brief asks about.","line":47,"path":"src/BMCIlluminatiPass.sol","reproduction":"State: any deployment with pending mystery tokens. Owner calls `transferOwnership(0x000...dEaD)` (or any address whose key is not held). Expected under a two-step scheme: nothing changes until the new owner accepts. Actual: `owner()` is immediately the unreachable address; every subsequent `setVRFConfig(...)` from the previous owner reverts `OwnableUnauthorizedAccount(prevOwner)`; if the configured key hash is later retired by Chainlink, `mintMystery` and `rerequestRandomness` revert at the coordinator for everyone and no code path can change the config, so all pending tokens stay `AwaitingRandomness` forever.","severity":"info","snippet":"contract BMCIlluminatiPass is ERC721, Ownable, ReentrancyGuard {","title":"Single-step ownership transfer: one mistyped transferOwnership permanently freezes setVRFConfig, the only lever that can follow a Chainlink lane or subscription change"},{"citation":"resolved","description":"`draw(uint256 seed)` is a pure function of a single 256-bit word, so it can produce at most 2^256 distinct permutations, while 460! is about 2^3300; the docstring overstates the reachable permutation space by a factor of roughly 2^3000. Each position's marginal distribution is still uniform (each `keccak256(seed, i) % remaining` index is uniform up to a 2^-247 modulo bias), so this is a documentation error rather than a bias, but a reviewer reading the claim as a fairness argument would be misled. Separately, the measured cost of `draw` is 497,723 gas and it returns 460 outcomes in memory; no production path can persist them (460 cold SSTOREs alone exceed 9M gas), so the only viable B design is the per-token derivation the review document already describes, and the only B consumer in the repository (`ExperimentalVRFConsumer` in test/BMCPassVRFChainlinkIntegration.t.sol) is test-only with a permissionless `closeMinting()`. Conclusion for the brief's question: B can handle a lost seed request only with the same timed, first-delivered-wins re-request as A, and it additionally needs a rule-based sale close that the brief does not define; it is not a safer design to adopt as it stands.","line":18,"path":"src/experimental/BMCPassVRFPrototype.sol","reproduction":"Input: any seed. `prototype.draw(1)` costs 497,723 gas (test/scratch/ReviewProbe.t.sol::test_PrototypeDrawGasAndDeterminism, log 'draw gas') and returns 460 values. Counting argument: the map seed -> permutation has domain size 2^256 < 460!, so the docstring's claim that any of 460! permutations can be derived is false for all but at most 2^256 of them.","severity":"info","snippet":"    /// @notice Derives one of 460! possible permutations\n    ///         from a supplied seed using a Fisher-Yates draw.","title":"Architecture B prototype: the '460! permutations' claim is impossible from a 256-bit seed, and the prototype as written has no deployable consumer"},{"citation":"resolved","description":"The suites assert the right behaviour wherever they look, but three edges of the recovery design are unexercised. (a) `rerequestRandomness` for a token that is `RandomnessStored` (word delivered, blocked behind an earlier token) is never asserted to revert `NotAwaitingRandomness`; the existing test (test/VRFRecovery.t.sol:186-203) only covers a token that was finalized inline. (b) `test_GasPerRevealBudgetCoversMeasuredCost` measures `finalizeReveals(1)` from an EOA call after the words were stored with 95k gas, so the measured 129,539 gas does not include the state the first inline reveal actually sees inside `rawFulfillRandomWords` (the 63/64 forwarding rule on `this.finalizeReveals`, and the one-time 0->1 transitions of `revealedCount` and `mysteryGenesisRevealed` on a Genesis first outcome). The design is safe either way because a failed batch is caught and the word kept (lines 355-363), but the README's claim that a 500k callback finalizes three reveals is asserted only through the project's own mock. (c) No test takes a token through two consecutive losses (original and replacement both fail their callback) to a third request that is fulfilled; `testRerequestRateLimitedPerToken` opens three requests but never delivers. The scratch probe in test/scratch/ReviewProbe.t.sol covers (a) and the mempool-relay interleaving (a fulfilment relayed from inside the minter's own `onERC721Received`), both of which behave correctly.","line":633,"path":"test/BMCIlluminatiPass.t.sol","reproduction":"(a) Mint 41 and 42; deliver only r42 so 42 is `RandomnessStored`; warp 7 days; call `rerequestRandomness(42)`. Expected and actual: revert `NotAwaitingRandomness(42)` (confirmed by the scratch probe), but no committed test asserts it. (b) Run `forge test --match-test test_GasPerRevealBudgetCoversMeasuredCost -vv`: logs 'first reveal gas: 129539', measured from the test contract, not from a coordinator callback. (c) grep the suites for a sequence of two failed callbacks followed by a fulfilled third request: none exists.","severity":"info","snippet":"        assertTrue(vrf.fulfillWithGas(ra, 11, 95_000));\n        assertTrue(vrf.fulfillWithGas(rb, 22, 95_000));","title":"Test coverage gaps around the recovery path: re-request on a RandomnessStored token, and the inline-batch gas budget measured outside the callback"}],"hash":"49e4a7ed8e049f09a4342abf6260f3eb56b41870c255b429ee2c4344b921151d","nodeId":"658fe99b-3ba1-4689-b36a-f5fea9afbb70","outcome":"completed","summary":"The review is complete and the findings file is written and validated.\n\n**Outcome: no critical, high or medium defect in either VRF architecture.** The production contract's control flow holds under the Execution Trace, Periphery and First Principles passes. The findings file holds 4 substantiated items, all low or informational, plus a coverage row for every one of the 13 verifier-listed entry points and 6 invariants.\n\n**What was traced and holds**\n\n- Architecture A: every request id ever issued stays mapped, the first delivered word is final, and finalization runs in strict mint order, so no holder, keeper, owner or mempool observer can reroll, reorder or pick a word. The owner's only lever is `setVRFConfig`, read only at request time. It can stall new requests visibly but cannot touch a stored word.\n- No unprivileged party can make the coordinator callback fail. The store path costs about 32k gas against a 150k minimum limit, and a relayed fulfilment inside a minter's own receiver hook leaves consistent state. Both were confirmed with scratch tests.\n- Required constants, prices, 500 cap, 77/423 split and permanent burning are preserved.\n\n**Findings reported**\n\n1. **Low.** The fixed 7-day re-request window plus strict ordering stalls every later reveal for a full week even when the lost request is already provably dead, as with a failed callback. Reproduced in a scratch test. Suggested fix: shorten the delay to about 48 hours and, if the live coordinator exposes consumed commitments, allow immediate replacement for those.\n2. **Info.** Single-step ownership. A mistyped transfer freezes `setVRFConfig`, the only lever for a Chainlink lane change. `Ownable2Step` is already vendored.\n3. **Info.** Architecture B prototype overstates its permutation space and has no deployable consumer. B is not a safer design as it stands; it needs the same timed re-request plus a sale-close rule the brief does not define.\n4. **Info.** Test gaps: re-request on a `RandomnessStored` token, the inline-batch gas budget measured outside the callback, and the double-loss sequence.\n\n**Not reached:** the live BMC `burnFrom` and the real coordinator's exact-gas call path, since there is no fork RPC and the real coordinator is not vendored. Recommendation to the judge: adopt A with the delay change in finding 1, and treat the remaining items as pre-deployment hygiene.","treeHash":null,"usage":{"cachedInputTokens":1250934,"inputTokens":290,"model":"claude-fable-5-1","outputTokens":43417,"runtime":"claude","turns":30,"wallClockMs":722304}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3beb94a990617d46","findings":[{"citation":"resolved","description":"finalizeReveals only advances while token GENESIS_PHASE_SUPPLY+revealedCount+1 is in RandomnessStored. A token whose request is never fulfilled stays AwaitingRandomness forever, and every token minted after it can never be revealed even when its own random word was delivered and stored. Chainlink VRF v2.5 makes this state reachable without any attacker: (1) the docs state that requests on an under-funded subscription 'remain pending for up to 24 hours before they expire' (docs.chain.link/vrf/v2-5/overview/subscription), and requestRandomWords is accepted on-chain regardless of balance, so a subscription that runs dry overnight during the 460-mint sale expires every request made in that window; (2) a callback that fails is still marked fulfilled by the coordinator with the word discarded (the vendored VRFCoordinatorV2_5Mock mirrors this: delete s_requests + RandomWordsFulfilled(success=false)); (3) coordinator decommissioning. There is no re-request, no expiry handling and no owner power that can repair it: setVRFConfig only affects future requests. Up to 459 holders who each burned 350,000 BMC would hold permanently pending tokens, and the 37 mystery Genesis would never be distributed. The subscription owner (normally the project owner) can also trigger this at will by simply not funding the subscription for 24h after a mint, which is an undisclosed owner power to freeze the collection. The existing tests testMissingEarlierRequestBlocksLaterReveal, testDelayedPredecessorDoesNotBurnAgainOrChangeSupply and testLaterRandomnessStoredUntilEarlierRequestArrives assert this freeze as expected behaviour instead of treating it as a defect.","line":321,"path":"src/BMCIlluminatiPass.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {ERC20Burnable} from \"@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol\";\nimport {VRFCoordinatorV2_5Mock} from \"@chainlink/contracts/src/v0.8/vrf/mocks/VRFCoordinatorV2_5Mock.sol\";\nimport {BMCIlluminatiPass} from \"src/BMCIlluminatiPass.sol\";\n\n/// @dev 18-decimal ERC20Burnable stand-in for BMC (same semantics as the project's MockBMC).\ncontract ProofBMC is ERC20, ERC20Burnable {\n    constructor() ERC20(\"BMC Illuminati\", \"BMC\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// @notice Proof: one VRF request that is never fulfilled (Chainlink v2.5 drops requests that stay\n///         unfunded for 24h; a callback that fails is marked fulfilled with the word lost) freezes\n///         every later token forever, including tokens whose own random word was already delivered\n///         and stored. The contract offers no recovery path and the owner cannot repair it.\n///\n///         Fails on the current code: tokens 42 and 43 have RandomnessStored but can never be Revealed.\n///         Passes once the design no longer makes a token's reveal depend on an earlier token's\n///         request (expiry/re-request handling, an order-independent draw, or a single-seed design).\ncontract HeadOfLineBlockingProof is Test {\n    ProofBMC bmc;\n    VRFCoordinatorV2_5Mock coordinator;\n    BMCIlluminatiPass pass;\n    uint256 subId;\n\n    address owner = makeAddr(\"owner\");\n    address alice = makeAddr(\"alice\");\n    address bob = makeAddr(\"bob\");\n    address carol = makeAddr(\"carol\");\n\n    function setUp() public {\n        bmc = new ProofBMC();\n        coordinator = new VRFCoordinatorV2_5Mock(0.1 ether, 1e9, 4e15);\n        pass = new BMCIlluminatiPass(owner, address(bmc), address(coordinator));\n        subId = coordinator.createSubscription();\n        coordinator.addConsumer(subId, address(pass));\n        coordinator.fundSubscription(subId, 100 ether);\n        vm.startPrank(owner);\n        pass.setGenesisImagesCID(\"bafyproof\");\n        pass.setVRFConfig(subId, keccak256(\"lane\"), 500_000, 3, false);\n        vm.stopPrank();\n        for (uint256 i = 0; i < 40; ++i) {\n            address minter = address(uint160(0x7000 + i));\n            bmc.mint(minter, 1_000_000e18);\n            vm.startPrank(minter);\n            bmc.approve(address(pass), 1_000_000e18);\n            pass.mintGenesis();\n            vm.stopPrank();\n        }\n    }\n\n    function _mint(address minter) internal returns (uint256 id, uint256 req) {\n        bmc.mint(minter, 350_000e18);\n        vm.startPrank(minter);\n        bmc.approve(address(pass), 350_000e18);\n        (id, req) = pass.mintMystery();\n        vm.stopPrank();\n    }\n\n    function test_LostEarlierRequestMustNotFreezeLaterDeliveredTokens() public {\n        (uint256 id41,) = _mint(alice); // request for 41 is never fulfilled (expired / lost)\n        (uint256 id42, uint256 req42) = _mint(bob);\n        (uint256 id43, uint256 req43) = _mint(carol);\n        assertEq(id41, 41);\n        assertEq(id42, 42);\n        assertEq(id43, 43);\n\n        // Chainlink delivers 42 and 43 normally.\n        uint256[] memory words = new uint256[](1);\n        words[0] = 0xB0B;\n        coordinator.fulfillRandomWordsWithOverride(req42, address(pass), words);\n        words[0] = 0xCA201;\n        coordinator.fulfillRandomWordsWithOverride(req43, address(pass), words);\n        assertEq(uint8(pass.revealStatusOf(id42)), uint8(BMCIlluminatiPass.RevealStatus.RandomnessStored));\n        assertEq(uint8(pass.revealStatusOf(id43)), uint8(BMCIlluminatiPass.RevealStatus.RandomnessStored));\n\n        // Far beyond Chainlink's 24h pending window: request 41 is gone for good.\n        vm.warp(block.timestamp + 30 days);\n        vm.roll(block.number + 216_000);\n\n        // Nothing the owner can do touches token 41 or the queue.\n        vm.prank(owner);\n        pass.setVRFConfig(subId, keccak256(\"other-lane\"), 2_500_000, 3, false);\n\n        // Expected: holders whose randomness was delivered get their reveal.\n        // Actual on current code: finalizeReveals returns 0 forever; 42 and 43 stay Pending.\n        pass.finalizeReveals(10);\n        assertEq(\n            uint8(pass.revealStatusOf(id42)),\n            uint8(BMCIlluminatiPass.RevealStatus.Revealed),\n            \"token 42 with delivered randomness is frozen behind lost request 41\"\n        );\n        assertEq(\n            uint8(pass.revealStatusOf(id43)),\n            uint8(BMCIlluminatiPass.RevealStatus.Revealed),\n            \"token 43 with delivered randomness is frozen behind lost request 41\"\n        );\n        assertTrue(pass.tierOf(id42) != BMCIlluminatiPass.Tier.Pending);\n        assertTrue(pass.tierOf(id43) != BMCIlluminatiPass.Tier.Pending);\n    }\n}","reproduction":"State: phase 1 complete (40 Genesis minted), subscription funded, VRF config set. Alice mints token 41 (request R41), Bob mints 42 (R42), Carol mints 43 (R43). The coordinator delivers R42 (word 0xB0B) and R43 (word 0xCA201); both tokens become RandomnessStored. R41 is never delivered (expired after >24h unfunded, or callback marked failed). Warp 30 days, owner calls setVRFConfig with any values. Call finalizeReveals(10). Expected: tokens 42 and 43, whose randomness is on-chain, are revealed. Actual: finalizeReveals returns 0 forever, revealStatusOf(42)==RandomnessStored (2), tierOf(42)==Pending, and the same for every later token; the collection can never complete.","severity":"high","snippet":"        while (done < maxCount && tokenId <= totalMinted && _revealStatus[tokenId] == RevealStatus.RandomnessStored) {","title":"Strict mint-order finalization turns one lost VRF request into a permanent freeze of every later token (head-of-line blocking)"},{"citation":"resolved","description":"The stated liveness argument ('requests remain pending while the subscription is unfunded and are fulfilled once it is funded again') is false for Chainlink VRF v2.5: the official docs say pending requests on an under-funded subscription expire after 24 hours. Because requestRandomWords does not check the subscription balance, mintMystery keeps accepting burns and opening requests while the subscription is empty; every such request older than 24h is lost, and the affected token (plus, per the high finding, every later token) is permanently pending. The only on-chain signal that a request is still open is the coordinator's pendingRequestExists(subId), which cannot distinguish 'still to be fulfilled' from 'expired off-chain', so the contract cannot detect the condition either. Neither the owner nor holders can trigger a replacement request. This is a distinct defect from the queue amplification: even an order-independent design needs an expiry/re-request path for the affected token itself.","line":38,"path":"src/BMCIlluminatiPass.sol","reproduction":"State: phase 1 complete, VRF config set, subscription balance 0 (ChainlinkMockIntegration.test_UnfundedSubscriptionKeepsTokenPendingUntilFunded shows the request is accepted while unfunded). Alice burns 350,000 BMC via mintMystery -> token 41, request R41, status AwaitingRandomness. No funding for 25 hours; Chainlink nodes drop R41. Owner funds the subscription afterwards. Expected: token 41 is eventually revealed (or a new request can be opened for it). Actual: no function can open another request for token 41; it stays AwaitingRandomness forever, and finalizeReveals(n) returns 0 for the rest of the collection's life.","severity":"medium","snippet":"///  Chainlink VRF v2.5 requests remain pending while the subscription is unfunded and are fulfilled","title":"Contract and README assume unfunded VRF requests wait indefinitely; Chainlink expires them after 24h and the contract has no recovery path for an expired request"},{"citation":"resolved","description":"draw(seed) is a pure function of the seed alone: outcome[i] for every mint position i is fixed and publicly computable the moment the seed exists. The prototype has no notion of sale state, so any consumer that stores the seed before all 460 mystery positions are sold (or that lets minting continue after the request is sent, since the VRF node knows the word before it submits the fulfilment transaction) lets a minter pick exactly the positions that map to Genesis and skip the rest, destroying the 37/460 ex-ante fairness. The experimental test consumer (ExperimentalVRFConsumer.requestOnce) can be called at any time and does not freeze minting, so the tests do not catch this. The function also costs 503,870 gas per call, so a production consumer must compute the permutation once (or lazily in views), not per token in the VRF callback.","line":17,"path":"src/experimental/BMCPassVRFPrototype.sol","reproduction":"Seed 987654321 (the value used in testVRFSeedProducesExactUniqueInventory). prototype.draw(987654321) gives outcomes < 37 (Genesis, tierOfOutcome == 2) at positions 10, 21, 38, 47 and 61 (outcomes 15, 9, 24, 19, 14) and Standard at every other position below 61. If positions map to token ids 41+i and the seed is public while position 10 is unsold, a minter who waits until totalMinted == 50 and then mints receives a guaranteed Genesis; every other minter who reads the same seed avoids the Standard positions. Expected: every mystery mint has the same 37/460 chance regardless of timing. Actual: outcome per position is known in advance.","severity":"medium","snippet":"    function draw(uint256 seed)","title":"Prototype single-seed permutation exposes every unsold position's outcome once the seed is known, enabling position targeting unless the seed is requested only after the sale is closed"},{"citation":"resolved","description":"Architecture B concentrates all randomness into one request. The only consumer in the repository (ExperimentalVRFConsumer) hard-blocks a second request and has no timeout, so the 24h Chainlink expiry or a failed callback (success=false, request deleted) leaves seed == 0 forever and no outcome can ever be derived. testUnfulfilledRequestDoesNotCreateSeed records this freeze as expected behaviour. A production version of B must have a permissionless re-request after a timeout (well past Chainlink's 24h window) with first-delivered-word-wins semantics so that neither the owner nor a front-runner of the fulfilment transaction can discard a delivered word (a design where a re-request invalidates the previous request would let anyone who sees the node's fulfilment transaction in the mempool reroll it).","line":23,"path":"test/BMCPassVRFChainlinkIntegration.t.sol","reproduction":"consumer.requestOnce() returns request id R. R is never fulfilled (subscription unfunded for >24h, or the callback fails). vm.warp(+200 days). Expected: a replacement request can be opened and the collection still reveals. Actual: consumer.requestOnce() reverts with 'already requested' (testCannotRequestSecondSeed), consumer.seed() stays 0, received() stays false, and prototype.draw has no seed to run on; 460 tokens would remain unrevealed permanently.","severity":"medium","snippet":"        require(requestId == 0, \"already requested\");","title":"Prototype's reference consumer allows exactly one VRF request with no re-request, so a single expired or failed request freezes the whole 460-token distribution"},{"citation":"resolved","description":"The launch recipe forbids anything configured after deployment rather than in a constructor. A freshly deployed BMCIlluminatiPass cannot mint at all until the owner sends setGenesisImagesCID and setVRFConfig, and setVRFConfig stays callable forever (subscription id, key hash, callback gas, confirmations, payment mode). The subscription can be created before deployment (its id does not depend on the consumer address), so subscriptionId, keyHash, callbackGasLimit, requestConfirmations and nativePayment can all be constructor arguments. The CID is a dynamic string, which the launch constructor does not support, but a CIDv1 can be passed as its 32-byte sha2-256 digest (bytes32) and rendered on-chain as the base16 CID string 'f01701220' + hex(digest) (dag-pb) or 'f01551220' + hex(digest) (raw), which gateways resolve. Keeping setVRFConfig as a bounded owner power is a documented trust assumption, not a bypass; the finding is the post-deployment initialisation.","line":186,"path":"src/BMCIlluminatiPass.sol","reproduction":"Deploy BMCIlluminatiPass(owner, bmc, coordinator) through the factory. Call mintGenesis() from a funded, approved account. Expected under the launch policy: the contract is fully configured and usable at deployment. Actual: revert GenesisCIDNotSet(); after setGenesisImagesCID, mintMystery still reverts VRFNotConfigured() until a second owner transaction; test_DeployScriptConfig asserts exactly this two-step state.","severity":"medium","snippet":"    function setGenesisImagesCID(string calldata cid) external onlyOwner {","title":"Launch-critical configuration (Genesis CID and VRF parameters) is set after deployment by owner transactions instead of in the constructor"},{"citation":"resolved","description":"The comment claims ~85k worst case per reveal. Measured on this code with the project's MockVRFCoordinator: finalizeReveals(1) costs 133,705 gas for the first reveal and 116,601 for the next one (cold storage of revealedCount, totalMinted, _slotSwap, mysteryGenesisRevealed, _tier, _artworkId). With the minimum allowed callbackGasLimit of 150,000 the computed batch is always 0, so every single reveal needs a separate permissionless finalizeReveals transaction paid by someone else. With larger limits an under-budgeted batch reverts inside try/catch and is retried by keepers; the stored word is never lost, so this is cost and UX, not safety. Raise GAS_PER_REVEAL to about 140k or re-measure under the real coordinator's _callWithExactGas.","line":95,"path":"src/BMCIlluminatiPass.sol","reproduction":"Config setVRFConfig(42, keyHash, 150_000, 3, false). Mint token 41, fulfill its request with the request's own callback gas limit (MockVRFCoordinator.fulfill). Expected per the natspec: the callback finalizes the reveal inline. Actual: revealStatusOf(41) == RandomnessStored (2); a separate finalizeReveals(1) call is required and costs 133,705 gas.","severity":"low","snippet":"    uint256 public constant GAS_PER_REVEAL = 110_000;","title":"GAS_PER_REVEAL (110k) is below the measured cost of one reveal, so the inline callback batch reverts or is skipped more often than designed"},{"citation":"resolved","description":"Drawing without replacement in mint order means the probability of Genesis for the next position is remainingMysteryGenesis()/remaining, which the contract publishes (the website shows 'remaining-inventory odds'). Every minter pays the same 350,000 BMC but their ex-ante chance depends on when they mint. This is symmetric public information, not a bypass, and the README discloses it, but it is a rarity bias relative to the stated 37/460 fairness and it is the reason informed buyers will wait while uninformed ones mint early. The single-seed design (B) gives every position the same 37/460 chance.","line":334,"path":"src/BMCIlluminatiPass.sol","reproduction":"State: revealedCount == 300 and mysteryGenesisRevealed == 10 (27 Genesis among 160 remaining). Next mystery mint: Genesis chance 27/160 = 16.9%. Alternative state: revealedCount == 300 and mysteryGenesisRevealed == 30: chance 7/160 = 4.4%. Expected under a flat-odds launch: 37/460 = 8.04% for everyone. Actual: the chance varies between those values and is readable before minting via remainingMysteryGenesis() and pendingReveals().","severity":"low","snippet":"        uint256 r = uint256(keccak256(abi.encode(word, tokenId))) % remaining;","title":"Odds of a Genesis outcome drift with reveal state and are public, so mint timing changes expected rarity (design A only)"},{"citation":"resolved","description":"VRFRecovery, VRFRecoveryAdditional and VRFSafetyProperties all pass on the current code because they only document the current behaviour; none of them fails if a token can never be revealed. Missing coverage: (a) a fulfilment where the callback fails (the Chainlink mock emits success=false and deletes the request) and the resulting permanent AwaitingRandomness state; (b) a request older than Chainlink's 24h window and any recovery path; (c) a fulfilment delivered at exactly the minimum callbackGasLimit of 150,000 through VRFCoordinatorV2_5Mock (only the project's own mock at 95k is tested); (d) ordering fuzz over more than three concurrently pending tokens showing outcomes are independent of delivery order; (e) BMCMainnetFork.t.sol, which needs an RPC and was not run here, so burnFrom against the real BMC token remains unverified; (f) nothing checks the Chainlink mock's unfunded-at-request path together with later funding beyond one token.","line":123,"path":"test/VRFRecovery.t.sol","reproduction":"Run forge test: 55 tests pass, including testMissingEarlierRequestBlocksLaterReveal which asserts finalizeReveals(10) == 0 after 200 days with token 42's randomness stored. Expected for a recovery suite: a test that fails while a holder with delivered randomness cannot be revealed. Actual: no test fails; the defect in the high finding is invisible to CI.","severity":"low","snippet":"        assertEq(pass.finalizeReveals(10), 0);","title":"VRF recovery and safety test suites assert the freeze as expected behaviour and never exercise a failed callback, an expired request, or recovery"},{"citation":"resolved","description":"The README tells holders that an unfunded request waits until funded and that the coordinator being decommissioned is the only way a token stays pending. Chainlink's documentation states that requests on an under-funded subscription expire after 24 hours. The privileged-powers table should list that the subscription owner can, by not funding the subscription for 24h, permanently freeze the affected token and (under the current design) every later one, and that no owner action can undo it.","line":64,"path":"README.md","reproduction":"Read README.md lines 63-67 against docs.chain.link/vrf/v2-5/overview/subscription ('If your balance is below the minimum, your requests remain pending for up to 24 hours before they expire'). Expected: documentation matches the dependency's behaviour. Actual: it promises indefinite pending.","severity":"info","snippet":"re-request or fallback. Chainlink VRF v2.5 keeps a request pending while the subscription is","title":"README misstates Chainlink VRF v2.5 pending-request behaviour and omits the owner's ability to freeze reveals by not funding"},{"citation":"resolved","description":"Read in full: src/BMCIlluminatiPass.sol, src/StandardArt.sol, src/interfaces/IBMCToken.sol, src/experimental/BMCPassVRFPrototype.sol, script/Deploy.s.sol, all eight test files and both mocks, the vendored VRFCoordinatorV2_5Mock and SubscriptionAPI, README and docs. Not reached: the live BMC token and the live VRF coordinator (no fork RPC; BMCMainnetFork.t.sol not run), and the full VRFCoordinatorV2_5 source (not vendored; behaviour taken from Chainlink docs and the vendored mock). All 55 existing tests pass; runtime size 23,488 bytes; no DELEGATECALL/CALLCODE/SELFDESTRUCT, no proxy, no receive/fallback. Entry points: setGenesisImagesCID (owner, once), setVRFConfig (owner, unbounded in time), mintGenesis/mintMystery (anyone paying), rawFulfillRandomWords (immutable coordinator only), finalizeReveals (anyone), standard ERC721/Ownable. No reroll, owner-assignment, selective-fulfilment or reentrancy path was found in A: the word is bound to the token before any reveal, re-delivery is ignored, in-order processing removes finalization-order choice, and _burnExact fails closed. ARCHITECTURE A verdict: cannot safely handle an expired or permanently unavailable request. One lost request freezes that token and all later ones permanently with no recovery (high finding); the owner/subscription owner can cause it by inaction; odds drift with timing. ARCHITECTURE B verdict: the prototype is not an architecture yet (pure function, no request, no mint, no state). As a design it is fairer (flat 37/460 odds, one request to fund and monitor, no ordering question) but it is only safe if (1) the seed is requested after minting is closed and never before, (2) a permissionless timed re-request exists with first-delivered-word-wins, and (3) the sale close is itself rule-based (sell-out or a deadline), so neither of the two existing designs satisfies the requirements as written. RECOMMENDATION: keep BMCIlluminatiPass's mint, burn, pricing, 40 sequential Genesis, storage and metadata, and replace the per-mint request with the single-seed model: mintMystery assigns position only; minting closes at 500 or at a fixed deadline (permissionless close); after close anyone may call requestSeed(); if no word has arrived after a timeout of at least 7 days anyone may call requestSeed() again; rawFulfillRandomWords stores the first word for any request id the contract issued and ignores later ones; outcomes come from the existing O(1) virtual Fisher-Yates driven by keccak256(seed, tokenId) in mint order via the existing permissionless finalizeReveals, so no 500k-gas permutation is needed and all existing invariant tests still apply. If per-mint requests must stay, the minimal fix is: a permissionless re-request for a token whose request is older than a timeout, first-delivered-wins (never invalidate an open request, to block mempool rerolls), plus the same strict order. RESIDUAL RISKS of the recommended design: Chainlink node operators know the word before submitting and, with two open requests, could choose which to deliver (mitigated by the long timeout); the reveal of all 460 happens only after close, so early minters wait longer; the subscription must stay funded for one request; the owner's setVRFConfig power remains a trust assumption. MAINNET READINESS: not ready. Blocking: the high finding, an expiry/re-request path, constructor-time configuration for the launch, a corrected README, and tests that fail on the freeze. Also pending: the real-BMC burnFrom fork check (O-3) and an external gas measurement under the real coordinator.","line":40,"path":"src/BMCIlluminatiPass.sol","reproduction":"Summary entry; the concrete inputs are in the individual findings above. The decisive state is: token 41 AwaitingRandomness with its request expired, token 42 RandomnessStored, finalizeReveals(10) == 0 permanently.","severity":"info","snippet":"contract BMCIlluminatiPass is ERC721, Ownable, ReentrancyGuard {","title":"Architecture comparison, recommendation and mainnet-readiness assessment (A: 460 in-order requests vs B: single-seed permutation)"}],"hash":"87618a78ea917c0dcc0f56b9b5ef8e947d0653f4038ecb0d99d6df7ec2f51b7a","nodeId":"36a63b1a-e49b-424a-be54-cfe392e3e870","outcome":"completed","summary":"Review complete. The findings file is at the repository root with 10 entries, one carrying a Foundry proof that fails on the current code.\n\n**Verdict: neither architecture can safely handle an expired or permanently unavailable VRF request.** Architecture A has no reroll, owner-assignment, selective-fulfilment or reentrancy path, but it cannot survive a single lost request. Architecture B is not yet an architecture, and its only reference consumer freezes completely on one lost request.\n\n**What I read.** All of `src/`, `script/Deploy.s.sol`, all eight test files and both mocks, the vendored Chainlink `VRFCoordinatorV2_5Mock` and `SubscriptionAPI`, README and docs. Not reached: the live BMC token and live coordinator (no fork RPC, so `BMCMainnetFork.t.sol` was not run) and the full `VRFCoordinatorV2_5` source, which is not vendored. All 55 existing tests pass. Runtime size is 23,488 bytes with no forbidden opcodes.\n\n**Findings, most severe first.**\n\n- **High, head-of-line blocking.** One request that is never fulfilled freezes that token and every later token forever, even those whose random word is already stored. Chainlink's docs state unfunded requests expire after 24 hours, and the coordinator accepts requests regardless of balance, so a subscription running dry overnight mid-sale triggers it. A failed callback also discards the word. No owner action can repair it. The proof test mints 41, 42, 43, delivers only 42 and 43, and asserts they reveal. It fails now with `2 != 3`.\n- **Medium, no expiry recovery.** The contract and README claim unfunded requests wait indefinitely. They do not. The subscription owner can freeze the collection by simply not funding for 24 hours, an undisclosed power.\n- **Medium, prototype position targeting.** `draw(seed)` fixes every position's outcome. For seed 987654321, positions 10, 21, 38, 47 and 61 are Genesis. Any consumer that stores the seed before the sale closes lets minters cherry-pick. The function also costs about 504k gas.\n- **Medium, prototype single request.** The reference consumer blocks a second request and has no timeout. One lost request leaves 460 tokens unrevealed.\n- **Medium, post-deployment configuration.** CID and VRF parameters are set by owner transactions after deployment, which the launch recipe forbids. Subscription id and VRF parameters can move to the constructor. The CID can be passed as a bytes32 digest and rendered as a base16 CIDv1.\n- **Low.** The 110k per-reveal gas budget is below the measured 116k to 134k, so the inline batch is skipped or reverts more than designed, with no loss. Genesis odds drift with reveal state and are public, so timing changes rarity. The recovery test suites assert the freeze as expected behaviour and never exercise a failed callback or recovery.\n\n**Recommended design.** Keep the mint, burn, pricing, 40 sequential Genesis, storage and metadata of `BMCIlluminatiPass`. Replace per-mint requests with one seed requested only after minting closes, at 500 or at a fixed deadline. Allow a permissionless re-request after a timeout of at least 7 days, with first-delivered-word-wins so no one can discard a delivered word. Drive the existing O(1) virtual Fisher-Yates from the seed and token id in mint order through the existing permissionless `finalizeReveals`. Residual risks: node operators see words before submitting, early minters wait until sale close, and the subscription must stay funded for one request.\n\n**Mainnet readiness: not ready.** Blocking items are the high finding, an expiry and re-request path, constructor-time configuration, a corrected README, tests that fail on the freeze, and the real-BMC `burnFrom` fork check.","treeHash":null,"usage":{"cachedInputTokens":1468974,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":43402,"runtime":"claude","turns":43,"wallClockMs":649543}},{"artifacts":[],"attempt":1,"bundleHash":"83d4657b52e6c2a28dd013105fe2d5c3afe86ec7a71dd2e0e98a3cb059617146","device":"a31e321b410aaa02","findings":[{"description":"Every mystery mint draws without replacement from one shared pool of 37 Genesis and 423 Standard outcomes, and reveals happen per mint. The chance that the next mint is Genesis therefore equals remainingMysteryGenesis() / (460 - revealedCount), a public value that drifts away from the ex-ante 37/460 as reveals land. In the extreme the pool runs out of one tier and a 350,000 BMC mint becomes a guaranteed Standard (or a guaranteed Genesis) while the price stays the same. Informed buyers can time their mints; uninformed buyers bear the complement. This is structural to per-mint reveals over a finite pool and is already disclosed in the README and the previous review (prior audit id aa16c116); it cannot be removed inside architecture A without deferring all reveals to the end of the sale (which is architecture B) or giving up the exact 37/423 inventory. Reported so the requester decides on disclosure or a design change rather than having a test bless it.","line":382,"path":"src/BMCIlluminatiPass.sol","reproduction":"Mint 37 mystery tokens and deliver words whose draw lands on Genesis each time (the test crafts them with a mirrored pool). Public view remainingMysteryGenesis() now returns 0 while remainingMysteryMints() is 423 and currentPrice() is still 350,000 BMC. Mint token 78 and deliver any VRF word: expected under the brief's 'no rarity bias' requirement is a 37/460-style chance of Genesis; actual is Tier.Standard for every one of 128 fuzzed words. Mirror: after 423 Standard reveals, the next 37 mints are Genesis for any word. Executable in test/VRFAdversarial.t.sol: testPoC_OddsDrift_GenesisPoolEmptiesSoLaterMintsAreGuaranteedStandard and testPoC_OddsDrift_StandardPoolEmptiesSoLaterMintsAreGuaranteedGenesis (both pass because they assert the observed drift, not that it is correct).","severity":"low","title":"Architecture A: conditional Genesis odds drift with reveal state, so equal-price mints are not equal-odds mints"},{"description":"setVRFConfig accepts any non-zero subscription id and key hash. A config the coordinator rejects (unknown key hash, a subscription the pass is not a consumer of, wrong payment mode) makes mintMystery and rerequestRandomness revert until the owner corrects it; the subscription owner can achieve the same by removing the consumer or leaving the subscription unfunded. No word, balance or outcome can be touched this way (verified by test_OwnerCanStallMintsAndRerequestsButNotOutcomes and the invariants), and the README discloses it as 'a stall, never a reroll', but the stall has no time bound and no permissionless escape: if the owner key is lost or hostile after a request is dropped, the affected token and every token behind it stay pending forever. Renouncing ownership removes the stall power but also the only way to repoint a decommissioned coordinator.","line":218,"path":"src/BMCIlluminatiPass.sol","reproduction":"Mint tokens 41 and 42; let 41's callback fail (word lost) and deliver 42. Owner sets a config the coordinator rejects (the mock's setRejectRequests(true) stands in for an invalid key hash). After 7 days rerequestRandomness(41) reverts and mintMystery reverts; 42 stays RandomnessStored indefinitely. Expected by the brief: expired randomness is recoverable without owner cooperation. Actual: recovery requires the owner to restore a valid config. Executable: test_OwnerCanStallMintsAndRerequestsButNotOutcomes in test/VRFAdversarial.t.sol.","severity":"low","title":"Owner and subscription owner hold an unbounded stall power over mints and recovery"},{"description":"rerequestRandomness never invalidates the earlier request, which is the right choice against mempool rerolls, but it means that whenever a replacement is opened while the original is merely very late (not actually dropped) the Chainlink node knows both words before submitting and could submit the one it prefers. The window opens only after a 7-day stall, both requests are visible on-chain, and the contract cannot distinguish the two cases, so this is a documented trust assumption on the VRF node rather than a code defect. No change recommended.","line":292,"path":"src/BMCIlluminatiPass.sol","reproduction":"Mint token 41; wait 7 days; call rerequestRandomness(41) twice 7 days apart so requests r1, r2, r3 are open. Deliver r2 first: the outcome is the one from r2's word; deliveries of r1 and r3 are ignored (test_ThreeOpenRequestsForOneTokenFirstDeliveredWins). Which of the three is delivered first is decided off-chain.","severity":"info","title":"Two open requests after a 7-day stall give the VRF node operator a choice of which word to deliver first"},{"description":"BMCPassVRFPrototype is a pure permutation; it requests nothing, mints nothing and has no notion of sale state. The only consumer in the repository, ExperimentalVRFConsumer in test/BMCPassVRFChainlinkIntegration.t.sol, is a test stand-in whose closeMinting() has no access control and no rule; a production version must close by sell-out or a fixed deadline the brief does not define, must request the seed only after that close, and must derive outcomes per token in O(1) rather than calling draw (measured about 505k gas) from the callback. For any seed that becomes public while positions are still for sale, a buyer can take a Genesis position with certainty (testFuzz_PoC_PublicSeedLetsBuyerTakeAGenesisPositionForAnySeed). B is not deployable from this repository.","line":22,"path":"src/experimental/BMCPassVRFPrototype.sol","reproduction":"prototype.draw(seed) for any seed; find the first index i with tierOfOutcome(outcomes[i]) == 2; every earlier index is Standard and index i is Genesis. A minter who waits for position i wins with probability 1.","severity":"info","title":"Architecture B has no production consumer and its only stand-in lets anyone close the sale"},{"description":"On Chainlink VRF v2.5 a request that the node drops after ~24h unfunded is never fulfilled on-chain, so the subscription's pending request count for the pass stays above zero. cancelSubscription and removeConsumer are believed to revert while pending requests exist, which would lock whatever LINK or ETH remains in the subscription after the sale. The production coordinator source is not vendored (only VRFCoordinatorV2_5Mock and SubscriptionAPI are), so this could not be confirmed offline. Operational cost only; no outcome or liveness impact on the pass.","line":38,"path":"src/BMCIlluminatiPass.sol","reproduction":"On a mainnet fork: create a subscription, add the pass, mint one mystery token with the subscription unfunded, wait for the node to drop it, fund, then call cancelSubscription: expected PendingRequestExists revert. Not executable here.","severity":"info","title":"Dropped VRF requests likely strand the subscription's remaining balance (to verify on a fork)"}],"hash":"87b9bf15fbb2ca03efd35adfee0eb7db2fe24f47e387272f1f8e0b72481ac2d1","nodeId":"ca334696-8e2c-4188-89a6-ad07aa211ba8","outcome":"completed","summary":"The work is complete and verified. The full suite passes with the scratch file included, and the scratch directory is deleted before submission.\n\n**Conclusion of the review.** Architecture A, the per-mint request design with the permissionless 7-day re-request already on this branch, is the one that can safely handle an expired or permanently lost VRF request. I found no reroll, owner outcome manipulation, or position-targeting path in it. Architecture B remains a pure permutation with no production consumer and no defined sale close, so it is not deployable from this repository. The one fairness limit of A is structural: the conditional Genesis odds of the next mint drift with public reveal state, so a 350,000 BMC mint can be a guaranteed Standard or a guaranteed Genesis. I reported that rather than asserting it is correct.\n\n**What I added, all under test/:**\n- `VRFArchitectureInvariants.t.sol`: a handler with five actors and nine actions (mints, deliveries in any order, failed callbacks, re-deliveries, re-requests, owner config changes, rejected requests, time) and nine invariants over burn conservation, the 500 cap, the mint-order reveal prefix, outcome finality and uniqueness, pool bounds, request mapping, the re-request gate, and one word per token. 48 runs of 80 calls pass.\n- `VRFAdversarial.t.sol`: 14 scenarios including three open requests per token, re-request attempts on stored words, an owner config change between storage and finalization, callback gas fuzzed across the whole accepted range, malformed callbacks, reentrancy from the receive hook, a full 460-token sale with 66 lost requests recovering to the exact inventory, and the odds-drift proofs.\n- `VRFPrototypeProperties.t.sol`: fuzzed permutation and edge seeds for `draw`, the per-seed position-targeting proof, a gas check showing `draw` must never run in a callback, and lost-seed and malformed-callback paths for the test consumer.\n- `VRF-INDEPENDENT-REVIEW.md`: the six requested deliverables, including residual risks and the mainnet readiness assessment.\n\n**Reported findings** are in `.imd-findings.json`: odds drift (low, with executable PoC tests), the owner and subscription-owner stall power with no time bound (low), node choice between two open requests after a stall (info), B having no production consumer (info), and dropped requests likely stranding subscription funds (info, needs a fork to confirm).\n\nTest results:\n\n| Run | Result |\n| --- | --- |\n| `forge build` | success |\n| `forge test` (default seed) | 98 passed, 1 fork-only skip |\n| `forge test --fuzz-seed 7` | 98 passed, 1 skip |\n\n**Still owed and not possible offline:** `burnFrom` and `requestRandomWords` against the live BMC token and coordinator on a mainnet fork, and confirmation of the dropped-request subscription behaviour. No paid work was started and nothing was deployed, broadcast, burned, or approved.","treeHash":"af34ff2cdeb6db0313593ffc0c440bd486f53963","usage":{"cachedInputTokens":3703285,"inputTokens":930,"model":"claude-fable-5-1","outputTokens":67362,"runtime":"claude","turns":56,"wallClockMs":1001590}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"5835e48821d8827d","findings":[{"citation":"resolved","description":"The brief asks whether either architecture can handle an EXPIRED or a PERMANENTLY UNAVAILABLE VRF request. Architecture A (as adapted) handles the expired case through `rerequestRandomness`, but not the permanently unavailable case, and the project's own documentation misstates the recovery. `VRF_COORDINATOR` is immutable (src/BMCIlluminatiPass.sol:114) and `setVRFConfig` (lines 218-226) only changes subscription id, key hash, callback gas, confirmations and payment mode. Yet README.md lines 83-85 say: \"If the coordinator were permanently decommissioned the owner would have to point `setVRFConfig` at a working one before re-requests could succeed\", and docs/VRF-ARCHITECTURE-REVIEW.md lines 116-117 repeat it (\"point `setVRFConfig` at a successor\"). No such call exists; docs/SECURITY-REVIEW.md F-3 deliberately removed `setCoordinator` as a reroll-by-proxy vector. Periphery fact that makes this worse: the real VRFCoordinatorV2_5 (v1.4.0, lines 282-284) states \"we do not check whether the keyHash is valid to save gas ... invalid keyHashes ... will simply not be fulfilled\", and the request has no balance check, so a decommissioned coordinator or a retired gas lane still ACCEPTS requests. Chainlink has retired VRF v1 and v2 on mainnet while leaving the coordinator contracts in place; over a 460-mint sale of unknown duration this is a realistic end state. Consequences at that point: (1) every token in `AwaitingRandomness` stays `Pending` forever, (2) every permissionless re-request (one per token per 7 days, forever) opens another request on the dead coordinator, (3) `mintMystery` continues to succeed and permanently burns 350,000 BMC per buyer for a token that can never be revealed, with no refund path, (4) the owner's only lever, `setVRFConfig`, cannot help. Architecture B as prototyped has no consumer at all, so it satisfies neither requirement. Conclusion for deliverable 4: NEITHER architecture, as it stands, safely handles a permanently unavailable VRF; A handles expiry only. Recommended fix that preserves F-3's intent (no owner reroll): add an owner-initiated, event-announced, timelocked coordinator migration that is only enabled when the network has been silent for a long window (for example no `RandomnessStored` for any token for 30 days while `pendingReveals() > 0`), keep every old request id mapped so first-delivered-word-wins still holds across coordinators (a word from the old coordinator for an awaiting token still counts, so the migration cannot discard anything), and additionally pause `mintMystery` (fail closed, no burn) while the silence condition holds so buyers stop burning BMC into an unservable queue. If the requester prefers to keep the coordinator immutable, the README and review must be corrected to say that a decommissioned coordinator strands every pending token and every later mint permanently, and the website must warn buyers.","line":114,"path":"src/BMCIlluminatiPass.sol","reproduction":"State: phase 1 complete (40 Genesis minted). Deploy with a coordinator that accepts `requestRandomWords` but is never served (test/mocks/MockVRFCoordinator.sol with no fulfil call models this exactly). 1) alice approves 350,000e18 BMC and calls mintMystery(): succeeds, token 41 is AwaitingRandomness, alice's balance is 0, totalBurned = 40,350,000e18. 2) owner calls setVRFConfig(2, keccak256(\"new-lane\"), 500_000, 3, true): VRF_COORDINATOR() is unchanged. 3) warp 7 days and call rerequestRandomness(41) 52 times over a year: each opens a new request on the same coordinator; status stays AwaitingRandomness, tierOf(41) == Pending, finalizeReveals(10) == 0. 4) bob mints: succeeds, burns another 350,000e18, pendingReveals() == 2. Expected per README: owner repoints the contract at a working coordinator and re-requests succeed. Actual: no function can change the coordinator; the burn is final and the tokens are unrevealable forever. Demonstrated by test/scratch/DeadCoordinator.t.sol::test_PermanentlyUnavailableVRFBurnsBMCWithNoEscape (passes on this code as a demonstration of the state).","severity":"medium","snippet":"IVRFCoordinatorV2Plus public immutable VRF_COORDINATOR;","title":"Permanently unavailable VRF (coordinator or lane retired) cannot be recovered: the coordinator is immutable, re-requests re-target it forever, mints keep burning BMC, and the README/review claim an ow"},{"citation":"resolved","description":"Seam execution x periphery x first principles. The contract's stated guarantee is that a wrong assumption \"fails closed rather than mints\" (README, pre-deployment checks) and that \"a bad config blocks new mystery mints and re-requests until corrected\" (README.md:91). The burn-mismatch and decimals guards do fail closed, but the VRF leg does not: the real VRFCoordinatorV2_5.requestRandomWords (v1.4.0 lines 255-290) checks only consumer membership, confirmations, callback gas and numWords. It does not check the subscription balance and explicitly does not check the key hash. Chainlink's own VRFCoordinatorV2_5Mock vendored in lib/ behaves the same way, and the project's test test_UnfundedSubscriptionKeepsTokenPendingUntilFunded already shows the unfunded case. So `_burnExact` destroys 350,000 BMC and then the request is accepted even when (a) the subscription balance is 0 (the node retries for about 24h and drops the request), or (b) the owner typed a key hash that no node serves. In both cases the buyer's BMC is gone, the token sits in AwaitingRandomness, the earliest remedy is a permissionless re-request after 7 days, and that remedy only works if a third party funds the subscription or the owner fixes the key hash. Economic consequence: the sale's liveness rests entirely on an off-chain funding duty with no on-chain guard; a buyer cannot tell from the contract whether their reveal is servable. Fix that preserves the design: read `VRF_COORDINATOR.getSubscription(cfg.subscriptionId)` (IVRFSubscriptionV2Plus, vendored) in `_requestRandomWord` and revert `mintMystery` when the balance in the configured payment asset (balance for LINK, nativeBalance for ETH) is below a constructor-set floor, so the burn rolls back exactly as it does today when the pass is not a consumer. The key-hash case cannot be checked on-chain; the README sentence must be corrected and the website should display the configured lane. Expose a `subscriptionCanPay()` view for the website either way.","line":281,"path":"src/BMCIlluminatiPass.sol","reproduction":"Using Chainlink's VRFCoordinatorV2_5Mock(0.1 ether, 1e9, 4e15): create a subscription, add the pass as consumer, do NOT fund it; complete phase 1. (a) alice mints: mintMystery succeeds, alice's BMC balance is 0, coordinator.fulfillRandomWords(req, pass) reverts InsufficientBalance; warp 7 days, rerequestRandomness(41) succeeds and its fulfilment reverts InsufficientBalance too; tierOf(41) == Pending. (b) fund 100 ether, owner calls setVRFConfig(subId, keccak256(\"no such lane\"), 500_000, 3, false); alice mints: succeeds and burns 350,000e18 although no node will ever serve that lane. Expected (README): the mint reverts and the burn rolls back. Actual: burn final, token pending. Demonstrated by test/scratch/UnpayableMint.t.sol (both tests pass on this code as demonstrations of the state).","severity":"low","snippet":"requestId = _requestRandomWord(tokenId);","title":"mintMystery does not fail closed when the request cannot be paid or served: an empty subscription or an unknown key hash still burns 350,000 BMC; the README's \"a bad config blocks new mystery mints\" i"},{"citation":"resolved","description":"The recovery design assumes requests are sometimes lost (24h unfunded expiry or a failed callback) and replaces them with `rerequestRandomness`. On the real VRFCoordinatorV2_5 (v1.4.0) `requestRandomWords` does `++consumerConfig.pendingReqCount` (line 286) and only `fulfillRandomWords` does `--s_consumers[rc.sender][rc.subId].pendingReqCount` (line 487). `pendingRequestExists(subId)` returns true while any consumer's count is above zero (lines 638-647), and both `removeConsumer` (line 652) and `cancelSubscription` (line 678) revert with PendingRequestExists while it is true. A request that a node dropped is never fulfilled, so the counter never returns to zero: after the first lost request the subscription owner can never cancel the subscription or remove the pass, and whatever LINK or ETH remains after the sale is stranded (only Chainlink's coordinator owner can release it via ownerCancelSubscription). The README tells the owner to keep the subscription funded for 460 requests plus re-requests, which encourages a large up-front balance that the design's own failure mode then locks. This is an economic consequence for the subscription owner, not the pass contract, and it is not a code defect in the pass; it belongs in the operational guidance and the mainnet readiness list.","line":133,"path":"README.md","reproduction":"Real coordinator semantics (vendored mock does not model pendingReqCount; verified in VRFCoordinatorV2_5.sol v1.4.0 at the cited lines). State: subscription funded with 500 LINK, pass is the only consumer. 1) A mystery mint opens request R1 while the balance is temporarily 0; the node drops R1 after about 24h; pendingReqCount for the pass is now 1 and stays 1. 2) Everything else is funded and fulfilled; the sale completes; 120 LINK remain. 3) Subscription owner calls cancelSubscription(subId, to): reverts PendingRequestExists. removeConsumer(subId, pass): reverts PendingRequestExists. Expected: the owner can recover the unspent 120 LINK after the sale. Actual: it is stranded permanently. Mitigation to document: fund in small tranches sized to the next few fulfilments and never pre-fund the whole sale; or accept the loss explicitly.","severity":"low","snippet":"3. Keep the subscription funded: 460 requests at mainnet VRF prices, plus any re-requests. A","title":"Every lost VRF request permanently locks the subscription: the real coordinator's pendingReqCount never decrements, so cancelSubscription and removeConsumer revert forever and the unspent LINK/ETH is "},{"citation":"resolved","description":"Not a new defect: the prior audit reported it (aa16c116) and the project disclosed it in the README (\"Odds\") and chose architecture A anyway. Recorded here because the brief's fairness deliverable needs the numbers and because it is the main economic argument for architecture B. In A the draw for the next finalized token is `keccak256(word, tokenId) % remaining` over the live pool, so the conditional Genesis probability at any moment is `remainingMysteryGenesis() / (460 - revealedCount)`, public on-chain. Early buyers take the ex-ante 37/460 = 8.04% with no information; a late buyer holds a free option to buy only when the remainder is rich. In the extreme state of 300 Standard reveals and 0 Genesis the figure is 37/160 = 23.1%, and a buyer who takes the whole remaining tail receives exactly all 37 Genesis for 160 x 350,000 = 56,000,000 BMC while the 300 early buyers burned 105,000,000 BMC for none. The information is symmetric, so no buyer is deceived, but the ex-ante symmetry the README implies (\"every mystery mint pays the same price\") hides that the option value is not symmetric: it accrues to whoever buys last. The mirror state (Genesis-heavy early run) leaves a tail nobody rational buys, which is a sell-out risk for the project. Architecture B (one seed requested after a rule-based close) has none of this: every position is exactly 37/460 and no state is observable before the close. If the requester wants A's instant reveals, the honest disclosure is the drift formula on the website at mint time (the website already shows provisional odds); if the requester wants fairness across positions, B with a close rule is the only option.","line":383,"path":"src/BMCIlluminatiPass.sol","reproduction":"test/scratch/OddsDrift.t.sol::test_LateBuyerCollectsEveryRemainingGenesis: phase 1 complete; 300 mystery mints by `early`, each word brute-forced so the reveal is Standard; then remainingMysteryGenesis() == 37 and 460 - revealedCount() == 160 (logged: 804 vs 2312 Genesis per 10,000 mints). `late` mints the remaining 160 positions with arbitrary words; after finalizeReveals, exactly 37 of tokens 341..500 are Genesis; totalBurned == 201,000,000e18. Expected under a position-fair design: 37/460 for every position regardless of timing. Actual: the late buyer's realized rate is 23.1% and, taking the whole tail, 100% of the remaining Genesis.","severity":"info","snippet":"uint256 remaining = MYSTERY_SUPPLY - revealedCount;","title":"Fairness (deliverable 3, accepted property aa16c116, quantified): revealing during the sale gives late buyers an option on the public pool state; a buyer who waits for a Genesis-heavy remainder is gua"},{"citation":"resolved","description":"Deliverable 1 (security comparison) and the brief's \"selective fulfilment\" criterion. Both architectures already accept that the VRF node knows a word before submitting and can withhold. The review's section 4 correctly lists, for A, that after a 7-day wait two requests are open and the node \"could choose which to deliver\" for ONE token. The same timed re-request is then recommended for B (docs/VRF-ARCHITECTURE-REVIEW.md:97-99: \"after 7 days without a word anyone may call it again; the first word delivered for any issued request id is final\") and the test consumer implements it (test/BMCPassVRFChainlinkIntegration.t.sol:35-52, 54-61). In B the seed determines all 460 outcomes, so a node that withholds the first seed for 7 days and then sees the second one can pick whichever of two full permutations it prefers (for example the one that puts Genesis on positions it or a bribing holder owns), and `testFirstDeliveredSeedWinsWhenOriginalArrivesLate` shows the contract will accept either. The comparison table row for B therefore understates the node trust required: the blast radius of node selectivity in B is the whole collection, in A one token per lost request. This is a design observation, not a code defect (B has no production consumer). It should be weighed in deliverable 4 if B is ever adopted: a commit-reveal of the seed by the project is not a fix (the project would then hold the reroll), so B needs either no re-request at all (accepting a permanent freeze on a lost seed) or a re-request that becomes possible only after a much longer silence than Chainlink's 24h drop window and is announced on-chain so the delay itself is evidence of withholding.","line":26,"path":"docs/VRF-ARCHITECTURE-REVIEW.md","reproduction":"With ExperimentalVRFConsumer and VRFCoordinatorV2_5Mock from test/BMCPassVRFChainlinkIntegration.t.sol: closeMinting(); first = requestSeed(); warp 7 days; second = requestSeed(); the node now holds proofs for both. Deliver second (seed 222): consumer.seed() == 222; deliver first (seed 111): still 222. Deliver first first instead: seed 111 stays. prototype.draw(111) and prototype.draw(222) are two different complete permutations (testDifferentSeedsProduceDifferentDistributions), and the node chose which one the collection gets. Expected per the comparison table: the node cannot change the distribution. Actual: a node that delays can select between two.","severity":"info","snippet":"| Selective fulfilment (VRF node) | Node sees the word before submitting; could withhold a request it dislikes, but cannot change it or deliver another. Withholding is visible and produces a stuck token | Node sees the whole distribution before submitting; cannot change it. Withholding freezes the whole collection |","title":"Architecture B comparison gap: with the timed re-request the review recommends for B, a VRF node that delays one seed past 7 days gets to choose between two complete distributions, which is a whole-co"}],"hash":"8f19825ef65b610798399bfd3145c39cae798237938a6546121fbdbe334a8931","nodeId":"cdeb56ee-7d9a-4ae5-a17d-7fb7ffcde425","outcome":"completed","summary":"Findings file validated (5 findings, 20 coverage rows, every snippet matches its line) and the full suite passes with my scratch demos added (77 passed, 1 fork-only skipped). Working tree is unchanged apart from the findings file and `test/scratch/`.\n\n## Verdict on the brief's central question\n\nNeither architecture safely handles a **permanently unavailable** VRF. Architecture A, as adapted on this branch, handles an **expired** request correctly, and I found no reroll, owner-manipulation or selective-fulfilment path for an unprivileged actor in it. The findings are in `.imd-findings.json`.\n\n**Findings, most severe first**\n\n1. **Medium. Permanent VRF loss has no recovery, and the docs claim one that does not exist.** The coordinator is immutable and `setVRFConfig` cannot change it, yet README lines 83 to 85 and the architecture review say the owner would \"point setVRFConfig at a working one\". The real v2.5 coordinator accepts requests for retired lanes without checking, so if Chainlink retires this coordinator or lane, every pending token is stuck forever, re-requests re-target the dead coordinator forever, and new mints keep burning 350,000 BMC each. Demonstrated in `test/scratch/DeadCoordinator.t.sol`.\n2. **Low. Mints do not fail closed when the request cannot be paid or served.** An empty subscription or an unknown key hash still burns BMC. The README's \"a bad config blocks new mystery mints\" is false for the key hash. Fix: check `getSubscription` balance against a floor before burning.\n3. **Low. Each lost request locks the subscription permanently.** The real coordinator's `pendingReqCount` only decrements on fulfilment, so `cancelSubscription` and `removeConsumer` revert forever after one dropped request. The funding guidance should say to fund in tranches.\n4. **Info. Odds drift, quantified.** Accepted property from the prior audit. In a Genesis-light state a late buyer can take the whole tail and collect all 37 Genesis for 56M BMC while early buyers burned 105M for none.\n5. **Info. Architecture B comparison gap.** With the timed re-request the review recommends for B, a node that delays one seed past 7 days chooses between two complete distributions, a whole-collection reroll. The comparison table says the node cannot change it.\n\n**Recommendation and readiness.** Keep architecture A with the current re-request, but add a timelocked, event-announced coordinator migration enabled only after a long network-wide silence, keep old request ids valid so first-delivered-wins survives the migration, and pause mystery mints during that silence so buyers stop burning into a dead queue. If the owner prefers an immutable coordinator, correct the README and warn buyers on the website. B is only viable with a rule-based sale close, no reveals during the sale, and a re-request window much longer than 7 days. Not mainnet-ready until finding 1 is decided, finding 2's guard is added, the fork test runs against live BMC, and the funding guidance reflects finding 3.\n\nNot reached: live `burnFrom` on a mainnet fork, and the real coordinator's exact callback gas accounting. Coordinator semantics were verified from the v1.4.0 source rather than a fork.","treeHash":null,"usage":{"cachedInputTokens":2524602,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":51439,"runtime":"claude","turns":44,"wallClockMs":759408}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"896d1238054266ca","findings":[{"citation":"resolved","description":"Trust-gap (access x first principles). README.md:84-85 and docs/VRF-ARCHITECTURE-REVIEW.md:116-117 state that if the Chainlink coordinator were permanently decommissioned 'the owner would have to point `setVRFConfig` at a working one before re-requests could succeed'. No such power exists: `VRF_COORDINATOR` is immutable (line 114) and `setVRFConfig` (lines 218-226) only takes subscriptionId, keyHash, callbackGasLimit, requestConfirmations and nativePayment. Every request path (`mintMystery` -> `_requestRandomWord` line 305, `rerequestRandomness` line 298) calls the immutable coordinator. So for the 'permanently unavailable VRF' case the assignment asks about, architecture A has no handling at all: (1) every token in `AwaitingRandomness` whose request was lost (failed callback or 24h-unfunded drop) can never be re-requested and stays Pending forever, and because finalization is strict mint-order every later token with a stored word also stays Pending; (2) `mintMystery` reverts forever, so the 460-mystery sale cannot complete. The holders of stuck tokens have already burned 350,000 BMC each. The in-code trust model (line 32 'The coordinator address is immutable', line 46 'There is no fallback randomness') is internally consistent; the defect is that the published trust model promises a mitigation the contract cannot perform, and the requester has not actually decided this trade-off. Note that adding an owner `setCoordinator` would reintroduce the reroll-by-proxy path the project's own finding F-3 rejected (owner points fulfilment at a coordinator it controls and delivers chosen words for every awaiting token), so the fix is a design decision: either (a) accept and document the Chainlink dependency truthfully (remove the README/docs claim, add this to the privileged-powers table as 'nobody can recover'), or (b) add a coordinator migration that cannot be used as a reroll, e.g. a two-step timelocked `proposeCoordinator`/`acceptCoordinator` (7+ days) that only applies to requests opened after acceptance, with the pending event visible on-chain so holders can react. Option (b) expands owner trust and must be an explicit scope decision.","line":114,"path":"src/BMCIlluminatiPass.sol","reproduction":"State: Genesis phase finished (40 mints). Alice mints mystery token 41 (request R1). Coordinator fulfils R1 with a failing callback (word lost; in the project mock `fulfillWithGas(R1, 1, 20_000)` returns false, status stays AwaitingRandomness). Then the coordinator stops accepting requests (deprecated, key deregistered; modelled by `MockVRFCoordinator.setRejectRequests(true)`). Warp 7 days. Expected per README: owner repoints the coordinator and `rerequestRandomness(41)` succeeds. Actual: `rerequestRandomness(41)` reverts ('mock: subscription not funded' from the coordinator); owner calls `setVRFConfig(2, keccak256('other'), 2_500_000, 3, true)` and `rerequestRandomness(41)` still reverts; `VRF_COORDINATOR()` is unchanged; Bob's `mintMystery()` reverts; `revealStatusOf(41) == AwaitingRandomness` and `totalBurned == 40_000_000e18 + 350_000e18` with no path forward. Verified by test/scratch/ReviewRepro.t.sol::test_CoordinatorUnavailable_NoRecoveryLever (passes on current code, i.e. the stuck state is reachable).","severity":"medium","snippet":"    IVRFCoordinatorV2Plus public immutable VRF_COORDINATOR;","title":"A permanently unavailable coordinator freezes pending tokens and the rest of the sale; the documented owner recovery lever (repointing the coordinator) does not exist"},{"citation":"resolved","description":"Economics x asymmetry. Outcomes are drawn without replacement and finalized during the sale, so the conditional probability that the next mint is Genesis equals `remainingMysteryGenesis() / (460 - revealedCount)` (both public; `remainingMysteryGenesis()` at line 461). Because every delivered word is also emitted (`RandomnessStored`, line 353) and finalization is strict mint-order, the pool state after all stored-but-unfinalized tokens is computable too, so the odds for the next position are known almost exactly even while the queue is blocked. Every minter pays the same 350,000 BMC, but a minter who reads this state mints only when the conditional odds are above the ex-ante 37/460 = 8.0% and abstains when they are below; the uninformed minters absorb the low-odds positions. Concrete drift already acknowledged in docs/VRF-ARCHITECTURE-REVIEW.md section 3: at 300 revealed, 27 Genesis left gives 27/160 = 16.9%, 7 left gives 7/160 = 4.4%. The bias is per position, not in the total (always exactly 37 Genesis), and the prior audit recorded it (aa16c116) and the README discloses it. It is reported here because the assignment asks for a rarity-bias determination: architecture A cannot remove it (the words are public), architecture B removes it only if the single seed is requested after a rule-based sale close. Recommendation: keep the disclosure, expose the conditional odds on-chain (a view returning numerator/denominator) so the information is equally available to everyone, and show it on the mint site; or move to B with a fixed close rule if flat 37/460 odds per position are a product requirement.","line":383,"path":"src/BMCIlluminatiPass.sol","reproduction":"State: 40 Genesis minted; mint and fully finalize mystery tokens 41..340 (300 reveals) with words such that `mysteryGenesisRevealed() == 30`. Then `remainingMysteryGenesis()` returns 7 and `pendingReveals()` returns 0, so for token 341 `remaining = 460 - 300 = 160` in `_finalize` and P(Genesis) = 7/160 = 4.4%. In the mirror state with `mysteryGenesisRevealed() == 10`, P(Genesis) for token 341 = 27/160 = 16.9%. Expected (flat odds): 37/460 = 8.0% for every position. Actual: the price is identical and the odds differ by almost 4x depending on when the minter chooses to mint. The arithmetic is visible in `_finalize` lines 383-398 (`picked < MYSTERY_GENESIS` over the current virtual pool).","severity":"low","snippet":"        uint256 remaining = MYSTERY_SUPPLY - revealedCount;\n        uint256 word = _randomWord[tokenId];\n        uint256 r = uint256(keccak256(abi.encode(word, tokenId))) % remaining;","title":"Rarity bias by position: the Genesis probability of each mystery mint is public and drifts with reveal state, so informed minters systematically buy better odds than uninformed ones at the same 350,00"},{"citation":"resolved","description":"Asymmetry between the writer and the reader of `_revealStatus`: `rawFulfillRandomWords` moves a token to `RandomnessStored` and emits its word (lines 351-353), but `tokenURI` (lines 499-511) branches only on `_tier == Pending` and serves the same 'awaiting Chainlink VRF randomness ... not yet determined on-chain' text for both `AwaitingRandomness` and `RandomnessStored`. Under strict mint-order finalization a token can sit in `RandomnessStored` for at least 7 days whenever an earlier token's request is lost (the recovery path requires `REREQUEST_DELAY`), and for shorter windows whenever more than three deliveries land before the queue drains. During that window the holder (or anyone reading events) computes `keccak256(abi.encode(word, tokenId)) % remaining` against the public virtual pool: if the draw index is >= 37 the token is Standard with certainty regardless of what the blocked predecessor later draws; if it is < 37 it is Genesis unless the predecessor collides on the same index (1/remaining). The token still trades on marketplaces as a 'Pending Reveal' with the ex-ante odds in its metadata, so an informed seller can offload a known-Standard token to a buyer who trusts the on-chain description. This is an information asymmetry the contract itself creates by describing determined state as undetermined. Fix (no design change): in `tokenURI`, when `_revealStatus[tokenId] == RandomnessStored`, say so (e.g. 'randomness delivered, awaiting in-order finalization') and/or add a view `provisionalOutcome(tokenId)` that returns the tier the stored word will produce given the current pool (with a flag when it depends on an unresolved predecessor), so buyers and sellers see the same thing.","line":576,"path":"src/BMCIlluminatiPass.sol","reproduction":"State: Genesis phase done. Alice mints 41 (request R41, never delivered). Bob mints 42 (request R42). Choose word w with `keccak256(abi.encode(w, 42)) % 459 >= 37` (e.g. the first w >= 5 that satisfies it) and deliver it for R42. Now `revealStatusOf(42) == RandomnessStored`, `tierOf(42) == Pending`, and the base64 JSON from `tokenURI(42)` contains 'The outcome (Genesis or Standard) is not yet determined on-chain'. Then for any ten different replacement words for 41 (after `warp(+7 days)`, `rerequestRandomness(41)`, deliver, `finalizeReveals(10)`), `tierOf(42)` is always Standard. Expected: metadata that does not assert the outcome is undetermined once the word is stored. Actual: it does. Verified by test/scratch/ReviewRepro.t.sol::test_StoredWordTokenStillClaimsUndetermined (passes on current code, i.e. the misleading state is reachable).","severity":"low","snippet":"            ' - Pending Reveal\",\"description\":\"A BMC Illuminati Pass mystery mint awaiting Chainlink VRF randomness. The outcome (Genesis or Standard) is not yet determined on-chain. Metadata becomes permanent once revealed.\",\"image\":\"data:image/svg+xml;base64,',","title":"Pending metadata claims the outcome is 'not yet determined on-chain' for RandomnessStored tokens whose word is public and whose tier is already fixed, enabling informed selling of known-Standard token"},{"citation":"resolved","description":"Access-control hygiene. The contract inherits OpenZeppelin `Ownable` (single-step `transferOwnership`, immediate `renounceOwnership`, lib/openzeppelin-contracts/contracts/access/Ownable.sol:76-89). The owner's only power, `setVRFConfig`, is also the only on-chain way to keep `mintMystery` and `rerequestRandomness` working when the subscription or gas lane changes: the subscription owner can cancel the subscription once no request is pending (Chainlink v2.5 `cancelSubscription`), Chainlink can deregister a key hash, or the project may need to move to a new funded subscription. A typo in `transferOwnership` (ownership to an address nobody controls) or a `renounceOwnership` call (README line 94 records that it 'freezes the VRF config forever') therefore turns any later config problem into a permanent stall of the remaining mystery sale and of every token whose request is later lost. OpenZeppelin `Ownable2Step` preserves the agreed design (same single owner, same power) and removes the mistake path; `renounceOwnership` can be overridden to revert if the requester wants the config lever to be permanent. This is a trust-assumption hardening, not a bypass.","line":47,"path":"src/BMCIlluminatiPass.sol","reproduction":"State: owner O, subscription S with the pass as consumer, no pending requests. O calls `transferOwnership(0x000...dead)` (succeeds in one step, no acceptance) or `renounceOwnership()`. Later the subscription owner cancels S (allowed while nothing is pending) or Chainlink retires the configured key hash. Expected: the owner updates `setVRFConfig` to a valid subscription/lane and minting resumes. Actual: `setVRFConfig` reverts with `OwnableUnauthorizedAccount` for every caller, `mintMystery` reverts at `VRF_COORDINATOR.requestRandomWords` (InvalidConsumer/InvalidSubscription/InvalidKeyHash) for the rest of the sale, and any token later stuck in `AwaitingRandomness` cannot be re-requested on a working configuration.","severity":"low","snippet":"contract BMCIlluminatiPass is ERC721, Ownable, ReentrancyGuard {","title":"Single-step ownership transfer and renounceOwnership can permanently lose the only lever that keeps VRF requests possible (subscription/key-hash config)"}],"hash":"c170cef426031290d70105085ba3e095903c53563754ade7f5631b4b075ace8b","nodeId":"ea270299-a98a-439c-b983-a3930f1ba398","outcome":"completed","summary":"The findings file validates: all four snippets match their cited lines and the coverage record answers all 13 entry points plus the invariants checked. Git shows no tracked changes (the findings file and `test/scratch/` are ignored paths), so nothing in the reviewed tree was modified.\n\n## Recap\n\n**Verdict on the assignment's central question.** Architecture A with the permissionless 7-day re-request is the only one of the two that handles an *expired* request (failed callback or 24-hour unfunded drop) without a reroll path. I traced every permission: the coordinator-only fulfilment on an immutable address, first-delivered-word-wins across every request id of a token, strict mint-order finalization, owner powers limited to future request parameters, and the re-request gate. No holder, keeper, owner or mempool watcher can discard or choose a word. Neither architecture handles a *permanently unavailable* VRF, and B exists only as a pure function plus a test-only consumer.\n\n**Findings written to `.imd-findings.json`** (4, reproduced in `test/scratch/ReviewRepro.t.sol`):\n\n1. **Medium.** The README and architecture review say the owner can repoint `setVRFConfig` at a successor coordinator if Chainlink decommissions the current one. The coordinator is immutable and no such lever exists. In that state every lost-request token stays pending forever, every later token with a stored word stays blocked, and `mintMystery` reverts for the rest of the sale. Adding a coordinator setter would reopen the reroll-by-proxy path the project itself rejected, so this is a design decision the requester must make explicitly.\n2. **Low.** Rarity bias by position: the conditional Genesis odds for the next mint are public and drift with reveal state (4.4% versus 16.9% at the same price in the documented example). Already disclosed, inherent to A, reported because the brief asks for a rarity-bias determination.\n3. **Low.** Pending metadata says the outcome is \"not yet determined on-chain\" for a token whose word is already stored and public. A blocked queue holds such tokens for at least 7 days, and their tier is computable, so a seller can offload a known-Standard token as a mystery.\n4. **Low.** Single-step `transferOwnership` and `renounceOwnership` can permanently lose the only config lever that keeps requests possible. `Ownable2Step` preserves the agreed design.\n\n**Residual trust assumptions, not findings.** A Chainlink node sees a word before submitting and can withhold; after a re-request it can pick between two live words (in B that choice covers the whole distribution). The subscription owner can delay by de-funding but not choose. The real `VRFCoordinatorV2_5` is not vendored, so its cancel/remove-while-pending rules were taken from Chainlink's published semantics rather than code in this tree. `StandardArt.sol` was outside my area and not reviewed.","treeHash":null,"usage":{"cachedInputTokens":1608638,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":52106,"runtime":"claude","turns":38,"wallClockMs":782928}}],"verification":[{"checks":[{"durationMs":7407,"exitCode":0,"name":"build","output":"Compiling 69 files with Solc 0.8.26\nSolc 0.8.26 finished in 7.22s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to view\n  --> test/BMCPassVRFPrototype.t.sol:46:5:\n   |\n46 |     function testSameSeedProducesSameDistribution() public {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (2018): Function state mutability can be restricted to view\n  --> test/BMCPassVRFPrototype.t.sol:53:5:\n   |\n53 |     function testDifferentSeedsProduceDifferentDistributions() public {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":699,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/BMCMainnetFork.t.sol:BMCMainnetForkTest\n[SKIP] test_RealBMCGenesisMint() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 439.58µs (105.25µs CPU time)\n\nRan 2 tests for test/VRFSafetyProperties.t.sol:VRFSafetyPropertiesTest\n[PASS] testDelayedPredecessorDoesNotBurnAgainOrChangeSupply() (gas: 1393730)\n[PASS] testOnlyCoordinatorMayDeliverRandomness() (gas: 472409)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 8.48ms (1.21ms CPU time)\n\nRan 1 test for test/VRFRecoveryAdditional.t.sol:VRFRecoveryAdditionalTest\n[PASS] testLaterRandomnessStoredUntilEarlierRequestArrives() (gas: 1383548)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 20.07ms (723.18µs CPU time)\n\nRan 4 tests for test/ChainlinkMockIntegration.t.sol:ChainlinkMockIntegrationTest\n[PASS] test_NotAConsumerReverts() (gas: 367150)\n[PASS] test_RequestAndFulfilThroughChainlinkMock() (gas: 779150)\n[PASS] test_SeveralRequestsOutOfOrderThroughChainlinkMock() (gas: 1950517)\n[PASS] test_UnfundedSubscriptionKeepsTokenPendingUntilFunded() (gas: 969009)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 138.77ms (2.21ms CPU time)\n\nRan 12 tests for test/VRFRecovery.t.sol:VRFRecoveryTest\n[PASS] testCallbackFailureLosesWordAndRerequestRecovers() (gas: 11793608)\n[PASS] testFirstDeliveredWordWinsOriginalFirst() (gas: 1192077)\n[PASS] testFirstDeliveredWordWinsReplacementFirst() (gas: 974415)\n[PASS] testFuzz_DeliveryOrderDoesNotChangeOutcomes(uint256,uint256) (runs: 64, μ: 4951162, ~: 4951164)\n[PASS] testLostEarlierRequestRecoversWithoutFreezingLaterTokens() (gas: 2322552)\n[PASS] testMinimumCallbackGasStoresWordThroughChainlinkMock() (gas: 11134333)\n[PASS] testOwnerHasNoSpecialRerequestPower() (gas: 563802)\n[PASS] testRerequestNotAllowedBeforeDelay() (gas: 725116)\n[PASS] testRerequestOnlyForTokensAwaitingRandomness() (gas: 761768)\n[PASS] testRerequestRateLimitedPerToken() (gas: 871324)\n[PASS] testRerequestUsesCurrentConfigAndRollsBackIfRequestFails() (gas: 989615)\n[PASS] testSeveralUnfundedTokensRecoverAfterFunding() (gas: 4524654)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 138.82ms (159.09ms CPU time)\n\nRan 4 tests for test/BMCPassVRFPrototype.t.sol:BMCPassVRFPrototypeTest\n[PASS] testDifferentSeedsProduceDifferentDistributions() (gas: 1109240)\n[PASS] testDistributionHasNoDuplicates() (gas: 4831168)\n[PASS] testInvalidOutcomeReverts() (gas: 8495)\n[PASS] testSameSeedProducesSameDistribution() (gas: 1109275)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 504.30ms (17.71ms CPU time)\n\nRan 7 tests for test/BMCPassVRFChainlinkIntegration.t.sol:BMCPassVRFChainlinkIntegrationTest\n[PASS] testCannotRequestSecondSeedBeforeTimeout() (gas: 325849)\n[PASS] testFirstDeliveredSeedWinsWhenOriginalArrivesLate() (gas: 659818)\n[PASS] testLostSeedRequestIsReplacedAfterTimeout() (gas: 725047)\n[PASS] testOnlyCoordinatorMaySupplySeed() (gas: 292102)\n[PASS] testPoC_PublicSeedExposesEveryPositionOutcome() (gas: 3182283)\n[PASS] testSeedCannotBeRequestedWhileMintingIsOpen() (gas: 31566)\n[PASS] testVRFSeedProducesExactUniqueInventory() (gas: 5903852)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 504.37ms (25.25ms CPU time)\n\nRan 43 tests for test/BMCIlluminatiPass.t.sol:BMCIlluminatiPassTest\n[PASS] testFuzz_SingleRevealAlwaysInInventory(uint256) (runs: 64, μ: 10984038, ~: 10980333)\n[PASS] test_CIDIsConstantAndUsedInGenesisMetadata() (gas: 1426565)\n[PASS] test_CallbackBatchIsGasAware() (gas: 12129868)\n[PASS] test_CallbackBatchWith500kRevealsThree() (gas: 12590278)\n[PASS] test_Constants() (gas: 82490)\n[PASS] test_ConstructorRejectsInvalidVRFConfig() (gas: 10746)\n[PASS] test_DeployScriptConfig() (gas: 10917420)\n[PASS] test_DeployScriptRequiresOwner() (gas: 5997455)\n[PASS] test_ExcessAllowanceOnlyBurnsPrice() (gas: 341754)\n[PASS] test_FinalizationOrderCannotBeChosen() (gas: 11976960)\n[PASS] test_FirstMinterGetsLegendaryGenesis1() (gas: 2070145)\n[PASS] test_FreshDeploymentMintsWithoutAnyOwnerCall() (gas: 307796)\n[PASS] test_FulfillRevealsAndLocksMetadata() (gas: 17272651)\n[PASS] test_Full500MintSimulation() (gas: 278134243)\n[PASS] test_FullMintOutWithInOrderFulfillment() (gas: 244467414)\n[PASS] test_GasPerRevealBudgetCoversMeasuredCost() (gas: 11583902)\nLogs:\n  first reveal gas: 129539\n  second reveal gas: 112439\n\n[PASS] test_GenesisRevertsAfterPhase1() (gas: 10532760)\n[PASS] test_Mints2To40AreSequentialGenesis() (gas: 14428859)\n[PASS] test_MysteryBurnRevertsIfVRFRequestFails() (gas: 10666741)\n[PASS] test_MysteryRevertsDuringPhase1() (gas: 455814)\n[PASS] test_MysteryUsesConstructorConfigWithoutOwnerCall() (gas: 10792755)\n[PASS] test_NoOwnerMintOrWithdrawSurface() (gas: 58291)\n[PASS] test_OnlyCoordinatorCanFulfill() (gas: 10898228)\n[PASS] test_OutOfOrderFulfillmentFinalizesInMintOrder() (gas: 12249861)\n[PASS] test_PendingMetadataIsNeutral() (gas: 13582571)\n[PASS] test_PermissionlessFinalizationWhenCallbackGasTooLow() (gas: 11065772)\n[PASS] test_Phase1CostsExactlyOneMillion() (gas: 390236)\n[PASS] test_Phase2CostsExactly350k() (gas: 10953694)\n[PASS] test_ReentrancyGenesisBlocked() (gas: 884668)\n[PASS] test_ReentrancyMysteryBlocked() (gas: 11367627)\n[PASS] test_RevertInsufficientAllowance() (gas: 179241)\n[PASS] test_RevertInsufficientBalance() (gas: 193091)\n[PASS] test_RevertNoAllowance() (gas: 144187)\n[PASS] test_RevertWhenTokenDecimalsUnexpected() (gas: 728852)\n[PASS] test_RevertWhenTokenDoesNotActuallyBurn() (gas: 745318)\n[PASS] test_StandardArtCombinationsUnique() (gas: 1482850)\n[PASS] test_StandardArtCoversEveryTrait() (gas: 743677)\n[PASS] test_StandardArtRendersWithoutGenesisTraits() (gas: 30070029)\n[PASS] test_StandardMetadataAfterReveal() (gas: 19961234)\n[PASS] test_TransfersAreStandard() (gas: 469634)\n[PASS] test_UnavailableRandomnessStaysPendingNoFallback() (gas: 10993135)\n[PASS] test_VRFConfigValidation() (gas: 253119)\n[PASS] test_ViewsRevertForNonexistent() (gas: 33596)\nSuite result: ok. 43 passed; 0 failed; 0 skipped; finished in 504.47ms (1.87s CPU time)\n\nRan 8 test suites in 507.69ms (1.82s CPU time): 73 tests passed, 0 failed, 1 skipped (74 total tests)\n","passed":true},{"durationMs":1674,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"BMCIlluminatiPass.approve(address,uint256)\",\"BMCIlluminatiPass.finalizeReveals(uint256)\",\"BMCIlluminatiPass.mintGenesis()\",\"BMCIlluminatiPass.mintMystery()\",\"BMCIlluminatiPass.rawFulfillRandomWords(uint256,uint256[])\",\"BMCIlluminatiPass.renounceOwnership()\",\"BMCIlluminatiPass.rerequestRandomness(uint256)\",\"BMCIlluminatiPass.safeTransferFrom(address,address,uint256)\",\"BMCIlluminatiPass.safeTransferFrom(address,address,uint256,bytes)\",\"BMCIlluminatiPass.setApprovalForAll(address,bool)\",\"BMCIlluminatiPass.setVRFConfig(uint256,bytes32,uint32,uint16,bool)\",\"BMCIlluminatiPass.transferFrom(address,address,uint256)\",\"BMCIlluminatiPass.transferOwnership(address)\"],\"files\":{\".github/workflows/main.yml\":43,\".gitignore\":5,\"ADAPTATION.md\":64,\"README.md\":161,\"artwork/genesis/genesis-01.png\":7910,\"artwork/genesis/genesis-02.png\":6998,\"artwork/genesis/genesis-03.png\":7016,\"artwork/genesis/genesis-04.png\":6664,\"artwork/genesis/genesis-05.png\":7186,\"artwork/genesis/genesis-06.png\":6703,\"artwork/genesis/genesis-07.png\":6488,\"artwork/genesis/genesis-08.png\":6807,\"artwork/genesis/genesis-09.png\":6793,\"artwork/genesis/genesis-10.png\":6550,\"artwork/genesis/genesis-11.png\":6125,\"artwork/genesis/genesis-12.png\":6418,\"artwork/genesis/genesis-13.png\":6298,\"artwork/genesis/genesis-14.png\":5909,\"artwork/genesis/genesis-15.png\":6142,\"artwork/genesis/genesis-16.png\":5727,\"artwork/genesis/genesis-17.png\":6019,\"artwork/genesis/genesis-18.png\":399,\"artwork/genesis/genesis-19.png\":369,\"artwork/genesis/genesis-20.png\":359,\"artwork/genesis/genesis-21.png\":330,\"artwork/genesis/genesis-22.png\":385,\"artwork/genesis/genesis-23.png\":391,\"artwork/genesis/genesis-24.png\":411,\"artwork/genesis/genesis-25.png\":393,\"artwork/genesis/genesis-26.png\":390,\"artwork/genesis/genesis-27.png\":386,\"artwork/genesis/genesis-28.png\":402,\"artwork/genesis/genesis-29.png\":375,\"artwork/genesis/genesis-30.png\":369,\"artwork/genesis/genesis-31.png\":421,\"artwork/genesis/genesis-32.png\":338,\"artwork/genesis/genesis-33.png\":389,\"artwork/genesis/genesis-34.png\":365,\"artwork/genesis/genesis-35.png\":352,\"artwork/genesis/genesis-36.png\":384,\"artwork/genesis/genesis-37.png\":365,\"artwork/genesis/genesis-38.png\":438,\"artwork/genesis/genesis-39.png\":374,\"artwork/genesis/genesis-40.png\":411,\"artwork/genesis/genesis-41.png\":380,\"artwork/genesis/genesis-42.png\":377,\"artwork/genesis/genesis-43.png\":399,\"artwork/genesis/genesis-44.png\":385,\"artwork/genesis/genesis-45.png\":352,\"artwork/genesis/genesis-46.png\":343,\"artwork/genesis/genesis-47.png\":362,\"artwork/genesis/genesis-48.png\":408,\"artwork/genesis/genesis-49.png\":325,\"artwork/genesis/genesis-50.png\":433,\"artwork/genesis/genesis-51.png\":377,\"artwork/genesis/genesis-52.png\":376,\"artwork/genesis/genesis-53.png\":401,\"artwork/genesis/genesis-54.png\":389,\"artwork/genesis/genesis-55.png\":361,\"artwork/genesis/genesis-56.png\":332,\"artwork/genesis/genesis-57.png\":414,\"artwork/genesis/genesis-58.png\":405,\"artwork/genesis/genesis-59.png\":409,\"artwork/genesis/genesis-60.png\":405,\"artwork/genesis/genesis-61.png\":369,\"artwork/genesis/genesis-62.png\":366,\"artwork/genesis/genesis-63.png\":379,\"artwork/genesis/genesis-64.png\":425,\"artwork/genesis/genesis-65.png\":406,\"artwork/genesis/genesis-66.png\":413,\"artwork/genesis/genesis-67.png\":350,\"artwork/genesis/genesis-68.png\":404,\"artwork/genesis/genesis-69.png\":392,\"artwork/genesis/genesis-70.png\":378,\"artwork/genesis/genesis-71.png\":401,\"artwork/genesis/genesis-72.png\":355,\"artwork/genesis/genesis-73.png\":405,\"artwork/genesis/genesis-74.png\":394,\"artwork/genesis/genesis-75.png\":390,\"artwork/genesis/genesis-76.png\":424,\"artwork/genesis/genesis-77.png\":381,\"artwork/tools/generate_genesis.py\":206,\"docs/SECURITY-REVIEW.md\":47,\"docs/TEST-RESULTS.md\":98,\"docs/VRF-ARCHITECTURE-REVIEW.md\":157,\"foundry.toml\":25,\"remappings.txt\":3,\"script/Deploy.s.sol\":78,\"src/BMCIlluminatiPass.sol\":581,\"src/StandardArt.sol\":236,\"src/experimental/BMCPassVRFPrototype.sol\":51,\"src/interfaces/IBMCToken.sol\":18,\"test/BMCIlluminatiPass.t.sol\":927,\"test/BMCMainnetFork.t.sol\":52,\"test/BMCPassVRFChainlinkIntegration.t.sol\":189,\"test/BMCPassVRFPrototype.t.sol\":64,\"test/ChainlinkMockIntegration.t.sol\":117,\"test/VRFRecovery.t.sol\":411,\"test/VRFRecoveryAdditional.t.sol\":71,\"test/VRFSafetyProperties.t.sol\":81,\"test/mocks/MockBMC.sol\":42,\"test/mocks/MockVRFCoordinator.sol\":72,\"website/app.js\":192,\"website/config.js\":11,\"website/index.html\":95,\"website/styles.css\":44},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":3421,"exitCode":0,"name":"slither","output":"[high/medium] reentrancy-balance at src/BMCIlluminatiPass.sol:322: Reentrancy in BMCIlluminatiPass._burnExact(address,uint256) (src/BMCIlluminatiPass.sol#322-333):\n[medium/medium] reentrancy-no-eth at src/BMCIlluminatiPass.sol:272: Reentrancy in BMCIlluminatiPass.mintMystery() (src/BMCIlluminatiPass.sol#272-285):\n[medium/medium] reentrancy-no-eth at src/BMCIlluminatiPass.sol:256: Reentrancy in BMCIlluminatiPass.mintGenesis() (src/BMCIlluminatiPass.sol#256-269):\n[medium/medium] uninitialized-local at src/experimental/BMCPassVRFPrototype.sol:25: BMCPassVRFPrototype.draw(uint256).pool (src/experimental/BMCPassVRFPrototype.sol#25) is a local variable never initialized\n[medium/medium] unused-return at src/BMCIlluminatiPass.sol:342: BMCIlluminatiPass.rawFulfillRandomWords(uint256,uint256[]) (src/BMCIlluminatiPass.sol#342-364) ignores return value by this.finalizeReveals(batch) (src/BMCIlluminatiPass.sol#363)\n[low/medium] reentrancy-benign at src/BMCIlluminatiPass.sol:256: Reentrancy in BMCIlluminatiPass.mintGenesis() (src/BMCIlluminatiPass.sol#256-269):\n[low/medium] reentrancy-benign at src/BMCIlluminatiPass.sol:322: Reentrancy in BMCIlluminatiPass._burnExact(address,uint256) (src/BMCIlluminatiPass.sol#322-333):\n[low/medium] reentrancy-benign at src/BMCIlluminatiPass.sol:272: Reentrancy in BMCIlluminatiPass.mintMystery() (src/BMCIlluminatiPass.sol#272-285):\n[low/medium] reentrancy-benign at src/BMCIlluminatiPass.sol:303: Reentrancy in BMCIlluminatiPass._requestRandomWord(uint256) (src/BMCIlluminatiPass.sol#303-319):\n[low/medium] reentrancy-benign at src/BMCIlluminatiPass.sol:272: Reentrancy in BMCIlluminatiPass.mintMystery() (src/BMCIlluminatiPass.sol#272-285):\n[low/medium] timestamp at src/BMCIlluminatiPass.sol:292: BMCIlluminatiPass.rerequestRandomness(uint256) (src/BMCIlluminatiPass.sol#292-300) uses timestamp for comparisons","passed":true},{"durationMs":876,"exitCode":0,"name":"aderyn","output":"[low] centralization-risk at src/BMCIlluminatiPass.sol:47: Centralization Risk (2 places)\n[low] costly-loop at src/BMCIlluminatiPass.sol:372: Costly operations inside loop\n[low] internal-function-used-once at src/StandardArt.sol:59: Internal Function Used Only Once (5 places)\n[low] large-numeric-literal at src/BMCIlluminatiPass.sol:90: Large Numeric Literal (7 places)\n[low] literal-instead-of-constant at src/StandardArt.sol:36: Literal Instead of Constant (27 places)","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"00ff9bf19e261f4acf2fe665cd7cf6b36686edda899e4c613fdfab4aefd0844e","verifiedTreeHash":"ae0daee3b29652de2811161c40278399c764f7df","verifierVersion":"0.1.0+fb8278c5"},{"checks":[{"durationMs":11565,"exitCode":0,"name":"build","output":"Compiling 72 files with Solc 0.8.26\nSolc 0.8.26 finished in 11.31s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to view\n  --> test/BMCPassVRFPrototype.t.sol:46:5:\n   |\n46 |     function testSameSeedProducesSameDistribution() public {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (2018): Function state mutability can be restricted to view\n  --> test/BMCPassVRFPrototype.t.sol:53:5:\n   |\n53 |     function testDifferentSeedsProduceDifferentDistributions() public {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":6215,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/BMCMainnetFork.t.sol:BMCMainnetForkTest\n[SKIP] test_RealBMCGenesisMint() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 360.81µs (109.86µs CPU time)\n\nRan 1 test for test/VRFRecoveryAdditional.t.sol:VRFRecoveryAdditionalTest\n[PASS] testLaterRandomnessStoredUntilEarlierRequestArrives() (gas: 1383548)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 11.31ms (1.02ms CPU time)\n\nRan 4 tests for test/ChainlinkMockIntegration.t.sol:ChainlinkMockIntegrationTest\n[PASS] test_NotAConsumerReverts() (gas: 367150)\n[PASS] test_RequestAndFulfilThroughChainlinkMock() (gas: 779150)\n[PASS] test_SeveralRequestsOutOfOrderThroughChainlinkMock() (gas: 1950517)\n[PASS] test_UnfundedSubscriptionKeepsTokenPendingUntilFunded() (gas: 969009)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 11.50ms (3.50ms CPU time)\n\nRan 2 tests for test/VRFSafetyProperties.t.sol:VRFSafetyPropertiesTest\n[PASS] testDelayedPredecessorDoesNotBurnAgainOrChangeSupply() (gas: 1393730)\n[PASS] testOnlyCoordinatorMayDeliverRandomness() (gas: 472409)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 11.82ms (1.25ms CPU time)\n\nRan 4 tests for test/BMCPassVRFPrototype.t.sol:BMCPassVRFPrototypeTest\n[PASS] testDifferentSeedsProduceDifferentDistributions() (gas: 1109240)\n[PASS] testDistributionHasNoDuplicates() (gas: 4831168)\n[PASS] testInvalidOutcomeReverts() (gas: 8495)\n[PASS] testSameSeedProducesSameDistribution() (gas: 1109275)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 128.74ms (19.96ms CPU time)\n\nRan 12 tests for test/VRFRecovery.t.sol:VRFRecoveryTest\n[PASS] testCallbackFailureLosesWordAndRerequestRecovers() (gas: 11793608)\n[PASS] testFirstDeliveredWordWinsOriginalFirst() (gas: 1192077)\n[PASS] testFirstDeliveredWordWinsReplacementFirst() (gas: 974415)\n[PASS] testFuzz_DeliveryOrderDoesNotChangeOutcomes(uint256,uint256) (runs: 64, μ: 4954639, ~: 4953169)\n[PASS] testLostEarlierRequestRecoversWithoutFreezingLaterTokens() (gas: 2322552)\n[PASS] testMinimumCallbackGasStoresWordThroughChainlinkMock() (gas: 11134333)\n[PASS] testOwnerHasNoSpecialRerequestPower() (gas: 563802)\n[PASS] testRerequestNotAllowedBeforeDelay() (gas: 725116)\n[PASS] testRerequestOnlyForTokensAwaitingRandomness() (gas: 761768)\n[PASS] testRerequestRateLimitedPerToken() (gas: 871324)\n[PASS] testRerequestUsesCurrentConfigAndRollsBackIfRequestFails() (gas: 989615)\n[PASS] testSeveralUnfundedTokensRecoverAfterFunding() (gas: 4524654)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 202.66ms (214.39ms CPU time)\n\nRan 7 tests for test/BMCPassVRFChainlinkIntegration.t.sol:BMCPassVRFChainlinkIntegrationTest\n[PASS] testCannotRequestSecondSeedBeforeTimeout() (gas: 325849)\n[PASS] testFirstDeliveredSeedWinsWhenOriginalArrivesLate() (gas: 659818)\n[PASS] testLostSeedRequestIsReplacedAfterTimeout() (gas: 725047)\n[PASS] testOnlyCoordinatorMaySupplySeed() (gas: 292102)\n[PASS] testPoC_PublicSeedExposesEveryPositionOutcome() (gas: 3182283)\n[PASS] testSeedCannotBeRequestedWhileMintingIsOpen() (gas: 31566)\n[PASS] testVRFSeedProducesExactUniqueInventory() (gas: 5903852)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 712.92ms (25.53ms CPU time)\n\nRan 43 tests for test/BMCIlluminatiPass.t.sol:BMCIlluminatiPassTest\n[PASS] testFuzz_SingleRevealAlwaysInInventory(uint256) (runs: 64, μ: 10982664, ~: 10980261)\n[PASS] test_CIDIsConstantAndUsedInGenesisMetadata() (gas: 1426565)\n[PASS] test_CallbackBatchIsGasAware() (gas: 12129868)\n[PASS] test_CallbackBatchWith500kRevealsThree() (gas: 12590278)\n[PASS] test_Constants() (gas: 82490)\n[PASS] test_ConstructorRejectsInvalidVRFConfig() (gas: 10746)\n[PASS] test_DeployScriptConfig() (gas: 10917420)\n[PASS] test_DeployScriptRequiresOwner() (gas: 5997455)\n[PASS] test_ExcessAllowanceOnlyBurnsPrice() (gas: 341754)\n[PASS] test_FinalizationOrderCannotBeChosen() (gas: 11976960)\n[PASS] test_FirstMinterGetsLegendaryGenesis1() (gas: 2070145)\n[PASS] test_FreshDeploymentMintsWithoutAnyOwnerCall() (gas: 307796)\n[PASS] test_FulfillRevealsAndLocksMetadata() (gas: 17272651)\n[PASS] test_Full500MintSimulation() (gas: 278134243)\n[PASS] test_FullMintOutWithInOrderFulfillment() (gas: 244467414)\n[PASS] test_GasPerRevealBudgetCoversMeasuredCost() (gas: 11583902)\nLogs:\n  first reveal gas: 129539\n  second reveal gas: 112439\n\n[PASS] test_GenesisRevertsAfterPhase1() (gas: 10532760)\n[PASS] test_Mints2To40AreSequentialGenesis() (gas: 14428859)\n[PASS] test_MysteryBurnRevertsIfVRFRequestFails() (gas: 10666741)\n[PASS] test_MysteryRevertsDuringPhase1() (gas: 455814)\n[PASS] test_MysteryUsesConstructorConfigWithoutOwnerCall() (gas: 10792755)\n[PASS] test_NoOwnerMintOrWithdrawSurface() (gas: 58291)\n[PASS] test_OnlyCoordinatorCanFulfill() (gas: 10898228)\n[PASS] test_OutOfOrderFulfillmentFinalizesInMintOrder() (gas: 12249861)\n[PASS] test_PendingMetadataIsNeutral() (gas: 13582571)\n[PASS] test_PermissionlessFinalizationWhenCallbackGasTooLow() (gas: 11065772)\n[PASS] test_Phase1CostsExactlyOneMillion() (gas: 390236)\n[PASS] test_Phase2CostsExactly350k() (gas: 10953694)\n[PASS] test_ReentrancyGenesisBlocked() (gas: 884668)\n[PASS] test_ReentrancyMysteryBlocked() (gas: 11367627)\n[PASS] test_RevertInsufficientAllowance() (gas: 179241)\n[PASS] test_RevertInsufficientBalance() (gas: 193091)\n[PASS] test_RevertNoAllowance() (gas: 144187)\n[PASS] test_RevertWhenTokenDecimalsUnexpected() (gas: 728852)\n[PASS] test_RevertWhenTokenDoesNotActuallyBurn() (gas: 745318)\n[PASS] test_StandardArtCombinationsUnique() (gas: 1482850)\n[PASS] test_StandardArtCoversEveryTrait() (gas: 743677)\n[PASS] test_StandardArtRendersWithoutGenesisTraits() (gas: 30070029)\n[PASS] test_StandardMetadataAfterReveal() (gas: 19961234)\n[PASS] test_TransfersAreStandard() (gas: 469634)\n[PASS] test_UnavailableRandomnessStaysPendingNoFallback() (gas: 10993135)\n[PASS] test_VRFConfigValidation() (gas: 253119)\n[PASS] test_ViewsRevertForNonexistent() (gas: 33596)\nSuite result: ok. 43 passed; 0 failed; 0 skipped; finished in 713.26ms (2.15s CPU time)\n\nRan 10 tests for test/VRFPrototypeProperties.t.sol:VRFPrototypePropertiesTest\n[PASS] testFuzz_DrawIsAPermutationWithExactInventory(uint256) (runs: 96, μ: 4886739, ~: 4886739)\n[PASS] testFuzz_PoC_PublicSeedLetsBuyerTakeAGenesisPositionForAnySeed(uint256) (runs: 64, μ: 667611, ~: 636462)\n[PASS] test_ArchitectureAHasNoPrecomputablePosition() (gas: 130517)\n[PASS] test_ConsumerLostSeedBlocksEveryPositionUntilRerequest() (gas: 4918965)\n[PASS] test_ConsumerRejectsWrongWordCountAndUnknownRequest() (gas: 323529)\n[PASS] test_ConsumerSeedIsFinalEvenAfterTwoOpenRequests() (gas: 2029027)\n[PASS] test_DrawEdgeSeeds() (gas: 14749001)\n[PASS] test_DrawGasExceedsMinimumCallbackBudget() (gas: 506619)\n[PASS] test_TierBoundaries() (gas: 33747)\n[PASS] test_TierOfOutcomeRejectsMax() (gas: 8591)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 722.84ms (984.10ms CPU time)\n\nRan 14 tests for test/VRFAdversarial.t.sol:VRFAdversarialTest\n[PASS] testFuzz_CallbackGasNeverCorruptsState(uint256,uint256) (runs: 200, μ: 839095, ~: 805318)\nLogs:\n  Bound result 519714\n\n[PASS] testPoC_OddsDrift_GenesisPoolEmptiesSoLaterMintsAreGuaranteedStandard(uint256) (runs: 128, μ: 24081129, ~: 24082584)\n[PASS] testPoC_OddsDrift_StandardPoolEmptiesSoLaterMintsAreGuaranteedGenesis() (gas: 237095682)\n[PASS] test_CallbackRejectsUnknownRequestAndEmptyWords() (gas: 511872)\n[PASS] test_CallbackWithSeveralWordsUsesOnlyTheFirst() (gas: 731828)\n[PASS] test_FullSaleWithLostRequestsRecoversExactInventory() (gas: 308556360)\n[PASS] test_MinimumCallbackGasAlwaysStoresTheWord() (gas: 13183646)\n[PASS] test_NoFallbackRandomness_WithoutAnyKeeperTheQueueWaits() (gas: 1422001)\n[PASS] test_OddsDrift_ConditionalProbabilityIsPublicAndMoves() (gas: 103188303)\n[PASS] test_OwnerCanStallMintsAndRerequestsButNotOutcomes() (gas: 1731368)\n[PASS] test_ReceiverHookCannotFinalizeOwnTokenOrRerequestIt() (gas: 1429031)\n[PASS] test_RerequestGriefingIsBoundedToOnePerTokenPerWeek() (gas: 2118632)\n[PASS] test_StoredWordBehindLostPredecessorCannotBeRerequested() (gas: 1914665)\n[PASS] test_ThreeOpenRequestsForOneTokenFirstDeliveredWins() (gas: 1297707)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 1.38s (2.76s CPU time)\n\nRan 1 test for test/VRFArchitectureInvariants.t.sol:VRFArchitectureInvariantsTest\n[PASS]\nVRFArchitectureInvariantsTest invariants:\n[PASS] invariant_AtMostOneWordPerToken\n[PASS] invariant_BurnConservation\n[PASS] invariant_NoRevealWithoutDelivery\n[PASS] invariant_OutcomesAreFinalAndUnique\n[PASS] invariant_PoolConservation\n[PASS] invariant_RequestMappingIsConsistent\n[PASS] invariant_RerequestGateHolds\n[PASS] invariant_RevealQueueIsAPrefixInMintOrder\n[PASS] invariant_SupplyCapAndPhaseOrder\n VRFArchitectureInvariantsTest invariants (runs: 48, calls: 3840, reverts: 0)\n\n╭-------------+----------------+-------+---------+----------╮\n| Contract    | Selector       | Calls | Reverts | Discards |\n+===========================================================+\n| PassHandler | deliver        | 414   | 0       | 0        |\n|-------------+----------------+-------+---------+----------|\n| PassHandler | finalize       | 456   | 0       | 0        |\n|-------------+----------------+-------+---------+----------|\n| PassHandler | mintGenesis    | 397   | 0       | 0        |\n|-------------+----------------+-------+---------+----------|\n| PassHandler | mintMystery    | 438   | 0       | 0        |\n|-------------+----------------+-------+---------+----------|\n| PassHandler | ownerSetConfig | 446   | 0       | 0        |\n|-------------+----------------+-------+---------+----------|\n| PassHandler | redeliver      | 423   | 0       | 0        |\n|-------------+----------------+-------+---------+----------|\n| PassHandler | rerequest      | 412   | 0       | 0        |\n|-------------+----------------+-------+---------+----------|\n| PassHandler | toggleReject   | 430   | 0       | 0        |\n|-------------+----------------+-------+---------+----------|\n| PassHandler | warp           | 424   | 0       | 0        |\n╰-------------+----------------+-------+---------+----------╯\n\nLogs:\n  Bound result 687701\n  Bound result 18\n  Bound result 1\n  Bound result 6\n  Bound result 19\n  Bound result 14\n  Bound result 687861\n  Bound result 15621\n  Bound result 278153\n  Bound result 7\n  Bound result 306605\n  Bound result 11\n  Bound result 10\n  Bound result 91240\n  Bound result 3\n  Bound result 1\n  Bound result 691197\n  Bound result 472936\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 5.94s (5.93s CPU time)\n\nRan 11 test suites in 5.95s (9.83s CPU time): 98 tests passed, 0 failed, 1 skipped (99 total tests)\n","passed":true},{"durationMs":1725,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"BMCIlluminatiPass.approve(address,uint256)\",\"BMCIlluminatiPass.finalizeReveals(uint256)\",\"BMCIlluminatiPass.mintGenesis()\",\"BMCIlluminatiPass.mintMystery()\",\"BMCIlluminatiPass.rawFulfillRandomWords(uint256,uint256[])\",\"BMCIlluminatiPass.renounceOwnership()\",\"BMCIlluminatiPass.rerequestRandomness(uint256)\",\"BMCIlluminatiPass.safeTransferFrom(address,address,uint256)\",\"BMCIlluminatiPass.safeTransferFrom(address,address,uint256,bytes)\",\"BMCIlluminatiPass.setApprovalForAll(address,bool)\",\"BMCIlluminatiPass.setVRFConfig(uint256,bytes32,uint32,uint16,bool)\",\"BMCIlluminatiPass.transferFrom(address,address,uint256)\",\"BMCIlluminatiPass.transferOwnership(address)\"],\"files\":{\".github/workflows/main.yml\":43,\".gitignore\":5,\"ADAPTATION.md\":64,\"README.md\":161,\"artwork/genesis/genesis-01.png\":7910,\"artwork/genesis/genesis-02.png\":6998,\"artwork/genesis/genesis-03.png\":7016,\"artwork/genesis/genesis-04.png\":6664,\"artwork/genesis/genesis-05.png\":7186,\"artwork/genesis/genesis-06.png\":6703,\"artwork/genesis/genesis-07.png\":6488,\"artwork/genesis/genesis-08.png\":6807,\"artwork/genesis/genesis-09.png\":6793,\"artwork/genesis/genesis-10.png\":6550,\"artwork/genesis/genesis-11.png\":6125,\"artwork/genesis/genesis-12.png\":6418,\"artwork/genesis/genesis-13.png\":6298,\"artwork/genesis/genesis-14.png\":5909,\"artwork/genesis/genesis-15.png\":6142,\"artwork/genesis/genesis-16.png\":5727,\"artwork/genesis/genesis-17.png\":6019,\"artwork/genesis/genesis-18.png\":399,\"artwork/genesis/genesis-19.png\":369,\"artwork/genesis/genesis-20.png\":359,\"artwork/genesis/genesis-21.png\":330,\"artwork/genesis/genesis-22.png\":385,\"artwork/genesis/genesis-23.png\":391,\"artwork/genesis/genesis-24.png\":411,\"artwork/genesis/genesis-25.png\":393,\"artwork/genesis/genesis-26.png\":390,\"artwork/genesis/genesis-27.png\":386,\"artwork/genesis/genesis-28.png\":402,\"artwork/genesis/genesis-29.png\":375,\"artwork/genesis/genesis-30.png\":369,\"artwork/genesis/genesis-31.png\":421,\"artwork/genesis/genesis-32.png\":338,\"artwork/genesis/genesis-33.png\":389,\"artwork/genesis/genesis-34.png\":365,\"artwork/genesis/genesis-35.png\":352,\"artwork/genesis/genesis-36.png\":384,\"artwork/genesis/genesis-37.png\":365,\"artwork/genesis/genesis-38.png\":438,\"artwork/genesis/genesis-39.png\":374,\"artwork/genesis/genesis-40.png\":411,\"artwork/genesis/genesis-41.png\":380,\"artwork/genesis/genesis-42.png\":377,\"artwork/genesis/genesis-43.png\":399,\"artwork/genesis/genesis-44.png\":385,\"artwork/genesis/genesis-45.png\":352,\"artwork/genesis/genesis-46.png\":343,\"artwork/genesis/genesis-47.png\":362,\"artwork/genesis/genesis-48.png\":408,\"artwork/genesis/genesis-49.png\":325,\"artwork/genesis/genesis-50.png\":433,\"artwork/genesis/genesis-51.png\":377,\"artwork/genesis/genesis-52.png\":376,\"artwork/genesis/genesis-53.png\":401,\"artwork/genesis/genesis-54.png\":389,\"artwork/genesis/genesis-55.png\":361,\"artwork/genesis/genesis-56.png\":332,\"artwork/genesis/genesis-57.png\":414,\"artwork/genesis/genesis-58.png\":405,\"artwork/genesis/genesis-59.png\":409,\"artwork/genesis/genesis-60.png\":405,\"artwork/genesis/genesis-61.png\":369,\"artwork/genesis/genesis-62.png\":366,\"artwork/genesis/genesis-63.png\":379,\"artwork/genesis/genesis-64.png\":425,\"artwork/genesis/genesis-65.png\":406,\"artwork/genesis/genesis-66.png\":413,\"artwork/genesis/genesis-67.png\":350,\"artwork/genesis/genesis-68.png\":404,\"artwork/genesis/genesis-69.png\":392,\"artwork/genesis/genesis-70.png\":378,\"artwork/genesis/genesis-71.png\":401,\"artwork/genesis/genesis-72.png\":355,\"artwork/genesis/genesis-73.png\":405,\"artwork/genesis/genesis-74.png\":394,\"artwork/genesis/genesis-75.png\":390,\"artwork/genesis/genesis-76.png\":424,\"artwork/genesis/genesis-77.png\":381,\"artwork/tools/generate_genesis.py\":206,\"docs/SECURITY-REVIEW.md\":47,\"docs/TEST-RESULTS.md\":98,\"docs/VRF-ARCHITECTURE-REVIEW.md\":157,\"foundry.toml\":25,\"remappings.txt\":3,\"script/Deploy.s.sol\":78,\"src/BMCIlluminatiPass.sol\":581,\"src/StandardArt.sol\":236,\"src/experimental/BMCPassVRFPrototype.sol\":51,\"src/interfaces/IBMCToken.sol\":18,\"test/BMCIlluminatiPass.t.sol\":927,\"test/BMCMainnetFork.t.sol\":52,\"test/BMCPassVRFChainlinkIntegration.t.sol\":189,\"test/BMCPassVRFPrototype.t.sol\":64,\"test/ChainlinkMockIntegration.t.sol\":117,\"test/VRF-INDEPENDENT-REVIEW.md\":136,\"test/VRFAdversarial.t.sol\":490,\"test/VRFArchitectureInvariants.t.sol\":380,\"test/VRFPrototypeProperties.t.sol\":196,\"test/VRFRecovery.t.sol\":411,\"test/VRFRecoveryAdditional.t.sol\":71,\"test/VRFSafetyProperties.t.sol\":81,\"test/mocks/MockBMC.sol\":42,\"test/mocks/MockVRFCoordinator.sol\":72,\"website/app.js\":192,\"website/config.js\":11,\"website/index.html\":95,\"website/styles.css\":44},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"87b9bf15fbb2ca03efd35adfee0eb7db2fe24f47e387272f1f8e0b72481ac2d1","verifiedTreeHash":"af34ff2cdeb6db0313593ffc0c440bd486f53963","verifierVersion":"0.1.0+fb8278c5"}]}