{"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":"fd7e1475-f106-44b3-98ff-c43efef5e88d","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"9cf71905ca2377009297dd0249525023505fae95291b3be047406249a0ed00b6","dependsOn":["build_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":"6b7a890ed69ed16f546a9097c5fc782a29d5c99d7ccc6b4a2dcc06a640d43342","dependsOn":["build_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":null,"dependsOn":["build_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","state":"failed"},{"acceptedSubmissionHash":"0e1a035bed885e84ca2427d706d72a8371a2565e6a2674004529f862c4979ddf","dependsOn":["build_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":"69ff3f07cba18bceb481c5b65f8b2f5ca9f4a070adb720832770dcd93be94b56","dependsOn":["build_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":"5d9ec4aac07564fad879f1b48eebac1e3dd540391d3986de1d947838bd5b4ff1","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"587567a5ef71546b5973ceaf6ef1dbbd590f49503b5ac9a6973ed86b89584857","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"587567a5ef71546b5973ceaf6ef1dbbd590f49503b5ac9a6973ed86b89584857","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":"42e664f3ee76261564ef7614f12b811419c5e2e371c35269c8cb088d2ea62c2c","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"4fb9931d2aa9da2055871a13a61bc66086fa9d26468ef0d3bf21b626da8c8669","dependsOn":["build_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":"A custom token: The One (NEO).\nToken name: The One\nToken symbol: NEO\nToken supply: 1 with 18 decimals, all minted once to the deployer in the constructor.\nWhat it does: At $100,000 market cap mint 1 NFT depicting Neo, the one. Transfer it to a random holder.","parentJobId":null,"planHash":"7b468efd0ce49be67558632a303cf4f4512b8a105703562289c93941092ab863","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"fd7e1475-f106-44b3-98ff-c43efef5e88d","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-1116-the-one"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51203","feedbackHash":"09f145e5e19c3164493db85beebe4531fe82213c94e2ec46acaa8ccc3b0c8c45","nodeKey":"audit_economics","submissionHash":"9cf71905ca2377009297dd0249525023505fae95291b3be047406249a0ed00b6","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52271","feedbackHash":"08277ace2c8880fc15b01f777f6a1a6cf1ef5e2f1318d16f9579c630309d03d8","nodeKey":"audit_flow","submissionHash":"6b7a890ed69ed16f546a9097c5fc782a29d5c99d7ccc6b4a2dcc06a640d43342","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52270","feedbackHash":"1fe6390d4ccd3b570711b1f363d8b8ac16a343963357c37e912a3de183b3f8ca","nodeKey":"audit_math","submissionHash":"0e1a035bed885e84ca2427d706d72a8371a2565e6a2674004529f862c4979ddf","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52252","feedbackHash":"5777226fc4e961b3298b40d63bd806115dfc2a8fe7dd807e3150c11f3e881893","nodeKey":"audit_permissions","submissionHash":"69ff3f07cba18bceb481c5b65f8b2f5ca9f4a070adb720832770dcd93be94b56","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52084","feedbackHash":"71640908c572cba802e8ad8dfa84ea6327becc946ec4eec3a49fa4834bfa1ed3","nodeKey":"build_contract_project","submissionHash":"5d9ec4aac07564fad879f1b48eebac1e3dd540391d3986de1d947838bd5b4ff1","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51224","feedbackHash":"f0edd2cac1c21851e54b2a7caf11d58308d70da040db7d0833763f980466fa05","nodeKey":"manifest","submissionHash":"42e664f3ee76261564ef7614f12b811419c5e2e371c35269c8cb088d2ea62c2c","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52087","feedbackHash":"6f234397f19c67b95843297bc9f4b04e3c8fa82d282433bea24ccbb56b2e55e2","nodeKey":"write_foundry_tests","submissionHash":"4fb9931d2aa9da2055871a13a61bc66086fa9d26468ef0d3bf21b626da8c8669","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"2cc999ae94bbe06c489ca3afa5862d54a259e0d4d537baaa88707340d5a3b2c4","state":"blocked","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f9a12c57a094ca8d","findings":[{"citation":"resolved","description":"Math Precision / Boundary (minimum-input check). The only numerical condition for an entry is balanceOfAt != 0, and award() selects uniformly over entries. A balance of 1 minor unit (1e-18 NEO) is therefore worth exactly as much as 0.5 NEO, and the number of entries an actor controls is bounded only by the number of minor units it holds, not by its share of supply. Because TheOne registers every address that ever receives a non-zero transfer (TheOne.sol:47-50) and the prize contract scans that registry without any weight or floor, an attacker who holds k wei can hold k entries. Measured in the scratch harness: creating one dust entry costs about 155,000 gas; scanning one costs about 32,000 gas. With 2 honest holders (0.5 NEO each) and 1000 dust addresses funded from 1000 wei, eligibleCount() is 1002 and every random word with word % 1002 >= 2 (99.8% of the word space) awards the NFT to an attacker address. The README documents that the draw is not Sybil-resistant and that dust recipients count, so this is a known design limit; it is reported because the objective is 'transfer it to a random holder' and under this formula the recipient is, in practice, whoever is willing to spend about 0.155 ETH-equivalent of gas at 1 gwei (or cents on an L2) before the threshold is observed. Any fix changes the agreed selection rule (balance weighting over the snapshot, or a minimum snapshot balance per entry), so it is a scope decision for the requester rather than a drop-in patch.","line":172,"path":"src/NeoAwakening.sol","reproduction":"State: block 100, token deployed, prize configured. Deployer transfers 0.5e18 to alice, 0.5e18-1000 to bob, then 1 wei to each of 1000 fresh addresses 0x10000..0x103E7 (same block). vm.roll(101); prize.trigger(); call prize.collectHolders(256) four times until state == ReadyToRequest. Expected (purpose): the two holders of 99.9999999999999% of supply dominate the draw. Actual: prize.eligibleCount() == 1002 with alice at index 0, bob at index 1 and the 1000 dust addresses at 2..1001. prize.requestRandomness(); coordinator fulfils with word 2; prize.award() -> prize.ownerOf(1) == 0x0000000000000000000000000000000000010000. Any word with word % 1002 >= 2 gives the same class of outcome.","severity":"medium","snippet":"            if (token.balanceOfAt(holder, snapshotBlock) != 0) _eligible.push(holder);","title":"Eligibility is a 1-wei boundary with equal weight: a 1000-wei holder buys ~99.8% of the draw"},{"citation":"resolved","description":"Boundary (self/sentinel address in an external-derived set). _eligible is built from every registry address with a non-zero snapshot balance and award() indexes it with randomWord % length and then calls _mint on the result. No address is excluded, including ones known at construction time to have no ERC-721 egress: NeoAwakening itself (it has no function that transfers a token it owns; ERC721.transferFrom requires msg.sender to be the owner or approved, and the contract never calls either), the TheOne ERC-20 contract, and the Uniswap v4 PoolManager, which by the launch design holds economics.poolBps of the supply and is therefore always an entrant with one share. Anyone can add the first two for 1 wei each (TheOne accepts transfers to any non-zero address). If the selection lands on any of them the single NFT is minted and permanently inaccessible, which is the prize's only purpose. With N eligible addresses the PoolManager alone is a 1/N chance of that outcome by construction; the README lists 'potentially inaccessible NFT recipients' as a known limit, but the three addresses above are deterministic dead ends rather than unknown custodians. Minimal fix that preserves the design: skip address(this), address(token) and any address the requester designates as non-custodial (e.g. the PoolManager) in collectHolders, or redraw the index over the remaining entries.","line":222,"path":"src/NeoAwakening.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {TheOne} from \"src/TheOne.sol\";\nimport {NeoAwakening} from \"src/NeoAwakening.sol\";\nimport {IPriceFeed} from \"src/interfaces/IPriceFeed.sol\";\nimport {IVRFCoordinatorV25} from \"src/interfaces/IVRFCoordinatorV25.sol\";\n\ncontract FeedStub is IPriceFeed {\n    function decimals() external pure returns (uint8) {\n        return 8;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, 100_000e8, block.timestamp, block.timestamp, 1);\n    }\n}\n\ncontract CoordinatorStub is IVRFCoordinatorV25 {\n    function requestRandomWords(RandomWordsRequest calldata) external pure returns (uint256) {\n        return 7;\n    }\n\n    function fulfill(NeoAwakening target, uint256 id, uint256 word) external {\n        uint256[] memory words = new uint256[](1);\n        words[0] = word;\n        target.rawFulfillRandomWords(id, words);\n    }\n}\n\n/// @notice Anyone can make the prize contract itself an entrant with 1 wei of NEO. If the draw lands on\n/// it, the only NFT is minted into NeoAwakening, which has no function that can move an ERC-721 out.\ncontract DeadEndWinnerTest is Test {\n    TheOne token;\n    NeoAwakening prize;\n    FeedStub feed;\n    CoordinatorStub vrf;\n    address alice = makeAddr(\"alice\");\n    address bob = makeAddr(\"bob\");\n\n    function setUp() public {\n        vm.roll(100);\n        vm.warp(1_000_000);\n        token = new TheOne();\n        prize = new NeoAwakening(address(token), address(this));\n        feed = new FeedStub();\n        vrf = new CoordinatorStub();\n        prize.setPriceFeed(address(feed), 1 hours);\n        prize.setRandomnessConfig(address(vrf), 1, bytes32(uint256(1)), 3, 150_000, false);\n        token.transfer(alice, 0.5 ether);\n        token.transfer(bob, 0.5 ether - 1);\n        // Registry order: deployer (balance 0 at snapshot), alice, bob, prize.\n        token.transfer(address(prize), 1);\n        vm.roll(101);\n    }\n\n    function testPrizeContractCannotBeAwardedToItself() public {\n        prize.trigger();\n        prize.collectHolders(256);\n        prize.requestRandomness();\n        // Eligible = [alice, bob, prize]; every word with word % 3 == 2 selects the prize contract.\n        vrf.fulfill(prize, prize.requestId(), 2);\n        prize.award();\n        address owner = prize.ownerOf(1);\n        assertTrue(owner != address(prize), \"NFT minted into NeoAwakening, which cannot transfer it out\");\n        assertTrue(owner != address(token), \"NFT minted into the ERC-20, which cannot transfer it out\");\n        // Nothing on NeoAwakening can move a token it owns: it is neither msg.sender-owner nor approved.\n        vm.expectRevert();\n        prize.transferFrom(address(prize), alice, 1);\n    }\n}","reproduction":"State: block 100, token deployed, prize configured. Deployer transfers 0.5e18 to alice, 0.5e18-1 to bob and 1 wei to address(prize). vm.roll(101); prize.trigger(); prize.collectHolders(256) -> _eligible == [alice, bob, address(prize)] (deployer excluded, snapshot balance 0). prize.requestRandomness(); coordinator fulfils with word 2 (any word with word % 3 == 2); prize.award(). Expected: the NFT ends with a holder who can transfer it. Actual: prize.ownerOf(1) == address(prize); prize.transferFrom(address(prize), alice, 1) from any caller reverts ERC721InsufficientApproval; no NeoAwakening function can move it. Same sequence with 1 wei to address(token) instead gives ownerOf(1) == address(token). The proof test fails on the current tree with 'NFT minted into NeoAwakening, which cannot transfer it out'.","severity":"low","snippet":"        address selected = _eligible[randomWord % _eligible.length];","title":"Eligible set admits dead-end addresses (the prize contract, the token, the PoolManager): a hit mints the only NFT where nothing can move it"}],"hash":"0e1a035bed885e84ca2427d706d72a8371a2565e6a2674004529f862c4979ddf","nodeId":"1329fd65-b007-461a-a625-bfd9b80805c0","outcome":"completed","summary":"Review complete. The findings file is at `.imd-findings.json` with two findings and coverage rows for all 17 entry points plus five invariant and lead rows.\n\n**Outcome of the math audit.** The arithmetic in both contracts holds under the Math Precision, Boundary and Numerical Gap checks. I traced every formula with concrete values and confirmed the edges with scratch tests:\n\n- Oracle normalisation is exact for every feed precision from 0 to 18, the overflow guard works, the threshold boundary is inclusive at exactly 100,000 USD, and the freshness window cannot underflow.\n- Snapshot block arithmetic cannot underflow, and a factory that empties itself in the deployment block is correctly excluded while its receivers are included.\n- Checkpoint casts cannot overflow, same-block churn collapses to the end-of-block value, and the eligible set can never be empty because supply is always held by registered addresses.\n- Batch cursor bounds never skip or double count, and the modulo selection has a non-zero divisor and negligible bias.\n\n**Two defects reported, both at the eligibility boundary.**\n\n1. **Medium.** Eligibility is a 1-wei boundary with equal weight. A holder of 1000 wei can create 1000 entries against two honest holders of 0.5 NEO each and wins about 99.8 percent of the time. Measured cost is about 155,000 gas per entry. The README acknowledges the draw is not Sybil-resistant, so I flagged this as a scope decision for the requester rather than a drop-in fix.\n2. **Low.** The eligible set admits dead-end addresses. The prize contract itself, the token contract, and by launch design the Uniswap v4 PoolManager can all be entrants, and none can move an ERC-721 out. A Foundry proof test is attached that fails on the current tree when the draw lands on the prize contract.\n\n**Not reached or not promoted.** The static analysis leads were checked and none held up as defects. The linear scan cost from dust registry growth was quantified at about 32,000 gas per holder and left as an acknowledged design limit, not a finding. Chain-specific coordinator limits and the real price feed adapter cannot be verified locally and remain operational responsibilities.","treeHash":null,"usage":{"cachedInputTokens":1343146,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":38900,"runtime":"claude","turns":38,"wallClockMs":557079}},{"artifacts":[],"attempt":1,"bundleHash":"f30ca7ce8586aa882cf02beba17182020f5b704e2524ba9d087afc04089a688b","device":"5dc170d977094c92","findings":[],"hash":"42e664f3ee76261564ef7614f12b811419c5e2e371c35269c8cb088d2ea62c2c","nodeId":"4ddd2ac4-b809-4d32-8943-b05f7bb9ca93","outcome":"completed","summary":"Created [launch.json](/home/imd1/.identitymd/work/fd7e1475-f106-44b3-98ff-c43efef5e88d/4ddd2ac4-b809-4d32-8943-b05f7bb9ca93/launch.json) with exact supply and economics, matching constructors, and owner configuration requirements.\n\nValidation against the supplied schema and compiled ABIs passed. `forge build` succeeded; `forge test` passed all 37 tests.\n\nOnly `launch.json` changed outside permitted scratch artifacts.","treeHash":"3f85bacfffa8953dcd02cd843f54efe6bbb6d8d9","usage":{"cachedInputTokens":195968,"inputTokens":37535,"model":"gpt-6-astra","outputTokens":3505,"runtime":"codex","turns":3,"wallClockMs":145631}},{"artifacts":[],"attempt":1,"bundleHash":"b7fbb885cc9fb65a360ecbd01add4da32445fcfdd68c47b481815346048627de","device":"553a378efc6a19cb","findings":[],"hash":"4fb9931d2aa9da2055871a13a61bc66086fa9d26468ef0d3bf21b626da8c8669","nodeId":"47e6559f-379f-4e05-9575-d9a7e80ab5fe","outcome":"completed","summary":"Added failure-path, boundary, fuzz, and draw lifecycle invariant tests under `test/`.\n\nOffline verification passed:\n- `forge build`\n- `forge test`: 52 passed, zero failures\n- Draw invariants: 24,576 randomized calls\n\nNo reproducible implementation defects found. All changes are within `test/`.","treeHash":"e2b17ba925aad6557f9f70adb889278c728312d1","usage":{"cachedInputTokens":1524480,"inputTokens":119275,"model":"gpt-6-astra","outputTokens":18113,"runtime":"codex","turns":7,"wallClockMs":495969}},{"artifacts":[],"attempt":1,"bundleHash":"c3e96d683a54796cd3f30091e17463bbda1679b95ba002c5f2b9f490a331be7a","device":"2564cef48373f7f3","findings":[],"hash":"5d9ec4aac07564fad879f1b48eebac1e3dd540391d3986de1d947838bd5b4ff1","nodeId":"724eef0c-d93f-42f4-817f-6cd4f5521df1","outcome":"completed","summary":"Implemented fixed-supply NEO, the $100,000-triggered VRF holder draw, and one NFT with embedded Neo artwork. Dependencies are vendored.\n\nVerified: `forge build`, all 37 local tests, and `forge fmt --check` pass.\n\n[README.md](/home/imd12/.identitymd/work/fd7e1475-f106-44b3-98ff-c43efef5e88d/724eef0c-d93f-42f4-817f-6cd4f5521df1/README.md) documents deployment, required oracle/VRF configuration, and operational limitations. The protected launch harness still requires network-supplied parameters.","treeHash":"709c22530ecbb770b2159127536aed78cab4c9f6","usage":{"cachedInputTokens":716160,"inputTokens":77258,"model":"gpt-6-astra","outputTokens":29627,"runtime":"codex","turns":6,"wallClockMs":829086}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"c0fc4ea4f50e3380","findings":[{"citation":"resolved","description":"collectHolders() admits every registry address with a nonzero end-of-block balance, with no exclusion of addresses that provably cannot act on an ERC-721. award() then uses _mint (no receiver check, by design) at src/NeoAwakening.sol:225. Two classes of such addresses exist. (a) Unprivileged amplifier: TheOne is a plain ERC-20, so anyone can transfer 1 wei to address(prize) or address(token) before the snapshot; both contracts expose no function that calls transferFrom/safeTransferFrom on themselves, so an NFT minted there is unreachable forever. (b) Baseline launch state: the factory seeds poolBps of the supply into the Uniswap v4 PoolManager and sends 10% to the MerkleDistributor in the launch transaction, so both are eligible entries in every launch from block one; neither can initiate an ERC-721 transfer. Right after launch, before any trades, the eligible set is exactly {MerkleDistributor, PoolManager, remainderTo}, i.e. a 2/3 chance the sole deliverable is burned if the requester's initialMarketCapWei already meets the $100,000 target or the price reaches it before organic holders appear. The README accepts contract winners as a design limit, but the self/token case is clearly unintended, and the PoolManager/distributor case is not a corner case: it is the state the launch creates. Impact: permanent loss of the one-of-one prize, which is the whole purpose of the application. Minimal fix preserving the design: skip holder == address(this) and holder == address(token) in collectHolders, and let the constructor take the addresses the launch itself makes holders of (PoolManager, and the distributor via factory.distributorOf(launchNumber)) as exclusions, or route the award through a claim(to) step that the selected address, or anyone on its behalf when it is a known non-actor, can complete.","line":172,"path":"src/NeoAwakening.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {TheOne} from \"src/TheOne.sol\";\nimport {NeoAwakening} from \"src/NeoAwakening.sol\";\nimport {IPriceFeed} from \"src/interfaces/IPriceFeed.sol\";\nimport {IVRFCoordinatorV25} from \"src/interfaces/IVRFCoordinatorV25.sol\";\n\ncontract FeedStub is IPriceFeed {\n    function decimals() external pure returns (uint8) {\n        return 8;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, 100_000e8, block.timestamp, block.timestamp, 1);\n    }\n}\n\ncontract CoordinatorStub is IVRFCoordinatorV25 {\n    function requestRandomWords(RandomWordsRequest calldata) external pure returns (uint256) {\n        return 7;\n    }\n\n    function fulfill(NeoAwakening target, uint256 id, uint256 word) external {\n        uint256[] memory words = new uint256[](1);\n        words[0] = word;\n        target.rawFulfillRandomWords(id, words);\n    }\n}\n\n/// @notice Anyone can make an address that can never move an ERC-721 (the prize contract itself, the\n/// token contract, a Uniswap v4 PoolManager, the MerkleDistributor) an eligible entry by sending it\n/// one wei of NEO before the snapshot. When the draw selects it, `award()` mints NFT 1 into it with\n/// `_mint` and no path exists to ever transfer it out: the sole deliverable is permanently lost.\ncontract StuckNftProofTest is Test {\n    TheOne token;\n    NeoAwakening prize;\n    FeedStub feed;\n    CoordinatorStub vrf;\n    address alice = makeAddr(\"alice\");\n    address bob = makeAddr(\"bob\");\n    address griefer = makeAddr(\"griefer\");\n\n    function setUp() public {\n        vm.roll(100);\n        vm.warp(1_000_000);\n        token = new TheOne();\n        prize = new NeoAwakening(address(token), address(this));\n        feed = new FeedStub();\n        vrf = new CoordinatorStub();\n        prize.setPriceFeed(address(feed), 1 hours);\n        prize.setRandomnessConfig(address(vrf), 1, keccak256(\"lane\"), 3, 150_000, false);\n        token.transfer(alice, 0.5 ether);\n        token.transfer(bob, 0.5 ether - 2);\n        token.transfer(griefer, 2);\n    }\n\n    function _draw(uint256 word) internal {\n        vm.roll(101);\n        prize.trigger();\n        prize.collectHolders(256);\n        prize.requestRandomness();\n        vrf.fulfill(prize, prize.requestId(), word);\n        prize.award();\n    }\n\n    function testPrizeContractItselfCanBeMadeAnEntryAndThenHoldsItsOwnNftForever() public {\n        // Unprivileged: one wei to the prize contract registers it as holder index 4 (deployer, alice, bob, griefer, prize).\n        vm.prank(griefer);\n        token.transfer(address(prize), 1);\n        // Eligible after the deployer (balance 0) is skipped: [alice, bob, griefer, prize]; word 3 selects index 3.\n        _draw(3);\n        address holder = prize.ownerOf(1);\n        // The NFT must land somewhere it can still be moved from. The prize contract exposes no ERC-721\n        // transfer initiated by itself, so this ownership is final and the award is lost.\n        assertTrue(holder != address(prize), \"NFT minted to NeoAwakening itself: no function can ever move it\");\n        assertTrue(holder == alice || holder == bob || holder == griefer, \"winner must be a holder that can act\");\n    }\n\n    function testTokenContractCanBeMadeAnEntryAndThenHoldsTheNftForever() public {\n        vm.prank(griefer);\n        token.transfer(address(token), 1);\n        _draw(3);\n        address holder = prize.ownerOf(1);\n        assertTrue(holder != address(token), \"NFT minted to the ERC-20 contract: no function can ever move it\");\n    }\n}","reproduction":"State: token deployed at block 100, deployer sends 0.5e18 to alice, 0.5e18-2 to bob, 2 wei to griefer; feed answers 100_000e8; coordinator configured. Griefer calls token.transfer(address(prize), 1) (any caller, 1 wei). Registry: [deployer, alice, bob, griefer, prize]. Block 101: trigger(); collectHolders(256) -> eligible = [alice, bob, griefer, prize] (deployer balance 0 skipped); requestRandomness(); coordinator delivers word 3; award(). Expected: NFT 1 owned by an address that can move it. Actual: prize.ownerOf(1) == address(prize); state == Awarded; no entry point of NeoAwakening calls transferFrom on itself, so the NFT is stuck permanently. Same with token.transfer(address(token), 1) -> ownerOf(1) == address(token). test/scratch/StuckNftProof.t.sol fails on this tree with 'NFT minted to NeoAwakening itself' and passes when collectHolders skips address(this) and address(token).","severity":"high","snippet":"            if (token.balanceOfAt(holder, snapshotBlock) != 0) _eligible.push(holder);","title":"Any address can be made an eligible entry for 1 wei, including contracts that can never move an ERC-721 (NeoAwakening itself, TheOne, the v4 PoolManager, the MerkleDistributor); award() then mints the"},{"citation":"resolved","description":"Trust-gap seam economics x asymmetry. The economic stake needed for one entry is 1 minor unit (1e-18 NEO) while the prize is a one-of-one NFT, and selection is uniform over addresses, not over balance. An attacker holding dust can create N entries for N transfers (measured 155.8M gas for 1,000 registrations in Foundry, about 156k gas each including checkpoint and registry pushes; on an L2 that is a few dollars for thousands of entries). trigger() is permissionless and snapshots block.number-1, so the attacker can also pick the moment: fund the addresses in block B-1 and trigger in block B. collectHolders is bounded per call but unbounded in total, so the same attacker also inflates the keeper's scan cost (N/256 transactions). The README states the draw is not Sybil-resistant; this finding records that the stated objective 'transfer it to a random holder' is therefore not delivered: in practice the NFT goes to whoever spends the most on dust. Minimal fixes that keep the equal-weight design: require a minimum snapshot balance per entry (e.g. 1e-6 of supply) or weight entries by snapshot balance; either bounds the cost of an entry to real exposure.","line":222,"path":"src/NeoAwakening.sol","reproduction":"State as in setUp of test/scratch/Leads.t.sol: alice 0.5e18, bob 0.5e18-10_000, attacker 10_000 wei. Attacker sends 1 wei to each of 1,000 fresh addresses (0xA000..0xA3E7) in block 100, then any caller triggers in block 101 and runs collectHolders(256) four times. Expected: alice and bob, who hold 99.999% of the supply, have the dominant chance. Actual: eligibleCount == 1003, of which 1,001 are attacker-controlled; sampling 64 keccak-derived random words gives 64 attacker-controlled outcomes; a uniform word selects alice or bob with probability 2/1003 ~ 0.2%. The scratch test testDustSybilDominatesTheDraw passes on this tree, printing the gas figure and the 64/64 count.","severity":"medium","snippet":"        address selected = _eligible[randomWord % _eligible.length];","title":"Entries cost 1 wei plus gas and are weighted per address, so an unprivileged dust-splitter takes the draw with near certainty; the 'random holder' guarantee does not hold against anyone willing to pay"},{"citation":"resolved","description":"Access x asymmetry, reported as a privileged trust assumption rather than a permission bypass. setRandomnessConfig accepts any address with code as the coordinator, and rawFulfillRandomWords trusts msg.sender == coordinator unconditionally. Nothing binds the configured address to a Chainlink deployment, and the lock at trigger only freezes whatever the owner chose last. The same holds for setPriceFeed: the owner decides when the $100,000 condition is 'observed'. So the requester (owner, also remainderTo and a holder) can: (1) wait for a favourable holder set, (2) point coordinator at a contract they control, (3) trigger, (4) deliver a chosen word that maps to their own address (or any address), (5) award. README line 74 says 'Ownership grants no balance control, exclusion, alternative winner, metadata mutation or post-trigger setting changes', and SECURITY.md line 13 says 'No owner power over ... NFT winner'. Both are false pre-trigger, which is the only time the owner matters. This is the intended ownership model and no unprivileged actor can exploit it, so it is not blocking; the documentation and the admission reviewers should treat the owner as fully trusted for fairness, or the design should pin the coordinator at construction (a static constructor argument) so the frozen address is reviewable before launch.","line":119,"path":"src/NeoAwakening.sol","reproduction":"Owner deploys OwnerCoordinator (returns requestId 1, exposes pick(word)). Owner calls setRandomnessConfig(address(ownerCoordinator), 1, keccak256('lane'), 3, 150_000, false) (passes: code.length != 0). Block 101: trigger(); collectHolders(256) -> eligible [alice, bob, attacker]; requestRandomness(); ownerCoordinator.pick(prize, 1); award(). Expected per README: owner has no way to choose the winner. Actual: prize.ownerOf(1) == bob, the owner's chosen index. Scratch test testOwnerCanChooseWinnerViaSelfControlledCoordinator passes on this tree.","severity":"low","snippet":"        coordinator = IVRFCoordinatorV25(coordinator_);","title":"Trust assumption not stated accurately: the owner can select the winner outright by configuring a self-controlled coordinator (or feed) before trigger, contradicting the README claim that ownership gr"},{"citation":"resolved","description":"Asymmetry between what the local validation checks (bounds 3..200 confirmations, 100k..2.5M gas, nonzero ids) and what the coordinator will accept (keyHash must be a gas lane on that chain, subId must exist and list this consumer, chain-specific limits). The only call that proves the configuration works, requestRandomWords, runs after the lock. trigger() is callable by anyone the instant the feed reads >= $100,000, so the owner has no guaranteed window to notice or correct a mistake: an unprivileged caller's trigger converts a recoverable misconfiguration into a permanent ReadyToRequest dead end, since requestRandomness reverts forever and setRandomnessConfig reverts ConfigurationLocked. Price feed has the same lock but is only read before the lock, so it is not affected. Preserving the design intent (no post-request reroll, no owner substitution of the randomness source once a request exists), the lock for the randomness parameters can move from trigger to the successful requestRandomness, or trigger could be restricted while the owner has not confirmed the configuration (e.g. an owner-only arm() step). Requires an owner error, so low; it is reported because the unprivileged trigger removes the owner's chance to fix it.","line":93,"path":"src/NeoAwakening.sol","reproduction":"Owner configures coordinator whose requestRandomWords reverts for the frozen parameters (stub reverts 'InvalidKeyHash', as the real coordinator does for an unknown gas lane). Block 101: attacker calls trigger() (succeeds: coordinator != 0, cap >= target); collectHolders(256). requestRandomness() reverts 'InvalidKeyHash'. Owner calls setRandomnessConfig(goodCoordinator, ...) -> reverts ConfigurationLocked. One year later requestRandomness() still reverts; state == ReadyToRequest; totalSupply() == 0 forever. Scratch test testRejectedRandomnessParametersStrandTheDrawForever passes on this tree.","severity":"low","snippet":"        if (state != State.AwaitingThreshold) revert ConfigurationLocked();","title":"Randomness configuration is frozen by a permissionless trigger before it has ever been exercised, so a coordinator-rejected parameter (wrong keyHash, wrong subscription, chain-specific limits) strands"}],"hash":"69ff3f07cba18bceb481c5b65f8b2f5ca9f4a070adb720832770dcd93be94b56","nodeId":"8de7e309-6a00-4621-ab61-85c73c1acf67","outcome":"completed","summary":"The review is complete. Findings are in `.imd-findings.json` with a coverage row for all 17 listed entry points plus three invariants, and no tracked file was changed.\n\n**What I found** (four findings, one with a Foundry proof):\n\n- **High, NFT can be minted into a dead end.** `collectHolders` admits any registry address with a nonzero snapshot balance, and `award` uses `_mint`. Anyone can send 1 wei to the prize contract or the token contract before the snapshot and make it an entry; if selected, the one-of-one NFT is owned by a contract with no transfer path, forever. The same holds for the two holders every launch creates by construction, the v4 PoolManager and the MerkleDistributor, which right after launch are two of the only three eligible entries. The proof in `test/scratch/StuckNftProof.t.sol` fails on this tree and passes when self and token are excluded.\n- **Medium, dust Sybil takes the draw.** Entries cost 1 wei plus gas and are weighted per address. In the scratch run, 1,000 dust registrations gave an attacker 1,001 of 1,003 entries against holders of 99.999% of the supply. The README acknowledges this, but the objective's \"random holder\" is not delivered.\n- **Low, trust assumption misdocumented.** The owner can set any code address as coordinator before trigger and deliver a chosen word, picking the winner. The README and SECURITY.md say ownership grants no \"alternative winner\" power. Reported as a trust assumption, not a bypass.\n- **Low, config frozen before first use.** Any caller's `trigger` locks randomness parameters that have never been exercised. A coordinator-rejected keyHash or subscription then strands the draw permanently, with no owner window to correct it.\n\n**Coverage.** All three guides were applied: the full permission map, inconsistent-guard and initialization checks, the four trust-gap seams, and the asymmetry diffs on request/fulfill, set/trigger lock, and the `_update` branches. ERC-20 and ERC-721 entry points, ownership, randomness delivery and the state machine hold. Oracle economics and the v4 launch harness itself were outside my area and not executed.","treeHash":null,"usage":{"cachedInputTokens":1300822,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":35173,"runtime":"claude","turns":38,"wallClockMs":460547}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"b414b10f97bca557","findings":[{"citation":"resolved","description":"collectHolders() gives every registry address whose end-of-snapshot-block balance is non-zero exactly one entry, regardless of balance. Eligibility costs one minor unit (1 wei of NEO) plus a transfer, and the attacker chooses the moment (anyone can call trigger() once the oracle reads >= USD 100,000). A holder of any size therefore converts the draw from 'random holder' into 'attacker wins with probability M/(N+M)' for M sybil addresses they fund, at roughly one ERC-20 transfer of gas per entry. The README records this as a known limit ('not Sybil-resistant'), but the requester's objective is 'transfer it to a random holder' and the implementation lets one party decide the winner for the price of gas, so it is reported for the requester to accept or change. Secondary effect of the same mechanism: every dust address also lengthens the scan collectHolders must complete (256 per call), so a large registry raises the cost of reaching ReadyToRequest for honest keepers. Possible fixes that keep the design: weight entries by snapshot balance (cumulative-balance selection over the eligible list), or require a minimum snapshot balance per entry; either choice is a requester decision.","line":172,"path":"src/NeoAwakening.sol","reproduction":"State: deployer transfers 0.3e18 to alice and 0.7e18 to bob at block 100; feed answers 100_000e8 (8 decimals); both services configured. Block 101: alice sends 1 wei to each of 98 fresh addresses she controls (98 x transfer(addr_i, 1)). Block 102: anyone calls trigger() -> snapshotBlock = 101; collectHolders(256) -> eligibleCount() == 100 (deployer excluded at 0 balance). For every randomWord in 0..99, eligibleAt(randomWord % 100) != bob in 99 of 100 cases, i.e. award() mints NFT 1 to an alice-controlled address with probability 0.99 although alice holds 30% of the supply. Expected per objective: a 'random holder'; actual: the outcome is chosen by whoever funds the most addresses. Verified with test/scratch/Repro.t.sol::test_sybilDominatesDraw (passes on current code, prints 99).","severity":"medium","snippet":"            if (token.balanceOfAt(holder, snapshotBlock) != 0) _eligible.push(holder);","title":"Draw is one-entry-per-address with no minimum balance: any holder splits 1-wei dust across addresses and makes the 'random holder' outcome near-certain for themselves"},{"citation":"resolved","description":"Every address with a non-zero snapshot balance is eligible and award() mints with _mint (no receiver check, deliberately so a contract cannot veto). In a custom-token launch the Uniswap v4 PoolManager always holds the pool's NEO and the MerkleDistributor holds unclaimed shares, so both are guaranteed entrants with one entry each; neither has any code path to transfer an ERC-721 out. The token contract itself, the NeoAwakening contract and 0x...dEaD become entrants if anyone sends them 1 wei. When randomWord selects one of these, the one-of-one prize is minted into a contract that can never move it: it is permanently lost, and no second round or administrative mint exists (state == Awarded forever). With E eligible addresses and D such dead entrants the probability of losing the prize is D/E; early in a launch with few holders this is material (e.g. 2 of 5 entrants). The README lists 'potentially inaccessible NFT recipients' as a known limit, but the objective is to transfer the NFT to a holder, and the pool manager and distributor are protocol fixtures, not holders. Design-preserving fixes: take $poolManager (and the factory, with distributorOf(launchNumber) looked up at collection time) as constructor arguments and skip them in collectHolders; and/or switch to a claim pattern where the winner (or an operator it approves) pulls the NFT, so an unreachable winner can be detected before the mint. Changing who is eligible is a requester decision.","line":222,"path":"src/NeoAwakening.sol","reproduction":"State: deploy a contract with no functions (stands in for the PoolManager) and send it 0.5e18 NEO; alice holds 0.5e18; block 101: trigger(); collectHolders(256) -> eligibleCount() == 2, eligibleAt(0) == the contract. requestRandomness(); coordinator delivers randomWord 0; award(). Actual: ownerOf(1) == the no-egress contract; transferFrom(contract, alice, 1) from the NeoAwakening owner reverts (ERC721InsufficientApproval) and there is no other path, so NFT 1 is stranded for good. Expected: the prize reaches a holder who can use it. Verified with test/scratch/Repro.t.sol::test_noEgressContractStrandsPrize.","severity":"medium","snippet":"        address selected = _eligible[randomWord % _eligible.length];\n        winner = selected;\n        state = State.Awarded;\n        _mint(selected, TOKEN_ID);","title":"Pool manager, distributor and other contracts without ERC-721 egress are entrants; if selected, award() mints the only NFT into an address that can never move it"},{"citation":"resolved","description":"configurable() allows setRandomnessConfig only in AwaitingThreshold, and trigger() (callable by anyone the moment the feed reads >= USD 100,000) moves the state out of it. The first and only moment the configuration is checked against the live coordinator is requestRandomness(), which is reachable only after the lock. The production VRF v2.5 coordinator reverts for an unregistered gas lane (InvalidKeyHash), for requestConfirmations outside the chain's [min,max], for callbackGasLimit above the chain maximum (GasLimitTooBig), and for a coordinator address that is any other contract; Chainlink also deprecates coordinators over time (v2 -> v2.5). None of these can be fixed from outside the contract (unlike subscription funding or consumer registration), so any one of them leaves the contract in ReadyToRequest permanently: no NFT, no reroll, no reconfiguration, and the owner's only other power (renounceOwnership) is disabled. The lock exists to stop an owner swapping the randomness source after a request is accepted; before any request has been accepted (requestId == 0) nothing is protected, because the owner could have chosen any coordinator before trigger anyway. Minimal design-preserving fix: let setRandomnessConfig run while state == ReadyToRequest && requestId == 0 (and keep it locked once a request id has been stored). The owner's trust position is unchanged; only the permanent-brick outcome goes away.","line":93,"path":"src/NeoAwakening.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {TheOne} from \"src/TheOne.sol\";\nimport {NeoAwakening} from \"src/NeoAwakening.sol\";\nimport {IPriceFeed} from \"src/interfaces/IPriceFeed.sol\";\nimport {IVRFCoordinatorV25} from \"src/interfaces/IVRFCoordinatorV25.sol\";\n\ncontract ProofFeed is IPriceFeed {\n    function decimals() external pure returns (uint8) {\n        return 8;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, 100_000e8, block.timestamp, block.timestamp, 1);\n    }\n}\n\n/// @dev Models the production VRF v2.5 coordinator's key-hash validation: a gas lane that is not\n/// registered reverts (InvalidKeyHash). A registered one is accepted and later fulfilled.\ncontract ProofCoordinator is IVRFCoordinatorV25 {\n    bytes32 public immutable registeredKeyHash;\n\n    error InvalidKeyHash(bytes32 keyHash);\n\n    constructor(bytes32 key) {\n        registeredKeyHash = key;\n    }\n\n    function requestRandomWords(RandomWordsRequest calldata req) external view returns (uint256) {\n        if (req.keyHash != registeredKeyHash) revert InvalidKeyHash(req.keyHash);\n        return 1;\n    }\n\n    function fulfill(NeoAwakening target, uint256 id, uint256 word) external {\n        uint256[] memory words = new uint256[](1);\n        words[0] = word;\n        target.rawFulfillRandomWords(id, words);\n    }\n}\n\n/// @notice Fails on the current code: once an unprivileged `trigger()` has locked a randomness\n/// configuration that the coordinator rejects, no request can ever be accepted and the owner cannot\n/// correct the configuration, so the draw is stuck forever. Passes once the owner may replace a\n/// randomness configuration while no request has been accepted (`requestId == 0`).\ncontract LockedConfigProofTest is Test {\n    bytes32 constant REAL_LANE = keccak256(\"registered gas lane\");\n    bytes32 constant TYPO_LANE = keccak256(\"typo\");\n\n    TheOne token;\n    NeoAwakening prize;\n    ProofFeed feed;\n    ProofCoordinator coordinator;\n    address requester = makeAddr(\"requester\");\n    address alice = makeAddr(\"alice\");\n    address keeper = makeAddr(\"keeper\");\n\n    function setUp() public {\n        vm.roll(100);\n        vm.warp(1_000_000);\n        token = new TheOne();\n        prize = new NeoAwakening(address(token), requester);\n        feed = new ProofFeed();\n        coordinator = new ProofCoordinator(REAL_LANE);\n        vm.startPrank(requester);\n        prize.setPriceFeed(address(feed), 1 hours);\n        prize.setRandomnessConfig(address(coordinator), 1, TYPO_LANE, 3, 150_000, false);\n        vm.stopPrank();\n        token.transfer(alice, 1 ether);\n        vm.roll(101);\n    }\n\n    function test_ownerCanRepairRejectedRandomnessConfigBeforeAnyRequestIsAccepted() public {\n        // Anyone may latch the threshold; the owner never gets to dry-run the request first.\n        vm.prank(keeper);\n        prize.trigger();\n        vm.prank(keeper);\n        prize.collectHolders(256);\n        assertEq(uint8(prize.state()), uint8(NeoAwakening.State.ReadyToRequest));\n\n        // The coordinator rejects the locked gas lane; the request reverts and can only ever revert.\n        vm.prank(keeper);\n        vm.expectRevert(abi.encodeWithSelector(ProofCoordinator.InvalidKeyHash.selector, TYPO_LANE));\n        prize.requestRandomness();\n        assertEq(prize.requestId(), 0, \"no request was ever accepted\");\n\n        // Expected: with no accepted request there is nothing to protect, so the owner can correct the\n        // gas lane and the draw completes. Actual on current code: ConfigurationLocked, forever.\n        vm.prank(requester);\n        prize.setRandomnessConfig(address(coordinator), 1, REAL_LANE, 3, 150_000, false);\n\n        vm.prank(keeper);\n        prize.requestRandomness();\n        assertEq(prize.requestId(), 1);\n        coordinator.fulfill(prize, 1, 0);\n        vm.prank(keeper);\n        prize.award();\n        assertEq(prize.ownerOf(1), alice);\n    }\n}","reproduction":"State: owner calls setRandomnessConfig(coordinator, 1, keccak256('typo'), 3, 150000, false) where the coordinator only accepts keyHash keccak256('registered gas lane'); alice holds the whole supply; feed reads 100_000e8. Block 101: a third party (keeper) calls trigger() then collectHolders(256) -> state == ReadyToRequest. requestRandomness() reverts with InvalidKeyHash(keccak256('typo')) and will revert on every retry. Owner calls setRandomnessConfig(coordinator, 1, keccak256('registered gas lane'), 3, 150000, false): actual = revert ConfigurationLocked(); expected = the configuration can be corrected because no request was ever accepted (requestId() == 0). totalSupply() stays 0 forever. Proof: test/scratch/LockedConfigProof.t.sol fails on current code with ConfigurationLocked() and passes once configurable() also admits state == ReadyToRequest && requestId == 0 (verified locally with that one-line change, then reverted).","severity":"medium","snippet":"        if (state != State.AwaitingThreshold) revert ConfigurationLocked();","title":"A permissionless trigger() freezes the randomness configuration before it has ever been exercised; if the coordinator rejects it, requestRandomness() reverts forever and the owner cannot repair it"}],"hash":"6b7a890ed69ed16f546a9097c5fc782a29d5c99d7ccc6b4a2dcc06a640d43342","nodeId":"bc1d80b0-200e-4271-8f2e-a04b7220a0e4","outcome":"completed","summary":"The review is complete. No tracked file changed, the existing suite still passes, and the findings file is in place.\n\n**Outcome:** three medium findings, all reproduced in scratch tests, written to `.imd-findings.json` with a coverage row for every one of the 17 listed entry points plus four invariant rows. No critical or high defect was found. The state machine, oracle validation, VRF wire format, callback authentication, checkpointed snapshot, and launch-flow behaviour of the token all held under tracing and under the existing invariant suite.\n\n**Findings**\n\n1. **Sybil-decided draw.** Each registry address with a non-zero snapshot balance gets exactly one entry, with no weight and no minimum. A holder of 30% of supply who spreads 1 wei to 98 fresh addresses wins with probability 0.99. The README acknowledges the limit, but the objective says \"random holder,\" so it is reported for the requester to accept or change.\n2. **Stranded prize.** The Uniswap v4 PoolManager and the MerkleDistributor are guaranteed entrants and have no way to move an ERC-721. If the random word selects one of them, the one-of-one NFT is minted into a contract that can never transfer it, and there is no second round. Suggested fix keeps the design: exclude the launch fixtures by constructor argument, or use a claim pattern.\n3. **Unrepairable VRF configuration.** A permissionless `trigger()` freezes the randomness settings before the coordinator has ever validated them. A wrong gas lane, out-of-range confirmations, an over-limit callback gas, or a deprecated coordinator leaves the contract in ReadyToRequest forever. This one carries a proof file that fails on the current code with `ConfigurationLocked()` and passes under a one-line change allowing reconfiguration while no request has been accepted. I verified both directions locally and restored the source afterwards.\n\n**Static-analysis leads checked and not promoted:** the reentrancy lines on `requestRandomness` are guarded by `nonReentrant` and the Requesting state; the `encodePacked` line in NeoArt is concatenation, not a hash input; the unused checkpoint returns and the bounded external calls in the collection loop are benign.\n\n**Coverage limits:** the Uniswap v4 protected harness could not run here because its factory and liquidity helpers are not in this tree, so launch-flow exactness rests on the local factory fixture tests and on reading the token's transfer path. Chain-specific behaviour of `block.number` on L2s was not assessed because no chain is named.","treeHash":null,"usage":{"cachedInputTokens":1335006,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":37345,"runtime":"claude","turns":38,"wallClockMs":563261}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"896d1238054266ca","findings":[{"citation":"resolved","description":"Eligibility is one entry per address with any nonzero balance at the snapshot block (src/NeoAwakening.sol:172), and award() picks `_eligible[randomWord % _eligible.length]` (src/NeoAwakening.sol:222). Entries are not weighted by balance and there is no minimum stake, so the economic guarantee of the product (\"transfer it to a random holder\") is purchasable with gas alone. At the USD 100,000 cap one minor unit of NEO is worth USD 1e-13, so an attacker who buys K wei from the pool and sends 1 wei to K fresh addresses in any block before trigger() owns K of the K+H entries. Measured on this code (test/scratch/GasCost.t.sol): a 1-wei transfer to a fresh address costs about 155,300 gas (registry push + two checkpoint pushes + balance slot). Economics: K=1,000 costs ~155M gas (about 0.16 ETH at 1 gwei on mainnet, a few cents on an L2) for a 1000/(1000+H) win probability; with H=1,000 real holders, K=10,000 (~1.55B gas, ~1.5 ETH at 1 gwei) gives ~91%. Because trigger() is permissionless and snapshots block N-1, the attacker can seed in block N-1 and call trigger() in block N the moment the feed reads >= USD 100,000, leaving honest holders no window. The same dust mechanism is also a griefing vector: 1 wei sent to K uncontrolled addresses (0x...dead, precompiles, the token contract itself) makes the prize unrecoverable with probability K/(K+H), and every entry costs uncompensated keepers ~32,000 gas to scan (8.2M gas per full 256-entry collectHolders batch). The README records that the draw is not Sybil-resistant, so the author has accepted address splitting as a design limit; it is reported here because it is the dominant economic failure of the stated guarantee, the attack is unprivileged, cheap and deterministic, and the loser is every genuine holder. Minimal fixes that keep the design: weight the selection by snapshot balance (cumulative balances, pick by randomWord % totalEligibleBalance), or require a minimum snapshot balance per entry. Note that balance weighting makes the sink problem in finding 2 more likely, so both should be fixed together.","line":172,"path":"src/NeoAwakening.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {TheOne} from \"src/TheOne.sol\";\nimport {NeoAwakening} from \"src/NeoAwakening.sol\";\nimport {IPriceFeed} from \"src/interfaces/IPriceFeed.sol\";\nimport {IVRFCoordinatorV25} from \"src/interfaces/IVRFCoordinatorV25.sol\";\n\ncontract FeedStub is IPriceFeed {\n    function decimals() external pure returns (uint8) {\n        return 8;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, 100_000e8, block.timestamp, block.timestamp, 1);\n    }\n}\n\ncontract CoordinatorStub is IVRFCoordinatorV25 {\n    function requestRandomWords(RandomWordsRequest calldata) external pure returns (uint256) {\n        return 7;\n    }\n\n    function fulfill(NeoAwakening target, uint256 word) external {\n        uint256[] memory words = new uint256[](1);\n        words[0] = word;\n        target.rawFulfillRandomWords(7, words);\n    }\n}\n\n/// @notice An attacker holding 1000 wei of NEO (worth about USD 1e-10 at the USD 100,000 cap) splits it\n/// across 1000 fresh addresses and thereby owns 1000 of the 1002 entries in the draw. The two real\n/// holders, who hold 99.9999999999999% of the supply, win with probability 2/1002.\ncontract SybilDustTest is Test {\n    TheOne token;\n    NeoAwakening prize;\n    CoordinatorStub vrf;\n    address alice = makeAddr(\"alice\");\n    address bob = makeAddr(\"bob\");\n    address attacker = makeAddr(\"attacker\");\n    uint256 constant DUST_ADDRESSES = 1000;\n\n    function setUp() public {\n        vm.roll(100);\n        vm.warp(1_000_000);\n        token = new TheOne();\n        prize = new NeoAwakening(address(token), address(this));\n        vrf = new CoordinatorStub();\n        prize.setPriceFeed(address(new FeedStub()), 1 hours);\n        prize.setRandomnessConfig(address(vrf), 1, keccak256(\"lane\"), 3, 150_000, false);\n        // Two real holders, plus the attacker buying a negligible 1000 wei of NEO.\n        token.transfer(alice, 0.3 ether);\n        token.transfer(bob, 0.7 ether - DUST_ADDRESSES);\n        token.transfer(attacker, DUST_ADDRESSES);\n        vm.roll(101);\n        // Block 101: the attacker funds 1000 fresh addresses with 1 wei each.\n        for (uint256 i; i < DUST_ADDRESSES; ++i) {\n            vm.prank(attacker);\n            token.transfer(address(uint160(0x51B1_0000 + i)), 1);\n        }\n        // Block 102: the attacker (or anyone) triggers; the snapshot is block 101.\n        vm.roll(102);\n        prize.trigger();\n        while (prize.state() == NeoAwakening.State.Snapshotting) {\n            prize.collectHolders(256);\n        }\n        prize.requestRandomness();\n    }\n\n    function _isDust(address a) internal pure returns (bool) {\n        return uint160(a) >= 0x51B1_0000 && uint160(a) < 0x51B1_0000 + DUST_ADDRESSES;\n    }\n\n    /// @dev Fails now: for these words the selected entry is a 1-wei attacker address.\n    /// Passes once selection weights entries by snapshot balance (or applies a minimum stake), because\n    /// 1000 wei out of 1e18 cannot win for these words under any balance-weighted rule.\n    function test_realHoldersWinForWordsThatCurrentlyLandOnDust() public {\n        assertEq(prize.eligibleCount(), 2 + DUST_ADDRESSES, \"entries: alice, bob, 1000 dust\");\n        vrf.fulfill(prize, 500);\n        prize.award();\n        address winner = prize.ownerOf(1);\n        assertFalse(_isDust(winner), \"a 1-wei attacker address won the one-of-one NFT\");\n        assertTrue(winner == alice || winner == bob, \"NFT must go to a real holder\");\n    }\n}","reproduction":"State: alice 0.3 NEO, bob 0.7 NEO - 1000 wei, attacker 1000 wei (block 100). Block 101: attacker sends 1 wei to 1000 fresh addresses. Block 102: anyone calls trigger() (feed 100_000e8, 8 decimals), collectHolders(256) x4, requestRandomness(). Expected: a holder-proportional or at least honest-majority outcome. Actual: eligibleCount()==1002, eligibleAt(2..1001) are the attacker's dust addresses; coordinator word 500 -> award() mints token 1 to eligibleAt(500), an attacker address; 1000 of every 1002 words select the attacker. Run: forge test --match-path test/scratch/SybilDust.t.sol (fails on the current code: the winner is a dust address).","severity":"medium","snippet":"            if (token.balanceOfAt(holder, snapshotBlock) != 0) _eligible.push(holder);","title":"Equal-weight entries let 1-wei dust addresses buy the draw: 1000 wei of NEO split over 1000 fresh addresses takes 99.8% of the one-of-one NFT"},{"citation":"resolved","description":"Every address with a nonzero snapshot balance is an entrant, and award() uses _mint with no receiver check (src/NeoAwakening.sol:225). In a custom-token launch the PoolManager always holds the pool reserves (economics.poolBps of the supply, seeded by the factory in the launch transaction) and the MerkleDistributor holds whatever swarm share is unclaimed, so both are entrants in every draw. Uniswap v4's PoolManager has no function that can call transferFrom/approve on an ERC-721 it owns and no onERC721Received, and its only outbound token calls use the `transfer(address,uint256)` selector, which OpenZeppelin ERC721 does not implement; a token minted to it is permanently locked. The same applies to TheOne itself, NeoAwakening itself, and any other contract that receives NEO without ERC-721 egress. The flow therefore completes without reverting while the end state contradicts the product (the NFT is awarded to nobody). With N eligible entries the per-draw probability of destroying the prize is at least 1/N (PoolManager) and typically 2/N (plus the distributor): for a launch with 48 traders that is about 4%. The README lists contract winners as a known limit; it is reported because the loss is total, the sink entries are created by the launch itself rather than by any holder's choice, and the deliberate `_mint` rationale (a receiver must not be able to veto the draw) is satisfied by a deterministic fallback. Minimal fix: in award(), if the selected address has code and does not return IERC721Receiver.onERC721Received (use ERC721's _checkOnERC721Received in a try/catch or a staticcall-safe probe), advance deterministically to the next eligible index (so a contract can only decline, never choose), or exclude the pool manager and distributor from _eligible at collection time using the launch addresses the token constructor can take.","line":225,"path":"src/NeoAwakening.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {TheOne} from \"src/TheOne.sol\";\nimport {NeoAwakening} from \"src/NeoAwakening.sol\";\nimport {IPriceFeed} from \"src/interfaces/IPriceFeed.sol\";\nimport {IVRFCoordinatorV25} from \"src/interfaces/IVRFCoordinatorV25.sol\";\n\ncontract FeedStub is IPriceFeed {\n    function decimals() external pure returns (uint8) {\n        return 8;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, 100_000e8, block.timestamp, block.timestamp, 1);\n    }\n}\n\ncontract CoordinatorStub is IVRFCoordinatorV25 {\n    function requestRandomWords(RandomWordsRequest calldata) external pure returns (uint256) {\n        return 7;\n    }\n\n    function fulfill(NeoAwakening target, uint256 word) external {\n        uint256[] memory words = new uint256[](1);\n        words[0] = word;\n        target.rawFulfillRandomWords(7, words);\n    }\n}\n\n/// @notice Stands in for the Uniswap v4 PoolManager (or the MerkleDistributor): a contract that holds\n/// NEO as pool reserves but has no function that can call transferFrom on an ERC-721 it owns and does\n/// not implement onERC721Received. Anything minted here is lost forever.\ncontract PoolManagerLike {\n    // No ERC-721 egress and no receiver hook, exactly like v4's PoolManager.\n}\n\ncontract NftSinkTest is Test {\n    TheOne token;\n    NeoAwakening prize;\n    CoordinatorStub vrf;\n    PoolManagerLike poolManager;\n    address alice = makeAddr(\"alice\");\n    address bob = makeAddr(\"bob\");\n\n    function setUp() public {\n        vm.roll(100);\n        vm.warp(1_000_000);\n        token = new TheOne();\n        prize = new NeoAwakening(address(token), address(this));\n        vrf = new CoordinatorStub();\n        poolManager = new PoolManagerLike();\n        prize.setPriceFeed(address(new FeedStub()), 1 hours);\n        prize.setRandomnessConfig(address(vrf), 1, keccak256(\"lane\"), 3, 150_000, false);\n        // Launch shape: the pool manager holds the seeded reserves, the rest is with two traders.\n        token.transfer(address(poolManager), 0.5 ether);\n        token.transfer(alice, 0.3 ether);\n        token.transfer(bob, 0.2 ether);\n        vm.roll(101);\n        prize.trigger();\n        prize.collectHolders(256);\n        prize.requestRandomness();\n    }\n\n    /// @dev The random word that selects the pool manager under `randomWord % eligibleCount`.\n    function _wordSelectingSink() internal view returns (uint256) {\n        uint256 n = prize.eligibleCount();\n        for (uint256 i; i < n; ++i) {\n            if (prize.eligibleAt(i) == address(poolManager)) return i;\n        }\n        revert(\"pool manager is not an entrant\");\n    }\n\n    /// @dev Fails now: the word selecting the pool manager's entry mints the one-of-one NFT into a\n    /// contract that can never move it. Passes once the draw refuses to settle on a recipient that\n    /// cannot take custody of an ERC-721 (no onERC721Received) and continues to one that can.\n    function test_prizeIsNotMintedIntoAContractThatCannotMoveIt() public {\n        assertEq(prize.eligibleCount(), 3, \"entries: pool manager, alice, bob\");\n        vrf.fulfill(prize, _wordSelectingSink());\n        prize.award();\n        address owner = prize.ownerOf(1);\n        assertTrue(owner != address(poolManager), \"the one-of-one NFT was minted into the pool manager and is lost\");\n        assertTrue(owner == alice || owner == bob, \"NFT must reach an account that can hold it\");\n    }\n\n    /// @dev Documents the loss: no third party can move the token out, and the sink has no call path of its own.\n    function test_nobodyElseCanMoveTheNftOutOfTheSink() public {\n        vrf.fulfill(prize, _wordSelectingSink());\n        prize.award();\n        if (prize.ownerOf(1) != address(poolManager)) return; // already fixed\n        vm.expectRevert();\n        prize.transferFrom(address(poolManager), alice, 1);\n        vm.prank(alice);\n        vm.expectRevert();\n        prize.transferFrom(address(poolManager), alice, 1);\n        assertEq(prize.ownerOf(1), address(poolManager));\n        assertEq(address(poolManager).code.length > 0, true, \"sink is a contract\");\n        assertTrue(false, \"prize permanently locked in a contract with no ERC-721 egress\");\n    }\n}","reproduction":"State: PoolManagerLike (contract with no code paths) holds 0.5 NEO, alice 0.3, bob 0.2 (block 100). Block 101: trigger(), collectHolders(256) -> eligible = [poolManager, alice, bob]; requestRandomness(); coordinator delivers word 0 (any word with word % 3 == 0). Expected: the NFT reaches an account that can hold and move it. Actual: ownerOf(1) == poolManager; transferFrom(poolManager, alice, 1) from any third party reverts ERC721InsufficientApproval and the sink has no call path of its own, so token 1 is lost forever. Run: forge test --match-path test/scratch/NftSink.t.sol (both tests fail on the current code).","severity":"medium","snippet":"        _mint(selected, TOKEN_ID);","title":"award() can mint the one-of-one NFT into the Uniswap v4 PoolManager (or another contract with no ERC-721 egress), destroying the prize with probability 1/eligibleCount per draw"},{"citation":"resolved","description":"requestRandomness() latches requestId and moves to WaitingForRandomness as soon as the coordinator returns a nonzero id (src/NeoAwakening.sol:197-200). From that state the only exit is a callback from the coordinator carrying that exact id; there is no timeout, re-request or owner override, and setRandomnessConfig is locked from trigger() onward. Chainlink's VRFCoordinatorV2_5 deliberately does not validate keyHash at request time (a request for an unserved gas lane is accepted and never fulfilled) and does not check the subscription balance at request time (an underfunded subscription fails at fulfilment, and a request that stays unfundable lapses). setRandomnessConfig only checks keyHash != 0 and subscriptionId != 0 (src/NeoAwakening.sol:116). Because trigger() is callable by anyone the moment a fresh feed reading is >= USD 100,000, and requestRandomness() by anyone the moment collection ends, an owner who configured a wrong lane, the wrong billing currency (nativePayment) or an unfunded subscription has no opportunity to correct it: an unprivileged keeper freezes the configuration and commits the request. The outcome is a permanent dependency failure: the NFT is never minted, award() reverts WrongState forever, while the snapshot and the holders' expectations remain committed. The README documents this as an accepted liveness trade-off and the trigger requires an owner misconfiguration, so this is recorded as a low-severity trust/operational finding rather than an exploit. Possible minimal mitigations that preserve the no-reroll commitment: allow a re-request only after a long timeout (e.g. 7 days) with the same snapshot, or require the owner to arm the request (a one-time `armRequest()` after funding) before requestRandomness() can be called.","line":200,"path":"src/NeoAwakening.sol","reproduction":"State: owner calls setRandomnessConfig(coordinator, 1, keccak256(\"typo lane\"), 3, 150000, false) (passes validation); alice holds 1 NEO; feed at 100_000e8. Block 101: alice calls trigger(), collectHolders(256), requestRandomness(); the coordinator (real v2.5 behaviour, mirrored by the stub) returns id 42 without validating the lane. Expected: a correctable state or an eventual draw. Actual: state == WaitingForRandomness forever; after 365 days award() reverts WrongState(WaitingForRandomness), requestRandomness() reverts WrongState, setRandomnessConfig reverts ConfigurationLocked, totalSupply() == 0. Demonstrated by test/scratch/StuckRequest.t.sol (a behaviour test, not a fix-gating proof).","severity":"low","snippet":"        state = State.WaitingForRandomness;","title":"A VRF request the coordinator accepts but never fulfils freezes the draw permanently, and permissionless trigger()/requestRandomness() remove the owner's window to correct the configuration or fund th"}],"hash":"9cf71905ca2377009297dd0249525023505fae95291b3be047406249a0ed00b6","nodeId":"e718cc65-d4f3-4f2d-89ed-a84d449cc303","outcome":"completed","summary":"Review complete. The findings are in `.imd-findings.json` at the repository root, with coverage rows for all 17 listed entry points plus three invariants.\n\n**What I found**\n\n- **Medium, dust-address Sybil control of the draw.** One entry per address with any nonzero snapshot balance, unweighted. An attacker who buys 1000 wei of NEO and sends 1 wei to 1000 fresh addresses owns 1000 of 1002 entries. Measured cost on this code is about 155k gas per dust address, and each entry then costs uncompensated keepers about 32k gas to scan. The same mechanism griefs the prize into dead addresses. The README calls the draw not Sybil-resistant, so I flagged it as a documented design limit but reported it because it is the dominant economic failure of \"random holder\". Proof attached, fails now.\n- **Medium, prize minted into a contract with no ERC-721 egress.** The Uniswap v4 PoolManager always holds the pool reserves and the distributor holds unclaimed swarm share, so both are entrants in every draw. The award uses a plain mint with no receiver check, so with probability at least 1/N the one-of-one NFT is locked forever. Proof attached, fails now. Note that the natural fix for the Sybil issue, balance weighting, makes this one more likely, so both need fixing together.\n- **Low, no recovery from an accepted but never fulfilled randomness request.** The coordinator does not validate the gas lane or subscription balance when accepting a request. Since trigger and request are permissionless, an owner misconfiguration gets frozen and committed before they can correct it. Documented as a liveness trade-off, reported with the concrete stuck state and no proof.\n\n**What holds**\n\nToken conservation, registry completeness, checkpoint ordering, the block N minus 1 snapshot against same-block and flash balances, the configuration lock, coordinator authentication, duplicate fulfilment handling, the single mint, and oracle normalisation all traced as intended. The slither reentrancy lead on the request path is blocked by the guard and the Requesting state. The aderyn hash-collision lead is a false positive since nothing is hashed.\n\n**Not reached**\n\nThe real MerkleDistributor and PoolManager code are not in the tree, so the sink finding relies on the v4 PoolManager's known interface and a stand-in contract. Live feed and VRF behaviour were not exercised against production services.","treeHash":null,"usage":{"cachedInputTokens":1593548,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":50236,"runtime":"claude","turns":42,"wallClockMs":773329}}],"verification":[{"checks":[{"durationMs":2146,"exitCode":0,"name":"build","output":"Compiling 51 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.03s\nCompiler run successful!\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/NeoAwakening.sol:29:49\n   │\n29 │     bytes4 private constant EXTRA_ARGS_V1_TAG = bytes4(keccak256(\"VRF ExtraArgsV1\"));\n   │                                                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'bytes4' is safe because [explain why]\n   │       // forge-lint: disable-next-line(unsafe-typecast)\n   │       \n   │       \n   ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/NeoAwakening.sol:140:85\n    │\n140 │             round == 0 || answer <= 0 || startedAt == 0 || startedAt > updatedAt || updatedAt > block.timestamp\n    │                                                                                     ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/NeoAwakening.sol:143:13\n    │\n143 │         if (block.timestamp - updatedAt > maxPriceAge) revert StalePrice();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/NeoAwakening.sol:145:13\n    │\n145 │         if (uint256(answer) > type(uint256).max / scale) revert InvalidOracleRound();\n    │             ━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/NeoAwakening.sol:146:16\n    │\n146 │         return uint256(answer) * scale;\n    │                ━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[calls-loop]: external call inside a loop\n    ╭▸ src/NeoAwakening.sol:171:30\n    │\n171 │             address holder = token.holderAt(i);\n    │                              ━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[calls-loop]: external call inside a loop\n    ╭▸ src/NeoAwakening.sol:172:17\n    │\n172 │             if (token.balanceOfAt(holder, snapshotBlock) != 0) _eligible.push(holder);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[reentrancy-no-eth]: external call can be reentered before `state` is updated\n    ╭▸ src/NeoAwakening.sol:187:22\n    │\n187 │           uint256 id = coordinator.requestRandomWords(\n    │ ┏━━━━━━━━━━━━━━━━━━━━━━┛\n188 │ ┃             IVRFCoordinatorV25.RandomWordsRequest({\n189 │ ┃                 keyHash: keyHash,\n190 │ ┃                 subId: subscriptionId,\n    ‡ ┃\n195 │ ┃             })\n196 │ ┃         );\n    │ ┗━━━━━━━━━┛\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/NeoAwakening.sol:201:9\n    │\n201 │         emit RandomnessRequested(id);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-oz-erc721-mint]: `ERC721._mint` does not check that the recipient can receive the token; use `_safeMint`\n    ╭▸ src/NeoAwakening.sol:225:9\n    │\n225 │         _mint(selected, TOKEN_ID);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-oz-erc721-mint\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ConfigurationAndOracle.t.sol:150:17\n    │\n150 │         vm.warp(block.timestamp + 1 hours);\n    │         ────────━━━━━━━━━━━━━━━─────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n   ╭▸ test/Conservation.invariant.t.sol:39:28\n   │\n39 │         uint256 previous = block.number;\n   │                            ━━━━━━━━━━━━\n40 │         vm.roll(previous + 1);\n   │         ───────────────────── `vm.roll` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockNumber()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":944,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 13 tests for test/ConfigurationAndOracle.t.sol:ConfigurationAndOracleTest\n[PASS] testBelowThresholdFailsAndExactThresholdSucceeds() (gas: 270482)\n[PASS] testCannotTriggerInTokenDeploymentBlock() (gas: 395674)\n[PASS] testConfigEventsAndReplacementBeforeTrigger() (gas: 1506504)\n[PASS] testConfigurationLocksAtSnapshotAndStaysLockedAfterAward() (gas: 912158)\n[PASS] testConfigurationRequiresOwnerAndOwnershipTransferRequiresAcceptance() (gas: 278615)\n[PASS] testFreshnessBoundaryAndStalePrice() (gas: 261045)\n[PASS] testFuzzOracleNormalizesAllSupportedPrecisions(uint8,uint64) (runs: 256, μ: 240327, ~: 273594)\nLogs:\n  Bound result 2\n  Bound result 960450356\n\n[PASS] testInvalidConstructorDependencies() (gas: 8196)\n[PASS] testOracleFailureAndOverflowFailClosed() (gas: 201985)\n[PASS] testRejectsInvalidFeedSettings() (gas: 191122)\n[PASS] testRejectsInvalidIncompleteFutureAndChangedPrecisionRounds() (gas: 842222)\n[PASS] testRejectsInvalidRandomnessSettings() (gas: 259854)\n[PASS] testUnconfiguredDeploymentCanBeConfiguredWithoutTokenChanges() (gas: 479138)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 8.27ms (10.94ms CPU time)\n\nRan 6 tests for test/TheOne.t.sol:TheOneTest\n[PASS] testAllowanceAndInsufficientFunds() (gas: 316615)\n[PASS] testFuzzTransfersConserveSupplyAndHistory(uint256,uint256) (runs: 256, μ: 409090, ~: 420937)\nLogs:\n  Bound result 2\n  Bound result 1\n\n[PASS] testHistoricalBalancesUseEndOfBlockAndDoNotDuplicateHolders() (gas: 707177)\n[PASS] testMetadataSupplyAndFactoryDeployment() (gas: 806114)\n[PASS] testNoPrivilegedMintFreezeSeizeOrUpgrade() (gas: 912740)\n[PASS] testRuntimeHasNoForbiddenOpcodes() (gas: 1693908)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 8.33ms (12.17ms CPU time)\n\nRan 1 test for test/Artwork.t.sol:ArtworkTest\n[PASS] testTokenMetadataContainsEmbeddedNeoArtworkAndRemainsImmutable() (gas: 7070019)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 11.00ms (10.28ms CPU time)\n\nRan 16 tests for test/NeoAwakening.t.sol:NeoAwakeningTest\n[PASS] testCoordinatorZeroRequestIdCannotLatchOrChargeARequest() (gas: 853996)\n[PASS] testEligibilityIsHistoricalAndUnaffectedByLateTransfersOrFlashBorrowing() (gas: 1240887)\n[PASS] testFormerHoldersExcludedAndNewHoldersAfterTriggerCannotEnter() (gas: 1025084)\n[PASS] testFullPermissionlessDrawMintsExactlyOneToSelectedHolder() (gas: 1046977)\n[PASS] testFuzzBatchBoundariesCannotChangeEligibility(uint256) (runs: 256, μ: 4431082, ~: 4378733)\nLogs:\n  Bound result 244\n\n[PASS] testFuzzRandomWordSelectsCommittedEligibleHolder(uint256) (runs: 256, μ: 759865, ~: 759902)\n[PASS] testInvalidTransitionsAndBatchBounds() (gas: 1026367)\n[PASS] testLostOrFailedCallbackCannotRerollButTokenRemainsTransferable() (gas: 1061105)\n[PASS] testMinimumConfiguredCallbackGasSufficesWithoutRecipientCalls() (gas: 703827)\n[PASS] testNFTStandardsMetadataApprovalAndTransfer() (gas: 1343893)\n[PASS] testOracleDropAfterCommitCannotCancelDraw() (gas: 755231)\n[PASS] testRecipientCannotRejectAwardAndSafeTransferFailureIsAtomic() (gas: 1215109)\n[PASS] testReentrantAndSynchronousCoordinatorCannotSetOutcomeDuringRequest() (gas: 785335)\n[PASS] testRequestFailureRollsBackAndCanRetryWithSameSnapshot() (gas: 1041688)\n[PASS] testRequestUsesChainlinkV25WireFormatAndConfiguredBilling() (gas: 656934)\n[PASS] testSpoofedWrongMalformedAndDuplicateFulfillmentsCannotChangeWinner() (gas: 974109)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 62.10ms (87.04ms CPU time)\n\nRan 1 test for test/Conservation.invariant.t.sol:ConservationInvariantTest\n[PASS]\nConservationInvariantTest invariants:\n[PASS] invariantHolderRegistryIsUniqueAndCoversAllBalances\n[PASS] invariantSupplyAndBalancesMatchModel\n ConservationInvariantTest invariants (runs: 64, calls: 4096, reverts: 0)\n\n╭-----------------+--------------+-------+---------+----------╮\n| Contract        | Selector     | Calls | Reverts | Discards |\n+=============================================================+\n| TransferHandler | advanceBlock | 2060  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| TransferHandler | transfer     | 2036  | 0       | 0        |\n╰-----------------+--------------+-------+---------+----------╯\n\nLogs:\n  Bound result 2561\n  Bound result 4294967295\n  Bound result 0\n  Bound result 500000000000000000\n  Bound result 63\n  Bound result 842\n  Bound result 90\n  Bound result 70\n  Bound result 342\n  Bound result 0\n  Bound result 99\n  Bound result 54\n  Bound result 19\n  Bound result 20\n  Bound result 87\n  Bound result 273\n  Bound result 233\n  Bound result 5550\n  Bound result 0\n  Bound result 165\n  Bound result 8\n  Bound result 2249\n  Bound result 1000000\n  Bound result 96\n  Bound result 16\n  Bound result 5573\n  Bound result 181438535890511739\n  Bound result 266982372076358970\n  Bound result 3209\n  Bound result 160\n  Bound result 650\n  Bound result 2400\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 858.50ms (857.54ms CPU time)\n\nRan 5 test suites in 859.59ms (948.20ms CPU time): 37 tests passed, 0 failed, 0 skipped (37 total tests)\n","passed":true},{"durationMs":67,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"NeoAwakening.acceptOwnership()\",\"NeoAwakening.approve(address,uint256)\",\"NeoAwakening.award()\",\"NeoAwakening.collectHolders(uint256)\",\"NeoAwakening.rawFulfillRandomWords(uint256,uint256[])\",\"NeoAwakening.requestRandomness()\",\"NeoAwakening.safeTransferFrom(address,address,uint256)\",\"NeoAwakening.safeTransferFrom(address,address,uint256,bytes)\",\"NeoAwakening.setApprovalForAll(address,bool)\",\"NeoAwakening.setPriceFeed(address,uint256)\",\"NeoAwakening.setRandomnessConfig(address,uint256,bytes32,uint16,uint32,bool)\",\"NeoAwakening.transferFrom(address,address,uint256)\",\"NeoAwakening.transferOwnership(address)\",\"NeoAwakening.trigger()\",\"TheOne.approve(address,uint256)\",\"TheOne.transfer(address,uint256)\",\"TheOne.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":94,\"SECURITY.md\":26,\"THIRD_PARTY.md\":15,\"art/neo.svg\":1,\"foundry.toml\":20,\"launch.json\":25,\"remappings.txt\":2,\"src/NeoArt.sol\":53,\"src/NeoAwakening.sol\":249,\"src/TheOne.sol\":54,\"src/interfaces/IPriceFeed.sol\":11,\"src/interfaces/IVRFCoordinatorV25.sol\":17,\"test/Artwork.t.sol\":62,\"test/AwakeningBase.sol\":62,\"test/ConfigurationAndOracle.t.sol\":212,\"test/Conservation.invariant.t.sol\":93,\"test/NeoAwakening.t.sol\":302,\"test/TheOne.t.sol\":152,\"test/mocks/Services.sol\":113},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"42e664f3ee76261564ef7614f12b811419c5e2e371c35269c8cb088d2ea62c2c","verifiedTreeHash":"3f85bacfffa8953dcd02cd843f54efe6bbb6d8d9","verifierVersion":"0.1.0+ad90ce4c"},{"checks":[{"durationMs":3892,"exitCode":0,"name":"build","output":"Compiling 56 files with Solc 0.8.26\nSolc 0.8.26 finished in 3.76s\nCompiler run successful!\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/NeoAwakening.sol:29:49\n   │\n29 │     bytes4 private constant EXTRA_ARGS_V1_TAG = bytes4(keccak256(\"VRF ExtraArgsV1\"));\n   │                                                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'bytes4' is safe because [explain why]\n   │       // forge-lint: disable-next-line(unsafe-typecast)\n   │       \n   │       \n   ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/NeoAwakening.sol:140:85\n    │\n140 │             round == 0 || answer <= 0 || startedAt == 0 || startedAt > updatedAt || updatedAt > block.timestamp\n    │                                                                                     ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/NeoAwakening.sol:143:13\n    │\n143 │         if (block.timestamp - updatedAt > maxPriceAge) revert StalePrice();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/NeoAwakening.sol:145:13\n    │\n145 │         if (uint256(answer) > type(uint256).max / scale) revert InvalidOracleRound();\n    │             ━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/NeoAwakening.sol:146:16\n    │\n146 │         return uint256(answer) * scale;\n    │                ━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[calls-loop]: external call inside a loop\n    ╭▸ src/NeoAwakening.sol:171:30\n    │\n171 │             address holder = token.holderAt(i);\n    │                              ━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[calls-loop]: external call inside a loop\n    ╭▸ src/NeoAwakening.sol:172:17\n    │\n172 │             if (token.balanceOfAt(holder, snapshotBlock) != 0) _eligible.push(holder);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[reentrancy-no-eth]: external call can be reentered before `state` is updated\n    ╭▸ src/NeoAwakening.sol:187:22\n    │\n187 │           uint256 id = coordinator.requestRandomWords(\n    │ ┏━━━━━━━━━━━━━━━━━━━━━━┛\n188 │ ┃             IVRFCoordinatorV25.RandomWordsRequest({\n189 │ ┃                 keyHash: keyHash,\n190 │ ┃                 subId: subscriptionId,\n    ‡ ┃\n195 │ ┃             })\n196 │ ┃         );\n    │ ┗━━━━━━━━━┛\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/NeoAwakening.sol:201:9\n    │\n201 │         emit RandomnessRequested(id);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-oz-erc721-mint]: `ERC721._mint` does not check that the recipient can receive the token; use `_safeMint`\n    ╭▸ src/NeoAwakening.sol:225:9\n    │\n225 │         _mint(selected, TOKEN_ID);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-oz-erc721-mint\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ConfigurationAndOracle.t.sol:150:17\n    │\n150 │         vm.warp(block.timestamp + 1 hours);\n    │         ────────━━━━━━━━━━━━━━━─────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n   ╭▸ test/Conservation.invariant.t.sol:39:28\n   │\n39 │         uint256 previous = block.number;\n   │                            ━━━━━━━━━━━━\n40 │         vm.roll(previous + 1);\n   │         ───────────────────── `vm.roll` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockNumber()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":7488,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/Artwork.t.sol:ArtworkTest\n[PASS] testTokenMetadataContainsEmbeddedNeoArtworkAndRemainsImmutable() (gas: 7070019)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 18.41ms (17.62ms CPU time)\n\nRan 1 test for test/SnapshotBatchBoundary.t.sol:SnapshotBatchBoundaryTest\n[PASS] testEmptyFirstBatchAcross256RetiredHoldersStillCompletes() (gas: 51275763)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 60.80ms (60.63ms CPU time)\n\nRan 13 tests for test/ConfigurationAndOracle.t.sol:ConfigurationAndOracleTest\n[PASS] testBelowThresholdFailsAndExactThresholdSucceeds() (gas: 270482)\n[PASS] testCannotTriggerInTokenDeploymentBlock() (gas: 395674)\n[PASS] testConfigEventsAndReplacementBeforeTrigger() (gas: 1506504)\n[PASS] testConfigurationLocksAtSnapshotAndStaysLockedAfterAward() (gas: 912158)\n[PASS] testConfigurationRequiresOwnerAndOwnershipTransferRequiresAcceptance() (gas: 278615)\n[PASS] testFreshnessBoundaryAndStalePrice() (gas: 261045)\n[PASS] testFuzzOracleNormalizesAllSupportedPrecisions(uint8,uint64) (runs: 256, μ: 239038, ~: 273582)\nLogs:\n  Bound result 0\n  Bound result 32490\n\n[PASS] testInvalidConstructorDependencies() (gas: 8196)\n[PASS] testOracleFailureAndOverflowFailClosed() (gas: 201985)\n[PASS] testRejectsInvalidFeedSettings() (gas: 191122)\n[PASS] testRejectsInvalidIncompleteFutureAndChangedPrecisionRounds() (gas: 842222)\n[PASS] testRejectsInvalidRandomnessSettings() (gas: 259854)\n[PASS] testUnconfiguredDeploymentCanBeConfiguredWithoutTokenChanges() (gas: 479138)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 253.58ms (117.24ms CPU time)\n\nRan 16 tests for test/NeoAwakening.t.sol:NeoAwakeningTest\n[PASS] testCoordinatorZeroRequestIdCannotLatchOrChargeARequest() (gas: 853996)\n[PASS] testEligibilityIsHistoricalAndUnaffectedByLateTransfersOrFlashBorrowing() (gas: 1240887)\n[PASS] testFormerHoldersExcludedAndNewHoldersAfterTriggerCannotEnter() (gas: 1025084)\n[PASS] testFullPermissionlessDrawMintsExactlyOneToSelectedHolder() (gas: 1046977)\n[PASS] testFuzzBatchBoundariesCannotChangeEligibility(uint256) (runs: 256, μ: 4443823, ~: 4378733)\nLogs:\n  Bound result 158\n\n[PASS] testFuzzRandomWordSelectsCommittedEligibleHolder(uint256) (runs: 256, μ: 759544, ~: 759902)\n[PASS] testInvalidTransitionsAndBatchBounds() (gas: 1026367)\n[PASS] testLostOrFailedCallbackCannotRerollButTokenRemainsTransferable() (gas: 1061105)\n[PASS] testMinimumConfiguredCallbackGasSufficesWithoutRecipientCalls() (gas: 703827)\n[PASS] testNFTStandardsMetadataApprovalAndTransfer() (gas: 1343893)\n[PASS] testOracleDropAfterCommitCannotCancelDraw() (gas: 755231)\n[PASS] testRecipientCannotRejectAwardAndSafeTransferFailureIsAtomic() (gas: 1215109)\n[PASS] testReentrantAndSynchronousCoordinatorCannotSetOutcomeDuringRequest() (gas: 785335)\n[PASS] testRequestFailureRollsBackAndCanRetryWithSameSnapshot() (gas: 1041688)\n[PASS] testRequestUsesChainlinkV25WireFormatAndConfiguredBilling() (gas: 656934)\n[PASS] testSpoofedWrongMalformedAndDuplicateFulfillmentsCannotChangeWinner() (gas: 974109)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 262.53ms (171.08ms CPU time)\n\nRan 6 tests for test/TheOne.t.sol:TheOneTest\n[PASS] testAllowanceAndInsufficientFunds() (gas: 316615)\n[PASS] testFuzzTransfersConserveSupplyAndHistory(uint256,uint256) (runs: 256, μ: 411107, ~: 420937)\nLogs:\n  Bound result 106\n  Bound result 23\n\n[PASS] testHistoricalBalancesUseEndOfBlockAndDoNotDuplicateHolders() (gas: 707177)\n[PASS] testMetadataSupplyAndFactoryDeployment() (gas: 806114)\n[PASS] testNoPrivilegedMintFreezeSeizeOrUpgrade() (gas: 912740)\n[PASS] testRuntimeHasNoForbiddenOpcodes() (gas: 1693908)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 263.65ms (41.53ms CPU time)\n\nRan 4 tests for test/OracleBoundaryProperties.t.sol:OracleBoundaryPropertiesTest\n[PASS] testEveryFeedPrecisionAcceptsExactTargetAndRejectsOneUnitBelow() (gas: 9843737)\n[PASS] testFuzzMaxAgeAcceptsBoundaryButRejectsNextSecond(uint256) (runs: 1000, μ: 320294, ~: 320545)\nLogs:\n  Bound result 16962\n\n[PASS] testFuzzNormalizationOverflowBoundaryFailsClosed(uint8) (runs: 1000, μ: 409784, ~: 409850)\nLogs:\n  Bound result 12\n\n[PASS] testMaximumSignedAnswerWithEighteenDecimalsDoesNotOverflow() (gas: 266105)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 263.62ms (358.56ms CPU time)\n\nRan 8 tests for test/TokenBoundaryProperties.t.sol:TokenBoundaryPropertiesTest\n[PASS] testFuzzFailedBalanceCheckRestoresFiniteAllowance(uint256) (runs: 1000, μ: 192434, ~: 192249)\nLogs:\n  Bound result 9618772514685901037552843413250970027214316\n\n[PASS] testFuzzFiniteAllowanceCannotBeOverspent(uint256,uint256) (runs: 1000, μ: 445797, ~: 450621)\nLogs:\n  Bound result 853581328655385325\n  Bound result 2603385701843625\n\n[PASS] testFuzzOldCheckpointsSurviveManyLaterWrites(uint256) (runs: 1000, μ: 4680426, ~: 4680430)\n[PASS] testMaximumApprovalSurvivesSpendingAndCanBeRevoked() (gas: 393868)\n[PASS] testMaximumTransferAmountFailsWithoutChangingSupplyOrHistory() (gas: 185326)\n[PASS] testSelfTransferStillChecksBalanceAndConsumesDelegatedAllowance() (gas: 209654)\n[PASS] testZeroAddressErrorsAreAtomicIncludingZeroAmount() (gas: 335140)\n[PASS] testZeroOneWeiAndFullSupplyRoundTrips() (gas: 831988)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 334.99ms (920.60ms CPU time)\n\nRan 1 test for test/Conservation.invariant.t.sol:ConservationInvariantTest\n[PASS]\nConservationInvariantTest invariants:\n[PASS] invariantHolderRegistryIsUniqueAndCoversAllBalances\n[PASS] invariantSupplyAndBalancesMatchModel\n ConservationInvariantTest invariants (runs: 64, calls: 4096, reverts: 0)\n\n╭-----------------+--------------+-------+---------+----------╮\n| Contract        | Selector     | Calls | Reverts | Discards |\n+=============================================================+\n| TransferHandler | advanceBlock | 2022  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| TransferHandler | transfer     | 2074  | 0       | 0        |\n╰-----------------+--------------+-------+---------+----------╯\n\nLogs:\n  Bound result 0\n  Bound result 0\n  Bound result 0\n  Bound result 1655\n  Bound result 0\n  Bound result 0\n  Bound result 0\n  Bound result 0\n  Bound result 0\n  Bound result 0\n  Bound result 0\n  Bound result 0\n  Bound result 9999999999999\n  Bound result 0\n  Bound result 4007\n  Bound result 7200\n  Bound result 0\n  Bound result 0\n  Bound result 1860\n  Bound result 0\n  Bound result 3312\n  Bound result 301\n  Bound result 209\n  Bound result 0\n  Bound result 1772\n  Bound result 226273723554\n  Bound result 334\n  Bound result 2970\n  Bound result 978\n  Bound result 538\n  Bound result 90\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 905.60ms (904.33ms CPU time)\n\nRan 2 tests for test/DrawLifecycle.invariant.t.sol:DrawLifecycleInvariantTest\n[PASS]\nDrawLifecycleInvariantTest invariants:\n[PASS] invariant_oneNFTConservesOwnershipAndOriginalWinner\n[PASS] invariant_onlyAuthenticatedCallbacksAdvanceOneDraw\n[PASS] invariant_snapshotMembershipIsFrozenAndComplete\n[PASS] invariant_tokenBalancesAndHistoryMatchTransfers\n DrawLifecycleInvariantTest invariants (runs: 256, calls: 24576, reverts: 0)\n\n╭-------------+------------------+-------+---------+----------╮\n| Contract    | Selector         | Calls | Reverts | Discards |\n+=============================================================+\n| DrawHandler | advanceBlock     | 3142  | 0       | 0        |\n|-------------+------------------+-------+---------+----------|\n| DrawHandler | callback         | 3057  | 0       | 0        |\n|-------------+------------------+-------+---------+----------|\n| DrawHandler | configureAttempt | 3090  | 0       | 0        |\n|-------------+------------------+-------+---------+----------|\n| DrawHandler | moveNFT          | 3040  | 0       | 0        |\n|-------------+------------------+-------+---------+----------|\n| DrawHandler | moveTokens       | 3109  | 0       | 0        |\n|-------------+------------------+-------+---------+----------|\n| DrawHandler | oracleFailure    | 3030  | 0       | 0        |\n|-------------+------------------+-------+---------+----------|\n| DrawHandler | progress         | 3070  | 0       | 0        |\n|-------------+------------------+-------+---------+----------|\n| DrawHandler | rejectedAction   | 3038  | 0       | 0        |\n╰-------------+------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 412346\n  Bound result 0\n  Bound result 73710\n  Bound result 9\n  Bound result 0\n  Bound result 95\n  Bound result 255\n  Bound result 456\n  Bound result 35200\n  Bound result 0\n  Bound result 0\n  Bound result 0\n  Bound result 0\n  Bound result 0\n  Bound result 0\n  Bound result 0\n  Bound result 0\n  Bound result 420400380627948\n  Bound result 257\n  Bound result 61\n  Bound result 700000000000000000\n  Bound result 5\n  Bound result 12323\n  Bound result 0\n  Bound result 452344272872866100\n  Bound result 0\n  Bound result 24690\n  Bound result 0\n  Bound result 0\n\n[PASS] testHandlerReachesAwardThroughPartialBatchesFailuresAndTransfers() (gas: 3964730)\nLogs:\n  Bound result 400000000000000000\n  Bound result 1\n  Bound result 1\n  Bound result 400000000000000000\n  Bound result 100000\n  Bound result 1\n  Bound result 100000000000000000\n  Bound result 256\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 7.39s (7.39s CPU time)\n\nRan 9 test suites in 7.39s (9.75s CPU time): 52 tests passed, 0 failed, 0 skipped (52 total tests)\n","passed":true},{"durationMs":61,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"NeoAwakening.acceptOwnership()\",\"NeoAwakening.approve(address,uint256)\",\"NeoAwakening.award()\",\"NeoAwakening.collectHolders(uint256)\",\"NeoAwakening.rawFulfillRandomWords(uint256,uint256[])\",\"NeoAwakening.requestRandomness()\",\"NeoAwakening.safeTransferFrom(address,address,uint256)\",\"NeoAwakening.safeTransferFrom(address,address,uint256,bytes)\",\"NeoAwakening.setApprovalForAll(address,bool)\",\"NeoAwakening.setPriceFeed(address,uint256)\",\"NeoAwakening.setRandomnessConfig(address,uint256,bytes32,uint16,uint32,bool)\",\"NeoAwakening.transferFrom(address,address,uint256)\",\"NeoAwakening.transferOwnership(address)\",\"NeoAwakening.trigger()\",\"TheOne.approve(address,uint256)\",\"TheOne.transfer(address,uint256)\",\"TheOne.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":94,\"SECURITY.md\":26,\"THIRD_PARTY.md\":15,\"art/neo.svg\":1,\"foundry.toml\":20,\"remappings.txt\":2,\"src/NeoArt.sol\":53,\"src/NeoAwakening.sol\":249,\"src/TheOne.sol\":54,\"src/interfaces/IPriceFeed.sol\":11,\"src/interfaces/IVRFCoordinatorV25.sol\":17,\"test/Artwork.t.sol\":62,\"test/AwakeningBase.sol\":62,\"test/ConfigurationAndOracle.t.sol\":212,\"test/Conservation.invariant.t.sol\":93,\"test/DrawLifecycle.invariant.t.sol\":107,\"test/NeoAwakening.t.sol\":302,\"test/OracleBoundaryProperties.t.sol\":72,\"test/SnapshotBatchBoundary.t.sol\":65,\"test/TheOne.t.sol\":152,\"test/TokenBoundaryProperties.t.sol\":187,\"test/handlers/DrawHandler.sol\":320,\"test/mocks/Services.sol\":113},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"4fb9931d2aa9da2055871a13a61bc66086fa9d26468ef0d3bf21b626da8c8669","verifiedTreeHash":"e2b17ba925aad6557f9f70adb889278c728312d1","verifierVersion":"0.1.0+ad90ce4c"},{"checks":[{"durationMs":2168,"exitCode":0,"name":"build","output":"Compiling 51 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.06s\nCompiler run successful!\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/NeoAwakening.sol:29:49\n   │\n29 │     bytes4 private constant EXTRA_ARGS_V1_TAG = bytes4(keccak256(\"VRF ExtraArgsV1\"));\n   │                                                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ├ note: consider disabling this lint if you're certain the cast is safe\n   │       \n   │       // casting to 'bytes4' is safe because [explain why]\n   │       // forge-lint: disable-next-line(unsafe-typecast)\n   │       \n   │       \n   ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/NeoAwakening.sol:140:85\n    │\n140 │             round == 0 || answer <= 0 || startedAt == 0 || startedAt > updatedAt || updatedAt > block.timestamp\n    │                                                                                     ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/NeoAwakening.sol:143:13\n    │\n143 │         if (block.timestamp - updatedAt > maxPriceAge) revert StalePrice();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/NeoAwakening.sol:145:13\n    │\n145 │         if (uint256(answer) > type(uint256).max / scale) revert InvalidOracleRound();\n    │             ━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/NeoAwakening.sol:146:16\n    │\n146 │         return uint256(answer) * scale;\n    │                ━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[calls-loop]: external call inside a loop\n    ╭▸ src/NeoAwakening.sol:171:30\n    │\n171 │             address holder = token.holderAt(i);\n    │                              ━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[calls-loop]: external call inside a loop\n    ╭▸ src/NeoAwakening.sol:172:17\n    │\n172 │             if (token.balanceOfAt(holder, snapshotBlock) != 0) _eligible.push(holder);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[reentrancy-no-eth]: external call can be reentered before `state` is updated\n    ╭▸ src/NeoAwakening.sol:187:22\n    │\n187 │           uint256 id = coordinator.requestRandomWords(\n    │ ┏━━━━━━━━━━━━━━━━━━━━━━┛\n188 │ ┃             IVRFCoordinatorV25.RandomWordsRequest({\n189 │ ┃                 keyHash: keyHash,\n190 │ ┃                 subId: subscriptionId,\n    ‡ ┃\n195 │ ┃             })\n196 │ ┃         );\n    │ ┗━━━━━━━━━┛\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/NeoAwakening.sol:201:9\n    │\n201 │         emit RandomnessRequested(id);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-oz-erc721-mint]: `ERC721._mint` does not check that the recipient can receive the token; use `_safeMint`\n    ╭▸ src/NeoAwakening.sol:225:9\n    │\n225 │         _mint(selected, TOKEN_ID);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-oz-erc721-mint\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ConfigurationAndOracle.t.sol:150:17\n    │\n150 │         vm.warp(block.timestamp + 1 hours);\n    │         ────────━━━━━━━━━━━━━━━─────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n   ╭▸ test/Conservation.invariant.t.sol:39:28\n   │\n39 │         uint256 previous = block.number;\n   │                            ━━━━━━━━━━━━\n40 │         vm.roll(previous + 1);\n   │         ───────────────────── `vm.roll` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockNumber()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":904,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 13 tests for test/ConfigurationAndOracle.t.sol:ConfigurationAndOracleTest\n[PASS] testBelowThresholdFailsAndExactThresholdSucceeds() (gas: 270482)\n[PASS] testCannotTriggerInTokenDeploymentBlock() (gas: 395674)\n[PASS] testConfigEventsAndReplacementBeforeTrigger() (gas: 1506504)\n[PASS] testConfigurationLocksAtSnapshotAndStaysLockedAfterAward() (gas: 912158)\n[PASS] testConfigurationRequiresOwnerAndOwnershipTransferRequiresAcceptance() (gas: 278615)\n[PASS] testFreshnessBoundaryAndStalePrice() (gas: 261045)\n[PASS] testFuzzOracleNormalizesAllSupportedPrecisions(uint8,uint64) (runs: 256, μ: 234902, ~: 201044)\nLogs:\n  Bound result 0\n  Bound result 902236\n\n[PASS] testInvalidConstructorDependencies() (gas: 8196)\n[PASS] testOracleFailureAndOverflowFailClosed() (gas: 201985)\n[PASS] testRejectsInvalidFeedSettings() (gas: 191122)\n[PASS] testRejectsInvalidIncompleteFutureAndChangedPrecisionRounds() (gas: 842222)\n[PASS] testRejectsInvalidRandomnessSettings() (gas: 259854)\n[PASS] testUnconfiguredDeploymentCanBeConfiguredWithoutTokenChanges() (gas: 479138)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 10.24ms (12.86ms CPU time)\n\nRan 1 test for test/Artwork.t.sol:ArtworkTest\n[PASS] testTokenMetadataContainsEmbeddedNeoArtworkAndRemainsImmutable() (gas: 7070019)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 18.88ms (17.86ms CPU time)\n\nRan 6 tests for test/TheOne.t.sol:TheOneTest\n[PASS] testAllowanceAndInsufficientFunds() (gas: 316615)\n[PASS] testFuzzTransfersConserveSupplyAndHistory(uint256,uint256) (runs: 256, μ: 407763, ~: 420889)\nLogs:\n  Bound result 878333542843239288\n  Bound result 357976887059055855\n\n[PASS] testHistoricalBalancesUseEndOfBlockAndDoNotDuplicateHolders() (gas: 707177)\n[PASS] testMetadataSupplyAndFactoryDeployment() (gas: 806114)\n[PASS] testNoPrivilegedMintFreezeSeizeOrUpgrade() (gas: 912740)\n[PASS] testRuntimeHasNoForbiddenOpcodes() (gas: 1693908)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 19.21ms (15.26ms CPU time)\n\nRan 16 tests for test/NeoAwakening.t.sol:NeoAwakeningTest\n[PASS] testCoordinatorZeroRequestIdCannotLatchOrChargeARequest() (gas: 853996)\n[PASS] testEligibilityIsHistoricalAndUnaffectedByLateTransfersOrFlashBorrowing() (gas: 1240887)\n[PASS] testFormerHoldersExcludedAndNewHoldersAfterTriggerCannotEnter() (gas: 1025084)\n[PASS] testFullPermissionlessDrawMintsExactlyOneToSelectedHolder() (gas: 1046977)\n[PASS] testFuzzBatchBoundariesCannotChangeEligibility(uint256) (runs: 256, μ: 4444030, ~: 4378733)\nLogs:\n  Bound result 150\n\n[PASS] testFuzzRandomWordSelectsCommittedEligibleHolder(uint256) (runs: 256, μ: 759459, ~: 759855)\n[PASS] testInvalidTransitionsAndBatchBounds() (gas: 1026367)\n[PASS] testLostOrFailedCallbackCannotRerollButTokenRemainsTransferable() (gas: 1061105)\n[PASS] testMinimumConfiguredCallbackGasSufficesWithoutRecipientCalls() (gas: 703827)\n[PASS] testNFTStandardsMetadataApprovalAndTransfer() (gas: 1343893)\n[PASS] testOracleDropAfterCommitCannotCancelDraw() (gas: 755231)\n[PASS] testRecipientCannotRejectAwardAndSafeTransferFailureIsAtomic() (gas: 1215109)\n[PASS] testReentrantAndSynchronousCoordinatorCannotSetOutcomeDuringRequest() (gas: 785335)\n[PASS] testRequestFailureRollsBackAndCanRetryWithSameSnapshot() (gas: 1041688)\n[PASS] testRequestUsesChainlinkV25WireFormatAndConfiguredBilling() (gas: 656934)\n[PASS] testSpoofedWrongMalformedAndDuplicateFulfillmentsCannotChangeWinner() (gas: 974109)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 64.35ms (93.49ms CPU time)\n\nRan 1 test for test/Conservation.invariant.t.sol:ConservationInvariantTest\n[PASS]\nConservationInvariantTest invariants:\n[PASS] invariantHolderRegistryIsUniqueAndCoversAllBalances\n[PASS] invariantSupplyAndBalancesMatchModel\n ConservationInvariantTest invariants (runs: 64, calls: 4096, reverts: 0)\n\n╭-----------------+--------------+-------+---------+----------╮\n| Contract        | Selector     | Calls | Reverts | Discards |\n+=============================================================+\n| TransferHandler | advanceBlock | 2106  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| TransferHandler | transfer     | 1990  | 0       | 0        |\n╰-----------------+--------------+-------+---------+----------╯\n\nLogs:\n  Bound result 0\n  Bound result 0\n  Bound result 0\n  Bound result 0\n  Bound result 339636123590475245\n  Bound result 2072\n  Bound result 0\n  Bound result 0\n  Bound result 4294967295\n  Bound result 0\n  Bound result 0\n  Bound result 195\n  Bound result 2112\n  Bound result 256\n  Bound result 5013\n  Bound result 0\n  Bound result 103\n  Bound result 3\n  Bound result 3714247997\n  Bound result 4015\n  Bound result 31536000\n  Bound result 349481726805368253\n  Bound result 86400\n  Bound result 2794\n  Bound result 31536092\n  Bound result 602\n  Bound result 20\n  Bound result 8\n  Bound result 38599588924682765\n  Bound result 992883934\n  Bound result 115330148\n  Bound result 2592000\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 815.16ms (813.69ms CPU time)\n\nRan 5 test suites in 816.49ms (927.83ms CPU time): 37 tests passed, 0 failed, 0 skipped (37 total tests)\n","passed":true},{"durationMs":35,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"NeoAwakening.acceptOwnership()\",\"NeoAwakening.approve(address,uint256)\",\"NeoAwakening.award()\",\"NeoAwakening.collectHolders(uint256)\",\"NeoAwakening.rawFulfillRandomWords(uint256,uint256[])\",\"NeoAwakening.requestRandomness()\",\"NeoAwakening.safeTransferFrom(address,address,uint256)\",\"NeoAwakening.safeTransferFrom(address,address,uint256,bytes)\",\"NeoAwakening.setApprovalForAll(address,bool)\",\"NeoAwakening.setPriceFeed(address,uint256)\",\"NeoAwakening.setRandomnessConfig(address,uint256,bytes32,uint16,uint32,bool)\",\"NeoAwakening.transferFrom(address,address,uint256)\",\"NeoAwakening.transferOwnership(address)\",\"NeoAwakening.trigger()\",\"TheOne.approve(address,uint256)\",\"TheOne.transfer(address,uint256)\",\"TheOne.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":94,\"SECURITY.md\":26,\"THIRD_PARTY.md\":15,\"art/neo.svg\":1,\"foundry.toml\":20,\"remappings.txt\":2,\"src/NeoArt.sol\":53,\"src/NeoAwakening.sol\":249,\"src/TheOne.sol\":54,\"src/interfaces/IPriceFeed.sol\":11,\"src/interfaces/IVRFCoordinatorV25.sol\":17,\"test/Artwork.t.sol\":62,\"test/AwakeningBase.sol\":62,\"test/ConfigurationAndOracle.t.sol\":212,\"test/Conservation.invariant.t.sol\":93,\"test/NeoAwakening.t.sol\":302,\"test/TheOne.t.sol\":152,\"test/mocks/Services.sol\":113},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":1714,"exitCode":0,"name":"slither","output":"[medium/medium] reentrancy-no-eth at src/NeoAwakening.sol:184: Reentrancy in NeoAwakening.requestRandomness() (src/NeoAwakening.sol#184-202):\n[medium/medium] unused-return at src/TheOne.sol:41: TheOne._update(address,address,uint256) (src/TheOne.sol#41-53) ignores return value by _balances[from].push(clock,SafeCast.toUint208(balanceOf(from))) (src/TheOne.sol#45)\n[medium/medium] unused-return at src/TheOne.sol:41: TheOne._update(address,address,uint256) (src/TheOne.sol#41-53) ignores return value by _balances[to].push(clock,SafeCast.toUint208(balanceOf(to))) (src/TheOne.sol#51)\n[low/medium] calls-loop at src/NeoAwakening.sol:165: NeoAwakening.collectHolders(uint256) (src/NeoAwakening.sol#165-180) has external calls inside a loop: holder = token.holderAt(i) (src/NeoAwakening.sol#171)\n[low/medium] calls-loop at src/NeoAwakening.sol:165: NeoAwakening.collectHolders(uint256) (src/NeoAwakening.sol#165-180) has external calls inside a loop: token.balanceOfAt(holder,snapshotBlock) != 0 (src/NeoAwakening.sol#172)\n[low/medium] reentrancy-benign at src/NeoAwakening.sol:184: Reentrancy in NeoAwakening.requestRandomness() (src/NeoAwakening.sol#184-202):\n[low/medium] timestamp at src/NeoAwakening.sol:135: NeoAwakening.marketCapUsd18() (src/NeoAwakening.sol#135-147) uses timestamp for comparisons","passed":true},{"durationMs":338,"exitCode":0,"name":"aderyn","output":"[high] abi-encode-packed-hash-collision at src/NeoArt.sol:45: `abi.encodePacked()` Hash Collision\n[high] reentrancy-state-change at src/NeoAwakening.sol:99: Reentrancy: State change after external call (6 places)\n[low] centralization-risk at src/NeoAwakening.sol:15: Centralization Risk (4 places)\n[low] costly-loop at src/NeoAwakening.sol:170: Costly operations inside loop\n[low] internal-function-used-once at src/NeoArt.sol:8: Internal Function Used Only Once\n[low] large-numeric-literal at src/NeoAwakening.sol:26: Large Numeric Literal (3 places)\n[low] literal-instead-of-constant at src/NeoAwakening.sol:89: Literal Instead of Constant (3 places)\n[low] unchecked-return at src/NeoAwakening.sol:242: Unchecked Return (2 places)\n[low] unsafe-oz-erc721-mint at src/NeoAwakening.sol:225: Unsafe `ERC721::_mint()`","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"5d9ec4aac07564fad879f1b48eebac1e3dd540391d3986de1d947838bd5b4ff1","verifiedTreeHash":"709c22530ecbb770b2159127536aed78cab4c9f6","verifierVersion":"0.1.0+ad90ce4c"}]}