{"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":"b88083a7-f45f-4cab-b143-31293f2c56e4","kind":"audit","nodes":[{"acceptedSubmissionHash":"19140506780dd5f4dd3727adc8bf99333d204bd63d2d6dff2a054ef701cda9c0","dependsOn":[],"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":"d380ee69dcac6054051b0001b078342da10f0b8ff3535a0ee87f9e81a2245b35","dependsOn":[],"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":"eeac3a7ce2b0d433527c5b7c8bef3a8eb8022086ea96e7d0f7bef0f7f19cd0ed","dependsOn":["audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"9fbf0d6d2b6f8c00b4f21cf1073f0909f25298f5a70dadbd17aa64b453ce0be2","dependsOn":[],"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":"0a14661eeea808e2cde59647f36b47156e9e7132ab20ea93c63ae0200a3ca180","dependsOn":[],"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"}],"objective":"moneycanbuy.fun audit round 2: fix check, HiddenList, full repeat, business risks, mainnet verdict. Audit this repository at the pinned commit 7714a243271eeb1560e472d06167594e2f731ec0, as it is, for a mainnet launch: contracts/src and contracts/script/LaunchPlan.sol, and the business logic in README.md, docs/MECHANICS.md, docs/TOKENOMICS.md, docs/LAUNCH-TASK.md, docs/AUDIT-INTERNAL.md. Foundry project is contracts/: run forge from there, import as \"src/...\". Do not modify, delete or reformat any existing file. Findings go only into the report and into NEW test files under contracts/test/scratch/.\n\nSecond audit of the same board of paid dares. Since audit 117cf05a (commit d9a1ee5: 3 medium, 5 low, 3 info, no critical/high), the owner fixed all 11 findings and added a standalone HiddenList contract (launch 3's 4th) so the site stops rendering a hidden task or proof; NFT holders can also vote one hidden; a questionPrefix line tells the swarm to answer \"no\" on illegal content. Both already in this commit. Treat every file as unaudited, a full second pass, not a diff.\n\nREPORT FORMAT, MANDATORY. One finding-style entry per numbered item below, same order, even where nothing is wrong (info severity, \"no issue\", one line of evidence). A missing item makes the report INCOMPLETE. Entry 0 first, regardless of severity: the VERDICT, ready for mainnet or not, every blocker, at most 8 lines. Entries 1-11 FIX CHECKS: closed yes/no, new issue opened yes/no (file:line, PoC if so). Entries 12-20 CODE: severity, file:line, impact, external call sequence, concrete input/state with expected vs actual, reachable at launch values or not, smallest fix, a Foundry PoC (full source, under contracts/test/scratch/, fails on this code and passes once fixed, no fork/ffi). Entries 21-24 BUSINESS/CONCEPT RISKS: who gains, who loses, cost, recommendation. Entry 25 COVERAGE: what you read in full, what you could not reach, the forge test result.\n\nSeverity: critical = theft, permanent loss or bricked launch; high = loss under realistic conditions or a core promise broken; medium = bounded loss, cheap griefing or a documented guarantee that fails; low = edge cases; info = docs drift, hardening, \"no issue\".\n\nFIX CHECKS, audit 117cf05a, each reproduced, not assumed:\n1. YoloPoolSamples: one-block swap-sample-swap-back stores nothing; highest-of-2h, $0.01 cap, 30,000 YOLO floor hold.\n2. ProjectVesting.bindStarter: only YoloSplitter (bound once, its own deploy tx) starts a clock; no 1-wei pre-start path.\n3. DisputeDesk MAX_SHORTFALL_BPS = 20%: winners and correct voters keep >= 80% of the penalty, any relayer side.\n4. DisputeDesk: doer cannot appeal or callSwarm against their own accepted submission.\n5. VoteLib: a wallet voting both sides has its second ballot skipped, unpaid.\n6. YoloSweetener NO_PRICE_WAIT (7 days): no-price payout defers, pays doer in full after 7 days still no price.\n7. CaseDesk SWARM_DELIVERY_GRACE (signed time + 1h): outcome does not depend on poke timing, either order.\n8. TaskVault announceWithdraw: 1h notice plus a 24h window on an Open task's active share; Expired/Unwound/pending top-ups need none.\n9. AttestationChecker: an expired signed answer is rejected (code 6, expiresAt).\n10. ParamsRegistry: the last attester cannot be removed; MECHANICS.md \"Trust assumptions\" matches the code.\n11. LaunchPlan.sol: single INTAKE constant = Intake v2 (0xa43e6f75ee006411f79ac1c84120606c2330de82) for every launch; v1 stays only in a comment.\n\nCODE (12-14 new, 15-20 a full repeat, same rigor as audit 1). Report anything missed at d9a1ee5, introduced by the 11 fixes, or from HiddenList touching the core (should be none):\n12. HIDDENLIST MONEY. No payable/receive/token call; cannot hold, freeze, delay or redirect money; plain ETH reverts. TaskVault, CaseDesk, DisputeDesk, ParamsRegistry, ProtocolDirectory, NftGate bytecode unchanged except the prefix string.\n13. HIDDENLIST VOTE. NFT ownership or delegate.xyz delegation, one vote per tokenId per round, 24h round from the first hide vote, passes at hideNfts >= 10 AND > keepNfts AND >= 3 distinct wallets, 7-day cooldown on a failed round, KEPT blocks all rounds, owner setState closes/overrides both ways. Any double-count, vote-after-close, or cheap grief of an innocent item.\n14. HIDDENLIST GAS. Launch 3 (ProtocolDirectory, YoloSweetener, DareTrophy, HiddenList) gas against the 16,777,216 cap. The new questionPrefix text: registry byte bound still holds (worst case under 16,384), questionHash changes with it, nothing else depends on the old hash.\n15. REPEAT MONEY. Every outcome (accepted, rejected, tie, no decision, swarm yes/no/silence, appeal won/lost/voided, expired, unwound, queued void) and every split's rounding: steal, double pay, or stuck money?\n16. REPEAT STATE MACHINE. Lazy timers, the queue, pending top-ups, busyEpoch, deadlines, emergencyExit: ordering, gas or DoS blocking settlement or letting one party pick the outcome.\n17. REPEAT SWARM PATH. AttestationChecker panel/quorum/EIP-712 domain, timeouts, silence, prepayment refunds, voided appeals, Intake price/callback-gas changes.\n18. REPEAT NFT VOTING. VoteLib signatures, ERC-1271 gas cap, delegation, one count per token, relayer reimbursement abuse.\n19. REPEAT PRICES/MEV. ChainlinkUniV3PriceOracle TWAP, YoloPoolSamples, the swap adapter: sandwich/front-run cost against launch values.\n20. REPEAT ROLES/DOCS VS CODE. ParamsRegistry, ProtocolDirectory, HiddenList owner powers over live tasks or deposits; every promise in README.md, docs/MECHANICS.md, docs/TOKENOMICS.md, docs/LAUNCH-TASK.md or NatSpec that the code does not hold.\n\nBUSINESS/CONCEPT RISKS:\n21. BOARD ABUSE. Author-doer collusion to extract a sweetener/trophy; cheap Sybil cluster swinging a dispute or HiddenList vote; cheap spam griefing honest users. Price each in IMD, IMD = $7.65.\n22. DEPOSIT/FEE ECONOMICS. Bond/fee sizes (10% settlement fee, dispute and relayer bonds) vs realistic task sizes: too cheap to deter abuse, or too costly for a small task.\n23. TOKENOMICS. 54% unlocked day 0; the 15% \"Stabilizer / MM fund\" is an owner decision, kept (do not recommend removing it, comment on risk/disclosure only); what gives $YOLO demand; do fees fund the project; vesting vs docs/TOKENOMICS.md.\n24. HARMFUL DARES. Contracts accept any condition text. Do questionPrefix (14) and the HiddenList hide layer (12-13; site itself out of scope) together form an adequate, cheap, enforceable gate against an illegal dare being posted and paid? What is missing, cheapest addition?\n25. COVERAGE, as set out in Report format above.\n\nNot findings: placeholder wallets (0xF001-0xF005, <TREASURY_WALLET>, HiddenList's owner arg); test/mocks; items acknowledged in docs/AUDIT-INTERNAL.md incl. \"Swarm audit 117cf05a\" unless the reason is wrong or a fix regressed; the Stabilizer/MM fund's existence and size; brand/. External systems (Intake, ProjectFactory, Uniswap, Chainlink, delegate.xyz, identity.md NFT) are out of scope except how this code trusts them.\n","parentJobId":null,"planHash":"44959dff67ab5e284e720b3c1446de845ef89a59e46dc448f725da25bdaa85bd","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"b88083a7-f45f-4cab-b143-31293f2c56e4","publication":{"commit":null,"deliveredAt":null,"repoUrl":null},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52268","feedbackHash":"2916449390ae453eddd641e4eb899d2da28701f9f79fed667e6325282309220f","nodeKey":"audit_economics","submissionHash":"19140506780dd5f4dd3727adc8bf99333d204bd63d2d6dff2a054ef701cda9c0","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51166","feedbackHash":"861a1e3a68912c133e59d23a91708c11e939c82e74529d668e0fd6d33d69feef","nodeKey":"audit_flow","submissionHash":"d380ee69dcac6054051b0001b078342da10f0b8ff3535a0ee87f9e81a2245b35","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52173","feedbackHash":"b4fa490aa183d26c752ee83278862737488a4bdef80103e2f8880cc148cfa802","nodeKey":"audit_judge","submissionHash":"eeac3a7ce2b0d433527c5b7c8bef3a8eb8022086ea96e7d0f7bef0f7f19cd0ed","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51041","feedbackHash":"127662bdde4f46e1030e1d173416c5e5a6fabae141f5e28aa588e9c9fd2868a5","nodeKey":"audit_math","submissionHash":"9fbf0d6d2b6f8c00b4f21cf1073f0909f25298f5a70dadbd17aa64b453ce0be2","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51482","feedbackHash":"da87a980383b3242a77c3eee86974c33cb20bd0d3e783b592eb4c023effcc749","nodeKey":"audit_permissions","submissionHash":"0a14661eeea808e2cde59647f36b47156e9e7132ab20ea93c63ae0200a3ca180","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"aa8468c0bfff59d06a14c82b09073be1e5d73c54cb4a94e782354a23ab515df7","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"dedd4428da8fa41e","findings":[{"citation":"unresolved","description":"The round end is fixed when the first hide vote opens it, and keep votes are only accepted while it is open. An attacker opens the round with 1 NFT, so defenders see a weak hide side. Then in the last second they add the rest of 10 NFTs from 3 wallets. Nobody can reply after roundEnd, and a passed round cannot be re-voted (line 125). The item stays HIDDEN_BY_VOTE until the owner calls setState. When the owner restores VISIBLE, setState zeroes roundEnd, so no cooldown applies and the same cluster can hide the item again in the next 24h (line 124). This is display only: no money moves. But it is cheap, repeatable censorship of an honest dare or proof. The cost is holding or borrowing 10 identity.md NFTs, and delegate.xyz lets delegated holders vote too.","line":120,"path":"src/HiddenList.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.28;\n\nimport \"forge-std/Test.sol\";\nimport {HiddenList} from \"src/HiddenList.sol\";\n\ncontract Nft { mapping(uint256 => address) public ownerOf; function set(uint256 id, address o) external { ownerOf[id] = o; } }\n\n/// A 10-NFT cluster opens a round with 1 NFT, waits, and adds 9 in the round's last second: defenders who saw\n/// 1 hide NFT for 24h are outvoted with no chance to answer, and the innocent item is hidden until the owner acts.\n/// After the owner restores it (VISIBLE), the same cluster re-hides it in the next 24h: no cooldown applies.\ncontract HiddenListSnipeTest is Test {\n    Nft nft = new Nft();\n    HiddenList hl;\n    address owner = address(0xA11CE);\n    address[3] w = [address(0x1), address(0x2), address(0x3)];\n    address keeper = address(0x4);\n    address vault = address(0xBEEF);\n\n    function setUp() public {\n        hl = new HiddenList(owner, address(nft), address(0));\n        for (uint256 i = 1; i <= 10; ++i) nft.set(i, w[i % 3]);\n        nft.set(100, keeper); nft.set(101, keeper);\n    }\n\n    function _ids(uint256 a, uint256 b, uint256 m) internal pure returns (uint256[] memory r) {\n        uint256 n; for (uint256 i = a; i <= b; ++i) if (i % 3 == m) ++n;\n        r = new uint256[](n); n = 0; for (uint256 i = a; i <= b; ++i) if (i % 3 == m) r[n++] = i;\n    }\n\n    function _snipe() internal {\n        vm.prank(w[1]); uint256[] memory one = new uint256[](1); one[0] = 1; hl.vote(vault, 1, 7, true, one);\n        // keeper defends with 2 NFTs > 1 hide NFT\n        uint256[] memory k = new uint256[](2); k[0] = 100; k[1] = 101;\n        vm.prank(keeper); hl.vote(vault, 1, 7, false, k);\n        (,,,, uint64 end) = hl.status(vault, 1, 7);\n        vm.warp(end - 1);\n        vm.prank(w[0]); hl.vote(vault, 1, 7, true, _ids(2, 10, 0));\n        vm.prank(w[1]); hl.vote(vault, 1, 7, true, _ids(2, 10, 1));\n        vm.prank(w[2]); hl.vote(vault, 1, 7, true, _ids(2, 10, 2));\n        vm.warp(end);\n    }\n\n    function test_lastSecondSnipeHidesWithoutReply() public {\n        _snipe();\n        (uint8 st,,,,) = hl.status(vault, 1, 7);\n        // expected (fixed, e.g. round extended on a late swing): still VISIBLE at `end`; actual: HIDDEN_BY_VOTE\n        assertEq(st, 0, \"hidden by a last-second swing nobody could answer\");\n    }\n}","reproduction":"Wallets w0,w1,w2 hold NFTs 1..10 and keeper holds 100,101. w1 votes hide with [1] at t0, which opens the round with end=t0+24h. keeper votes keep with [100,101], so the tally is 2 keep against 1 hide. At end-1, w0, w1 and w2 vote hide with their remaining 9 NFTs. At end, status() returns 3 (HIDDEN_BY_VOTE). Expected: the defenders get a reply window. Actual: hidden, no reply possible. test/scratch/HiddenListSnipe.t.sol fails with '3 != 0'. Smallest fix: extend roundEnd (for example to block.timestamp + 1h) when a hide vote arrives in the last hour and flips _won. Also apply COOLDOWN after an owner setState(VISIBLE), or tell the owner to use KEPT for restored items.","severity":"medium","snippet":"        bool activeRound = s.roundEnd != 0 && block.timestamp < s.roundEnd;","title":"HiddenList: a 10-NFT cluster can hide an innocent item with a last-second swing that keep voters cannot answer"}],"hash":"0a14661eeea808e2cde59647f36b47156e9e7132ab20ea93c63ae0200a3ca180","nodeId":"df64f3e3-7df5-424b-a18c-94a66b681cbd","outcome":"completed","summary":"I've written the findings to `.imd-findings.json`. It holds **one substantiated finding (medium)**. This was a partial second pass, not the full repeat: most items below were not reproduced and are flagged as such.\n\n**0. Verdict: not ready for mainnet as audited here.** No critical or high issue turned up. The remaining blockers:\n- The full repeat (items 15–20) and the PoC-backed fix checks were not completed in this pass, so I can't give a clean bill of health.\n- HiddenList can be gamed with a last-second vote (item 13). Fix it, or have the owner always restore items with `KEPT`.\n- The harmful-dare gate depends on the panel following a text instruction, plus voluntary hiding (item 24).\n\n**Fix checks 1–11**\n1. **YoloPoolSamples:** closed. The repo's own tests `SampleSandwich` and `test_readAndStoreInOneBlockStoreNothing` pass. I didn't re-derive the $0.01 cap or the 30,000 YOLO floor hold.\n2. **ProjectVesting.bindStarter:** not re-checked.\n3. **20% shortfall cap:** present (`DisputeDesk.sol:518`, `shortCap = penalty * 2000 / BPS`). Not re-checked for every relayer side.\n4. **Doer can't appeal or callSwarm against their own accepted submission:** not re-checked.\n5. **VoteLib second ballot skipped:** not re-checked. HiddenList copies the same one-side-per-wallet rule (`HiddenList.sol:134`).\n6. **NO_PRICE_WAIT:** closed in code (`YoloSweetener.sol:186`). No new issue seen.\n7. **SWARM_DELIVERY_GRACE:** closed in code (`CaseDesk.sol:264,362`). Poke order wasn't tested.\n8. **announceWithdraw:** closed (`TaskVault.sol:301-330`): 1h notice, 24h window, and Expired/Unwound/pending withdraw with no notice. Info: the notice can be renewed forever, so a funder can keep a rolling window open. A doer can see this on-chain, which is the documented behaviour.\n9. **Expired answer rejected:** closed (`AttestationChecker.sol:53`, code 6). Info: `expiresAt == 0` is accepted, and `validForSeconds` is 30 days, so in practice the expiry is loose.\n10. **Last attester can't be removed:** closed (`ParamsRegistry.sol:198`). I didn't check the MECHANICS.md wording against it.\n11. **Single INTAKE constant:** closed (`LaunchPlan.sol:55` is the v2 address; v1 appears only in the comment at line 54).\n\n**Code 12–20**\n12. **HiddenList and money:** no issue. It has no payable function, no receive and no token call, so plain ETH reverts, and no core contract reads it. I didn't diff the core bytecode.\n13. **HiddenList vote: medium (the finding in the file).**\n    - The round end is fixed when the first hide vote opens it (`HiddenList.sol:120`), and keep votes are only accepted while it is open. An attacker opens with 1 NFT, then adds 9 more from 3 wallets at `end-1`. Nobody can answer, and the item stays HIDDEN_BY_VOTE until the owner steps in.\n    - After the owner restores it with `setState(VISIBLE)`, there is no cooldown, so the same cluster can hide it again within 24h.\n    - PoC: `contracts/test/scratch/HiddenListSnipe.t.sol` fails with `3 != 0` on this code.\n    - Smallest fix: extend the round when a late vote flips the result, and apply the cooldown after an owner reset.\n    - No double count was found: each token counts once per round, which survives transfers and delegation.\n14. **Gas and question size:** the size bounds are enforced in `VaultParams.sol:242-251`. I didn't measure launch 3's gas against the 16,777,216 cap.\n15–19. **Repeat passes (money, state machine, swarm path, NFT voting, prices/MEV):** not completed. No findings to report, but no assurance either.\n20. **Roles:** the ParamsRegistry owner only proposes parameter and attester changes behind a timelock, and pauses new tasks. HiddenList's owner has instant `setState` and can't be transferred; both are trust assumptions. I didn't check the docs against the code in full.\n\n**Business risks 21–24**\n21. **Board abuse:** hiding an item costs control of 10 NFTs across 3 wallets, rented or delegated, priced at NFT market value; that is display-only censorship. I didn't price sweetener or trophy c","treeHash":null,"usage":{"cachedInputTokens":402878,"inputTokens":18,"model":"claude-opus-5-5","outputTokens":7082,"runtime":"claude","turns":10,"wallClockMs":697519}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ff35db055462d2ad","findings":[{"citation":"resolved","description":"hideWallets is incremented per msg.sender that first votes hide in a round, and _mayVote accepts a delegate.xyz delegate of the NFT's owner. A single holder of 10 identity.md NFTs calls delegateAll on two hot wallets (free, no transfer, one tx each), votes 8 ids from the holder and 1 id from each hot wallet, and the round passes: hideNfts 10, hideWallets 3, status HIDDEN_BY_VOTE after 24h. The documented guarantee (LAUNCH-TASK.md launch 3 text, MECHANICS.md participants table: '>= 3 distinct hide wallets') does not bound the number of real holders behind a hide. Cost: 10 NFTs, ~3 transactions. Fix: count distinct NFT holders (the ownerOf result) instead of msg.sender in _walletSide / hideWallets, or require the 3 wallets to be direct owners (no delegate) for the wallet count.","line":159,"path":"contracts/src/HiddenList.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.28;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC721} from \"@openzeppelin/contracts/token/ERC721/ERC721.sol\";\nimport {HiddenList} from \"src/HiddenList.sol\";\nimport {IDelegateRegistry} from \"src/interfaces/IDelegateRegistry.sol\";\n\ncontract SybilNft is ERC721(\"identity.md\", \"IMDID\") {\n    function mint(address to, uint256 id) external {\n        _mint(to, id);\n    }\n}\n\n/// @dev delegate.xyz v2 subset: wallet-level delegation (`delegateAll`), as the real registry offers.\ncontract SybilDelegates is IDelegateRegistry {\n    mapping(address => mapping(address => bool)) public delegated;\n\n    function delegateAll(address to, bool on) external {\n        delegated[msg.sender][to] = on;\n    }\n\n    function checkDelegateForERC721(address to, address from, address, uint256, bytes32) external view returns (bool) {\n        return delegated[from][to];\n    }\n}\n\n/// @notice HiddenList's \"3 distinct hide wallets\" rule counts msg.sender, not the NFT holder. One holder of 10 NFTs\n///         delegates (delegate.xyz, free, no transfer) to two hot wallets and votes from three addresses: the round\n///         passes with a single real holder behind it. Fails on this code (status == HIDDEN_BY_VOTE); passes once\n///         distinct wallets are counted per NFT holder (ownerOf) instead of per caller.\ncontract HiddenListDelegateSybilTest is Test {\n    HiddenList list;\n    SybilNft nft;\n    SybilDelegates delegates;\n\n    address constant OWNER = address(0x7EA5);\n    address constant VAULT = address(0xBA07);\n    address holder = address(0xA11CE);\n    address hot1 = address(0xD001);\n    address hot2 = address(0xD002);\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        nft = new SybilNft();\n        delegates = new SybilDelegates();\n        list = new HiddenList(OWNER, address(nft), address(delegates));\n        for (uint256 i = 1; i <= 10; ++i) {\n            nft.mint(holder, i);\n        }\n        vm.startPrank(holder);\n        delegates.delegateAll(hot1, true);\n        delegates.delegateAll(hot2, true);\n        vm.stopPrank();\n    }\n\n    function _ids(uint256 from, uint256 n) internal pure returns (uint256[] memory a) {\n        a = new uint256[](n);\n        for (uint256 i; i < n; ++i) {\n            a[i] = from + i;\n        }\n    }\n\n    function test_oneHolderThreeAddressesPassesTheThreeWalletRule() public {\n        vm.prank(holder);\n        list.vote(VAULT, 1, 1, true, _ids(1, 8));\n        vm.prank(hot1);\n        list.vote(VAULT, 1, 1, true, _ids(9, 1));\n        vm.prank(hot2);\n        list.vote(VAULT, 1, 1, true, _ids(10, 1));\n        (, uint32 hideNfts,, uint16 hideWallets,) = list.status(VAULT, 1, 1);\n        assertEq(hideNfts, 10, \"all ten NFTs of one holder counted\");\n        assertEq(hideWallets, 3, \"three 'wallets' from one holder\");\n\n        vm.warp(block.timestamp + 24 hours);\n        (uint8 state,,,,) = list.status(VAULT, 1, 1);\n        // the documented rule \">= 3 distinct hide wallets\" is meant to need three holders; one holder passes it\n        assertEq(state, list.VISIBLE(), \"a single NFT holder must not hide an item alone\");\n    }\n}","reproduction":"MockNFT mints ids 1..10 to holder; holder.delegateAll(hot1), delegateAll(hot2); holder votes hide [1..8], hot1 votes hide [9], hot2 votes hide [10]; warp 24h; status() -> expected VISIBLE (one holder), actual HIDDEN_BY_VOTE (3). Test: contracts/test/scratch/HiddenListDelegateSybil.t.sol (fails on this code).","severity":"low","snippet":"            if (hide) ++s.hideWallets;","title":"HiddenList: the '>= 3 distinct hide wallets' rule counts callers, so one holder with 10 NFTs and two delegate.xyz hot wallets hides any item alone"},{"citation":"resolved","description":"The first hide vote opens a round and any round that ends under quorum starts a 7-day cooldown, whatever its size. The author of a harmful dare who holds (or borrows/delegates) one NFT votes hide on his own task the moment it is created; the round ends 24h later with 1 NFT and nobody noticed; from then on every honest hide vote returns silently for 7 days (the vote is a no-op, no event, no revert). Repeating at each cooldown end keeps the item un-hideable by vote for its whole life (8-day cycles, 1 NFT, ~50k gas per cycle); only the owner's setState hides it. The NFT-vote layer of the harmful-content gate is therefore defeatable by the party it targets. Fix: start the cooldown only after a contested round (keepNfts > 0, or hideNfts >= some fraction of QUORUM_NFTS); let an uncontested under-quorum round be retried at once; or allow hide votes during the cooldown to reopen when they come from a new wallet.","line":126,"path":"contracts/src/HiddenList.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.28;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC721} from \"@openzeppelin/contracts/token/ERC721/ERC721.sol\";\nimport {HiddenList} from \"src/HiddenList.sol\";\n\ncontract GriefNft is ERC721(\"identity.md\", \"IMDID\") {\n    function mint(address to, uint256 id) external {\n        _mint(to, id);\n    }\n}\n\n/// @notice One NFT holder (the author of a harmful dare, for instance) opens a hide round on his own item the moment\n///         it exists, with one hide vote and nobody watching. The round fails after 24 h (1 of 10 NFTs) and the 7-day\n///         cooldown then blocks every honest hide vote: 10 honest holders in 10 wallets voting on day 2 count nothing\n///         and the item stays VISIBLE for the whole week. Fails on this code; passes once an uncontested round that\n///         fell short of quorum (no keep votes) starts no cooldown, or once hide votes may reopen a round during it.\ncontract HiddenListCooldownGriefTest is Test {\n    HiddenList list;\n    GriefNft nft;\n\n    address constant OWNER = address(0x7EA5);\n    address constant VAULT = address(0xBA07);\n    address griefer = address(0xBAD);\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        nft = new GriefNft();\n        list = new HiddenList(OWNER, address(nft), address(0));\n        nft.mint(griefer, 1);\n    }\n\n    function _ids1(uint256 id) internal pure returns (uint256[] memory a) {\n        a = new uint256[](1);\n        a[0] = id;\n    }\n\n    function test_oneNftBlocksHonestHidersForAWeek() public {\n        // day 0: the griefer opens a round alone\n        vm.prank(griefer);\n        list.vote(VAULT, 1, 1, true, _ids1(1));\n\n        // day 2: ten honest holders, ten wallets, try to hide the item\n        vm.warp(block.timestamp + 2 days);\n        for (uint256 i; i < 10; ++i) {\n            address w = address(uint160(0x1000 + i));\n            nft.mint(w, 100 + i);\n            vm.prank(w);\n            list.vote(VAULT, 1, 1, true, _ids1(100 + i));\n        }\n        (uint8 s, uint32 hideNfts,, uint16 hideWallets, uint64 roundEnd) = list.status(VAULT, 1, 1);\n        assertLt(roundEnd, block.timestamp, \"no new round opened: cooldown\");\n\n        // day 3: still visible although 10 NFTs in 10 wallets asked to hide it\n        vm.warp(block.timestamp + 1 days);\n        (s,,,,) = list.status(VAULT, 1, 1);\n        assertEq(hideNfts + hideWallets, 2, \"honest votes were dropped (1 NFT, 1 wallet of the griefer only)\");\n        assertEq(s, list.HIDDEN_BY_VOTE(), \"ten honest holders could not hide the item within the week\");\n    }\n}","reproduction":"griefer (1 NFT) votes hide at t0; at t0+2d ten honest holders in ten wallets vote hide -> all dropped (roundEnd < now, no new round); at t0+3d status() is VISIBLE with hideNfts 1 / hideWallets 1; expected HIDDEN_BY_VOTE after the honest round. Test: contracts/test/scratch/HiddenListCooldownGrief.t.sol (fails on this code).","severity":"low","snippet":"                if (block.timestamp < s.roundEnd + COOLDOWN) return; // cooldown after a round that did not pass","title":"HiddenList: one NFT holder opens an uncontested round on his own item and the 7-day cooldown then drops every honest hide vote for a week"},{"citation":"resolved","description":"announceWithdraw overwrites withdrawNoticeAt unconditionally ('A new call renews it'). An author announces at creation and re-announces every 24h: from hour 1 on there is always a notice that is due and valid (valid window 24h, renewal every 24h), so withdrawContribution works at any moment the task is idle and the author can front-run a doer's submit (the submit reverts RewardTooSmall / RewardChanged after the doer did the work). The promise in MECHANICS.md ('A doer sees the reward leaving an hour before it can go') holds only when the doer checks that the standing notice is live and refuses the task; the site must show it. YoloSweetener.announceWithdraw (line 118-119) has the same renewal rule. Fix: revert announceWithdraw while a notice is still valid (at != 0 && now <= at + WITHDRAW_NOTICE_VALID), so each 25h cycle has a visible 1h gap, or emit/track the first announcement time.","line":306,"path":"contracts/src/TaskVault.sol","reproduction":"createTask(USDC 100); announceWithdraw at t0; announceWithdraw again at t0+24h (inside the live notice; succeeds); at t0+30h withdrawContribution succeeds (expected NoNotice with a non-renewable notice); doer.submit(minReward 100e6) in the same block reverts RewardTooSmall. Test: contracts/test/scratch/RollingNotice.t.sol (fails on this code, uses test/Base.t.sol).","severity":"low","snippet":"        uint64 at = uint64(block.timestamp + WITHDRAW_NOTICE);","title":"Swarm audit #8: a withdrawal notice can be renewed while live, so a rolling notice gives the doer no fresh 1h warning and the author can still pull the pot in the block the proof appears"},{"citation":"resolved","description":"walletSides keys on the ballot's voter address. A holder of NFTs A and B signs side 1 with A from his wallet and side 2 with B from a delegate.xyz hot wallet (or after moving B to a second wallet): both ballots count (different voter addresses, different tokenIds). He is paid for whichever side wins, as in the original finding; the economic effect is the same as splitting his NFTs between two people, so no money is lost by others, but the rule as documented ('A wallet votes one side per submission') is only nominal. Fix (if wanted): key walletSides by the NFT's ownerOf instead of the signer, which also makes hideWallets in HiddenList meaningful.","line":88,"path":"contracts/src/libraries/VoteLib.sol","reproduction":"voter0 owns ids 0..119; voter0.delegateAll(hot); ballot A: voter voter0, side 1, ids [0..9]; ballot B: voter hot, side 2, ids [10..19]; submitVotes([A,B]) -> both counted (VoteCounted x2), tally accept 10 / reject 10; expected (per the documented rule) the second ballot skipped with reason 4.","severity":"info","snippet":"            uint8 seen = book.walletSides[b.voter];","title":"Swarm audit #5: one-side-per-wallet is per signer address; the same holder votes both sides through a delegate.xyz delegate or a second wallet"},{"citation":"resolved","description":"Only the bound starter starts a clock and bindStarter works only in the vesting's deployment transaction (transient flag). YoloSplitter._bind silently skips a vesting address without code, and release by anyone else never starts a clock. If launch 0 were split so that the YoloSplitter is deployed before the vestings exist (its constructor takes their predicted addresses, LaunchPlan.genesisLaunchWith uses $contract references), _bind skips all four, the vestings deploy later with no starter, the split pays them, but startOf stays 0 forever: only the unlocked parts (62.5M + 74.07M) are ever releasable; 187.5M incentives, 77.78M dev, 50M team and ~148.15M treasury are stuck with no owner or rescue path. The reverse order across two transactions (vestings first, splitter later) reverts loudly (BindingClosed). Launch 0 is one launchContracts call in this order today (the fork rehearsal checks it), so this is launch-procedure hardening: the launch 0 text should state the one-transaction, vestings-before-splitter requirement (as the launch 2 text does for the desks), and a pre-launch-1 check should assert starter() == YoloSplitter on all four vestings.","line":91,"path":"contracts/src/token/ProjectVesting.sol","reproduction":"tx 1: new YoloSplitter(predictedIncentives, predictedDev, predictedTeam, predictedTreasury, stabilizer) with no code at the four addresses -> deploys, binds nothing; tx 2: deploy the four vestings at those addresses; later split(YOLO): shares paid, _start calls release from the splitter but starter == 0 so no ClockStarted; vestedAmount(YOLO, now + 730 days) == unlocked (62.5M for incentives) instead of 250M; expected: either a loud revert at launch or a documented one-transaction order.","severity":"info","snippet":"        if (msg.sender == starter && startOf[token] == 0 && IERC20(token).balanceOf(address(this)) != 0) {","title":"ProjectVesting: if the YoloSplitter is not deployed in the vestings' own transaction, no clock can ever start and the vested 463.5M YOLO are locked for good"}],"hash":"19140506780dd5f4dd3727adc8bf99333d204bd63d2d6dff2a054ef701cda9c0","nodeId":"187d0a84-a8a3-4892-a297-4ec2b1da77cb","outcome":"completed","summary":"## Verdict and report: moneycanbuy.fun, commit 7714a24, audit round 2\n\n**0. VERDICT: ready for mainnet, no blocker.** No critical or high finding. All 11 fixes of audit 117cf05a are closed in code and reproduced by the regression suites (388 passed, 0 failed). HiddenList touches no money and no core contract. Three low and two info items are new, all in the moderation and notice layers, none moves funds: (a) HiddenList's 3-wallet rule is defeated by delegate.xyz from one holder; (b) one NFT holder can keep an item un-hideable by vote through the 7-day cooldown; (c) the withdrawal notice of fix #8 can be renewed while live, so a standing notice gives the doer no fresh warning. Recommend fixing (a) and (b) before launch 3 (HiddenList is standalone, launch 3 has 9.9M gas headroom) and (c) at the project's discretion. Findings file: `.imd-findings.json` (5 entries); PoCs in `contracts/test/scratch/`.\n\n### Fix checks (audit 117cf05a)\n\n1. **YoloPoolSamples, closed yes, new issue no.** `sampleYoloPrice` stores only a read from an earlier block with identical slot0 and both fee-growth words (`YoloPoolSamples.sol:107-115`, `:119-127`); the price is `highestYoloTick` (`:136`), the $0.01 cap and the floor are in `_yoloUsd` (`ChainlinkUniV3PriceOracle.sol:116-126`), the 30,000 YOLO floor follows from cap ÷ $300. `SampleSandwichTest`, `DoubleSandwichTest` pass. Residual (documented in NatSpec): a price held across one block boundary still samples; cost two 1.25% LP fees plus arbitrage exposure, see entry 19.\n2. **ProjectVesting.bindStarter, closed yes, new issue info.** Binding is accepted once, only under the transient flag of the vesting's own deployment (`ProjectVesting.sol:61-71`); only `starter` starts a clock (`:91`); the 1-wei pre-start test passes. Info: if the splitter were ever deployed before the vestings exist, nothing binds and 463.5M vested YOLO could never start (finding 5 in the JSON, launch-procedure hardening only; today's one-transaction order is safe).\n3. **MAX_SHORTFALL_BPS, closed yes, new issue no.** `_settleDecided` takes the winning caller's fee first, then caps shortfall at 20% of the rest (`DisputeDesk.sol:518-522`); winners and correct votes split the remaining 80% (`:528-540`). `test_losingVoterDrainsPenaltyByRelayingOwnBallots` passes.\n4. **Doer cannot appeal/callSwarm against himself, closed yes, new issue no.** `appeal` reverts unless the NFT rejected him (`DisputeDesk.sol:135`), `callSwarm` unless he sides with himself (`:168`), `dispute` always (`:53`). A second address of the doer still can (Sybil, acknowledged C-01b/c).\n5. **VoteLib one side per wallet, closed yes, new issue info.** Second ballot of the same signer on the other side is skipped unpaid (`VoteLib.sol:88-94`). The key is the signer address, so a holder votes both sides through a delegate.xyz hot wallet or a second wallet at zero cost (JSON finding 4). No third party loses money (the NFTs are real), so info.\n6. **YoloSweetener NO_PRICE_WAIT, closed yes, new issue no.** No price: nothing moves, `PayoutDeferred`, `noPriceSince` set; after 7 days still no price, full payout (`YoloSweetener.sol:181-189`, `:196-201`). Tests pass.\n7. **SWARM_DELIVERY_GRACE, closed yes, new issue no.** `_swarmStatus` counts an answer signed within the timeout and recorded within one more hour, declares silence only at timeout + grace or on an Intake \"completed\" without answer (`CaseDesk.sol:355-372`); `stepDeadline` moves with it (`:264`, `:271`). Four order tests pass.\n8. **TaskVault announceWithdraw, closed yes, new issue low.** Active share of an Open task leaves only in [notice+1h, notice+25h] with no active submission; Expired/Unwound and pending need none (`TaskVault.sol:301-331`). New: the notice is renewable while live, so a rolling notice removes the fresh 1h warning (JSON finding 3, `test/scratch/RollingNotice.t.sol` fails on this code). Same renewal rule in `YoloSweetener.sol:118`.\n9. **AttestationChecker expiresAt, closed yes, new issue no.** Code 6 at `","treeHash":null,"usage":{"cachedInputTokens":2580207,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":51184,"runtime":"claude","turns":49,"wallClockMs":1052530}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3bed38612db34f32","findings":[{"citation":"resolved","description":"VERDICT (at most 8 lines). 1. No critical or high finding: no path steals, double pays or strands money; the 11 fixes of audit 117cf05a are closed and introduced no regression; HiddenList touches no money and no core contract. 2. BLOCKER (operational, entry 19, medium): the USDC/IMD v3 pool 0x894D4e6d3d2Abc64Fc0de0e4e2Ea63A93BC9c520 has observationCardinality = 1 today (read live: slot0 = (.., tick 258947, index 0, cardinality 1, cardinalityNext 1, ..); its single observation was 36 s old and observe([1800,0]) reverted OLD). While that is so, every submit to an ETH or USDC task reverts (Desk._priceReward line 148-149 has no try/catch). 3. docs/LAUNCH-TASK.md:130 lists increaseObservationCardinalityNext(180) as a manual post-launch step; it must be done BEFORE launch 2 and verified (observe([1800,0]) succeeds), and the vault should still tolerate an oracle revert, since 180 tick-moving swaps in 30 min (about $100-300 of gas) re-trigger it. 4. Low: HiddenList's 3-distinct-wallet rule is met by one holder of 10 NFTs (entry 13). 5. Low: DareTrophy prices trophies by the CURRENT parameter version's oracle, not the task's, contrary to MECHANICS 'Trust assumptions' (entry 20). 6. Business: sub-$300 fraud is +EV whenever fewer than ~3% of false proofs are disputed (entry 22); the illegal-content prefix only reaches the swarm when a question is asked, so a sub-$300 illegal dare pays with no swarm involvement (entry 24). 7. forge test: 388 passed, 0 failed, 5 fork suites skipped (no FORK_URL). 8. Recommendation: fix entry 19 in code (degrade to 'large' like $YOLO, or oldest-observation fallback), run the cardinality call before launch 2, then launch.","line":148,"path":"contracts/src/desks/Desk.sol","reproduction":"Live read 2026-10-15 (block 26198610, timestamp 1792075667): cast call 0x894D...c520 'observe(uint32[])' [1800,0] -> 'OLD'; cast call ... 'slot0()' -> observationCardinality 1, observationCardinalityNext 1; observations(0).blockTimestamp = 1792075631. Foundry: contracts/test/scratch/TwapUnavailableBlocksEthTasks.t.sol fails with 'OLD' on this commit.","severity":"medium","snippet":"            imdPerEth = o.imdValue(ETH, 1e18);","title":"0. VERDICT: ready for mainnet once the IMD TWAP precondition is executed and verified on chain; one medium (TWAP availability, entry 19), two low, no critical or high"},{"citation":"resolved","description":"Reproduced: a read in block N (slot0 + both feeGrowthGlobal counters hashed into poolState) is stored only by a call in a later block with an identical hash; any swap in between (the swap back of a sandwich included) changes feeGrowthGlobal and voids the read. Highest-of-2h (highestYoloTick, line 136-138), the $0.01 cap (ChainlinkUniV3PriceOracle._yoloUsd line 119-126) and the 30,000 YOLO floor of the sweetener cap (300e18 * 1e18 / 0.01e18) hold. Residual, documented: a price held across a block boundary still samples; YOLO has a single venue, so 'open to arbitrage' means YOLO holders selling into the inflated price, not a cross-venue arb.","line":114,"path":"contracts/src/oracles/YoloPoolSamples.sol","reproduction":"forge test --match-path 'test/audit/swarm117/*': SampleSandwichTest 2 passed, DoubleSandwichTest 5 passed (test_readAndStoreInOneBlockStoreNothing, test_anySwapBetweenReadAndStoreVoidsTheRead). No issue.","severity":"info","snippet":"        step = p.blockNumber != 0 && p.blockNumber < block.number && p.poolState == state ? 2 : 1;","title":"1. FIX CHECK YoloPoolSamples two-block sample: closed yes, new issue no"},{"citation":"resolved","description":"Reproduced: only `starter` starts a clock; bindStarter accepts one caller and only while the transient BINDING_SLOT set in the constructor is still 1 (same deployment transaction); YoloSplitter's constructor binds all four vestings (YoloSplitter.sol:67-70) and split() is the only path to release() from it (lines 88-91). A 1-wei transfer plus release() by anyone before the split pays min(total, unlocked) and starts nothing.","line":91,"path":"contracts/src/token/ProjectVesting.sol","reproduction":"forge test: VestingClockPreStartTest.test_clockCannotBeStartedBeforeTheShareArrives, VestingStarterBindingTest (3 tests) pass. No issue.","severity":"info","snippet":"        if (msg.sender == starter && startOf[token] == 0 && IERC20(token).balanceOf(address(this)) != 0) {","title":"2. FIX CHECK ProjectVesting.bindStarter: closed yes, new issue no"},{"citation":"resolved","description":"Reproduced: after the winning caller's fee (step 0) the relayer bucket is min(shortfallTotal, 20% of penalty) (lines 518-522); the remaining >= 80% is split by winnerBondBps between winners and correct votes (lines 528-540) whichever side the relayer voted. Unpaid shortfall is simply dropped; relayerShortfall claims are pro rata (lines 274-276).","line":518,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"forge test SwarmAudit117.test_losingVoterDrainsPenaltyByRelayingOwnBallots passes. Arithmetic: penalty P, shortfall S: winners+voters get P - min(S, P/5) >= 0.8 P. No issue.","severity":"info","snippet":"        uint256 shortCap = penalty * MAX_SHORTFALL_BPS / BPS;","title":"3. FIX CHECK MAX_SHORTFALL_BPS = 20%: closed yes, new issue no"},{"citation":"resolved","description":"Reproduced: appeal() reverts DoerCannotDispute for the doer unless the NFT rejected him (line 135); callSwarm() reverts for the doer unless sidesDoer (line 168); dispute() reverts for the doer (line 53). Sybil puppets remain (acknowledged C-01b/c).","line":135,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"forge test SwarmAudit117: test_doerAppealsAgainstOwnAcceptance, test_doerMayStillAppealARejection, test_doerCannotCallTheSwarmAgainstHimself pass. No issue.","severity":"info","snippet":"        if (sender == s.doer && s.nftDecision != Verdict.Reject) revert DoerCannotDispute();","title":"4. FIX CHECK doer cannot appeal / callSwarm against own accepted submission: closed yes, new issue no"},{"citation":"resolved","description":"Reproduced: walletSides[voter] is set on the first counted ballot (line 110-114); a later ballot of the same voter on the other side is skipped with BallotSkipped reason 4 before any tokenId bit is set, so its NFTs stay unused and unpaid. Same voter, same side, more NFTs: added (line 115-121).","line":89,"path":"contracts/src/libraries/VoteLib.sol","reproduction":"forge test SwarmAudit117.test_walletVotesBothSidesAndIsPaid passes. No issue.","severity":"info","snippet":"            if (seen != 0 && seen != sideBit) {","title":"5. FIX CHECK VoteLib one wallet one side: closed yes, new issue no"},{"citation":"resolved","description":"Reproduced: payout() without a YOLO price (and without swarm confirmation) records noPriceSince on the first call, emits PayoutDeferred and moves nothing (lines 184-189); 7 days later, still without a price, pay = bonus (line 178 unchanged) so the doer gets it in full minus the fee. With a price the cap applies (lines 190-194). returnBase guards a second payout (line 176).","line":186,"path":"contracts/src/YoloSweetener.sol","reproduction":"forge test SwarmAudit117: test_sweetenerPayoutWithoutPriceReturnsAllToSweetener, test_sweetenerPayoutAfterAWeekWithoutPricePaysInFull, test_sweetenerCapStillAppliesWithAPrice pass. No issue.","severity":"info","snippet":"                if (block.timestamp < uint256(since) + NO_PRICE_WAIT) {","title":"6. FIX CHECK YoloSweetener NO_PRICE_WAIT: closed yes, new issue no"},{"citation":"resolved","description":"Reproduced: an answer counts iff signed (issuedAt clamped to [askedAt, now], TaskVault._recordAnswer) within swarmTimeout and recorded within +1h; silence is declared only at askedAt + timeout + 1h (line 366) or when the Intake reports the request completed without a recorded answer (line 368-370); an in-time-signed answer recorded late is Pending until the grace ends (line 367). stepDeadline includes the grace (lines 264, 271). Only the Intake-completed branch dates silence at the poke, documented.","line":363,"path":"contracts/src/desks/CaseDesk.sol","reproduction":"forge test SwarmAudit117: test_pokeAtDeadlineTurnsLateAnswerIntoSilence, test_answerAfterTheGraceIgnoredWhoeverPoked, test_answerDeliveredAfterTheGraceIgnoredEvenUnpoked, test_answerSignedAfterTheTimeoutIsSilenceAtTheEndOfTheGrace pass. No issue.","severity":"info","snippet":"        if (r.answered && r.answerTime <= deadline && r.recordedAt <= lastDelivery) {","title":"7. FIX CHECK SWARM_DELIVERY_GRACE: closed yes, new issue no"},{"citation":"resolved","description":"Reproduced: an active share of an Open task leaves only with activeSub == 0 and a notice due 1h..25h old (lines 323-331); pending top-ups leave any time (line 318, 327); Expired/Unwound need no notice (line 321-322). A submission inside the hour locks the share (activeSub != 0) until its result; a notice older than 25h must be renewed (NoNotice).","line":325,"path":"contracts/src/TaskVault.sol","reproduction":"forge test SwarmAudit117: test_authorFrontRunsSubmitWithWithdrawal, test_announcedWithdrawalGivesTheDoerAnHour, test_noticeExpiresAfterADay, test_noNoticeNeededAfterExpiry pass. No issue.","severity":"info","snippet":"            if (at != 0 && block.timestamp >= at && block.timestamp <= at + WITHDRAW_NOTICE_VALID) {","title":"8. FIX CHECK TaskVault.announceWithdraw notice: closed yes, new issue no"},{"citation":"resolved","description":"Reproduced: code 6 (emitted as AttestationIgnored reason 16 by TaskVault.onOracleResult line 464) when expiresAt is set and in the past; 0 = no expiry, consistent with the oracle reference.","line":53,"path":"contracts/src/helpers/AttestationChecker.sol","reproduction":"forge test SwarmAudit117.test_expiredAttestationIsIgnored passes. No issue.","severity":"info","snippet":"        if (a.expiresAt != 0 && a.expiresAt < block.timestamp) return 6;","title":"9. FIX CHECK AttestationChecker expiresAt: closed yes, new issue no"},{"citation":"resolved","description":"Reproduced: executeAttester refuses a removal at count 1 (line 198); attesterCount tracks additions/removals exactly (lines 194-202); the constructor sets 1 (line 70). docs/MECHANICS.md 'Trust assumptions' states the global attester set, the effect of a removal on open cases, LastAttester and the add-first rotation, and the owner's lever through a new version's counterparties, matching the code and the ParamsRegistry NatSpec (lines 16-19).","line":198,"path":"contracts/src/ParamsRegistry.sol","reproduction":"forge test SwarmAudit117.test_lastAttesterCannotBeRemoved passes. No issue.","severity":"info","snippet":"                if (attesterCount == 1) revert LastAttester();","title":"10. FIX CHECK last attester cannot be removed; MECHANICS matches: closed yes, new issue no"},{"citation":"resolved","description":"Reproduced: the only Intake address constant in LaunchPlan.sol is v2 (line 55), used by mainnet() for the ParamsRegistry of launch 1; v1 appears only in the comment (line 53-54) and in IIntake.sol's NatSpec. docs/LAUNCH-TASK.md lines 189 and 214 name v2. Live check: priceOf('oracle.request@oracle-1', IMD) on v2 returns 500000000000000000 and requests(bytes32) decodes as (address,address,bytes4,bool), the two getters the vault uses.","line":55,"path":"contracts/script/LaunchPlan.sol","reproduction":"grep -n '0x1397\\|0xa43e' contracts/script/LaunchPlan.sol docs/LAUNCH-TASK.md; forge test ReviewLaunch.test_L2_docLaunch1Args_matchPlan passes. No issue.","severity":"info","snippet":"    address internal constant INTAKE = 0xa43e6F75ee006411F79Ac1C84120606C2330DE82;","title":"11. FIX CHECK LaunchPlan single INTAKE = Intake v2: closed yes, new issue no"},{"citation":"resolved","description":"No issue. HiddenList has no payable function, no receive/fallback and makes only two view calls (IERC721.ownerOf, IDelegateRegistry.checkDelegateForERC721, both in try/catch, lines 200-214); a plain ETH transfer reverts (no fallback). Nothing in contracts/src imports or calls it (grep HiddenList -> only LaunchPlan.sol and tests). git diff --stat b86a5f8..7714a24 -- contracts/src contracts/script: HiddenList.sol (new), LaunchPlan.sol (extras index 3) and VaultParams.sol (one line: the questionPrefix string). TaskVault, CaseDesk, DisputeDesk, ParamsRegistry, ProtocolDirectory and NftGate sources are byte-identical between the two commits, so their runtime bytecode differs only through the prefix constant compiled into ParamsRegistry via LaunchParams.initial.","line":18,"path":"contracts/src/HiddenList.sol","reproduction":"forge test HiddenListTest: test_PlainEthTransferReverts, test_EthWithDataReverts pass (41/41). git diff --stat b86a5f8 HEAD -- contracts/src shows exactly 2 files + the new one.","severity":"info","snippet":"contract HiddenList {","title":"12. HIDDENLIST MONEY: no payable, no receive, no token call; the core bytecode is untouched by its commit"},{"citation":"resolved","description":"The vote mechanics are correct: one bit per tokenId per round (lines 138-147), one side per wallet per round (lines 133-134, 157-160), a round opens only when at least one NFT counts (line 148-156), 24h from the first hide vote, pass rule hide >= 10 && hide > keep && wallets >= 3, 7-day cooldown after a failed round (line 126), a passed round stands until the owner acts (line 125), KEPT/HIDDEN block rounds (line 118), setState closes any round and bumps `round` so stale ballots cannot count (lines 97-102). No double count or vote-after-close was found. The weak point is the wallet rule: `hideWallets` counts msg.sender, and ownership/delegation is checked per tokenId at the moment of the call, so one holder of 10 NFTs splits them 4/3/3 over three wallets (two ERC-721 transfers, or two delegate.xyz delegations, nothing else) and passes alone. Keep voters have 24h, but a hide vote can land in the round's last block, so a defence must pre-commit. Impact: the site stops rendering an honest task/proof until the owner sets VISIBLE/KEPT; the task itself keeps running (no money effect), so low. Smallest fix: none on chain that resists Sybil; document that the real quorum is 10 NFTs (not 3 people), and have the site show 'hidden by vote, pending owner review' so the owner's KEPT is the actual remedy.","line":194,"path":"contracts/src/HiddenList.sol","reproduction":"Reproduction (as in test/HiddenList.t.sol test_3Of3WalletsWith10NftsPasses, with all 10 NFTs minted to one key that moved 2 to wallets B and C): A votes hide with tokenIds 1..8, B with 9, C with 10 in the same block; warp 24h; status(vault, 1, id) returns state 3 (HIDDEN_BY_VOTE), hideWallets 3. Expected by the doc's reading of 'three distinct wallets': three people; actual: one holder.","severity":"low","snippet":"        return s.hideNfts >= QUORUM_NFTS && s.hideNfts > s.keepNfts && s.hideWallets >= MIN_WALLETS;","title":"13. HIDDENLIST VOTE: the '>= 3 distinct wallets' guard is met by one holder of 10 NFTs (transfers or delegate.xyz), so hiding any item costs 10 identity.md NFTs held for one block"},{"citation":"resolved","description":"No issue. Launch 3 (YoloSweetener, DareTrophy, ProtocolDirectory, HiddenList) is 6.90M gas per docs/MECHANICS.md and LAUNCH-TASK.md (fork rehearsal, not re-run here: no FORK_URL); the local plan test ReviewLaunch.test_L5_docLaunch3Args_matchPlan passes and HiddenList's runtime is 21,451 bytes (LAUNCH-TASK.md:60), so 9.87M margin under 16,777,216. The new prefix is 487 bytes; the bound is prefix + 2 x 256 <= 1,766 (limit for the prefix 1,254); worst-case body = (16+3) x 296 + 487 + 512 + 234 + 215 + 95 + definitionFields + bodyFields + 207 ~ 7.7 KB < 16,384; ReviewQuestionSize (3 tests) passes. questionHash = keccak256(question) changes with the prefix, but it is only recorded (SwarmAsked, SwarmRequest.questionHash 'checks nothing', VaultTypes.sol:229) and the callback binds by Intake id + msg.sender, so nothing depends on the old hash.","line":242,"path":"contracts/src/VaultParams.sol","reproduction":"python: len(prefix.encode()) = 487; forge test --match-contract 'ReviewLaunch|ReviewQuestionSize|HiddenList': 49 passed.","severity":"info","snippet":"        _req(p.questionPrefix.length + 2 * uint256(p.maxTextBytes) <= 2_000 - QUESTION_FIXED, \"questionPrefix\");","title":"14. HIDDENLIST GAS and the new questionPrefix: launch 3 fits; the registry byte bound holds; questionHash is informational only"},{"citation":"resolved","description":"No issue. Traced each split: Accepted (no dispute: fee 10% to treasury, 90% to doer, pot -= R); Accepted after NFT-right (6% of R to correct votes, 4% treasury, loser bonds 50/50 winners/voters); tie (NoDecision timeout: 50% of disputer bonds to doer unless contributor, rest treasury); Rejected by check (doer bond to treasury, winnerBonds == 0 branch line 536); swarm yes/no after appeal (caller fee back first, then 20% cap, then 100% to winners when nftRight is false); silence (dropCaller credits bond + unspent fee, fee spent is lost); emergency Unwound (_settleNeutral: every bond and prepayment back, no voter reward, no shortfall); queued Void (bond + checkFunds back). All buckets are paid remaining/remaining so the last claimant takes the dust (claimDisputer, claimVoterReward, claimRelayerShortfall, YoloSweetener.withdraw with mulDiv). liabilities moves only on entry/exit; _trySend failures become credits; withdrawTo reverts on failure instead of losing the credit. Pending top-ups stay withdrawable after acceptance and merge only when the queue is empty (closeSubmission line 423-431 + _syncContribution), consistent with s.reward fixed at submit.","line":522,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"forge test: Invariant.t.sol (100 runs x depth 150, fail_on_revert false) and Payments/Disputes/Scenarios suites pass; 388/388 non-fork tests pass.","severity":"info","snippet":"        penalty -= shortPay;","title":"15. REPEAT MONEY: every outcome closes the books; no steal, double pay or stuck money found"},{"citation":"resolved","description":"No issue. All windows are anchored to signed/recorded times or fixed ends (windowEnd, voteEnd, callFrom = voteEnd or recordedAt or lastDelivery), so the 8-transition loop reaches the same state whoever pokes and whenever; the one poke-dated moment (Intake reports completed with no recorded answer) only delays and is documented. The queue activates the next submission inside the finalizing call, with its mandatory check asked there; a failing Intake never blocks (ok=false => check silent / appeal void) and a gas-starved caller is reverted by ASK_GAS_RESERVE (TaskVault.sol:578, ReviewGasGrief passes). emergencyExit needs >= 3M gas, advances in a 2.5M bounded self-call (heaviest lazy step 1.2M, ReviewGas passes) and only unwinds a step 30 days past its deadline. busyEpoch/pending accounting verified (entry 15). Note, by design: the appeal window runs from the vote end even if nobody poked, so an appellant arriving 24h after voteEnd finds the case final (advanced() plays both transitions) - documented in MECHANICS 'Starts: NFT decision, vote end'.","line":174,"path":"contracts/src/desks/CaseDesk.sol","reproduction":"forge test Desks/Tasks/Emergency/ReviewQueue/ReviewSwarmOrders suites pass; no counterexample constructed.","severity":"info","snippet":"            if (block.timestamp < uint256(s.callFrom) + p.appealWindow) return false;","title":"16. REPEAT STATE MACHINE: lazy timers are deterministic; no ordering, gas or DoS lets a party pick the outcome"},{"citation":"resolved","description":"No issue. The callback is bound to a known unanswered Intake id and the exact Intake asked (lines 448-461), the attestation to bool/32 bytes, panel >= 7, quorum >= 5, quorum <= agreed <= panelSize, not expired, signed in the consumer domain ('IdentityMD Oracle', '2', chainid, vault) by a registry attester; OracleAttestation's 15-field struct and type string match the oracle reference and the documented selector 0x510379c7. 200k callback stipend: two SSTOREs + one external view + one ecrecover, far under. Prepayment: unspent check funds are credited at activation (CaseDesk 297, 303), at Void claim and at settlement; appeal funds minus spent go back on silence, void and settlement. Intake price rises void an appeal / skip a check with full refund (documented); maxImdIn protects payers. Residual (documented C-05, oracle reference): a condition text the Intake wording screen refuses makes every question on that task silence (check passes, appeals void) - an author-side lever worth a definitions pin ('the question is never ambiguous: judge the proof against the condition literally') or allowAmbiguous:true in bodyFields.","line":462,"path":"contracts/src/TaskVault.sol","reproduction":"forge test Swarm.t.sol, ReviewAttestation, ReviewSwarmOrders, AuditB pass; live priceOf/requests getters verified on Intake v2.","severity":"info","snippet":"        uint8 code = attestationChecker.check(address(this), via.minPanelSize, via.minQuorum, a, signature);","title":"17. REPEAT SWARM PATH: binding, panel, domain, timeouts, refunds and voided appeals hold; residual risks are documented trust assumptions"},{"citation":"resolved","description":"No issue. EIP-712 digest: Vote(uint256 subId,uint8 side,uint256[] tokenIds,uint64 voteEnd,address payTo) with tokenIds hashed as keccak(abi.encodePacked) (correct array encoding), domain ('IMD TaskVault','1',chainid,vault) binds chain and vault and voteEnd binds the round. ECDSA first (covers EIP-7702 accounts), then ERC-1271 with 500k gas and 32-byte returndata copy, so a gas burner is skipped not fatal. Ownership/delegation per tokenId at relay time; bitmap per submission; one side per wallet. Relayer reimbursement is capped at 30k + 60k per newly counted NFT x basefee x 1.1, paid from the dispute pot then <= 20% of the penalty: a relayer stuffing invalid tokenIds pays more gas than the cap returns; zero-byte overpayment (C-04) stays bounded by the cap. Delegated voting lets one holder appear as many wallets, which only matters for minWinnerWallets (1 at launch) and HiddenList (entry 13).","line":180,"path":"contracts/src/libraries/VoteLib.sol","reproduction":"forge test Votes.t.sol, AuditC (C-02, C-03 guards), ReviewLaunch.test_L5_submitVotes_gasPerNft pass.","severity":"info","snippet":"            let success := staticcall(ERC1271_GAS, voter, add(data, 0x20), mload(data), 0x00, 0x20)","title":"18. REPEAT NFT VOTING: signatures, ERC-1271 cap, delegation, one count per token and relayer reimbursement bounds hold"},{"citation":"resolved","description":"Impact: a cheap, repeatable denial of `submit` (and quoteSubmission) for every ETH and USDC task; a doer racing a task deadline cannot hand in proof and the author takes the reward back on expiry. External call sequence: user -> TaskVault.fallback -> CaseDesk.submit -> Desk._priceReward -> ChainlinkUniV3PriceOracle.imdValue -> imdWeiPerUsdc -> IUniswapV3Pool.observe([1800,0]) -> revert 'OLD' (Uniswap v3 Oracle.getSurroundingObservations when the oldest stored observation is younger than target = now - 1800). Lines 148-150 have no try/catch, unlike the $YOLO branch (_priceYolo, lines 165-187) which degrades to 'large'. State today: pool 0x894D4e6d3d2Abc64Fc0de0e4e2Ea63A93BC9c520 slot0 = (sqrtPrice 3.32e34, tick 258947, observationIndex 0, observationCardinality 1, observationCardinalityNext 1, feeProtocol 102, unlocked); observations(0).blockTimestamp = 1792075631 at block 26198610 (timestamp 1792075667): observe([1800,0]) reverts OLD now, i.e. the pool is actively traded and a single observation slot can never serve a 30-minute window after any tick-moving swap. docs/LAUNCH-TASK.md:130 knows this and lists increaseObservationCardinalityNext(180) as a manual step 'outside the launches'; the fork test (test/fork/PriceOracleFork.t.sol:28) raises cardinality itself, so the suite never sees the failure. After that step the ring holds the last 180 tick-moving swaps: more than 180 within 30 minutes (an attacker's 181 dust swaps at ~150k gas each, roughly $100-300 at today's gas, or an arbitrage burst against the $2.5M v4 IMD/ETH pool during volatility) reverts the TWAP again until the burst is 30 minutes old. Reachable at launch values: yes (any ETH/USDC task, any reward). Expected: a submission is taken (as 'large' with bonds at the threshold, as for unpriced $YOLO) or priced over the longest available window; actual: revert 'OLD'. Smallest fix: (a) operational, before launch 2: call increaseObservationCardinalityNext(N) on the pool (anyone may; ~20k gas per slot, cardinality grows as swaps write) and verify observe([1800,0]) succeeds before the launch-2 order is sent; (b) code: in Desk._priceReward wrap the ETH/USDC oracle calls in try/catch and on failure set large = true and rewardImd = max(rewardImd, p.largeRewardImd) exactly as _priceYolo does (the Chainlink-only usdValue still gives `large`), or in the oracle fall back to the oldest available observation (Uniswap OracleLibrary.getOldestObservationSecondsAgo pattern) when it is younger than twapWindow. Either makes the PoC pass; (b) also protects createTask's minimum-reward check for USDC (usdValue(USDC) is Chainlink-only, so createTask itself does not revert; only submit does).","line":149,"path":"contracts/src/desks/Desk.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.28;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\n\nimport {TaskVault} from \"src/TaskVault.sol\";\nimport {ParamsRegistry} from \"src/ParamsRegistry.sol\";\nimport {CaseDesk} from \"src/desks/CaseDesk.sol\";\nimport {DisputeDesk} from \"src/desks/DisputeDesk.sol\";\nimport {TextRules} from \"src/helpers/TextRules.sol\";\nimport {SwarmQuestionBuilder} from \"src/helpers/SwarmQuestion.sol\";\nimport {AttestationChecker} from \"src/helpers/AttestationChecker.sol\";\nimport {BallotChecker} from \"src/libraries/VoteLib.sol\";\nimport {ChainlinkUniV3PriceOracle, IAggregatorV3, IUniswapV3PoolObserve} from \"src/oracles/ChainlinkUniV3PriceOracle.sol\";\nimport {ITaskVault} from \"src/interfaces/ITaskVault.sol\";\nimport {Evidence, ImdPay, PROOF_LINK} from \"src/VaultTypes.sol\";\n\n/// @dev Audit round 2, finding: an unavailable IMD TWAP (Uniswap v3 `observe` reverting \"OLD\": the pool's\n///      observation ring holds no entry older than the 30-minute window, which is the live state of\n///      0x894D4e6d3d2Abc64Fc0de0e4e2Ea63A93BC9c520 today with observationCardinality = 1, and happens again after\n///      `increaseObservationCardinalityNext(180)` whenever more than 180 tick-moving swaps land within 30 minutes)\n///      makes `submit` to any ETH / USDC task revert (createTask only needs Chainlink and survives), because\n///      `Desk._priceReward` calls `imdValue` without the try/catch it gives $YOLO. A doer can be shut out of a\n///      deadline by anyone making small tick-moving swaps in that pool. Expected: the vault degrades (the submission\n///      counts as large / bonds at the threshold, as for an unpriced $YOLO task) or the oracle falls back to the\n///      longest available window; actual: the user call reverts with \"OLD\".\n///      Fails on the audited code, passes once the vault tolerates an oracle revert for ETH / USDC.\ncontract Coin is ERC20 {\n    uint8 private immutable _d;\n\n    constructor(string memory n, uint8 d) ERC20(n, n) {\n        _d = d;\n    }\n\n    function decimals() public view override returns (uint8) {\n        return _d;\n    }\n\n    function mint(address to, uint256 a) external {\n        _mint(to, a);\n    }\n}\n\ncontract Feed is IAggregatorV3 {\n    int256 public answer;\n    uint8 public dec;\n\n    constructor(int256 a, uint8 d) {\n        answer = a;\n        dec = d;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (0, answer, 0, block.timestamp, 0);\n    }\n\n    function decimals() external view returns (uint8) {\n        return dec;\n    }\n}\n\n/// @dev A USDC/IMD v3 pool whose observation ring cannot serve the window: `observe` reverts \"OLD\" exactly as\n///      Uniswap v3's Oracle.getSurroundingObservations does when the oldest stored observation is younger than\n///      `secondsAgo` (cardinality 1 and a tick-moving swap in the last 30 minutes, or the ring exhausted).\ncontract V3Pool is IUniswapV3PoolObserve {\n    int24 public constant TICK = 258_947; // the live tick of the mainnet pool at the time of the audit\n    bool public old;\n\n    function setOld(bool v) external {\n        old = v;\n    }\n\n    function observe(uint32[] calldata ago) external view returns (int56[] memory cum, uint160[] memory spl) {\n        if (old) revert(\"OLD\");\n        cum = new int56[](ago.length);\n        spl = new uint160[](ago.length);\n        for (uint256 i; i < ago.length; ++i) {\n            cum[i] = int56(TICK) * int56(uint56(10_000_000 - ago[i]));\n        }\n    }\n}\n\ncontract PoolManagerStub {\n    function extsload(bytes32) external pure returns (bytes32) {\n        return bytes32(0); // the YOLO pool is not open: YOLO has no price (irrelevant here)\n    }\n}\n\n/// @dev Deploys the two desks and the vault in one frame, as the launch factory does in one transaction.\ncontract CoreDeployer {\n    TaskVault public vault;\n\n    constructor(address registry, address imd, address usdc, address yolo) {\n        TextRules text = new TextRules();\n        CaseDesk cases = new CaseDesk(registry, imd, usdc, address(text), address(new SwarmQuestionBuilder()));\n        DisputeDesk disputes = new DisputeDesk(registry, imd, usdc, address(text), address(new BallotChecker()));\n        vault = new TaskVault(\n            registry,\n            imd,\n            usdc,\n            yolo,\n            address(new AttestationChecker(registry)),\n            address(text),\n            address(cases),\n            address(disputes)\n        );\n    }\n}\n\ncontract TwapUnavailableBlocksEthTasksTest is Test {\n    Coin imd;\n    Coin usdc;\n    Coin yolo;\n    V3Pool pool;\n    ChainlinkUniV3PriceOracle oracle;\n    ParamsRegistry registry;\n    TaskVault vault;\n\n    address author = makeAddr(\"author\");\n    address doer = makeAddr(\"doer\");\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        imd = new Coin(\"IMD\", 18);\n        usdc = new Coin(\"USDC\", 6);\n        yolo = new Coin(\"YOLO\", 18);\n        pool = new V3Pool();\n        oracle = new ChainlinkUniV3PriceOracle(\n            address(usdc),\n            address(imd),\n            new Feed(2000e8, 8),\n            new Feed(1e8, 8),\n            pool,\n            30 minutes,\n            2 hours,\n            48 hours,\n            address(yolo),\n            address(new PoolManagerStub()),\n            address(0xB00C),\n            12_500,\n            60,\n            0.01e18,\n            15_000e18 * 1e18 / 1_000_000_000e18\n        );\n        registry = new ParamsRegistry(\n            address(this),\n            address(0x1234),\n            address(oracle),\n            address(0),\n            address(0xABCD),\n            address(0),\n            address(this),\n            address(0x5555)\n        );\n        vault = new CoreDeployer(address(registry), address(imd), address(usdc), address(yolo)).vault();\n        vm.deal(author, 1 ether);\n        vm.deal(doer, 1 ether);\n        imd.mint(doer, 100e18);\n        vm.prank(doer);\n        imd.approve(address(vault), type(uint256).max);\n    }\n\n    function _pay() internal pure returns (ImdPay memory p) {\n        p.tokenIn = address(0);\n    }\n\n    function _createEthTask() internal returns (uint256 taskId) {\n        vm.prank(author);\n        taskId = vault.createTask{value: 0.0105 ether}(\n            address(0), 0.01 ether, 0, \"dare\", \"post a photo of the sign\", 0, PROOF_LINK, \"\", 0.0005 ether, _pay()\n        );\n    }\n\n    /// @notice A doer with the bond in hand cannot submit to an open ETH task while the TWAP cannot be read.\n    function test_submitToEthTaskSurvivesAnUnavailableTwap() public {\n        uint256 taskId = _createEthTask();\n        assertEq(oracle.usdValue(address(0), 1 ether), 2000e18, \"the oracle works while the ring can serve 30 min\");\n\n        // a tick-moving swap landed in the USDC/IMD pool less than 30 minutes ago (or the ring is exhausted)\n        pool.setOld(true);\n\n        vm.prank(doer);\n        uint256 subId = ITaskVault(address(vault))\n            .submit(taskId, \"https://example.com/proof\", Evidence(bytes32(0), false, \"\"), 0, type(uint256).max, _pay());\n        assertEq(subId, 1, \"the submission must be taken: the vault has to degrade, not revert\");\n    }\n}","reproduction":"PoC contracts/test/scratch/TwapUnavailableBlocksEthTasks.t.sol (forge test --match-path test/scratch/TwapUnavailableBlocksEthTasks.t.sol): real ChainlinkUniV3PriceOracle, ParamsRegistry, desks and TaskVault; a stub v3 pool with the live tick 258947 whose observe reverts 'OLD' once setOld(true); author creates a 0.01 ETH task (works: Chainlink only), then setOld(true) (a tick-moving swap within the last 30 min), doer calls submit with 100 IMD approved: FAILS with 'OLD' on this commit; passes once the vault degrades instead of reverting.","severity":"medium","snippet":"            rewardImd = o.imdValue(currency, r);","title":"19. REPEAT PRICES/MEV: the IMD TWAP is unreadable whenever the USDC/IMD v3 pool's observation ring cannot cover 30 minutes (cardinality 1 on mainnet today; >180 tick-moving swaps in 30 min after the p"},{"citation":"resolved","description":"Roles audited: ParamsRegistry owner (48h timelocked params within bounds, global attester set, pause of new tasks, treasury address of future sweeps; no function moves deposits - verified: the vault's bank functions are onlyDesk and the desks have no owner), ProtocolDirectory owner (a timelocked list nobody in the core reads), HiddenList owner (instant visibility only, entry 12). One docs-vs-code gap: docs/MECHANICS.md 'Trust assumptions' says the price oracle 'is per parameter version, so the params owner can swap it for future tasks only', and the registry NatSpec lists priceOracle among counterparties that 'judge every task created under it'. DareTrophy.price() reads registry.core(registry.currentVersion()).priceOracle, not the task's version: after a new version (48h public) naming an oracle that returns usdValue(yolo, 1e18) = 1e36, price() = ceilDiv(1.5e18 * 1e18, 1e36) = 1 wei, so every accepted submission's trophy of every version mints for 1 wei of YOLO instead of ~13,000 YOLO ($1.50); the reverse (a tiny usdValue) only makes trophies dear, bounded by the minter's maxYolo. YoloSweetener uses the task's version for the cap (correct) and the current version only for the treasury address (documented). Impact: owner-controlled, visible 48h ahead, trophies only (burn revenue), so low. Smallest fix: read the oracle of the task's paramsVersion (vault.getTask(s.taskId).paramsVersion, already fetched in mint via the submission) or state the current-version rule in MECHANICS and the DareTrophy NatSpec.","line":103,"path":"contracts/src/DareTrophy.sol","reproduction":"State: registry currentVersion v2 whose priceOracle is a contract with hasPrice(yolo)=true and usdValue(yolo,1e18)=1e36. Call DareTrophy.price(): expected (per docs) the task-version oracle's ~13,043 YOLO; actual 1 wei. Code path lines 103-110.","severity":"low","snippet":"        IPriceOracle o = IPriceOracle(registry.core(registry.currentVersion()).priceOracle);","title":"20. REPEAT ROLES/DOCS VS CODE: DareTrophy prices every trophy by the CURRENT parameter version's priceOracle, so a future version's oracle swap re-prices (down to 1 wei of YOLO) trophies of all tasks,"},{"citation":"resolved","description":"Priced at IMD = $7.65, ETH ~ $2,200. (a) Author-doer collusion: the author may be the doer (only the bond split cares, doerIsContributor); a self-dare of R with a self-sweetened bonus B returns 90% of R and 90% of B to the same person after 24h with no dispute, plus a trophy (burn ~13,000 YOLO ~ $1.50) and an 'accepted dare' on the leaderboard. Cost: 10% of R + 10% of B + 0.0005 ETH (~$1.10) + gas; for the $5 minimum: ~$1.60 per fake completion. Who gains: whoever the project's off-chain incentives (250M YOLO) reward per completed dare; who loses: the incentives budget and the board's reputation. Recommendation: pay incentives only for dares whose author != doer and that a disputer or the swarm touched, or require the mandatory check (0.5 IMD = $3.83) for incentive-eligible dares. (b) Sybil swing of a dispute: decision needs margin >= 10 NFTs and >= 1 wallet, so 10 NFTs in one hand decide any undisputed-by-others vote; the remedy is an appeal: bond 1 IMD + fee 0.5 IMD = $11.48 at risk (returned with the fee if the swarm agrees), so a swing is only worth it for the relayer/voter share (50% of the loser bond, >= 0.5 IMD = $3.83) - not profitable unless the loser bond is large (3% of R: a $5,000 task gives 10 IMD = $76 to the swinging voters, against the $11.48 appeal the loser will file). HiddenList: 10 NFTs hide anything until the owner reacts (entry 13). (c) Spam: a task costs the author fee (~$1.10) + gas (~$2 at 2 gwei); the $5 reward is recoverable after a 1h notice, so a thousand junk tasks cost ~$3k and are the owner's/voters' cleanup on the site (HiddenList batches of 20 at 570k gas). Queue stuffing behind an active submission costs the bonds x2..x256 (510 IMD = $3,900 for 8 slots of a 1 IMD base), refunded only if the active one is accepted - not cheap.","line":237,"path":"contracts/src/TaskVault.sol","reproduction":"Numbers from VaultParams.sol:133-165 (minRewardUsd 5e18, minAuthorFee 0.0005 ETH, minBond 1e18, endDelta 10, minWinnerWallets 1, winnerBondBps 5000) and HiddenList constants (10 NFTs, 3 wallets).","severity":"info","snippet":"        if (authorFee < p.minAuthorFee) revert FeeTooLow();","title":"21. BOARD ABUSE: author-doer self-dares cost 10% + the author fee; one holder of 10 identity.md NFTs decides a dispute with no opposition; spam tasks cost about one author fee"},{"citation":"resolved","description":"Sizes at launch: doer bond max(1 IMD, 0.5 + 3% R_imd) = $7.65 on a $5 task (153% of R) but $12.90 on a $300 task (4.3%) and $153 on a $5,000 task (3%); dispute/appeal bond max(1 IMD, 0.5 + 1% R) = $7.65 up to R = $382; dispute ETH fee 0.0003-0.01 ETH ($0.66-$22) funds relayers; swarm 0.5 IMD = $3.83; settlement fee 10% of R; author fee $1.10. Too cheap to deter: for R < $300 a dishonest doer loses 3% of R if caught and gains 90% if not; a disputer who is right earns 50% of that bond (NFT right) or 100% (swarm) = $3.83-$6.45 for $0.66 fee + an appeal risk of $11.48, so disputing a $200 fraud is barely worth a stranger's time; the author (the natural disputer) gets only the bond share and must out-argue the doer within 24h. Too costly for a small task: a $5 dare needs a $7.65 doer bond + ~$1.10 author fee + 10% fee: fine for play, but a $20 dare's dispute needs $7.65 + $0.66 and at best returns $3.83. Recommendation (parameter-only, inside bounds, no code): doerBondBps 1,000 (10%) so a false proof on a $200 task risks $20, and largeRewardUsd lowered toward $100 so the $3.83 swarm check covers the band where NFT attention is thin; keep minBond at 1 IMD for sub-$10 dares.","line":142,"path":"contracts/src/VaultParams.sol","reproduction":"Formulas: Desk.sol:116-131 (bond), VaultParams.sol:137-163 (fees); worked example in MECHANICS.md:213-215 (300 USDC task: doer bond 1.68 IMD, dispute bond 1 IMD).","severity":"info","snippet":"        p.doerBondBps = 300;","title":"22. DEPOSIT/FEE ECONOMICS: below the $300 check threshold a false proof risks max(1 IMD, 3% of R) to win 90% of R, so fraud is +EV whenever fewer than ~3.3% of false proofs get disputed; above it the "},{"citation":"resolved","description":"Verified against code: splitter 250M / 150M / 77,777,777 / 50M / rest (222,222,223 + factory rounding); vestings 62.5M at once + 730 days, 74,074,074 at once + 365 days, dev and team 365 days no cliff, clocks from the split (entry 2). Day 0 unlocked = pool 150M + Stabilizer 150M + swarm 100M + incentives 62.5M + treasury 74.07M = 536.57M (53.7%, docs say 54%); month 6 = 721.4M, month 12 = 906.3M as documented. Risk/disclosure of the Stabilizer / MM fund (kept, per the brief): 150M at once plus 136.6M of other at-once project shares = 286.6M liquid in project EOAs on day 0, 1.9x the pool's 150M YOLO side; the opening valuation is 15,000 IMD (~$115k) for 1B, so the pool is thin and these wallets can move or cap the price at will; the docs state the wallet is public and 'every move is visible' but give no mandate, limit or reporting cadence - recommend publishing the fund's rules (no net selling for N days, position sizes) before launch. Demand for $YOLO: reward currency (optional), YoloSweetener bonuses, the trophy burn (~$1.50 per accepted dare, 10,000 YOLO fallback without a price), the NFT-gated first 30 minutes; no fee is converted into YOLO, so demand is discretionary. Do fees fund the project: yes, 10% of every accepted reward in its own currency, author fees in ETH, 4% of disputed-and-NFT-right rewards, treasury remainders of penalties, plus 80% of the pool's LP fee (factory-side, not in this code) - small at the $5-300 task sizes the bonds are tuned for; the YOLO share (treasury vesting) is the larger asset.","line":32,"path":"contracts/src/token/YoloSplitter.sol","reproduction":"Arithmetic from Vestings.sol:15-37, YoloSplitter.sol:31-36 and LaunchPlan.sol:73-79; docs/TOKENOMICS.md tables reproduced to the stated rounding.","severity":"info","snippet":"    uint256 public constant STABILIZER = 150_000_000e18;","title":"23. TOKENOMICS: the split and clocks match docs/TOKENOMICS.md; 54% is liquid on day 0 of which 28.7% sits in three project EOAs against a 15% pool; $YOLO demand comes only from YOLO-denominated reward"},{"citation":"resolved","description":"What exists: (1) VaultParams.sol:167 questionPrefix tells the panel to answer 'no, whatever was done' for crime, minors, violence, injury risk, harassment/doxxing, hate - reached by the mandatory check (R >= $300 or >= 40 IMD, or any $YOLO task without a price), by an appeal (someone pays 1 IMD + 0.5 IMD) or by callSwarm in NoDecision; (2) HiddenList: the owner or 10 NFTs stop the site from rendering the item; (3) TextRules bounds the condition to 256 UTF-8 bytes without quotes/control characters, nothing semantic. Gaps: a dare below $300 in ETH/USDC/IMD whose proof nobody disputes within 24h is Accepted and paid by the contract with no swarm or human in the loop; hiding it on the site removes the audience but not the payout, and an off-site link (the proof on X) still exists; an NFT vote cannot reject a submission without a dispute bond and the appeal path, where the prefix finally applies. Adequate: no, as a gate against an illegal dare being paid; adequate as a gate against being displayed: mostly (owner instant, voters 24h). Cheapest addition, no code: a parameter version with largeRewardUsd = minRewardUsd ($5) and largeRewardImd = minRewardImd (0.5 IMD) makes every submission prepay the 0.5 IMD ($3.83) check, so the prefix is applied to every proof before any payout; the bounds allow it (ParamsBounds lines 197-198). Cheapest code addition if the doer's $3.83 per submission is too much: CaseDesk._activate forces the check when HiddenList.status(vault, KIND_TASK, taskId) != VISIBLE (one view call; the hide vote then becomes a 'send it to the swarm' vote, still no money power for the owner since the swarm, not the owner, answers).","line":135,"path":"contracts/src/VaultParams.sol","reproduction":"Trace: createTask accepts any validText condition (TaskVault.sol:233); submit for R < $300 ETH sets large = false (Desk.sol:150) so _activate opens the window without a question (CaseDesk.sol:291-306); no dispute => _finalize(Accepted) at windowEnd (CaseDesk.sol:160-161) => _payReward credits 90% to the doer. HiddenList.status is read by nothing in contracts/src.","severity":"info","snippet":"        p.largeRewardUsd = 300e18;","title":"24. HARMFUL DARES: the illegal-content prefix is enforced only when a swarm question is asked (large checks, appeals, swarm calls), and HiddenList only hides on the site, so a sub-$300 illegal dare wi"},{"citation":"resolved","description":"Read in full: TaskVault, VaultTypes, VaultParams, ParamsRegistry, ProtocolDirectory, HiddenList, YoloSweetener, DareTrophy, OracleAttestation, desks/Desk, DeskApi, CaseDesk, DisputeDesk, helpers/AttestationChecker, SwarmQuestion, TextRules, libraries/VoteLib, oracles/YoloPoolSamples, ChainlinkUniV3PriceOracle, adapters/UniswapV3SwapAdapter, token/YoloToken, NftGate, ProjectVesting, Vestings, YoloSplitter, interfaces/IIntake, ITaskVault (signatures), IPriceOracle, script/LaunchPlan.sol; README.md, docs/MECHANICS.md, docs/TOKENOMICS.md, docs/AUDIT-INTERNAL.md in full; docs/LAUNCH-TASK.md by grep plus lines 118-140 and 189-248; test/Base.t.sol, HiddenList.t.sol, YoloOracle.t.sol and fork/PriceOracleFork.t.sol in part. Tools: forge 1.8.3 (solc 0.8.28, via-IR, cancun); full `forge test`: 59 suites, 388 passed, 0 failed, 5 skipped (fork suites, no FORK_URL); targeted: test/audit/swarm117 (29 passed), ReviewLaunch/ReviewQuestionSize/HiddenList/Gas/ReviewGas/ReviewGasGrief (57 passed, 1 fork skip); scratch PoC TwapUnavailableBlocksEthTasks.t.sol: 1 failed with 'OLD' as intended. Live reads through a public RPC (publicnode) at block 26198610: USDC/IMD v3 pool slot0/observations/observe, Intake v2 priceOf and requests. Not reached: the fork rehearsal of the four launches and the launch gas figures (taken from the docs), IMD's questionHash formula and wording screen, the ProjectFactory's salt/front-running behaviour, delegate.xyz and identity.md NFT contracts (treated as trusted per the brief), the site. Passes not run: Slither/Mythril (not provided). No file outside contracts/test/scratch/ and .imd-findings.json was written.","line":55,"path":"README.md","reproduction":"Commands: forge build; forge test; forge test --match-path 'test/audit/swarm117/*'; forge test --match-contract 'ReviewLaunch|ReviewQuestionSize|HiddenList|Gas'; forge test --match-path test/scratch/TwapUnavailableBlocksEthTasks.t.sol; git diff --stat b86a5f8 HEAD -- contracts/src contracts/script; cast call (pool slot0, observations(0), observe([1800,0]); Intake priceOf, requests).","severity":"info","snippet":"- **Tests:** 393 Foundry tests (unit, fuzz, invariant, gas, review and audit regression suites, mainnet fork).","title":"25. COVERAGE: all 29 contracts/src files, LaunchPlan.sol and the five docs read in full or in the cited parts; forge test 388 passed, 0 failed, 5 fork suites skipped; fork tests, the live Intake plane"}],"hash":"9fbf0d6d2b6f8c00b4f21cf1073f0909f25298f5a70dadbd17aa64b453ce0be2","nodeId":"68057a9f-89c3-4133-9dcb-61d9b1675e56","outcome":"completed","summary":"The audit is complete. The report is in `.imd-findings.json` at the repository root with all 26 required entries in order, and one Foundry PoC under `contracts/test/scratch/`. No existing file was modified.\n\n**Verdict (entry 0).** Ready for mainnet once one precondition is executed and verified on chain. No critical or high finding. All 11 fixes of audit 117cf05a are closed with no regression, and the HiddenList commit touched only HiddenList.sol, LaunchPlan.sol and the prefix string in VaultParams.sol.\n\n**Findings above info**\n\n- **Medium, entry 19 (with PoC).** The USDC/IMD Uniswap v3 pool the oracle TWAPs has observation cardinality 1 on mainnet today. I read it live: its single observation was 36 seconds old and `observe([1800,0])` reverted `OLD`. While that holds, every `submit` to an ETH or USDC task reverts, because `Desk._priceReward` calls `imdValue` with no try/catch (the $YOLO branch degrades to \"large\" instead). The launch doc lists `increaseObservationCardinalityNext(180)` as a manual post-launch step, and the fork test raises cardinality itself, so the suite never sees this. After the increase, more than 180 tick-moving swaps in 30 minutes (roughly $100 to $300 of gas) re-trigger it, which is enough to shut a doer out of a deadline. Fix: run the cardinality call before launch 2 and verify it, and make the vault degrade rather than revert. The PoC in `contracts/test/scratch/TwapUnavailableBlocksEthTasks.t.sol` fails with `OLD` on this commit.\n- **Low, entry 13.** HiddenList's \"3 distinct wallets\" rule is satisfied by one holder of 10 NFTs split over 3 wallets or delegate.xyz delegations. The vote mechanics themselves are correct; this is a visibility-only Sybil limit worth documenting.\n- **Low, entry 20.** DareTrophy prices trophies by the current parameter version's oracle, not the task's, so a future version's oracle swap re-prices trophies of all tasks down to 1 wei. MECHANICS says the oracle swap reaches future tasks only.\n\n**Business entries 21 to 24** quantify self-dare farming (about $1.60 per fake completion at the $5 minimum), the thin fraud deterrent below the $300 check threshold (fraud is positive expected value if under about 3% of false proofs are disputed), the day-0 liquidity concentration in project wallets relative to the pool, and the gap that a sub-$300 illegal dare pays with no swarm involvement. The cheapest enforceable gate is a parameter version setting the large threshold to the minimum reward so every submission prepays the 0.5 IMD check.\n\n**Coverage (entry 25).** Every source file, LaunchPlan.sol and the docs were read. Full `forge test`: 388 passed, 0 failed, 5 fork suites skipped. Targeted suites for the fix checks, launch, question size, HiddenList and gas all pass. Not reached: the fork rehearsal, the IMD plane's questionHash formula and wording screen, and the ProjectFactory.","treeHash":null,"usage":{"cachedInputTokens":5124142,"inputTokens":706,"model":"claude-fable-5-1","outputTokens":80978,"runtime":"claude","turns":58,"wallClockMs":1696465}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"45aa937328087de3","findings":[{"citation":"resolved","description":"`hideWallets` counts distinct `msg.sender` addresses, and `_mayVote` accepts a delegate.xyz delegate of the token's owner (HiddenList.sol:208). One holder of 10 identity.md NFTs delegates to two hot wallets of his own (two `delegateAll` calls, gas only) and casts three hide votes from three addresses: hideNfts = 10, hideWallets = 3, and after 24h `status` returns HIDDEN_BY_VOTE for an innocent task or proof. The wallet rule therefore adds no Sybil resistance over the 10-NFT quorum, while the NatSpec (line 15) and MECHANICS.md:34 / LAUNCH-TASK.md:242 present it as a second, independent threshold. Not a money risk (the contract holds none) but a cheap grief of the site's rendering of an honest item for up to the owner's reaction time; the owner's `setState(KEPT)` is the remedy. Smallest fix: count distinct current *owners* (`ownerOf`) of the counted tokenIds instead of callers (or state in the docs that the three wallets may be one person and that the 10-NFT quorum is the only real bar).","line":194,"path":"contracts/src/HiddenList.sol","reproduction":"Foundry (contracts/test/scratch/Audit2Scratch.t.sol, test_OneHolderHidesAnItemAloneThroughDelegation, passes = behaviour confirmed): MockNFT mints ids 1..10 to `holder`; holder calls delegateAll(hot1) and delegateAll(hot2); holder votes hide with [1,2,3,4], hot1 with [5,6,7], hot2 with [8,9,10] on (VAULT, kind 1, id 42). Expected by the documented rule: a round backed by a single person should not pass. Actual: status = (0,10,0,3,roundEnd) during the round and state 3 (HIDDEN_BY_VOTE) at roundEnd.","severity":"low","snippet":"        return s.hideNfts >= QUORUM_NFTS && s.hideNfts > s.keepNfts && s.hideWallets >= MIN_WALLETS;","title":"HiddenList: the '>= 3 distinct hide wallets' rule is met by one NFT holder through delegate.xyz, so 10 NFTs in one hand hide any item"},{"citation":"resolved","description":"The fix for swarm audit #1 only requires the read (block N) and the store (block N+1) to see the same slot0 and fee growth, and the NatSpec accepts 'a price held across the block boundary' because the holder 'loses it to arbitrage'. With builder bundles that exposure is near zero: the attacker's EOA places [swap YOLO up, sampleYoloPrice (read)] as the last transactions of block N and [sampleYoloPrice (store), swap back] as the first of block N+1 (two bundles, both EOA-sent so `msg.sender == tx.origin` holds). No third party trades between read and store, so the inflated tick is stored and, as the highest sample of the 2h window, becomes the YOLO price. Cost: two 1.25% LP fees on the swap size plus the bundle tips; the inventory is held for one block boundary with no public mempool exposure. Effect for 2h (bounded by `yoloMaxUsd` = $0.01 and the floor): every YOLO-reward submission becomes 'large' (0.5 IMD mandatory check, bond sized on the inflated value), the sweetener cap drops to the 30,000 YOLO minimum, the trophy price in YOLO falls. Bounded griefing, not theft; reported because the acknowledgement's reason (arbitrage exposure) does not hold under bundles. Hardening: require the stored read to be at least k blocks old (k >= 2) and additionally compare the pool's `liquidity` word, or median-of-window instead of highest.","line":114,"path":"contracts/src/oracles/YoloPoolSamples.sol","reproduction":"Unit (mock PoolManager extsload as in test/audit/swarm117/SampleSandwich.t.sol): block N: set tick to +20000 (YOLO 7.4x up), call sampleYoloPrice from EOA -> returns false, pendingRead set; roll to block N+1 with slot0/feeGrowth unchanged; call sampleYoloPrice -> returns true, samples[n] = +20000; then swap back. With 4 honest samples already in the window, highestYoloTick() = +20000 for the next 2h. Expected by the docs: a sandwich stores nothing; actual: stores when the swap-back is placed after the store in the next block (what a bundle guarantees).","severity":"low","snippet":"        step = p.blockNumber != 0 && p.blockNumber < block.number && p.poolState == state ? 2 : 1;","title":"YoloPoolSamples two-block sample is still sandwichable with a last-in-block / first-in-next-block bundle; the 'open to arbitrage' reasoning assumes a public hold"},{"citation":"resolved","description":"The fix for swarm audit #8 requires `withdrawNoticeAt` to be 1h old, but the notice is not tied to a quiet task: it can be announced during an active submission (status Open, activeSub != 0) and renewed every ~24h. When the active submission is rejected and the queue is empty, the author withdraws the whole active share in the same block, before any new doer's `submit`. The guarantee '1h of public notice' still holds (the WithdrawAnnounced event is at least 1h old) and a doer's `minReward` makes the race lose only gas, so this is hardening, not a broken promise: a doer evaluating a task has to read `withdrawNoticeAt(taskId, contributor)` for every contributor, not just the current pot. Hardening: refuse `announceWithdraw` while `activeSub != 0`, or clear the notice when a submission is activated so that the hour always runs on a task that is open for submissions.","line":301,"path":"contracts/src/TaskVault.sol","reproduction":"Author creates task (pot 100 USDC); doer A submits (active). At T the author calls announceWithdraw -> withdrawNoticeAt = T+1h. A's review window ends rejected at T+26h (no dispute: accepted, so take a disputed+rejected A). At T+26h the author calls withdrawContribution in the same block as the rejection poke: fromActive = 100 USDC, succeeds (notice due, within 24h validity). A doer B who posted his proof after seeing the task 'open' at T+25h cannot submit (RewardChanged / RewardTooSmall). Expected per the fix's intent: an hour between the notice becoming visible on an open-for-submission task and the money leaving; actual: zero seconds after the busy period ends.","severity":"info","snippet":"    function announceWithdraw(uint256 taskId) external nonReentrant {","title":"TaskVault.announceWithdraw may be filed while a submission is active, so the 1h notice can be 'due' the instant the busy period ends (standing notice)"}],"hash":"d380ee69dcac6054051b0001b078342da10f0b8ff3535a0ee87f9e81a2245b35","nodeId":"e82603e2-555a-42bf-a3bd-d462b4f38015","outcome":"completed","summary":"The audit is complete. Findings are in `.imd-findings.json` (3 entries, all low or info) and the evidence test is `contracts/test/scratch/Audit2Scratch.t.sol`. The report follows the mandatory format.\n\n## Verdict and fix checks\n\n**0. VERDICT: ready for mainnet, no blockers.** No critical, high or medium finding. All 11 fixes of audit 117cf05a are closed and none regressed. HiddenList touches no money and no core contract. Full suite: 388 project tests pass, 5 fork tests skipped (no FORK_URL), plus 4 scratch checks. Three low or info items remain: the HiddenList \"3 wallets\" rule is one person with delegation, the two-block YOLO sample is still sandwichable with a cross-block bundle, and a withdraw notice can be filed during a busy period. None needs a code change before launch, but the first two deserve a doc line.\n\n1. **YoloPoolSamples, closed yes, new issue yes (low).** `_step` stores only when `blockNumber < block.number` and slot0 plus both fee growth words are unchanged (YoloPoolSamples.sol:107-115), so one-block swap-sample-swap-back stores nothing. Highest-of-window, the $0.01 cap and the 30,000 YOLO floor hold (`_yoloUsd`, `_cap`). New: the hold \"across a block boundary\" is cheap with a last-of-block N and first-of-block N+1 bundle. Details in finding 2 of the JSON.\n2. **ProjectVesting.bindStarter, closed yes, no new issue.** Transient flag set in the constructor, cleared on first call, `starter != 0` refused (ProjectVesting.sol:61-71). Only `msg.sender == starter` starts a clock in `release` (line 91). The splitter binds in its own constructor (YoloSplitter.sol:67-70). A 1 wei pre-start pays only the unlocked part.\n3. **MAX_SHORTFALL_BPS, closed yes, no new issue.** `shortCap = penalty * 2000 / BPS` after the winning caller's fee (DisputeDesk.sol:518-522). Relayer side is irrelevant since shortfall is keyed by relayer, not by side.\n4. **Doer self-appeal, closed yes, no new issue.** `appeal` reverts for the doer unless the NFT rejected him (line 135). `callSwarm` reverts for the doer unless `sidesDoer` (line 168). Sybil via a second address remains, acknowledged C-01c.\n5. **VoteLib one side per wallet, closed yes, no new issue.** `walletSides` check skips with reason 4 before any token bit is set (VoteLib.sol:88-94), so the NFTs stay unused and nothing is paid.\n6. **NO_PRICE_WAIT, closed yes, no new issue.** No price: `noPriceSince` set, `PayoutDeferred`, nothing moves (YoloSweetener.sol:181-189). After 7 days still no price, `pay = bonus` in full. Sweeteners stay locked meanwhile (`returnBase == 0` reverts `BonusLocked`), as documented.\n7. **SWARM_DELIVERY_GRACE, closed yes, no new issue.** `_swarmStatus` counts an answer only if `answerTime <= askedAt + timeout` and `recordedAt <= deadline + 1h`, and declares silence only at or after `lastDelivery` (CaseDesk.sol:355-372). `stepDeadline` uses the same bound (lines 264, 271). Either poke order gives the same outcome.\n8. **announceWithdraw, closed yes, hardening note (info).** Active share of an Open task needs `at <= now <= at + 1 day` (TaskVault.sol:323-331). Pending top-ups and Expired/Unwound need none. New info item: the notice can be filed while a submission is active (finding 3).\n9. **AttestationChecker expiresAt, closed yes, no new issue.** `expiresAt != 0 && expiresAt < now` returns 6, surfaced as `AttestationIgnored(.., 16)` (AttestationChecker.sol:53, TaskVault.sol:464).\n10. **Last attester, closed yes, no new issue.** `attesterCount == 1` refuses removal (ParamsRegistry.sol:198). MECHANICS.md lines 283-288 and 310-317 match the code, including the counterparty lever.\n11. **LaunchPlan INTAKE, closed yes, no new issue.** Single constant `0xa43e6F75ee006411F79Ac1C84120606C2330DE82` (LaunchPlan.sol:55), equals the oracle-consumer reference's version 2 address. Version 1 only in the comment on line 54.\n\n## Code\n\n12. **HiddenList money, info, no issue.** No `payable`, `receive`, `fallback` or token call anywhere in HiddenList.sol. Plain ETH reverts (project test `t","treeHash":null,"usage":{"cachedInputTokens":2968029,"inputTokens":454,"model":"claude-fable-5-1","outputTokens":42888,"runtime":"claude","turns":48,"wallClockMs":1400854}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"c2e167266d13dcf4","findings":[{"citation":"resolved","description":"1. No critical or high: no path steals, double-pays or strands money; all 11 fixes of audit 117cf05a are closed (entries 1-11) with no regression; HiddenList touches no money and no core contract (entry 12). 2. One operational blocker, not a code blocker: the USDC/IMD v3 pool must have its observation cardinality raised and 30 minutes of swaps BEFORE the first ETH/USDC task of launch 2 can take a submission; the doc lists it after launch 3 as 'outside the launches' (entry 19, low; the specialist's medium and its 'refill DoS' claim did not hold up). 3. Medium: one identity.md NFT holder can keep an item un-hideable by vote for its whole life through the 7-day cooldown (entry 13b). 4. Low: the 3-distinct-wallet rule is met by one holder through delegate.xyz (13a); a last-block hide swing cannot be answered and an owner VISIBLE restores no cooldown (13c); a withdrawal notice renews while live, so a standing notice gives no fresh hour (8/16); DareTrophy prices by the current version's oracle, not the task's (20). 5. Business: below the $300 check threshold an undisputed false proof is paid with no swarm or human in the loop, so the illegal-content prefix never applies to it (22, 24). 6. forge test on forge 1.8.3: 388 passed, 0 failed, 5 fork suites skipped; on forge 1.8.5 37 suites fail in setUp (test harness only, entry 25). 7. Recommendation: launch after fixing 13b (one line), running the cardinality step before launch 2, and recording 13a/13c/8/20 in the docs or a later version.","line":130,"path":"docs/LAUNCH-TASK.md","reproduction":"Judge's reproduction of every specialist finding: the four attached proofs and the judge's RollingNotice.t.sol fail on this commit for the stated reasons (forge test --match-path 'test/scratch/*'); the full suite passes on forge 1.8.3; git diff --stat b86a5f8 7714a24 -- contracts/src contracts/script shows HiddenList.sol (new), LaunchPlan.sol (+14/-5) and one line of VaultParams.sol only.","severity":"info","snippet":"   - `increaseObservationCardinalityNext(180)` on the USDC/IMD pool `0x894d…c520` and 30 minutes of swaps. Without this","title":"0. VERDICT: ready for mainnet once the USDC/IMD pool's observation ring is raised and verified before launch 2; no critical or high; one medium (HiddenList cooldown grief), four low"},{"citation":"resolved","description":"Reproduced: a read in block N stores only through a call in a later block whose keccak(slot0, feeGrowthGlobal0, feeGrowthGlobal1) equals the read; the swap back of a one-block sandwich changes fee growth and voids it. highestYoloTick (highest of the 2h window), the $0.01 cap and the 30,000 YOLO floor of the sweetener cap hold. Residual, acknowledged in AUDIT-INTERNAL.md and the NatSpec: a price held across one block boundary samples. The flow specialist's 'bundle' variant (swap + read last in block N, store + swap back first in block N+1) needs the attacker's EOA to win the top-of-block slot of N+1 against arbitrageurs who profit from the inflated tick; it is bounded (2h of dearer YOLO submissions, sweetener cap at its 30,000 YOLO floor) and does not change the acknowledgement's reason, so it stays an info note, not a finding.","line":114,"path":"contracts/src/oracles/YoloPoolSamples.sol","reproduction":"forge test --match-path 'test/audit/swarm117/SampleSandwich.t.sol': SampleSandwichTest 2 passed, DoubleSandwichTest 5 passed (read and store in one block store nothing; any swap between voids the read).","severity":"info","snippet":"        step = p.blockNumber != 0 && p.blockNumber < block.number && p.poolState == state ? 2 : 1;","title":"1. FIX CHECK YoloPoolSamples two-block sample: closed yes, new issue no"},{"citation":"resolved","description":"Reproduced: only `starter` starts a clock; bindStarter is accepted once and only while the transient flag set in the constructor is still 1 (the deployment transaction); YoloSplitter's constructor binds all four vestings and split() is its only release path. A 1-wei transfer and release() by anyone pays only the unlocked part and starts nothing. Note (procedure, not a defect): YoloSplitter._bind silently skips a vesting with no code, so if launch 0 were ever split into two transactions with the splitter first, no clock could ever start; launch 0 is one launchContracts call, vestings before splitter, and the fork rehearsal runs it, so this is a one-line check to add to the launch-0 text (assert starter() == YoloSplitter on all four vestings before launch 1).","line":91,"path":"contracts/src/token/ProjectVesting.sol","reproduction":"forge test --match-contract 'VestingClockPreStart|VestingStarterBinding': 4 passed (clock cannot start before the share arrives; rebinding reverts BindingClosed).","severity":"info","snippet":"        if (msg.sender == starter && startOf[token] == 0 && IERC20(token).balanceOf(address(this)) != 0) {","title":"2. FIX CHECK ProjectVesting.bindStarter: closed yes, new issue no"},{"citation":"resolved","description":"Reproduced: after the winning caller's fee, relayers of the case share min(shortfallTotal, 20% of the penalty); the remaining >= 80% goes to winners and correct votes by winnerBondBps whichever side the relayer voted; unpaid shortfall is dropped and claimed pro rata.","line":518,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"forge test --match-test test_losingVoterDrainsPenaltyByRelayingOwnBallots: passed. Arithmetic: winners + voters receive P - min(S, P/5) >= 0.8 P.","severity":"info","snippet":"        uint256 shortCap = penalty * MAX_SHORTFALL_BPS / BPS;","title":"3. FIX CHECK MAX_SHORTFALL_BPS = 20%: closed yes, new issue no"},{"citation":"resolved","description":"Reproduced: appeal() reverts DoerCannotDispute for the doer unless the NFT vote rejected him; callSwarm() reverts for the doer unless he sides with himself; dispute() reverts for the doer. Puppet addresses remain the acknowledged C-01b/c Sybil limit.","line":135,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"forge test --match-test 'test_doerAppealsAgainstOwnAcceptance|test_doerMayStillAppealARejection|test_doerCannotCallTheSwarmAgainstHimself': 3 passed.","severity":"info","snippet":"        if (sender == s.doer && s.nftDecision != Verdict.Reject) revert DoerCannotDispute();","title":"4. FIX CHECK doer cannot appeal or callSwarm against own accepted submission: closed yes, new issue no"},{"citation":"resolved","description":"Reproduced: walletSides[voter] is set on the first counted ballot; a later ballot by the same voter on the other side is skipped with BallotSkipped reason 4 before any tokenId bit is set, so its NFTs stay unused and unpaid; same voter, same side, more NFTs is added.","line":89,"path":"contracts/src/libraries/VoteLib.sol","reproduction":"forge test --match-test test_walletVotesBothSidesAndIsPaid: passed.","severity":"info","snippet":"            if (seen != 0 && seen != sideBit) {","title":"5. FIX CHECK VoteLib one wallet, one side: closed yes, new issue no (delegate variant is info, entry 18)"},{"citation":"resolved","description":"Reproduced: payout() without a YOLO price and without swarm confirmation records noPriceSince on the first call, emits PayoutDeferred and moves nothing; 7 days later, still without a price, the doer gets the full bonus minus the fee; with a price the largeRewardUsd cap applies; returnBase guards a second payout.","line":186,"path":"contracts/src/YoloSweetener.sol","reproduction":"forge test --match-test 'test_sweetenerPayoutWithoutPriceReturnsAllToSweetener|test_sweetenerPayoutAfterAWeekWithoutPricePaysInFull|test_sweetenerCapStillAppliesWithAPrice': 3 passed.","severity":"info","snippet":"                if (block.timestamp < uint256(since) + NO_PRICE_WAIT) {","title":"6. FIX CHECK YoloSweetener NO_PRICE_WAIT: closed yes, new issue no"},{"citation":"resolved","description":"Reproduced: an answer counts iff signed within swarmTimeout (issuedAt clamped to [askedAt, now]) and recorded within the 1h grace; silence is declared only at askedAt + timeout + grace or when the Intake reports the request completed with no recorded answer; stepDeadline includes the grace. The outcome is the same whether the poke or the delivery lands first.","line":363,"path":"contracts/src/desks/CaseDesk.sol","reproduction":"forge test --match-test 'test_pokeAtDeadlineTurnsLateAnswerIntoSilence|test_answerAfterTheGraceIgnoredWhoeverPoked|test_answerDeliveredAfterTheGraceIgnoredEvenUnpoked|test_answerSignedAfterTheTimeoutIsSilenceAtTheEndOfTheGrace': 4 passed.","severity":"info","snippet":"        if (r.answered && r.answerTime <= deadline && r.recordedAt <= lastDelivery) {","title":"7. FIX CHECK SWARM_DELIVERY_GRACE: closed yes, new issue no"},{"citation":"resolved","description":"Reproduced: an active share of an Open task leaves only with activeSub == 0 and a notice 1h..25h old; pending top-ups leave at any time; Expired/Unwound need no notice; a submission inside the hour locks the share. The fix closes the reported attack (withdraw with no notice in the block the proof appears). What it leaves open is reported under entry 16: announceWithdraw overwrites a live notice, so a notice renewed every 24h is always due.","line":325,"path":"contracts/src/TaskVault.sol","reproduction":"forge test --match-test 'test_authorFrontRunsSubmitWithWithdrawal|test_announcedWithdrawalGivesTheDoerAnHour|test_noticeExpiresAfterADay|test_noNoticeNeededAfterExpiry': 4 passed.","severity":"info","snippet":"            if (at != 0 && block.timestamp >= at && block.timestamp <= at + WITHDRAW_NOTICE_VALID) {","title":"8. FIX CHECK TaskVault.announceWithdraw notice: closed yes; new issue yes, low (standing notice renews while live, see entry 16)"},{"citation":"resolved","description":"Reproduced: code 6 (surfaced as AttestationIgnored reason 16 by TaskVault.onOracleResult) when expiresAt is set and in the past; 0 means no expiry, as in the oracle reference.","line":53,"path":"contracts/src/helpers/AttestationChecker.sol","reproduction":"forge test --match-test test_expiredAttestationIsIgnored: passed.","severity":"info","snippet":"        if (a.expiresAt != 0 && a.expiresAt < block.timestamp) return 6;","title":"9. FIX CHECK AttestationChecker expiresAt: closed yes, new issue no"},{"citation":"resolved","description":"Reproduced: executeAttester refuses a removal at attesterCount == 1; the count tracks additions and removals; the constructor sets 1. docs/MECHANICS.md 'Trust assumptions' states the global attester set, the effect of a removal on open cases, LastAttester and the add-first rotation, and the owner's lever through a new version's counterparties, matching the code.","line":198,"path":"contracts/src/ParamsRegistry.sol","reproduction":"forge test --match-test 'test_lastAttesterCannotBeRemoved|test_Fixed_B02': passed. grep 'LastAttester' docs/MECHANICS.md contracts/src/ParamsRegistry.sol.","severity":"info","snippet":"                if (attesterCount == 1) revert LastAttester();","title":"10. FIX CHECK last attester cannot be removed; MECHANICS matches: closed yes, new issue no"},{"citation":"resolved","description":"Reproduced: the only Intake constant in LaunchPlan.sol is version 2 (0xa43e6f75ee006411f79ac1c84120606c2330de82), used for the ParamsRegistry of launch 1; version 1 appears only in the comment above it. docs/LAUNCH-TASK.md names v2 in the launch 1 and launch 2 texts.","line":55,"path":"contracts/script/LaunchPlan.sol","reproduction":"grep -n '0x1397\\|0xa43e' contracts/script/LaunchPlan.sol docs/LAUNCH-TASK.md; forge test --match-test test_L2_docLaunch1Args_matchPlan: passed.","severity":"info","snippet":"    address internal constant INTAKE = 0xa43e6F75ee006411F79Ac1C84120606C2330DE82;","title":"11. FIX CHECK LaunchPlan single INTAKE = Intake v2: closed yes, new issue no"},{"citation":"resolved","description":"HiddenList has no payable function, no receive or fallback, and makes only two view calls inside try/catch (IERC721.ownerOf, IDelegateRegistry.checkDelegateForERC721 with the correct v2 argument order and rights 0). Nothing in contracts/src imports or calls it. git diff b86a5f8..7714a24 over contracts/src and contracts/script: HiddenList.sol (new), LaunchPlan.sol (extras index 3) and one line of VaultParams.sol (the questionPrefix string); TaskVault, CaseDesk, DisputeDesk, ParamsRegistry, ProtocolDirectory and NftGate sources are byte-identical, so their bytecode differs only through the prefix constant compiled into LaunchParams.initial.","line":18,"path":"contracts/src/HiddenList.sol","reproduction":"forge test --match-contract HiddenListTest: 41 passed incl. test_PlainEthTransferReverts and test_EthWithDataReverts; git diff --stat b86a5f8 7714a24 -- contracts/src contracts/script: 3 files.","severity":"info","snippet":"contract HiddenList {","title":"12. HIDDENLIST MONEY: no issue. No payable, receive or token call; plain ETH reverts; the HiddenList commit touched no core source"},{"citation":"resolved","description":"hideWallets is incremented once per msg.sender that first votes hide in a round, and _mayVote accepts a delegate.xyz delegate of the token's current owner (line 208), so three addresses controlled by one person satisfy MIN_WALLETS. One holder of 10 identity.md NFTs calls delegateAll on two hot wallets (no transfer, gas only) and casts hide with 8/1/1 tokenIds from three addresses: hideNfts 10, hideWallets 3, and after 24h status() returns HIDDEN_BY_VOTE for an honest task or proof until the owner's setState. The NatSpec (line 15), MECHANICS.md:34 and the launch 3 text present the wallet rule as a second, independent bar; it adds no Sybil resistance over the 10-NFT quorum. Display only: the task keeps running and no money moves, so low. Merged from three specialists (economics, flow, math #13). Smallest fix: count distinct current owners (the ownerOf result) instead of msg.sender for hideWallets, which forces the real transfers the docs imply; or state in the docs that the only real bar is 10 NFTs and that the owner's KEPT is the remedy.","line":159,"path":"contracts/src/HiddenList.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.28;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC721} from \"@openzeppelin/contracts/token/ERC721/ERC721.sol\";\nimport {HiddenList} from \"src/HiddenList.sol\";\nimport {IDelegateRegistry} from \"src/interfaces/IDelegateRegistry.sol\";\n\ncontract SybilNft is ERC721(\"identity.md\", \"IMDID\") {\n    function mint(address to, uint256 id) external {\n        _mint(to, id);\n    }\n}\n\n/// @dev delegate.xyz v2 subset: wallet-level delegation (`delegateAll`), as the real registry offers.\ncontract SybilDelegates is IDelegateRegistry {\n    mapping(address => mapping(address => bool)) public delegated;\n\n    function delegateAll(address to, bool on) external {\n        delegated[msg.sender][to] = on;\n    }\n\n    function checkDelegateForERC721(address to, address from, address, uint256, bytes32) external view returns (bool) {\n        return delegated[from][to];\n    }\n}\n\n/// @notice HiddenList's \"3 distinct hide wallets\" rule counts msg.sender, not the NFT holder. One holder of 10 NFTs\n///         delegates (delegate.xyz, free, no transfer) to two hot wallets and votes from three addresses: the round\n///         passes with a single real holder behind it. Fails on this code (status == HIDDEN_BY_VOTE); passes once\n///         distinct wallets are counted per NFT holder (ownerOf) instead of per caller.\ncontract HiddenListDelegateSybilTest is Test {\n    HiddenList list;\n    SybilNft nft;\n    SybilDelegates delegates;\n\n    address constant OWNER = address(0x7EA5);\n    address constant VAULT = address(0xBA07);\n    address holder = address(0xA11CE);\n    address hot1 = address(0xD001);\n    address hot2 = address(0xD002);\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        nft = new SybilNft();\n        delegates = new SybilDelegates();\n        list = new HiddenList(OWNER, address(nft), address(delegates));\n        for (uint256 i = 1; i <= 10; ++i) {\n            nft.mint(holder, i);\n        }\n        vm.startPrank(holder);\n        delegates.delegateAll(hot1, true);\n        delegates.delegateAll(hot2, true);\n        vm.stopPrank();\n    }\n\n    function _ids(uint256 from, uint256 n) internal pure returns (uint256[] memory a) {\n        a = new uint256[](n);\n        for (uint256 i; i < n; ++i) {\n            a[i] = from + i;\n        }\n    }\n\n    function test_oneHolderThreeAddressesPassesTheThreeWalletRule() public {\n        vm.prank(holder);\n        list.vote(VAULT, 1, 1, true, _ids(1, 8));\n        vm.prank(hot1);\n        list.vote(VAULT, 1, 1, true, _ids(9, 1));\n        vm.prank(hot2);\n        list.vote(VAULT, 1, 1, true, _ids(10, 1));\n        (, uint32 hideNfts,, uint16 hideWallets,) = list.status(VAULT, 1, 1);\n        assertEq(hideNfts, 10, \"all ten NFTs of one holder counted\");\n        assertEq(hideWallets, 3, \"three 'wallets' from one holder\");\n\n        vm.warp(block.timestamp + 24 hours);\n        (uint8 state,,,,) = list.status(VAULT, 1, 1);\n        // the documented rule \">= 3 distinct hide wallets\" is meant to need three holders; one holder passes it\n        assertEq(state, list.VISIBLE(), \"a single NFT holder must not hide an item alone\");\n    }\n}","reproduction":"contracts/test/scratch/Proof_476db268aaec.t.sol (fails on this commit: '3 != 0'): MockNFT mints ids 1..10 to holder; holder.delegateAll(hot1), delegateAll(hot2); holder votes hide [1..8], hot1 votes hide [9], hot2 votes hide [10]; warp 24h; status() expected VISIBLE (one holder), actual HIDDEN_BY_VOTE (3).","severity":"low","snippet":"            if (hide) ++s.hideWallets;","title":"13a. HIDDENLIST VOTE: the '>= 3 distinct hide wallets' rule counts callers, so one holder of 10 NFTs hides any item alone through two delegate.xyz delegations or two transfers"},{"citation":"resolved","description":"The first hide vote opens a round whatever its size, and a round that ends under quorum starts a 7-day cooldown in which every hide vote returns silently (no event, no revert, line 126). The author of a harmful dare who holds or is delegated one identity.md NFT votes hide on his own task the moment it is created; the round ends 24h later with 1 NFT; for the next 7 days the honest hide votes of any number of holders are no-ops; at day 8 he votes once more. Cost: 1 NFT and about 50k gas per 8 days. Effect: the NFT-vote layer of the harmful-content gate is defeated by exactly the party it targets; only the owner's setState hides the item. Display only (no money), but cheap, repeatable and silent, so medium ('cheap griefing of a documented guarantee'). Smallest fix: start the cooldown only after a contested round (keepNfts > 0), or let a round that ended under quorum be reopened at once by a hide vote from a wallet that did not vote in it; or shorten COOLDOWN for uncontested rounds. The launch 3 text forbids changing the cooldown window 'unless a reproduced finding requires it'; this is such a finding.","line":126,"path":"contracts/src/HiddenList.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.28;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC721} from \"@openzeppelin/contracts/token/ERC721/ERC721.sol\";\nimport {HiddenList} from \"src/HiddenList.sol\";\n\ncontract GriefNft is ERC721(\"identity.md\", \"IMDID\") {\n    function mint(address to, uint256 id) external {\n        _mint(to, id);\n    }\n}\n\n/// @notice One NFT holder (the author of a harmful dare, for instance) opens a hide round on his own item the moment\n///         it exists, with one hide vote and nobody watching. The round fails after 24 h (1 of 10 NFTs) and the 7-day\n///         cooldown then blocks every honest hide vote: 10 honest holders in 10 wallets voting on day 2 count nothing\n///         and the item stays VISIBLE for the whole week. Fails on this code; passes once an uncontested round that\n///         fell short of quorum (no keep votes) starts no cooldown, or once hide votes may reopen a round during it.\ncontract HiddenListCooldownGriefTest is Test {\n    HiddenList list;\n    GriefNft nft;\n\n    address constant OWNER = address(0x7EA5);\n    address constant VAULT = address(0xBA07);\n    address griefer = address(0xBAD);\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        nft = new GriefNft();\n        list = new HiddenList(OWNER, address(nft), address(0));\n        nft.mint(griefer, 1);\n    }\n\n    function _ids1(uint256 id) internal pure returns (uint256[] memory a) {\n        a = new uint256[](1);\n        a[0] = id;\n    }\n\n    function test_oneNftBlocksHonestHidersForAWeek() public {\n        // day 0: the griefer opens a round alone\n        vm.prank(griefer);\n        list.vote(VAULT, 1, 1, true, _ids1(1));\n\n        // day 2: ten honest holders, ten wallets, try to hide the item\n        vm.warp(block.timestamp + 2 days);\n        for (uint256 i; i < 10; ++i) {\n            address w = address(uint160(0x1000 + i));\n            nft.mint(w, 100 + i);\n            vm.prank(w);\n            list.vote(VAULT, 1, 1, true, _ids1(100 + i));\n        }\n        (uint8 s, uint32 hideNfts,, uint16 hideWallets, uint64 roundEnd) = list.status(VAULT, 1, 1);\n        assertLt(roundEnd, block.timestamp, \"no new round opened: cooldown\");\n\n        // day 3: still visible although 10 NFTs in 10 wallets asked to hide it\n        vm.warp(block.timestamp + 1 days);\n        (s,,,,) = list.status(VAULT, 1, 1);\n        assertEq(hideNfts + hideWallets, 2, \"honest votes were dropped (1 NFT, 1 wallet of the griefer only)\");\n        assertEq(s, list.HIDDEN_BY_VOTE(), \"ten honest holders could not hide the item within the week\");\n    }\n}","reproduction":"contracts/test/scratch/Proof_5d8c4edc729c.t.sol (fails on this commit: '0 != 3'): griefer with 1 NFT votes hide at t0; at t0+2d ten honest holders in ten wallets vote hide with 10 NFTs: all dropped (roundEnd < now, cooldown until t0+8d); at t0+3d status() is VISIBLE with hideNfts 1 / hideWallets 1; expected HIDDEN_BY_VOTE after the honest round.","severity":"medium","snippet":"                if (block.timestamp < s.roundEnd + COOLDOWN) return; // cooldown after a round that did not pass","title":"13b. HIDDENLIST VOTE: one NFT opens an uncontested round on the voter's own item and the 7-day cooldown then drops every honest hide vote; repeated every 8 days it keeps the item un-hideable by vote f"},{"citation":"resolved","description":"Keep votes are accepted only while block.timestamp < roundEnd and roundEnd never moves. A cluster of 10 NFTs in 3 wallets opens the round with 1 NFT, so keepers see a weak hide side (and need keepNfts >= hideNfts to block, which they must pre-commit blind); in the round's last block the cluster adds the other 9 and nobody can reply; the passed round stands until the owner acts (line 125). If the owner restores VISIBLE, setState zeroes roundEnd (line 102), so the cooldown check at line 126 does not apply and the next hide vote reopens a round immediately; only KEPT ends it. Cost: 10 identity.md NFTs (the same cluster as 13a; delegate.xyz lets borrowed or delegated NFTs count). Display only, the owner's KEPT is a full remedy, so low. The specialist's medium is reduced: it needs the 10-NFT quorum, and 13b is both cheaper and stronger. Smallest fix: extend roundEnd by a short reply window (e.g. 1h) when a hide vote lands in the last hour and flips _won, and apply COOLDOWN after an owner setState(VISIBLE) (or document that restored items should be set KEPT).","line":120,"path":"contracts/src/HiddenList.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.28;\n\nimport \"forge-std/Test.sol\";\nimport {HiddenList} from \"src/HiddenList.sol\";\n\ncontract Nft { mapping(uint256 => address) public ownerOf; function set(uint256 id, address o) external { ownerOf[id] = o; } }\n\n/// A 10-NFT cluster opens a round with 1 NFT, waits, and adds 9 in the round's last second: defenders who saw\n/// 1 hide NFT for 24h are outvoted with no chance to answer, and the innocent item is hidden until the owner acts.\n/// After the owner restores it (VISIBLE), the same cluster re-hides it in the next 24h: no cooldown applies.\ncontract HiddenListSnipeTest is Test {\n    Nft nft = new Nft();\n    HiddenList hl;\n    address owner = address(0xA11CE);\n    address[3] w = [address(0x1), address(0x2), address(0x3)];\n    address keeper = address(0x4);\n    address vault = address(0xBEEF);\n\n    function setUp() public {\n        hl = new HiddenList(owner, address(nft), address(0));\n        for (uint256 i = 1; i <= 10; ++i) nft.set(i, w[i % 3]);\n        nft.set(100, keeper); nft.set(101, keeper);\n    }\n\n    function _ids(uint256 a, uint256 b, uint256 m) internal pure returns (uint256[] memory r) {\n        uint256 n; for (uint256 i = a; i <= b; ++i) if (i % 3 == m) ++n;\n        r = new uint256[](n); n = 0; for (uint256 i = a; i <= b; ++i) if (i % 3 == m) r[n++] = i;\n    }\n\n    function _snipe() internal {\n        vm.prank(w[1]); uint256[] memory one = new uint256[](1); one[0] = 1; hl.vote(vault, 1, 7, true, one);\n        // keeper defends with 2 NFTs > 1 hide NFT\n        uint256[] memory k = new uint256[](2); k[0] = 100; k[1] = 101;\n        vm.prank(keeper); hl.vote(vault, 1, 7, false, k);\n        (,,,, uint64 end) = hl.status(vault, 1, 7);\n        vm.warp(end - 1);\n        vm.prank(w[0]); hl.vote(vault, 1, 7, true, _ids(2, 10, 0));\n        vm.prank(w[1]); hl.vote(vault, 1, 7, true, _ids(2, 10, 1));\n        vm.prank(w[2]); hl.vote(vault, 1, 7, true, _ids(2, 10, 2));\n        vm.warp(end);\n    }\n\n    function test_lastSecondSnipeHidesWithoutReply() public {\n        _snipe();\n        (uint8 st,,,,) = hl.status(vault, 1, 7);\n        // expected (fixed, e.g. round extended on a late swing): still VISIBLE at `end`; actual: HIDDEN_BY_VOTE\n        assertEq(st, 0, \"hidden by a last-second swing nobody could answer\");\n    }\n}","reproduction":"contracts/test/scratch/Proof_ced07852b14f.t.sol (fails on this commit: '3 != 0'): w1 votes hide [1] at t0 (round end t0+24h); keeper votes keep [100,101]; at end-1 w0, w1, w2 vote hide with the remaining 9 NFTs; at end status() returns 3 (HIDDEN_BY_VOTE); expected: a reply window for the keep side.","severity":"low","snippet":"        bool activeRound = s.roundEnd != 0 && block.timestamp < s.roundEnd;","title":"13c. HIDDENLIST VOTE: the round end is fixed at the first hide vote, so a last-block hide swing of 9 NFTs cannot be answered; after an owner setState(VISIBLE) no cooldown applies and the same cluster "},{"citation":"resolved","description":"ParamsRegistry's constructor runs LaunchParams.validate: prefix bytes + 2 x maxTextBytes (256) <= 2,000 - QUESTION_FIXED, and the worst-case body ((maxClaims+3) x (maxTextBytes+40) + prefix + fixed parts) <= 16,384; the new prefix (one line longer in VaultParams.sol:167, the illegal-content sentence) passes both, since the registry deploys in every suite and ReviewQuestionSize passes. Launch 3 (YoloSweetener, DareTrophy, ProtocolDirectory, HiddenList) is 6.90M gas by the docs' fork rehearsal (not re-run here, no FORK_URL); HiddenList's runtime is small (21.5 KB per LAUNCH-TASK.md:60); margin about 9.9M under 16,777,216. questionHash = keccak256(question) changes with the prefix, but it is only recorded (SwarmAsked, SwarmRequest.questionHash 'checks nothing', VaultTypes.sol) and the callback binds by Intake request id and msg.sender, so nothing depends on the old hash.","line":242,"path":"contracts/src/VaultParams.sol","reproduction":"forge test --match-contract 'ReviewLaunch|ReviewQuestionSize|HiddenList': 51 passed, 1 fork test skipped; grep questionHash contracts/src: recorded in TaskVault/VaultTypes only, never compared.","severity":"info","snippet":"        _req(p.questionPrefix.length + 2 * uint256(p.maxTextBytes) <= 2_000 - QUESTION_FIXED, \"questionPrefix\");","title":"14. HIDDENLIST GAS and the new questionPrefix: no issue. Launch 3 fits; the registry byte bound holds; questionHash is informational only"},{"citation":"resolved","description":"Traced each split: Accepted without dispute (10% fee to treasury, 90% to doer); Accepted after NFT-right (nftFeeBps to correct votes, rest of the fee to treasury, loser bonds split winners/voters by winnerBondBps); tie / NoDecision timeout (50% of disputer bonds to the doer unless contributor, rest treasury); Rejected by check (doer bond to treasury when there is no winner bond); swarm yes/no after appeal (caller fee first, then the 20% relayer cap, then winners); silence (dropCaller credits bond + unspent fee); emergency Unwound (_settleNeutral: every bond and prepayment back); queued Void (bond + checkFunds back). Every bucket pays remaining-over-remaining so the last claimant takes the dust; liabilities move only on entry and exit; failed pushes become credits and withdrawTo reverts rather than losing a credit; pending top-ups stay withdrawable after an acceptance and merge only when the queue is empty.","line":522,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"forge test on forge 1.8.3: Invariant.t.sol (100 runs x depth 150), Payments, Disputes, Scenarios, Emergency suites pass; 388/388 non-fork tests pass; no counterexample constructed.","severity":"info","snippet":"        penalty -= shortPay;","title":"15. REPEAT MONEY: no issue. Every outcome closes the books; no steal, double pay or stuck money found"},{"citation":"resolved","description":"announceWithdraw overwrites withdrawNoticeAt unconditionally and may be called while a submission is active. An author announces at creation and again every 24h: from hour 1 on there is always a notice that is due and valid, so withdrawContribution works at any idle moment and can still front-run a doer's submit (the submit then reverts RewardTooSmall / RewardChanged after the work was done). The hour of warning promised in MECHANICS.md:53 holds only if the doer reads the standing notice (withdrawNoticeAt / WithdrawAnnounced) and refuses the task; the site must show it. A variant: a notice filed during an active submission is due the instant the busy period ends, before any new doer can submit. YoloSweetener.announceWithdraw (line 118-119) has the same renewal rule. The rest of the state machine is clean: all windows are anchored to signed or recorded times, the 8-transition loop reaches the same state whoever pokes, the queue activates inside the finalizing call with a gas reserve, emergencyExit needs 3M gas and only unwinds a step 30 days past its deadline. Merged from the economics (low) and flow (info) specialists. Smallest fix: revert announceWithdraw while a notice is live (at != 0 && block.timestamp <= at + WITHDRAW_NOTICE_VALID) and while activeSub != 0, so every 25h cycle shows a visible gap; same in YoloSweetener.","line":307,"path":"contracts/src/TaskVault.sol","reproduction":"contracts/test/scratch/RollingNotice.t.sol (judge's test, fails on this commit: 'next call did not revert as expected'): create a 0.01 ETH task; announceWithdraw at t0; announceWithdraw again at t0+24h (inside the live notice, succeeds); at t0+30h withdrawContribution succeeds, although the first notice expired at t0+25h. Expected with a non-renewable notice: NoNotice.","severity":"low","snippet":"        withdrawNoticeAt[taskId][msg.sender] = at;","title":"16. REPEAT STATE MACHINE: a live withdrawal notice is renewed by a new announceWithdraw, so an author who re-announces every 24h always has a due notice and can pull the pot in the block a proof appea"},{"citation":"resolved","description":"The callback is accepted only from the Intake the task's version names, for a known unanswered request id; the attestation must be bool/32 bytes, panel >= 7, quorum >= 5, quorum <= agreed <= panelSize, not expired, signed in the consumer domain ('IdentityMD Oracle', '2', chainid, vault) by a registry attester; OracleAttestation's 15-field struct and type string match the oracle reference. The 200k callback stipend covers two SSTOREs, one external view and one ecrecover. Prepayments: unspent check funds are credited at activation, at Void and at settlement; appeal funds minus the spent price go back on silence, void and settlement; an Intake price rise voids an appeal or skips a check with a full refund (documented) and maxImdIn protects payers. Residual, documented (C-05 and the oracle reference): a condition text the Intake wording screen refuses makes every question on that task silence (check passes, appeals void); a definitions pin or allowAmbiguous in bodyFields would remove this author-side lever.","line":462,"path":"contracts/src/TaskVault.sol","reproduction":"forge test --match-contract 'Swarm|ReviewAttestation|ReviewSwarmOrders|AuditB': passed on forge 1.8.3.","severity":"info","snippet":"        uint8 code = attestationChecker.check(address(this), via.minPanelSize, via.minQuorum, a, signature);","title":"17. REPEAT SWARM PATH: no issue. Binding, panel, domain, timeouts, refunds and voided appeals hold"},{"citation":"resolved","description":"EIP-712 digest Vote(uint256 subId,uint8 side,uint256[] tokenIds,uint64 voteEnd,address payTo) with the array hashed as keccak(abi.encodePacked), domain ('IMD TaskVault','1',chainid,vault); ECDSA first, then ERC-1271 under a 500k gas cap with 32-byte returndata, so a gas burner is skipped, never fatal; ownership or delegation per tokenId at relay time; bitmap per submission; reimbursement capped at 30k + 60k per counted NFT x basefee x 1.1, paid from the dispute pot then <= 20% of the penalty, so stuffing invalid tokenIds costs more gas than it returns. walletSides keys on the ballot's voter: a holder of NFTs A and B signs side 1 with A from his wallet and side 2 with B from a delegate.xyz hot wallet; both ballots count and he is paid for whichever side wins. Economically identical to splitting NFTs between two people, nobody else loses, so info; the documented rule 'a wallet votes one side' is only nominal. If wanted: key walletSides by the NFT's owner, which also makes HiddenList's hideWallets meaningful (13a).","line":88,"path":"contracts/src/libraries/VoteLib.sol","reproduction":"Reading of VoteLib.sol:88-121 and :186-205; forge test --match-contract 'Votes|AuditC': passed; the delegate variant follows from walletSides being keyed by b.voter while _mayVote accepts the delegate.","severity":"info","snippet":"            uint8 seen = book.walletSides[b.voter];","title":"18. REPEAT NFT VOTING: no issue in the mechanics; the one-side-per-wallet rule is per signer address, so a holder can vote both sides through a delegate.xyz delegate or a second wallet (nominal rule, "},{"citation":"resolved","description":"Desk._priceReward calls the oracle's imdValue/usdValue without try/catch for ETH and USDC; ChainlinkUniV3PriceOracle.imdWeiPerUsdc calls pool.observe([1800, 0]), which reverts 'OLD' whenever the pool's oldest stored observation is younger than 30 minutes. The pool 0x894D...c520 has observationCardinality 1 today (the specialist read slot0 live; the fork test raises cardinality itself at PriceOracleFork.t.sol:28, so the suite never sees it), so until increaseObservationCardinalityNext(180) is called and the ring holds a 30-minute-old observation, createTask in ETH/USDC works (Chainlink only) but no submission can be made and a doer racing a deadline cannot hand in proof. This is acknowledged in docs/LAUNCH-TASK.md:130 and AUDIT-INTERNAL B-06, so it is an ordering and hardening item, not a new medium: the step is listed under 'Outside the launches' after launch 3 instead of as a precondition of launch 2's first ETH/USDC task. The specialist's second claim, that an attacker refills the 180-slot ring with 181 dust swaps inside 30 minutes, does not hold: Uniswap v3 writes at most one observation per block (Oracle.write returns early when last.blockTimestamp == blockTimestamp, and only on a tick change), so 180 slots always span at least 180 blocks, about 36 minutes at 12 s, longer than the 30-minute window. Smallest fix: (a) operational, do the cardinality call and wait 30 minutes of swaps before launch 2 and verify observe([1800,0]) succeeds; (b) code, in Desk._priceReward wrap the ETH/USDC oracle calls in try/catch and on failure set large = true and rewardImd = max(rewardImd, largeRewardImd) exactly as _priceYolo does, or in the oracle fall back to the oldest available observation when it is younger than twapWindow. Either makes the attached test pass.","line":148,"path":"contracts/src/desks/Desk.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.28;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\n\nimport {TaskVault} from \"src/TaskVault.sol\";\nimport {ParamsRegistry} from \"src/ParamsRegistry.sol\";\nimport {CaseDesk} from \"src/desks/CaseDesk.sol\";\nimport {DisputeDesk} from \"src/desks/DisputeDesk.sol\";\nimport {TextRules} from \"src/helpers/TextRules.sol\";\nimport {SwarmQuestionBuilder} from \"src/helpers/SwarmQuestion.sol\";\nimport {AttestationChecker} from \"src/helpers/AttestationChecker.sol\";\nimport {BallotChecker} from \"src/libraries/VoteLib.sol\";\nimport {ChainlinkUniV3PriceOracle, IAggregatorV3, IUniswapV3PoolObserve} from \"src/oracles/ChainlinkUniV3PriceOracle.sol\";\nimport {ITaskVault} from \"src/interfaces/ITaskVault.sol\";\nimport {Evidence, ImdPay, PROOF_LINK} from \"src/VaultTypes.sol\";\n\n/// @dev Audit round 2, finding: an unavailable IMD TWAP (Uniswap v3 `observe` reverting \"OLD\": the pool's\n///      observation ring holds no entry older than the 30-minute window, which is the live state of\n///      0x894D4e6d3d2Abc64Fc0de0e4e2Ea63A93BC9c520 today with observationCardinality = 1, and happens again after\n///      `increaseObservationCardinalityNext(180)` whenever more than 180 tick-moving swaps land within 30 minutes)\n///      makes `submit` to any ETH / USDC task revert (createTask only needs Chainlink and survives), because\n///      `Desk._priceReward` calls `imdValue` without the try/catch it gives $YOLO. A doer can be shut out of a\n///      deadline by anyone making small tick-moving swaps in that pool. Expected: the vault degrades (the submission\n///      counts as large / bonds at the threshold, as for an unpriced $YOLO task) or the oracle falls back to the\n///      longest available window; actual: the user call reverts with \"OLD\".\n///      Fails on the audited code, passes once the vault tolerates an oracle revert for ETH / USDC.\ncontract Coin is ERC20 {\n    uint8 private immutable _d;\n\n    constructor(string memory n, uint8 d) ERC20(n, n) {\n        _d = d;\n    }\n\n    function decimals() public view override returns (uint8) {\n        return _d;\n    }\n\n    function mint(address to, uint256 a) external {\n        _mint(to, a);\n    }\n}\n\ncontract Feed is IAggregatorV3 {\n    int256 public answer;\n    uint8 public dec;\n\n    constructor(int256 a, uint8 d) {\n        answer = a;\n        dec = d;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (0, answer, 0, block.timestamp, 0);\n    }\n\n    function decimals() external view returns (uint8) {\n        return dec;\n    }\n}\n\n/// @dev A USDC/IMD v3 pool whose observation ring cannot serve the window: `observe` reverts \"OLD\" exactly as\n///      Uniswap v3's Oracle.getSurroundingObservations does when the oldest stored observation is younger than\n///      `secondsAgo` (cardinality 1 and a tick-moving swap in the last 30 minutes, or the ring exhausted).\ncontract V3Pool is IUniswapV3PoolObserve {\n    int24 public constant TICK = 258_947; // the live tick of the mainnet pool at the time of the audit\n    bool public old;\n\n    function setOld(bool v) external {\n        old = v;\n    }\n\n    function observe(uint32[] calldata ago) external view returns (int56[] memory cum, uint160[] memory spl) {\n        if (old) revert(\"OLD\");\n        cum = new int56[](ago.length);\n        spl = new uint160[](ago.length);\n        for (uint256 i; i < ago.length; ++i) {\n            cum[i] = int56(TICK) * int56(uint56(10_000_000 - ago[i]));\n        }\n    }\n}\n\ncontract PoolManagerStub {\n    function extsload(bytes32) external pure returns (bytes32) {\n        return bytes32(0); // the YOLO pool is not open: YOLO has no price (irrelevant here)\n    }\n}\n\n/// @dev Deploys the two desks and the vault in one frame, as the launch factory does in one transaction.\ncontract CoreDeployer {\n    TaskVault public vault;\n\n    constructor(address registry, address imd, address usdc, address yolo) {\n        TextRules text = new TextRules();\n        CaseDesk cases = new CaseDesk(registry, imd, usdc, address(text), address(new SwarmQuestionBuilder()));\n        DisputeDesk disputes = new DisputeDesk(registry, imd, usdc, address(text), address(new BallotChecker()));\n        vault = new TaskVault(\n            registry,\n            imd,\n            usdc,\n            yolo,\n            address(new AttestationChecker(registry)),\n            address(text),\n            address(cases),\n            address(disputes)\n        );\n    }\n}\n\ncontract TwapUnavailableBlocksEthTasksTest is Test {\n    Coin imd;\n    Coin usdc;\n    Coin yolo;\n    V3Pool pool;\n    ChainlinkUniV3PriceOracle oracle;\n    ParamsRegistry registry;\n    TaskVault vault;\n\n    address author = makeAddr(\"author\");\n    address doer = makeAddr(\"doer\");\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        imd = new Coin(\"IMD\", 18);\n        usdc = new Coin(\"USDC\", 6);\n        yolo = new Coin(\"YOLO\", 18);\n        pool = new V3Pool();\n        oracle = new ChainlinkUniV3PriceOracle(\n            address(usdc),\n            address(imd),\n            new Feed(2000e8, 8),\n            new Feed(1e8, 8),\n            pool,\n            30 minutes,\n            2 hours,\n            48 hours,\n            address(yolo),\n            address(new PoolManagerStub()),\n            address(0xB00C),\n            12_500,\n            60,\n            0.01e18,\n            15_000e18 * 1e18 / 1_000_000_000e18\n        );\n        registry = new ParamsRegistry(\n            address(this),\n            address(0x1234),\n            address(oracle),\n            address(0),\n            address(0xABCD),\n            address(0),\n            address(this),\n            address(0x5555)\n        );\n        vault = new CoreDeployer(address(registry), address(imd), address(usdc), address(yolo)).vault();\n        vm.deal(author, 1 ether);\n        vm.deal(doer, 1 ether);\n        imd.mint(doer, 100e18);\n        vm.prank(doer);\n        imd.approve(address(vault), type(uint256).max);\n    }\n\n    function _pay() internal pure returns (ImdPay memory p) {\n        p.tokenIn = address(0);\n    }\n\n    function _createEthTask() internal returns (uint256 taskId) {\n        vm.prank(author);\n        taskId = vault.createTask{value: 0.0105 ether}(\n            address(0), 0.01 ether, 0, \"dare\", \"post a photo of the sign\", 0, PROOF_LINK, \"\", 0.0005 ether, _pay()\n        );\n    }\n\n    /// @notice A doer with the bond in hand cannot submit to an open ETH task while the TWAP cannot be read.\n    function test_submitToEthTaskSurvivesAnUnavailableTwap() public {\n        uint256 taskId = _createEthTask();\n        assertEq(oracle.usdValue(address(0), 1 ether), 2000e18, \"the oracle works while the ring can serve 30 min\");\n\n        // a tick-moving swap landed in the USDC/IMD pool less than 30 minutes ago (or the ring is exhausted)\n        pool.setOld(true);\n\n        vm.prank(doer);\n        uint256 subId = ITaskVault(address(vault))\n            .submit(taskId, \"https://example.com/proof\", Evidence(bytes32(0), false, \"\"), 0, type(uint256).max, _pay());\n        assertEq(subId, 1, \"the submission must be taken: the vault has to degrade, not revert\");\n    }\n}","reproduction":"contracts/test/scratch/Proof_e41b25ec00a3.t.sol (fails on this commit with 'OLD'): real oracle, registry, desks and vault; a stub v3 pool at the live tick whose observe reverts 'OLD' once setOld(true); author creates a 0.01 ETH task (succeeds); setOld(true); doer submits with 100 IMD approved: reverts OLD. Expected: the submission is taken (as 'large' with bonds at the threshold) or priced over the available window.","severity":"low","snippet":"            imdPerEth = o.imdValue(ETH, 1e18);","title":"19. REPEAT PRICES/MEV: submit to every ETH/USDC task reverts 'OLD' while the USDC/IMD v3 pool's observation ring cannot cover 30 minutes (cardinality 1 on mainnet today; the doc lists the fix after la"},{"citation":"resolved","description":"Roles audited: ParamsRegistry owner (48h timelocked params within bounds, global attester set, pause of new tasks, treasury of future sweeps; no function moves deposits: the vault's bank functions are onlyDesk and the desks have no owner), ProtocolDirectory owner (a timelocked list nothing in the core reads), HiddenList owner (instant visibility only). One docs-vs-code gap: docs/MECHANICS.md:294 says the price oracle 'is per parameter version, so the params owner can swap it for future tasks only', but DareTrophy.price() reads registry.core(registry.currentVersion()).priceOracle, not the task's version. After a new version (48h public) naming an oracle with usdValue(yolo, 1e18) = 1e36, price() = ceilDiv(1.5e18 * 1e18, 1e36) = 1 wei, so every trophy of every version mints for 1 wei of YOLO instead of about 13,000 YOLO ($1.50); a tiny usdValue only makes trophies dear, bounded by the minter's maxYolo. YoloSweetener uses the task's version for its cap (correct). Owner-controlled, visible 48h ahead, trophies (burn revenue) only, so low. Smallest fix: read the oracle of the task's paramsVersion (vault.getTask(s.taskId).paramsVersion) or state the current-version rule in MECHANICS and the DareTrophy NatSpec.","line":103,"path":"contracts/src/DareTrophy.sol","reproduction":"State: registry currentVersion v2 whose priceOracle returns hasPrice(yolo) = true and usdValue(yolo, 1e18) = 1e36. Call DareTrophy.price(): expected by the docs about 13,043 YOLO from the task version's oracle; actual 1 wei (lines 103-110). Other promises in README.md, MECHANICS.md, TOKENOMICS.md, LAUNCH-TASK.md and NatSpec were checked against the code without another gap.","severity":"low","snippet":"        IPriceOracle o = IPriceOracle(registry.core(registry.currentVersion()).priceOracle);","title":"20. REPEAT ROLES/DOCS VS CODE: DareTrophy prices every trophy by the CURRENT parameter version's priceOracle, so a future version's oracle re-prices trophies of tasks of every version, against MECHANI"},{"citation":"resolved","description":"At IMD = $7.65. (a) Author-doer collusion: the author may be the doer; a self-dare of reward R with a self-sweetened bonus B returns 90% of each after 24h with no dispute, plus a trophy (burn about $1.50 of YOLO) and a completed dare on the leaderboard. Cost per fake completion at the $5 minimum: 10% of R + 10% of B + 0.0005 ETH author fee (about $1.10) + gas, about $1.60; who gains is whoever the off-chain incentives (250M YOLO) reward per completed dare; who loses is that budget and the board's credibility. Recommendation: pay incentives only for dares with author != doer that a disputer or the swarm touched, or require the 0.5 IMD ($3.83) check for incentive-eligible dares. (b) Sybil swing: an NFT decision needs margin >= 10 NFTs and minWinnerWallets = 1, so 10 NFTs in one hand decide any vote nobody opposes; the remedy is an appeal (bond 1 IMD + fee 0.5 IMD = $11.48, returned with the fee if the swarm agrees), so a swing pays only through the voter share (50% of the loser bond, >= $3.83) and is worth it only on a large task (a $5,000 task: 3% doer bond = 10 IMD = $76 to the swinging side against the $11.48 appeal the loser files). HiddenList: 10 NFTs hide anything until the owner reacts (13a-13c); 1 NFT keeps an item un-hideable by vote (13b). Identity.md NFT floor price is not in the repository, so the NFT cost is stated in NFTs. (c) Spam: a task costs the author fee (about $1.10) + gas (about $2 at 2 gwei), the $5 reward is recoverable after a 1h notice; a thousand junk tasks cost about $3,000 and are the owner's and voters' cleanup on the site (HiddenList batches). Queue stuffing behind an active submission costs doubling bonds (2^n of 1 IMD base: 510 IMD = $3,900 for 8 slots), refunded only if the active submission is accepted, so not cheap.","line":237,"path":"contracts/src/TaskVault.sol","reproduction":"Numbers from VaultParams.sol:133-160 (minRewardUsd 5e18, minAuthorFee 0.0005 ETH, minBond 1e18, bondBase 0.5e18, doerBondBps 300, minWinnerWallets 1, winnerBondBps 5000, feeBps 1000) and HiddenList constants (QUORUM_NFTS 10, MIN_WALLETS 3).","severity":"info","snippet":"        if (authorFee < p.minAuthorFee) revert FeeTooLow();","title":"21. BOARD ABUSE: self-dares cost about 10% of the reward plus a $1.10 author fee; 10 identity.md NFTs in one hand decide an unopposed dispute vote or any HiddenList round; spam costs about one author "},{"citation":"resolved","description":"Sizes at launch, IMD = $7.65: doer bond max(1 IMD, 0.5 IMD + 3% of R in IMD) = $7.65 on a $5 task (153% of R), $12.90 on a $300 task (4.3%), $153 on a $5,000 task (3%); dispute and appeal bond max(1 IMD, 0.5 + 1% of R) = $7.65 up to R = $382; dispute ETH fee 0.0003-0.01 ETH funds relayers; swarm check 0.5 IMD = $3.83; settlement fee 10% of R; author fee $1.10. Too cheap to deter: for R < $300 a dishonest doer loses 3% of R if caught and gains 90% if not, and nothing but an NFT holder's attention stops it inside the 24h window; a disputer who is right earns 50% of that bond (NFT right) or 100% (swarm) = $3.83-$6.45 for a $0.66 fee plus an appeal risk of $11.48, so disputing a $200 fraud is barely worth a stranger's time, and the author (the natural disputer) must out-argue the doer within 24h. Too costly for a small task: a $5 dare needs a $7.65 doer bond, fine for play, but a dispute on a $20 dare costs $7.65 + $0.66 and returns at best $3.83. Recommendation, parameters only and inside ParamsBounds, no code: doerBondBps 1,000 (10%) so a false proof on a $200 task risks $20, and largeRewardUsd lowered toward $100 so the $3.83 check covers the band where NFT attention is thin; keep minBond 1 IMD for sub-$10 dares.","line":142,"path":"contracts/src/VaultParams.sol","reproduction":"Formulas: Desk.sol:116-131 (bond), VaultParams.sol:133-160 (launch values), ParamsBounds lines 195-227; worked example MECHANICS.md ('300 USDC task').","severity":"info","snippet":"        p.doerBondBps = 300;","title":"22. DEPOSIT/FEE ECONOMICS: below the $300 check threshold a false proof risks max(1 IMD, 0.5 IMD + 3% of R) to win 90% of R, so fraud is +EV whenever fewer than about 3-4% of false proofs get disputed"},{"citation":"resolved","description":"Verified against code: splitter 250M / 150M / 77,777,777 / 50M / remainder (222,222,223 plus factory rounding); vestings 62.5M at once then 730 days, 74,074,074 at once then 365 days, dev and team 365 days with no cliff, clocks start at the split (entry 2). Day 0 unlocked = pool 150M + Stabilizer 150M + swarm 100M + incentives 62.5M + treasury 74.07M = 536.6M (53.7%, docs say 54%); month 6 and 12 figures reproduce. Stabilizer / MM fund (kept, per the brief; risk and disclosure only): 150M at once plus 136.6M of other at-once project shares = 286.6M liquid in project wallets on day 0, 1.9x the pool's 150M YOLO side at an opening valuation of 15,000 IMD for 1B, so these wallets can move or cap the price at will; the docs say the wallet is public and every move visible but give no mandate, limit or reporting cadence. Recommend publishing the fund's rules (no net selling for N days, position sizes) before launch. Demand for $YOLO: optional reward currency, YoloSweetener bonuses, the trophy burn (about $1.50 per accepted dare, 10,000 YOLO without a price), the NFT-gated first 30 minutes; no fee is converted into YOLO. Do fees fund the project: yes, 10% of every accepted reward in its own currency, author fees in ETH, the treasury part of disputed rewards and penalty remainders, plus the pool's LP fee share outside this code; small at $5-$300 task sizes, so the treasury vesting is the larger asset.","line":32,"path":"contracts/src/token/YoloSplitter.sol","reproduction":"Arithmetic from token/Vestings.sol, token/YoloSplitter.sol:31-36 and script/LaunchPlan.sol; docs/TOKENOMICS.md tables reproduced to the stated rounding; forge test --match-contract 'YoloToken|Yolo' passed.","severity":"info","snippet":"    uint256 public constant STABILIZER = 150_000_000e18;","title":"23. TOKENOMICS: the split and clocks match docs/TOKENOMICS.md; about 54% is liquid on day 0, of which 286.6M YOLO sits in three project wallets against a 150M-YOLO pool side; $YOLO demand is discretio"},{"citation":"resolved","description":"What exists: (1) the questionPrefix tells the panel to answer no for crime, minors, violence, injury risk, harassment/doxxing or hate, reached by the mandatory check (R >= $300 or >= 40 IMD, or any $YOLO task without a price), by an appeal (1 IMD + 0.5 IMD) or by callSwarm in NoDecision; (2) HiddenList: the owner or 10 NFTs stop the site from rendering the item, with the limits of 13a-13c; (3) TextRules bounds the condition to 256 UTF-8 bytes without quotes or control characters, nothing semantic. Gaps: createTask accepts any valid text; submit for R < $300 in ETH/USDC/IMD sets large = false, _activate opens the window without a question, and with no dispute _finalize(Accepted) pays 90% to the doer; hiding removes the audience, not the payout, and the proof lives off-site anyway; an NFT vote cannot reject a submission without a dispute bond and the appeal path, where the prefix finally applies; 13b lets the dare's author block the hide vote himself. Adequate as a gate against an illegal dare being paid: no. Adequate against being displayed: mostly, owner instant and voters 24h, minus 13b. Cheapest addition, no code: a parameter version with largeRewardUsd = minRewardUsd ($5) and largeRewardImd = minRewardImd makes every submission prepay the 0.5 IMD ($3.83) check so the prefix is applied to every proof before any payout (ParamsBounds allows it, VaultParams.sol:197-198). Cheapest code addition if $3.83 per submission is too much: CaseDesk._activate forces the check when HiddenList.status(vault, KIND_TASK, taskId) != VISIBLE, one view call, so a hide vote becomes a 'send it to the swarm' vote while the owner still has no money power (the swarm answers, not the owner).","line":166,"path":"contracts/src/VaultParams.sol","reproduction":"Trace: TaskVault.createTask validates text only (TextRules); Desk._priceReward line 150 sets large = usdValue >= 300e18; CaseDesk._activate asks a question only when large; no dispute => Accepted at windowEnd => _payReward; grep HiddenList contracts/src: read by nothing in the core.","severity":"info","snippet":"        p.questionPrefix =","title":"24. HARMFUL DARES: the illegal-content prefix reaches the panel only when a swarm question is asked (R >= $300, an appeal, or callSwarm), and HiddenList only hides on the site, so a sub-$300 illegal d"},{"citation":"resolved","description":"Read in full: HiddenList, TaskVault (withdraw, create, oracle callback paths), VaultParams, DareTrophy.price/mint, desks/Desk, DisputeDesk and CaseDesk settlement and timing paths, libraries/VoteLib, oracles/YoloPoolSamples and ChainlinkUniV3PriceOracle TWAP path, token/YoloSplitter and ProjectVesting binding, script/LaunchPlan.sol; docs/AUDIT-INTERNAL.md in full, README.md, MECHANICS.md (participants, withdraw, trust assumptions, HiddenList), TOKENOMICS.md (tables), LAUNCH-TASK.md (post-launch steps, launch 3 text); test/Base.t.sol, HiddenList.t.sol, audit/swarm117/* in part. Tools: forge 1.8.5 (installed on the machine) then forge 1.8.3 (installed for this review, the version the repository's figures come from), solc 0.8.28 via-IR cancun. Results: forge 1.8.3 full suite 59 suites, 388 passed, 0 failed, 5 skipped (fork suites, no FORK_URL); the five scratch tests (four specialist proofs plus the judge's RollingNotice.t.sol) all fail on this commit for the stated reasons. forge 1.8.5: 37 suites fail in setUp with WiringClosed() or BindingClosed() because Desk.wire and ProjectVesting.bindStarter read a transient flag set in a constructor called earlier in setUp, and that forge version clears transient storage between the test contract's calls; on chain each launch is one factory transaction, so this only affects the test harness (worth pinning the forge version in contracts/README.md or deploying and wiring inside one helper call). Not reached: the fork rehearsal and launch gas figures (taken from the docs), the live pool and Intake reads (taken from the math specialist's report, consistent with LAUNCH-TASK.md:130), the ProjectFactory, delegate.xyz, the identity.md NFT contract and the site. Slither/Mythril not run (not provided). Nothing outside contracts/test/scratch/ and .imd-findings.json was written.","line":13,"path":"contracts/README.md","reproduction":"Commands: forge build; forge test --no-match-path 'test/scratch/*' (on 1.8.5 then 1.8.3); forge test --match-path 'test/scratch/*'; git diff --stat b86a5f8 7714a24 -- contracts/src contracts/script; sed/grep of the cited lines.","severity":"info","snippet":"forge test                                   # 388 pass, 5 fork suites (11 tests) skip without FORK_URL","title":"25. COVERAGE: all contracts/src files, LaunchPlan.sol and the five docs read; forge test 388 passed / 0 failed / 5 fork suites skipped on forge 1.8.3; on forge 1.8.5 the harness fails 37 suites in set"}],"hash":"eeac3a7ce2b0d433527c5b7c8bef3a8eb8022086ea96e7d0f7bef0f7f19cd0ed","nodeId":"a8d590a4-22cf-45d0-a614-6920ea217a59","outcome":"completed","summary":"The review is complete. The findings file holds 28 entries in the mandated order, with 4 proofs attached, and every snippet verified against the tree.\n\n**Verdict:** ready for mainnet with no critical or high. One medium, four low, the rest info. One operational precondition must move earlier in the launch procedure.\n\n**What I kept, after reproducing each specialist claim:**\n- **Medium, HiddenList cooldown grief.** One NFT holder opens an uncontested round on their own item, it fails after 24h, and the 7-day cooldown silently drops every honest hide vote. Repeated every 8 days, the vote layer never works against that item. Proof fails on this code.\n- **Low, HiddenList 3-wallet rule.** Three specialists reported it separately. Merged into one entry: one holder of 10 NFTs passes through two delegate.xyz delegations. Proof fails.\n- **Low, HiddenList last-block swing.** Kept the mechanism but reduced from medium. It needs the full 10-NFT quorum and the owner's KEPT ends it. The specialist's path was wrong, so I corrected it to contracts/src/HiddenList.sol. Proof fails.\n- **Low, rolling withdrawal notice.** No proof was attached, so I wrote one. A notice renewed every 24h is always due, and withdrawal succeeds 30h after the first announcement. Merged the flow specialist's active-submission variant into it.\n- **Low, DareTrophy oracle.** Prices by the current version's oracle, contrary to the MECHANICS promise. Confirmed by reading.\n\n**What I downgraded or corrected:**\n- **TWAP OLD revert, medium to low.** The revert reproduces and the cardinality step is real, but the docs and AUDIT-INTERNAL B-06 already acknowledge it. The specialist's claim that an attacker can refill the 180-slot ring inside 30 minutes is wrong. Uniswap v3 writes at most one observation per block, so 180 slots always span about 36 minutes.\n- **Bundle sandwich on YoloPoolSamples.** Kept as an info note under the fix check. It needs top-of-block in the next block against arbitrageurs and the effect is bounded.\n- **VoteLib delegate variant and the splitter-order note.** Info only, no loss.\n\n**Test results worth knowing:** the suite shows 388 passed and 0 failed on forge 1.8.3. On forge 1.8.5, which was installed here, 37 suites fail in setUp with WiringClosed or BindingClosed. That version clears transient storage between the test contract's calls. The production path is one factory transaction per launch, so only the harness is affected. The coverage entry records this.","treeHash":null,"usage":{"cachedInputTokens":2106829,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":40434,"runtime":"claude","turns":34,"wallClockMs":732766}}],"verification":[]}