{"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":"f2d8d3ed-cfb4-482e-9e2f-20029e73b6fb","kind":"audit","nodes":[{"acceptedSubmissionHash":"e2fb9c4706eaff3ad7571fd251e06c12e0e897ba2c3fea2335803f001b5d3ea3","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":"de82b5dc36e12cb3e1dbb3059707452270ed22a4a72228c0a6c6c75cf8742ef8","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":"f51fe8183052302a79aca1c72b34d614624c905268046a4ff01a60bcf29a9a71","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":"aa835cca5ece87d19898f3f6f374d90232980b43f8b7ebe706f9d2ab133762a5","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":"1092f6366d072d8af94296e43cb6a4a1ddac38e8ff2f8f6bff626f0cb81e44c2","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":"Audit the protocol logic and role scenarios of moneycanbuy.fun for abuse. Repository at the pinned commit 5f6af8f1052f555553aad41a0e12fc1d29fa90b6, as it is. Run forge at the repository root (foundry.toml); code in contracts/src/, import as \"src/...\". Do not modify any existing file; findings go only into the report and NEW files under contracts/test/audit/.\n\nSCOPE. Walk every role through a task from creation to the last payout and look for logic abuse: holding someone else's money, skipping or capturing the dispute, voting twice, collusion against third parties, what a user cannot learn from the chain alone. Code security and payout amounts are other orders; a wrong percentage or a bug with no role scenario is one info line here.\n\nSOURCE OF TRUTH. The rules below win over docs and comments. Code: contracts/src/ (TaskVault, desks/, VotePower, YoloPredict, YoloSweetener, YoloLockHub, HiddenList).\n\nRULES.\nR1 Task: reward in ETH, USDC, IMD or YOLO (min 0.002 ETH, 5 USDC, 1 IMD, 1,000 YOLO), fixed deadline. Anyone tops up while Open before the deadline; a top-up during a check joins the reward at once. Nobody withdraws while Open; Expired: each contributor takes back his own; a Completed task takes no new money.\nR2 Submit: anyone, the author too; bond 0.5 IMD + 3% of the author's own share; one active, the rest queued and judged even after the deadline; on acceptance or cancel queued ones void, bond back. The doer writes why it is honest at submission. A doer whose YOLO lock (YoloLockHub) runs from submission to payout gets up to 4% of the reward extra; any power counts, no threshold.\nR3 Dispute: 24 h from activation, anyone but the doer, one disputer per submission, bond 0.5 IMD + 1% of the author's own share; reason required, proof link optional.\nR4 Vote: 24 h; DisputeDesk.vote(subId, side, payTo, tokenIds); NFT ids owned now or delegated (delegate registry v2); each id once per dispute; side fixed; own transaction and gas. Instant decision at a 20-vote lead; at the end a 10-vote lead and >= 1 wallet decide; else no decision.\nR5 Swarm: after a decision anyone appeals within 24 h (the doer only a rejection); a dispute side pays the fee from its bond, an outsider posts bond + fee; anyone asks the question in a later call; an appeal not asked within 6 h voids with a full refund. No decision: anyone calls the swarm within 24 h; nobody calls = accepted, disputer bond to the doer. Answer within 6 h (+1 h delivery grace) is final; silence restores the prior state; max 3 calls per submission, then as if never called.\nR6 Predict: YOLO, irrevocable, before the deadline, paused while a submission is active; Completed = WILL DO, Expired = WON'T DO, cancelled = full refund. Anyone predicts, author and doer too.\nR7 The dev wallet may cancel an open task in any phase: everyone takes back what he put in except swarm fees already paid, the dev wallet nothing. The dev wallet also sets the YOLO, ETH and IMD prices by hand (USDC = $1) and the buyback wallet. The params owner may pause new tasks. HiddenList hides on the site only.\nR8 Frontends read the chain via public RPCs; no project server, keeper or relayer.\n\nALREADY FIXED IN THIS COMMIT (passing PoC in contracts/test/audit/): maxImdIn caps the IMD bond part too (MaxImdInCapTest); a late askAppealSwarm reverts (LateAppealAskTest); a 0 bps stake keeps the staked split (StakeFloorZeroSplitTest); stepDeadline while Appealed (StepDeadlineAppealedTest).\n\nACCEPTED BY DESIGN, documented in docs/MECHANICS.md Known limitations. Not findings unless the code differs from the documented rule: 20+ NFTs disputing from a second wallet; undisputed in 24 h = accepted, WON'T DO predictors dispute; an appeal not asked in 6 h void, full refund, new 24 h window; the queue judged in turn; one dispute per submission; cancelTask in any open phase, spent swarm fees kept; a started YoloLockHub week counted whole.\n\nREPORT FORMAT, MANDATORY. One entry per item, same order, even \"no issue\"; a missing item = INCOMPLETE. Entry 0: VERDICT, max 8 lines, every blocker. Items 1-11: severity, scenario as numbered steps (who calls what, when, at what cost), file:line, loss or delay in tokens at a 100 USDC reward and a 10,000 YOLO prediction pool, smallest fix, and a Foundry PoC (full source, contracts/test/audit/, fails now and passes once fixed, no fork, no ffi) if real, else one line why not. Severity: critical = someone takes or permanently freezes another's money; high = realistic loss, or a rule above broken; medium = bounded loss, a lock over 7 days, or cheap griefing; low = edge case; info = text drift, \"no issue\".\n\n1. ROLE WALK. For author, contributor, doer, queued doer, disputer, voter, delegate, appellant, outside caller, predictor, staked doer, dev wallet, params owner: each call in each phase. Flag a call allowed outside its rule, or a rule with no path.\n2. HOSTAGE. Who keeps another's money locked, how long, at what cost, beyond the documented queue and unasked-appeal paths: a phase with no exit, a lock with no cost. Give the longest path from the deadline to the author's refund.\n3. SKIPPING THE DISPUTE. Getting a fake accepted with no real check beyond the documented one-dispute slot: the window opening at a quiet hour through queue order, an instant decision by own NFTs, a cancel or the deadline in between.\n4. DOUBLE VOTE. One NFT counted twice in a dispute: owner and delegate, transfer after a vote, delegation changed mid-vote, several transactions, payTo tricks; same for HiddenList votes. Can one payTo be paid on both sides?\n5. COLLUSION VS PREDICTORS. Author + doer against outside predictors: a task meant to fail, a submission timed to freeze predictions, the deferred 2.5% + 2.5%, empty sides. Give the cheapest profitable plan an honest predictor cannot stop by disputing.\n6. COLLUSION VS AUTHOR AND CONTRIBUTORS. Doer + disputer, doer + voters, doer + appellant; top-ups during a check; the author submitting to his own task.\n7. THE DEV WALLET AS A PLAYER. Beyond the documented cancel: anything the dev wallet or a friend of it gains, in money or outcome.\n8. ORDER AND RACES. Same-block front-running of dispute, vote, appeal, callSwarm, topUp, predict, cancel; lazy timers (poke) played late changing an outcome or a winner; deadline vs an active submission.\n9. KNOWING BEFORE PAYING. From views alone, can a user learn before each call its full cost (bond, fee, max IMD in), the end of the current step, what he can claim and when? A view that lies or reverts in some phase is a finding.\n10. NO SERVER. Can a third-party frontend with only an RPC rebuild every task, text, reason, vote and swarm answer (texts kept as hashes plus events)? Does any rule need someone of the project to send a transaction (keeper, relayer, poke)?\n11. COVERAGE. Files read in full, what was not reached, forge build and test result (passed/failed).\n\nNot findings (by design, unless worse than stated): self-dispute from a second wallet; a single possible doer predicting WON'T DO and not doing; an author profiting from a task nobody can do; one late position on an empty side taking the other pool; NFT holders not voting on small tasks; one stake lock for all submissions; many wallets in predictions; fees of silent swarm calls kept; test/mocks, brand/, landing/.\n","parentJobId":null,"planHash":"b0d83fdf4b4a04af55653d03726d7405598bf02459cf34fc34e1b4bee980c205","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"f2d8d3ed-cfb4-482e-9e2f-20029e73b6fb","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":"51483","feedbackHash":"b83e8771b8ccf829d9f9ddefd2b62e42cd142257c3ef16707c86b916b8843a1e","nodeKey":"audit_economics","submissionHash":"e2fb9c4706eaff3ad7571fd251e06c12e0e897ba2c3fea2335803f001b5d3ea3","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"53909","feedbackHash":"af641ac251c3ab4f72041820c52675e1bdd462ef98c4620b928b12551e457d4e","nodeKey":"audit_flow","submissionHash":"de82b5dc36e12cb3e1dbb3059707452270ed22a4a72228c0a6c6c75cf8742ef8","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52145","feedbackHash":"c4d0155a7f184c6ed1c3de98d5db48ed954010a2a8d25fc1550f560b14eefb59","nodeKey":"audit_judge","submissionHash":"f51fe8183052302a79aca1c72b34d614624c905268046a4ff01a60bcf29a9a71","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52180","feedbackHash":"93b8250f2307c6023b6d63510d0b1d686d20775796a94f70f7ab215afbde49f7","nodeKey":"audit_math","submissionHash":"aa835cca5ece87d19898f3f6f374d90232980b43f8b7ebe706f9d2ab133762a5","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50961","feedbackHash":"c66301ba3de7dc90963ce77ca0e5fecde66421b98e9e3e2a3101836ff4869fbc","nodeKey":"audit_permissions","submissionHash":"1092f6366d072d8af94296e43cb6a4a1ddac38e8ff2f8f6bff626f0cb81e44c2","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"f0e9ff02fe9348cd7ff0620344a4c2aa15ebac6ce8ca2fa75389c8c6c86cb04f","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"d68ba89c0b30801c","findings":[{"citation":"resolved","description":"CaseDesk.step, Phase.Appealed timeout (line 206-212): when an appeal was not asked within swarmTimeout (6 h) it is voided and the submission goes back to Decided, but `callFrom` (the start of the new appeal window) is set to block.timestamp, i.e. whenever somebody happens to poke, not to the void moment `appealedAt + swarmTimeout`. Every other lazy transition is played from its own moment (vote end: `callFrom = voteEnd`; silence: `callFrom = at`). Documented rule (docs/MECHANICS.md Known limitations): 'An appeal not asked within 6 h is voided with a full refund and reopens the 24 h appeal window'. Consequences for the roles: (1) the submission is not final at appealedAt + 6 h + 24 h; a poke at or after that moment still leaves it Decided for a further 24 h, so the doer (on a rejected fake: the author/contributors; on an accepted honest one: the doer waiting for the reward) waits one extra window per void unless someone pokes at exactly the void second; (2) with no keeper (R8) the appellant can wait any time after the void and then poke + appeal in one transaction (appeal() plays the void via advanced() and then files), pushing finality back by (idle time + 24 h) per void instead of the documented 24 h, three times, at zero cost (full refund of bond and fee). stepDeadline(subId) reports the window from the late poke, so a user reading the chain cannot know when the step really ends before the poke. Loss at a 100 USDC reward: no money taken, but the 100 USDC pot (plus 10,000 YOLO prediction pool, which resolves only with the task) stays locked up to 3 x (idle + 24 h) beyond the rule; a defender has to poke at the void second to keep the documented timing. Fix: `s.callFrom = uint64(uint256(s.appealedAt) + p.swarmTimeout);` (the void moment), which also makes _advance finalize in one call once voidAt + appealWindow has passed. Verified: the proof fails on this commit and passes with that one-line change.","line":209,"path":"contracts/src/desks/CaseDesk.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.28;\n\nimport {Base} from \"../Base.t.sol\";\nimport \"../../src/VaultTypes.sol\";\n\n/// @dev Finding: CaseDesk.step, Phase.Appealed timeout, sets `callFrom = block.timestamp` (the poke time) instead of\n///      the void moment `appealedAt + swarmTimeout`. The documented rule (MECHANICS \"Known limitations\") is: an appeal\n///      not asked within 6 h is void and reopens the 24 h appeal window. The code reopens a 24 h window from whenever\n///      somebody pokes, so the submission is never final at `appealedAt + 6 h + 24 h`; a poke at that moment (or any\n///      later one) still leaves it Decided for another 24 h, and the appellant may poke + re-appeal in one transaction\n///      at any later time (no keeper, R8). Every other lazy timer (vote end, silence) is played from its own moment.\ncontract LazyAppealVoidWindowTest is Base {\n    uint256 internal t;\n    uint256 internal s;\n\n    function _rejectedAndAppealed() internal returns (uint256 appealedAt) {\n        t = _create(author, address(usdc), 100e6);\n        s = _submit(doer, t);\n        _dispute(dave, s);\n        _vote(s, 1, 2, 120, 20); // 20 NFT reject: instant decision\n        assertEq(uint8(_phase(s)), uint8(Phase.Decided));\n        _appeal(doer, s); // the doer appeals the rejection, never asks it\n        appealedAt = _now();\n        assertEq(uint8(_phase(s)), uint8(Phase.Appealed));\n    }\n\n    /// @dev Poke at void + 24 h: the appeal window that reopened at the void moment has ended, the submission must be\n    ///      final (Rejected: the NFT decision stands). Today it is still Decided with a fresh 24 h window.\n    function test_PokeAfterVoidPlusWindow_IsFinal() public {\n        uint256 appealedAt = _rejectedAndAppealed();\n        uint256 voidAt = appealedAt + 6 hours;\n        vm.warp(voidAt + 24 hours);\n        vault.poke(t);\n        assertEq(uint8(_phase(s)), uint8(Phase.Final), \"void + appealWindow passed: must be final\");\n        assertEq(uint8(_sub(s).outcome), uint8(Outcome.Rejected));\n        assertEq(uint8(vault.getTask(t).status), uint8(TaskStatus.Open)); // deadline not reached: task stays open\n    }\n\n    /// @dev The window that `stepDeadline` reports after the void must be voidAt + appealWindow, whatever the poke time.\n    function test_WindowAfterVoid_StartsAtTheVoidMoment() public {\n        uint256 appealedAt = _rejectedAndAppealed();\n        uint256 voidAt = appealedAt + 6 hours;\n        vm.warp(voidAt + 20 hours); // a late poke\n        vault.poke(t);\n        assertEq(uint8(_phase(s)), uint8(Phase.Decided));\n        assertEq(vault.stepDeadline(s), voidAt + 24 hours, \"the reopened window counts from the void, not the poke\");\n    }\n\n    /// @dev Without a poke by anyone else, the appellant re-appeals days later in the same transaction as the poke:\n    ///      the finality of the rejection is pushed back by (poke delay + 24 h) per void instead of 24 h.\n    function test_LateReAppealKeepsTheDisputeOpen() public {\n        uint256 appealedAt = _rejectedAndAppealed();\n        uint256 voidAt = appealedAt + 6 hours;\n        vm.warp(voidAt + 10 days); // nobody poked for 10 days\n        // the doer's appeal call plays the void first, then files a new appeal: with the window counted from the\n        // void moment this must revert (the submission is final); today it succeeds\n        vm.prank(doer);\n        vm.expectRevert(WrongPhase.selector);\n        vault.appeal{value: ETH_BOND_HEADROOM}(s, type(uint256).max, _noSwap());\n    }\n}","reproduction":"1. author creates a 100 USDC task; doer submits; dave disputes; 20 NFTs vote reject (instant Decided). 2. doer appeals the rejection at T (Phase.Appealed) and never calls askAppealSwarm. 3. Nobody pokes until T + 6 h + 24 h; then anyone pokes. Expected: the void happened at T + 6 h and the reopened 24 h window ended at T + 30 h, so the submission is Final (Rejected). Actual: phase is Decided with callFrom = T + 30 h and stepDeadline = T + 54 h. 4. Variant: nobody pokes for 10 days; the doer calls appeal() at T + 6 h + 10 d. Expected: WrongPhase (final). Actual: the appeal is accepted and the submission stays open.","severity":"medium","snippet":"            s.callFrom = uint64(block.timestamp);","title":"Voided appeal reopens the appeal window from the poke time, not the void moment: a rejected/accepted submission never becomes final without a timely poke and can be re-appealed in the same transaction"},{"citation":"resolved","description":"CaseDesk._swarmStatus: when the Intake reports the request completed without a recorded answer (status 1/2), the moment of silence is `block.timestamp` of whoever pokes, and step() sets `callFrom = at`, so the new 24 h window to appeal / call the swarm starts at the poke, not at the Intake's completion. Unlike the Appealed-void case this is bounded: after askedAt + swarmTimeout + SWARM_DELIVERY_GRACE the moment becomes `lastDelivery`, so the shift is at most 7 h, and whoever wants the window to start early can poke. Same class as the medium finding (lazy timer sets a window from the poke), lower impact. Fix: have the Intake-completed branch return a deterministic moment (e.g. min(block.timestamp, lastDelivery) is what it does; to remove the dependence entirely, use lastDelivery or record the completion time), or accept and document the <= 7 h drift.","line":444,"path":"contracts/src/desks/CaseDesk.sol","reproduction":"Task 100 USDC, doer submits, dave disputes, no votes -> NoDecision; dave calls the swarm at T; the Intake marks the request completed with status 2 (no consensus) at T+10 min. Nobody pokes until T+5 h; poke. Expected (deterministic timers): callFrom = T+10 min (or lastDelivery = T+7 h); actual: callFrom = T+5 h, stepDeadline = T+29 h. A poke at T+10 min would have given stepDeadline = T+24 h 10 min: the end of the window depends on who poked when.","severity":"low","snippet":"            if (completed) return (SwarmStatus.Silent, false, uint64(block.timestamp));","title":"Silence declared from an Intake 'completed' report starts the next call window at the poke time (bounded by the delivery grace)"},{"citation":"resolved","description":"_fundCall pays a side's FIRST call from its dispute bond (fromBond = min(fee, bondImd)) and every later call wholly from the wallet; claimDisputer and the doer's refund subtract _sideSpent. There is no view of _sideSpent (ITaskVault has getAppeal/getDispute/quoteDisputeBond/quoteSwarmFee but nothing for it), so the exact IMD a side must approve and what it will get back can only be derived from SwarmCalled/Appealed/SwarmAsked events. maxImdIn protects against overpaying, but the quoted cost of the call is not readable on chain. Fix: expose `sideSpent(subId)` (or include it in getAppeal/getDispute).","line":30,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"Doer calls the swarm for himself in NoDecision (fee 0.5 IMD taken from the 0.5 IMD bond), swarm silent, new window. The doer wants to call again: cost is now 0.5 IMD from the wallet; quoteSwarmFee(subId) = 0.5 and quoteDisputeBond(subId) = 0.5 IMD + 1 USDC suggest 0 (side pays from bond) or 1.5 IMD + 1 USDC (stranger); no view returns the real 0.5 IMD. The same for claimDisputer: getDisputer(subId, dave).bond shows 0.5 IMD while the payout is 0.5 - sideSpent.disputer.","severity":"info","snippet":"    mapping(uint256 => SideSpent) internal _sideSpent;","title":"KNOWING BEFORE PAYING: a dispute side cannot read from any view whether its next swarm call is paid from its bond or from its wallet (SideSpent has no view)"},{"citation":"resolved","description":"The view returns askedAt + 6 h + 1 h, while _swarmStatus declares silence at once when the Intake's requests(rid) reports completed with no recorded answer; a user reading the view waits up to 7 h for a step that any poke would end now. Text/view drift only; no money moves differently.","line":329,"path":"contracts/src/desks/CaseDesk.sol","reproduction":"Ask the swarm at T; Intake completes with status 2 at T+5 min; stepDeadline(subId) = T+7 h; poke at T+6 min moves the submission to NoDecision/Decided already.","severity":"info","snippet":"        if (ph == Phase.Asked) return uint256(_bank().requests(s.request).askedAt) + p.swarmTimeout + SWARM_DELIVERY_GRACE;","title":"stepDeadline in Phase.Asked reports the end of the grace even when a poke now would already declare silence (Intake completed without answer)"},{"citation":"resolved","description":"appeal() fixes sidesDoer = (decision == Reject). The disputer appealing a Reject stands for the doer, so _fundCall treats them as an outsider (full bond, full fee from the wallet), but _settleDecided's `appellantIsStranger` excludes d.disputer from the 0.5 IMD off-the-top refund a winning stranger gets. Inconsistent, no abuse path (they can only lose their own bond or win the normal split). Low value; note for text/role drift (item 1 role walk).","line":631,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"100 USDC task, dave disputes, 20 NFTs reject, dave appeals (sidesDoer = true, bond 0.5 IMD + 1 USDC + fee 0.5 IMD), askAppealSwarm, swarm answers accept. dave 'wins' the appeal: penalty = doer bond? no: doerWins -> penalty = disputersBond (dave's own dispute bond) + 0; dave gets appPart of his own dispute bond back but not the 0.5 IMD stranger fee a third party would get.","severity":"info","snippet":"        bool appellantIsStranger = hasCaller && a.appellant != f.doer && a.appellant != d.disputer;","title":"The one disputer appealing against its own winning Reject decision is charged a full stranger bond + fee but is excluded from the stranger fee refund"},{"citation":"resolved","description":"R7 allows it, so this is a documented trust assumption, not a bypass: (1) BuybackBurner.setBuybackWallet (dev wallet only) names where the ETH/USDC/IMD 'burn' shares of a staked acceptance (half of the stakers' 5% part plus the NFT 5% nobody earned) are sent directly at payout; the dev wallet may name itself. (2) DevPrices.setYoloPriceUsd (x10 per call, unlimited calls) sizes r in CaseDesk._stakeBps; a very high YOLO price makes any 1-week lock of 1 YOLO worth the full 400 bps for a friend doer. Both are paid out of the 10% fee the treasury/burn would otherwise get; no author, contributor, disputer, voter or predictor loses anything. cancelTask in any phase (documented) also returns a friend's losing prediction, losing doer bond or losing dispute bond in full.","line":670,"path":"contracts/src/TaskVault.sol","reproduction":"devWallet calls setYoloPriceUsd repeatedly up to 1e30; friend locks 1 YOLO for 1 week and submits to a 100 USDC task: _stakeBps = 400 (r rounds to 0 or below power). Accepted undisputed: friend gets 90 + 4 = 94 USDC; treasury 3, burn 3 (to the dev-set buyback wallet) instead of treasury 10.","severity":"info","snippet":"            address wallet = IBurnerWallet(burner).buybackWallet();","title":"Dev wallet trust surface beyond cancel: buyback wallet and YOLO price are set by the dev wallet and move burn shares and the 4% stake bonus (from the fee, not from users)"},{"citation":"resolved","description":"TaskVault sets `ever` in createTask/topUp and exposes isContributor(); no desk calls it, and _settleDecided/_splitCurrency explicitly pay a winning contributor like anyone else ('Every winner gets their share, also one who put money into the task'). The comment states the opposite rule. No behavioural effect.","line":181,"path":"contracts/src/VaultTypes.sol","reproduction":"grep: isContributor is declared in IVault and TaskVault only; `.ever` is only written. A contributor who disputes and wins receives the pro-rata share (DisputeDesk._splitCurrency line 777-781), contrary to the comment.","severity":"info","snippet":"    bool ever; // ever contributed to this task: as a winner they get only their own bond back","title":"Text drift: Contribution.ever / isContributor are written but never read by any desk; the field comment describes a rule that no longer exists"}],"hash":"1092f6366d072d8af94296e43cb6a4a1ddac38e8ff2f8f6bff626f0cb81e44c2","nodeId":"535256bd-64aa-48a9-9aae-ce9b99c6418c","outcome":"completed","summary":"Findings file written (1 medium with PoC, 1 low, 5 info). Report follows.\n\n**0. VERDICT.** No critical or high. One medium (`CaseDesk.step` restarts the appeal window at the poke time after a voided appeal, so finality drifts and a zero-cost appellant can re-appeal in the same transaction days later). One low (silence from an Intake \"completed\" report is also timed at the poke, bounded to 7 h). Five info items: no view of a side's spent bond, `stepDeadline` overstating the Asked step, the disputer-as-appellant fee asymmetry, dev-wallet trust surface, and a stale `Contribution.ever` comment. No path lets one role take or permanently freeze another's money; every rule R1–R8 has a code path and no call runs outside its rule. Build and the 670 existing tests pass. Blocker to ship: fix the medium (one line) and add the PoC in `contracts/test/audit/`.\n\n**1. ROLE WALK.** Info, no issue beyond the entries below. Author: create, top up, dispute (not own submission as doer), vote, appeal, callSwarm, predict, sweeten; withdraw only Expired/Unwound (`TaskVault.sol:339`). Contributor: same minus create. Doer: submit any Open task before the deadline, no dispute of own submission (`DisputeDesk.sol:99`), appeal only a Reject and callSwarm only for itself (`DisputeDesk.sol:213,261`), first own call paid from the bond (`DisputeDesk.sol:296-302`). Queued doer: nothing until activated; `claimVoidSubmission` only on Completed/Unwound (`CaseDesk.sol:145`), and a queue cannot survive expiry. Disputer: one per submission inside the Review window; claims via `claimDisputer`. Voter/delegate: `vote` in Voting only, side fixed, id once (`DisputeDesk.sol:146-156`). Appellant and outside caller: Decided / NoDecision only, side forced by the decision. Predictor: Open, before deadline, no active submission (`YoloPredict.sol:137-139`). Staked doer: lock read strictly before the submission block, must still run at payout (`YoloLockHub.sol:108-113`). Dev wallet: cancel, prices, buyback wallet. Params owner: pause new tasks, timelocked params, bind sweetener. Drift: the disputer appealing its own Reject is treated as a stranger for cost but not for the fee refund (`DisputeDesk.sol:631`), see info entry.\n\n**2. HOSTAGE.** Medium, the entry with the PoC. Steps: (1) 100 USDC task; doer submits a fake, dave disputes, 20 NFTs reject (instant Decided). (2) The doer appeals at T (fee from its own bond, nothing from the wallet) and never asks. (3) Nobody pokes until T+30 h; a poke then leaves the submission Decided with a fresh window to T+54 h (`CaseDesk.sol:209` sets `callFrom = block.timestamp`). (4) Alternatively the doer waits 10 days and calls `appeal` again in one transaction; expected `WrongPhase`, actual accepted. Loss: none taken; the 100 USDC pot and the 10,000 YOLO pool stay locked an extra idle-time + 24 h per void, three voids max, at zero cost. Fix: `s.callFrom = uint64(uint256(s.appealedAt) + p.swarmTimeout);`. PoC `contracts/test/scratch/LazyAppealVoidWindow.t.sol` (full source in the findings file) fails on this commit and passes with that line. Longest path from the deadline to the author's refund per junk submission activated at the deadline, defender active: Review 24 h + vote 24 h + appeal filed at 23:59 and asked at once (7 h) = 79 h; with three silent swarm answers 48 h + 3×31 h = 141 h (5.9 days) at a cost to the junk doer of 0.5 IMD + 3 USDC bond plus two wallet-paid fees. Passive defender: today unbounded (each void waits for a poke); after the fix 48 h + 3×30 h = 138 h. N junk submissions multiply it (documented queue limitation).\n\n**3. SKIPPING THE DISPUTE.** Info, no issue beyond the documented slot. Queue order: the next submission activates at the poke that rejects the previous one (`CaseDesk.sol:352`), so the junk doer picks the start hour, but the window is always a full 24 h, so no hour is unreachable. Instant decision with own 20 NFTs: documented. Cancel or deadline mid-check changes nothing (`TaskVault.sol:641` expires only with no a","treeHash":null,"usage":{"cachedInputTokens":1847867,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":53668,"runtime":"claude","turns":33,"wallClockMs":1087184}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"c722c2e9ac9aa084","findings":[{"citation":"resolved","description":"The stake bonus compares the doer's YoloLockHub power with the author's reward converted to YOLO at the dev-set price (DevPrices.yoloValue). DevPrices.setYoloPriceUsd has no timelock and only a 10x-per-call sanity step, so the dev wallet can raise yoloPriceUsd by any factor in a few transactions right before a friend's submission, making r tiny so that any lock (1 YOLO for 1 week) reaches the full 400 bps, then move the price back. The 4% comes out of the 10% fee (the treasury's half), so no user loses, but the treasury does, and R7/MECHANICS present the price as sizing the bonus, not as a lever to redirect 4 USDC of every 100 USDC reward. Trust assumption to document, or read the price with a delay/floor.","line":520,"path":"contracts/src/desks/CaseDesk.sol","reproduction":"Setup: USDC task, author reward 100e6, DevPrices yoloPriceUsd = 0.0001e18 (reward = 1,000,000 YOLO). Doer locks 1e18 YOLO for 1 week (power 1e18/52). Dev calls setYoloPriceUsd 10x six times (0.0001 -> 100 USD/YOLO): r = 100e6*1e18/1e6*1e18/100e18 = 1e18, power(1.9e16) -> 7 bps; one more step (1000 USD/YOLO) -> r = 1e17 < power -> 400 bps. Doer submits, is accepted: _payReward credits doer 90e6 + 4e6, treasury gets 0.5e6 instead of 5e6 (stakers 5e6 - bonus 4e6 = 1e6, half burned). Expected under an honest price: stakeBps 0 or 7, treasury 5e6.","severity":"low","snippet":"        return (uint16(power >= r ? MAX_STAKE_BONUS_BPS : MAX_STAKE_BONUS_BPS * power / r), true);","title":"Dev wallet can move up to 4% of any accepted reward from the treasury fee to a friendly doer through DevPrices"},{"citation":"resolved","description":"_sideSpent decides (a) how much of the fixed IMD bond a winning disputer or doer gets back (claimDisputer line 348, settle lines 569/696), (b) whether a side's next appeal/callSwarm is paid from the bond or from the wallet (_fundCall line 299, which sets the IMD the caller must have and the minimum maxImdIn). It has no getter, no field in getDispute/getDisputer/getSettlement/getAppeal, and _charge emits no event; after the ask, getAppeal().fromBond is zeroed and after silence/void the Appeal is deleted. A frontend can only re-derive it from SwarmAsked.paid together with who the caller of that question was. Item 9 of the brief asks that full cost and claimable amounts be readable from views.","line":30,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"USDC task reward 100e6, fee 0.5e18 IMD. Disputer D disputes (bond 0.5e18 IMD + 1e6 USDC). NFT decides Accept at vote end. D appeals: _fundCall takes fromBond = 0.5e18, pay = 0; askAppealSwarm spends 0.5e18 -> _sideSpent.disputer = 0.5e18. Swarm answers reject: D wins. Views: getDisputer(sub, D).bond.imd = 0.5e18, getSettlement(sub).disputerReward.imd = doer bond 0.5e18 (minus 0). Expected from views: claimDisputer pays 1.0e18 IMD. Actual: payout.imd = 0.5e18 - 0.5e18 + 0.5e18 = 0.5e18. Likewise before a second call by D the needed wallet IMD is fee (0.5e18), while the views suggest 0 (bond still shows 0.5e18). Fix: add a public getter for _sideSpent (or return it in getDisputer/getSettlement).","severity":"low","snippet":"    mapping(uint256 => SideSpent) internal _sideSpent;","title":"No view exposes the IMD a side already spent from its bond (_sideSpent): claim amounts and the wallet cost of a side's next swarm call cannot be learned from the chain's views"},{"citation":"resolved","description":"Every other lazy transition anchors the window that follows to the deterministic moment the step ended (vote end -> callFrom = voteEnd; silence -> callFrom = lastDelivery), so the outcome and timing do not depend on who pokes when (CaseDesk._swarmStatus comment). The Appealed -> Decided void path instead starts the new 24 h appeal window at the time of the poke. A filed-but-never-asked appeal therefore holds the submission for 6 h plus however long nobody pokes plus a fresh 24 h, instead of a fixed 30 h; stepDeadline(Appealed) = appealedAt + 6 h understates when the submission can become final. No money is taken, anyone can poke, but the author's refund (or the doer's payout) is delayed by the unpoked gap at no cost to the appellant.","line":209,"path":"contracts/src/desks/CaseDesk.sol","reproduction":"Submission NFT-decided Reject at T. Doer appeals at T+23h (fee from bond, 0 wallet outlay), never calls askAppealSwarm, nobody pokes. At T+23h+6h+40h someone calls poke: step voids the appeal and sets callFrom = T+69h, so the submission is Final (author can withdraw) only at T+93h. Expected with the deterministic anchor (callFrom = appealedAt + swarmTimeout = T+29h): Final at T+53h and the same poke at T+69h finalizes it at once. Fix: s.callFrom = s.appealedAt + p.swarmTimeout (and emit AppealVoided with that time).","severity":"low","snippet":"            s.callFrom = uint64(block.timestamp);","title":"Appealed timeout anchors the new appeal window to the poke time, not to appealedAt + swarmTimeout, so a late poke lengthens the lock by the unpoked time"},{"citation":"resolved","description":"_swarmStatus (line 443-444) returns Silent at block.timestamp as soon as IIntake.requests(rid).completed is true with no recorded answer, so the Asked step can end (and the next 24 h window start) before the time the view reports. A frontend showing the view's time as 'nothing moves before' is wrong by up to 7 h.","line":329,"path":"contracts/src/desks/CaseDesk.sol","reproduction":"callSwarm at T (askedAt = T). Intake marks the request completed without a valid callback at T+1h. stepDeadline(sub) = T+7h, yet poke at T+1h moves the submission to NoDecision with callFrom = T+1h and the window to call again ends at T+25h, not T+31h.","severity":"info","snippet":"        if (ph == Phase.Asked) return uint256(_bank().requests(s.request).askedAt) + p.swarmTimeout + SWARM_DELIVERY_GRACE;","title":"stepDeadline(Asked) returns askedAt + timeout + grace, but silence can be declared earlier when the Intake reports the request completed"},{"citation":"resolved","description":"R3 says the dispute bond is 1% of the author's own share. The code freezes base at submission; an author top-up during the check (allowed, joins the reward at once) raises the share and the doer's payout but not the dispute/appeal bond. Only a wording drift; freezing is the safer behaviour (no bond can be moved under a disputer between quote and transaction).","line":111,"path":"contracts/src/desks/CaseDesk.sol","reproduction":"Author reward 100e6 USDC, doer submits (base 100e6). Author tops up 900e6 during the Review window. Dispute bond rew = 1e6 (1% of 100e6), while 1% of the author's share now is 10e6.","severity":"info","snippet":"        s.base = uint128(t.base); // a part of the pot","title":"R3 text drift: the dispute/appeal bond base is the author's share at submission time, not the author's share at dispute time"},{"citation":"resolved","description":"On a staked acceptance half of the stakers' fee part plus any unearned NFT part is 'burned' (DisputeDesk._payReward -> TaskVault._burn). For ETH, USDC and IMD this is a direct send to BuybackBurner.buybackWallet, which the dev wallet (owner of DevPrices) can change with one transaction and no timelock (BuybackBurner.setBuybackWallet). R7 documents that the dev wallet sets the buyback wallet; the consequence that these fee shares are dev-directed rather than destroyed should be stated as a trust assumption. No user money is involved (fee only).","line":670,"path":"contracts/src/TaskVault.sol","reproduction":"USDC task 100e6 accepted with a staked doer at 0 bps (floor): stakers part 5e6, bonus 0, half = 2.5e6 sent to buybackWallet in the same transaction; dev had set buybackWallet to any address beforehand.","severity":"info","snippet":"            address wallet = IBurnerWallet(burner).buybackWallet();","title":"Trust assumption: the ETH/USDC/IMD 'burn' share is a transfer to a wallet the dev wallet chooses at will"},{"citation":"resolved","description":"getTask exposes queueHead/queueTail but not the queued submission ids; a frontend must rebuild the order from Submitted(queued = true) events (ids are increasing, so the order is derivable). RPC-only frontends can do this; it is only a convenience gap for item 10/9.","line":106,"path":"contracts/src/TaskVault.sol","reproduction":"Task with active sub 1 and queued subs 2, 3: getTask(t).queueHead = 0, queueTail = 2; no view returns [2,3].","severity":"info","snippet":"    mapping(uint256 => mapping(uint256 => uint64)) internal _queue; // taskId => index => subId","title":"The submission queue order is not readable from views (no getter for _queue)"}],"hash":"aa835cca5ece87d19898f3f6f374d90232980b43f8b7ebe706f9d2ab133762a5","nodeId":"26a5fa7d-a2e8-487a-a08d-2df81b019610","outcome":"completed","summary":"The test run finished after all. Item 11 is updated: `forge test` (all suites except the fork tests) passed.\n\n| Result | Count |\n|---|---|\n| Test suites | 79 |\n| Passed | 670 |\n| Failed | 0 |\n| Skipped | 1 |\n\nEverything else in the report stands: findings are in `.imd-findings.json` (three low, four info, none critical or high, so no PoC file was needed), the source build passed with warnings only, and the included audit PoCs for the four already-fixed items pass with the rest of the suite.","treeHash":null,"usage":{"cachedInputTokens":464340,"inputTokens":6,"model":"claude-fable-5-1","outputTokens":416,"runtime":"claude","turns":2,"wallClockMs":2480585}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"782f24113d209be2","findings":[{"citation":"resolved","description":"Item 6/8 (collusion vs author, lazy timers). A side of the dispute calling for itself posts no bond and its first fee comes from its dispute bond (_fundCall, lines 296-311). For a doer the NFT decided against, that 0.5 IMD bond part is already lost, so appeal(subId, 0, noSwap) costs nothing: no IMD, no reward-currency, no bond. Every rational rejected doer therefore appeals: it delays the final rejection by up to 24 h (appeal filed at the end of the window) + 6 h (ask) + 7 h (answer/grace), and because _settleDecided subtracts _sideSpent.doer from penaltyImd (line 637) a correct swarm answer leaves 0 IMD for the disputer and the correct NFT voters (they keep only the 3% reward-currency part). With a 100 USDC reward: disputer + voters lose 0.5 IMD of expected reward, the author's payout/refund is delayed ~37 h, the doer pays 0 extra. Symmetric for a disputer the NFT decided against. Rule R5 ('a dispute side pays the fee from its bond') makes this by design, so low; but the incentive is that the losing side always appeals for free and the winners' IMD share is always 0 after a vote. Smallest fix: let a side's fee come from its bond only when it wins (charge it from the wallet otherwise), or exclude the from-bond part from penalty reduction when the appealing side loses.","line":306,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"Base fixture: _create(author, usdc, 100e6); _submit(doer, t); _dispute(dave, s); 20 NFTs vote reject (instant Decided). vm.prank(doer); vault.appeal(s, 0, noSwap) succeeds with maxImdIn = 0; getAppeal(s) = {bond 0/0, funds 0, fromBond 0.5e18}; doer IMD/USDC balances unchanged. _askAppealSwarm(s); swarm answers false; poke. Expected (fair): winners share the loser's 0.5 IMD + 3 USDC. Actual: getSettlement(s).voterBond.imd == 0, disputerReward.imd == 0, voterBond.rew == 1.5e6, disputerReward.rew == 1.5e6. Confirmed with a scratch Foundry test (passes on current code, i.e. documents the behaviour; no fail-then-pass test since this is the documented rule).","severity":"low","snippet":"        uint256 pay = fee - fromBond;","title":"Rejected doer appeals at zero cost and the free appeal strips the IMD penalty owed to the disputer and the correct voters"},{"citation":"resolved","description":"Item 9 (knowing before paying). _fundCall charges a side's first call from its bond only while its SideSpent is 0 (line 299); later calls are paid wholly from the wallet. claimDisputer (line 348) and the doer's credit (lines 569, 696) return bond.imd minus _sideSpent. Neither getDispute, getDisputer, getAppeal nor getSettlement return SideSpent and there is no getter, so from views alone the doer/disputer cannot compute (a) the IMD its next appeal/callSwarm will pull (fee or fee minus fromBond), nor (b) the IMD it will get back. ITaskVault.quoteDisputeBond's note 'Appealing or calling the swarm costs the same bond plus quoteSwarmFee' is wrong for a side (no bond, fee from bond) and for IMD tasks (maxImdIn must also cover bond.rew). Only the event trail (SwarmCalled/Appealed + SwarmAsked.paid) lets a reader reconstruct it. Smallest fix: add `function sideSpent(uint256 subId) external view returns (SideSpent memory)` and route it in TaskVault._route, and correct the quote note.","line":30,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"State: subId in NoDecision, disputer D already made one callSwarm that stayed silent (sideSpent.disputer = 0.5e18). D now calls vault.quoteDisputeBond(subId) + vault.quoteSwarmFee(subId) = 0.5 IMD + 1 USDC + 0.5 IMD and reads getAppeal(subId) = empty (deleted by dropCaller). Expected: a view telling D the next call costs the full 0.5 IMD fee from the wallet and that a win returns bond.imd - 0.5 IMD. Actual: no view returns _sideSpent; D learns it only when callSwarm pulls 0.5 IMD (or reverts CostAboveMax if maxImdIn was set from the first call's 0).","severity":"low","snippet":"    mapping(uint256 => SideSpent) internal _sideSpent;","title":"No view exposes _sideSpent: a dispute side cannot learn from the chain what its next swarm call costs or what claimDisputer/credit will return"},{"citation":"resolved","description":"Item 2/8 (hostage, lazy timers). Every other lazy transition fixes the next window at a deterministic moment (voteEnd, lastDelivery) so 'the outcome never depends on who pokes when' (comment at line 428-429). The Appealed void is the exception: the void is due at appealedAt + swarmTimeout (stepDeadline, line 328) but the new appeal window runs from whenever step() happens to be played. An appellant who files, never asks, and whose opponent does not poke keeps the submission in Appealed indefinitely, and when a poke finally comes a fresh full 24 h window opens from that moment, so the total hold per void cycle is unbounded instead of 24 h + 6 h. Three voids are allowed (MAX_SWARM_CALLS). No winner changes; the loss is time on the author's/doer's locked money (100 USDC reward: each cycle adds the idle time to the 30 h). The same pattern at line 444 (Intake reports completed: silence 'at' = now). Smallest fix: `s.callFrom = s.appealedAt + p.swarmTimeout;` (and for line 444 use min(now, lastDelivery) consistently or keep the deadline).","line":209,"path":"contracts/src/desks/CaseDesk.sol","reproduction":"Base fixture: reach Phase.Decided (20 reject votes), _appeal(doer, s) at T0 (appealedAt = T0). Nobody calls askAppealSwarm or poke. Warp to T0 + 6 h + 3 days, vault.poke(t). Expected: callFrom = T0 + 6 h, so the appeal window ended at T0 + 30 h and _endUncalled already finalised the rejection on this poke. Actual: phase Decided with callFrom = T0 + 6 h + 3 days, a new 24 h appeal window opens 3 days late; the doer can appeal again for free (previous finding) and repeat (3 asks).","severity":"low","snippet":"            s.callFrom = uint64(block.timestamp);","title":"Void of an unasked appeal starts the new 24 h appeal window at the poke time, not at the void moment: a late poke lengthens the hold"},{"citation":"resolved","description":"Item 9/11. CaseDesk._doerBond uses _pctBond(base, doerBondBps) where base is the author's own share only (VaultTypes.sol:97 'the $YOLO add-on are not part of any bond'). The interface text a frontend relies on says otherwise. Also ITaskVault.sol:130 'Appealing or calling the swarm costs the same bond plus quoteSwarmFee' is wrong for dispute sides. Fix: correct both comments.","line":56,"path":"contracts/src/interfaces/ITaskVault.sol","reproduction":"Task with 100 USDC reward and a 10,000 YOLO add-on: vault.quoteSubmission(taskId) returns {imd 0.5e18, rew 3e6}; the comment promises an extra 3% of 10,000 YOLO. Expected per comment: a YOLO part; actual: none (Bond has no YOLO field).","severity":"info","snippet":"    /// @notice The bond a new submission to `taskId` would need now: 0.5 IMD fixed + 3% R (and 3% of a $YOLO","title":"Doc drift: quoteSubmission says the doer bond includes 3% of the $YOLO add-on; the code excludes the add-on from every bond"},{"citation":"resolved","description":"Item 1/7 (params owner). vote() reads only instantDelta/minWinnerWallets from the version; eligibility comes from the immutable `votePower` (line 35) whose nft and delegateRegistry are fixed at deployment. ParamsRegistry.sol:18-19 tells users 'a new version may name other counterparties (intake, nft, delegateRegistry, devPrices), which then judge every task created under it'. For nft/delegateRegistry that is false (good: the params owner cannot swap the electorate; but the documented trust surface is wider than reality and the two fields are validated for nothing). Fix: drop the two fields from the doc/validate or wire them.","line":185,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"Propose and activate a version 2 with nft = another ERC-721 and delegateRegistry = 0; create a task under v2; dispute; a holder of the new NFT calls vault.vote -> NotTokenHolder; a holder of the original identity.md NFT votes fine. Expected per ParamsRegistry comment: the new NFT judges v2 tasks; actual: the original one does.","severity":"info","snippet":"            (,, uint32 instantDelta, uint32 minWallets) = ParamsRegistry(registry).voteParams(version);","title":"Params.nft / Params.delegateRegistry per version are dead: votes always use the immutable VotePower, contrary to the registry's trust note"},{"citation":"resolved","description":"Item 9/11. _settleDecided refunds only bond + unspent funds to a winning caller (lines 699-705) plus bondFixedImd off the top for a stranger appellant (641-645); a winning side (doer/disputer) or a NoDecision stranger caller via callSwarm (also 'stranger', gets the 0.5) is never reimbursed `spent` itself beyond that. The struct comment and docs disagree with each other and with the code on what a swarm call costs a winner.","line":245,"path":"contracts/src/VaultTypes.sol","reproduction":"USDC 100 task, NoDecision, disputer D calls callSwarm(sidesDoer=false) from bond (fromBond 0.5e18, spent 0.5e18), swarm answers false. claimDisputer pays D bond.imd - 0.5e18 + share of doer penalty (0.5e18 fixed part + 3e6) -> D nets its fee back only through the loser's bond, not as 'reimbursement'; when the loser's IMD part was itself consumed by the loser's own free appeal (finding 1) D's fee is simply gone. Expected per comment: spent reimbursed from the loser; actual: no code path reads Appeal.spent.","severity":"info","snippet":"    uint256 spent; // paid to the Intake for his question (reimbursed from the loser if he wins)","title":"Appeal.spent comment promises reimbursement of a winning caller's fee; only a stranger appellant gets 0.5 IMD back"}],"hash":"de82b5dc36e12cb3e1dbb3059707452270ed22a4a72228c0a6c6c75cf8742ef8","nodeId":"4afb15e3-0521-4776-8a4c-49c35fa3faa6","outcome":"completed","summary":"All six snippets match the tree. The findings file is written, the scratch test is removed, and no repository file was changed. Here is the report in the mandated format.\n\n**0. VERDICT.** No critical or high finding. No blocker. Every rule R1–R8 has a code path that enforces it and I found no call allowed outside its rule, no double counting of an NFT, no way to hold another party's money beyond the documented queue and unasked-appeal paths, and no dev-wallet gain beyond the documented cancel. Three low findings (all incentive or observability, none a loss of principal) and three info items. Build passes, 670 tests pass.\n\n**1. ROLE WALK.** No issue. Author/contributor: `createTask`, `topUp` (Open, before deadline, also during a check), `withdrawContribution` (Expired/Unwound only), `poke`. Doer: `submit` (Open, before deadline, queued if busy), `appeal` only of a rejection, `callSwarm` only for own side, never `dispute`. Queued doer: `claimVoidSubmission` only on Completed/Unwound. Disputer: one per submission, Review only, not the doer. Voter/delegate: `vote` only in Voting before `voteEnd`, id bitmap per dispute, side fixed per wallet. Appellant/outside caller: Decided/NoDecision only, within `callFrom + 24h`. Predictor: Open, before deadline, `activeSub == 0`. Staked doer: power read strictly before the submission block, lock must still run at payout. Dev wallet: `cancelTask` (Open only after timers), prices. Params owner: pause new tasks, versions behind 48 h. Info drift: `Params.nft`/`delegateRegistry` per version are dead, votes use the immutable VotePower (contracts/src/desks/DisputeDesk.sol:185).\n\n**2. HOSTAGE.** Low. Longest path per submission from the deadline to the author's refund with a cooperative poker: submit at deadline−1 → Review 24 h → dispute at the last second → Vote 24 h → Decided → appeal at +24 h → unasked void at +6 h → ×3 → ≈ 24+24+3×30 = 114 h, or with asked-and-silent cycles 24+24+3×37 = 159 h ≈ 6.6 days, then the next queued junk (documented). The non-documented part: the void of an unasked appeal sets the new window from the poke time (contracts/src/desks/CaseDesk.sol:209), so with no poke the Appealed phase has no end and each void cycle restarts a full 24 h late. Cost to the holder: zero (a rejected doer appeals for free, see 6). Fix: `s.callFrom = s.appealedAt + p.swarmTimeout`. The author's counter is `poke` plus `askAppealSwarm` (free), so no permanent freeze.\n\n**3. SKIPPING THE DISPUTE.** No issue beyond the documented. Queue order: the next submission's window opens at the poke that finalizes the previous one, but it is a full 24 h, and `Activated` is emitted. Instant decision by 20 own NFTs is documented; the appeal window and the swarm remain. A cancel is dev-only; the deadline does not end a Review window.\n\n**4. DOUBLE VOTE.** No issue. Owner then delegate, delegate then owner, transfer after a vote and delegation change are all caught by the per-dispute id bitmap (DisputeDesk.sol:148-154) checked before `firstNotHeld`. A payTo can accumulate votes on both sides (A votes accept payTo=B, B votes reject), but a payTo is paid for the final side only, once (`claimed`). HiddenList: id bitmap per round, one side per wallet per round, distinct real owners for the quorum.\n\n**5. COLLUSION VS PREDICTORS.** No issue beyond the design. The author+doer freeze (a junk submission) blocks both sides equally, and a junk left undisputed is accepted, so the colluders must dispute themselves (swarm fee lost per cycle). The deferred 2.5 %+2.5 % and the empty-side rule pay author+doer only when outsiders bet against a completable task, which is the market's purpose. On a 10,000 YOLO pool: 500 YOLO deferred on completion, documented.\n\n**6. COLLUSION VS AUTHOR AND CONTRIBUTORS.** Low. A doer the NFT rejected appeals at zero cost (contracts/src/desks/DisputeDesk.sol:306, `maxImdIn = 0` succeeds) and the swarm's confirming answer leaves 0 IMD for the disputer and correct voters (penalty minus `_sideSpent`, line ","treeHash":null,"usage":{"cachedInputTokens":1548255,"inputTokens":322,"model":"claude-fable-5-1","outputTokens":54424,"runtime":"claude","turns":34,"wallClockMs":1106598}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f96a0c89c039c22a","findings":[{"citation":"resolved","description":"appeal() prepays the live fee F0 (a.funds + a.fromBond). The ask is lazy (askAppealSwarm, another transaction). If the Intake price is F0+1 when the ask runs, _askAppeal takes the void branch: dropCaller, back to Decided, ++asks. An appellant on the side the NFT decision favours, for example a friend of a doer whose own 20 NFTs won an instant accept, can file the appeal at the old price just before a known price change. The appeal is then voided. Doing this 3 times reaches MAX_SWARM_CALLS and makes the NFT decision final, and nobody else can top up the appeal or replace it in the meantime. Each void gives a full refund, so the attack costs only gas. Its success depends on Intake price changes, which the attacker does not control, so this is low.","line":407,"path":"contracts/src/desks/CaseDesk.sol","reproduction":"1) Submission in Decided(Accept). 2) Bob calls appeal(subId, max, payIn) with the swarm price at 10 IMD, so a.funds is 10 IMD. 3) The Intake price goes to 11 IMD. 4) Anyone calls askAppealSwarm(subId, rt). Expected: the swarm is asked (at least the honest party can add the difference). Actual: AppealVoided(subId, 11e18), the phase goes back to Decided and asks becomes 1. After 3 such rounds, _endUncalled finalizes Accepted and a fake proof is paid (100 USDC reward minus the 10% fee to the doer).","severity":"low","snippet":"        if (price != 0 && price <= funds) {","title":"A prepaid appeal is voided, and uses up one of the 3 swarm calls, when the Intake price rises between appeal() and askAppealSwarm()"},{"citation":"resolved","description":"Item 9: there is a view for the resolution, but none for the amount a predictor will receive. A frontend has to rebuild the claim from markets, splits, positionsOf and paidIn, and this result shifts as other winners claim, because of mulDiv rounding on poolLeft/pointsLeft. There is no loss here, only a gap in what a user can learn from views before paying.","line":260,"path":"contracts/src/YoloPredict.sol","reproduction":"Task Completed, 10,000 YOLO pool. Calling claim through eth_call is the only way to learn the exact payout. No claimable(taskId, who) view exists.","severity":"info","snippet":"    function resolution(uint256 taskId) external view returns (Resolution) {","title":"YoloPredict has no view that returns what a position can claim"}],"hash":"e2fb9c4706eaff3ad7571fd251e06c12e0e897ba2c3fea2335803f001b5d3ea3","nodeId":"b2ed1078-0242-46e4-977d-28f47600cbf1","outcome":"completed","summary":"# moneycanbuy.fun audit: role scenarios and economic abuse\n\nI found no critical or high issue. One low finding and one info finding are in `.imd-findings.json`. I did not write any Foundry PoC, because no finding is critical or high. The full forge test run did not finish here: `forge test` was stopped at its 500-second limit while still compiling (`via_ir = true`). So I cannot report a passed or failed result.\n\n**0. VERDICT**\n- No blockers.\n- None of the 13 roles in item 1 has a call allowed outside its rule, or a rule with no code path. The same holds for the claim calls I checked in `DisputeDesk`, `CaseDesk`, `TaskVault`, `YoloPredict` and `YoloLockHub`.\n- Every money-locking phase has a timeout or an exit.\n- One NFT cannot be counted twice in a dispute: each id is marked once per dispute (`DisputeDesk.sol:148-154`).\n- One low finding: an Intake price rise can void a prepaid appeal, which still uses up one of the 3 swarm calls.\n- One info finding: YoloPredict has no view that returns what a position can claim.\n- I could not get a test result (see item 11).\n\n**1. ROLE WALK — info, no issue**\n- **Author:** creates the task (blocked when tasks are paused); can submit and dispute on their own task; withdraws only once the task is Expired or Unwound.\n- **Contributor:** top-ups go through `topUp`, which requires Open and before the deadline. Withdrawals go through `withdrawContribution`, which rejects Open and pays 0 on Completed.\n- **Doer:** `submit` stops at the deadline. The doer cannot dispute, and cannot appeal an acceptance (`DisputeDesk.sol:99,213,261`).\n- **Queued doer:** activated in turn by `closeSubmission`, even after the deadline. `claimVoidSubmission` returns the bond on Completed or Unwound.\n- **Disputer:** only in Review or Voting, inside the window, one per submission. Claims through `claimDisputer`.\n- **Voter and delegate:** `vote` runs during Voting before `voteEnd`, through `VotePower`. The side is fixed per wallet.\n- **Appellant and outside caller:** appeals only in Decided; swarm calls only in NoDecision. The fee comes from the bond via `_fundCall`.\n- **Predictor:** checks are at `YoloPredict.sol` `predict`.\n- **Staked doer:** `_stakeBps` checks the lock at submission and `_lockRuns` checks it at payout.\n- **Dev wallet:** `cancelTask` only. Prices are set in DevPrices.\n- **Params owner:** pausing new tasks; `bindSweetener` can be set once.\n\n**2. HOSTAGE — info, no issue beyond the documented paths**\n- Every phase has a timer:\n  - Review 24 h\n  - Voting 24 h\n  - Decided or NoDecision 24 h\n  - Appealed: void after 6 h\n  - Asked: silent after 6 h + 1 h\n  - All capped at 3 swarm calls (void calls count).\n- Longest path for one submission, from the deadline to the author's refund: 24 h + 24 h + 3 × (24 h + 7 h) + 24 h ≈ 165 h, then the next queued submission starts. A queue of N submissions multiplies this; that is documented as \"the queue judged in turn\".\n- Each queued junk submission costs 0.5 IMD + 3 USDC on a 100 USDC reward.\n\n**3. SKIPPING THE DISPUTE — info, nothing beyond documented behaviour**\n- An undisputed submission is accepted after 24 h; this is documented.\n- A self-held 20-NFT instant decision is within rule R4, and can be appealed.\n- A cancel triggers Unwound, which refunds everyone.\n- The deadline does not stop the active submission.\n- The only extra route is the low finding in item 8.\n\n**4. DOUBLE VOTE — no issue**\n- Each id is set in a per-dispute bitmap before the holder check (`DisputeDesk.sol:148-156`). So a transfer, a new delegation or a second transaction cannot reuse an id.\n- One `payTo` can collect counts on both sides through different voters. `_claimVoter` pays only the final side's count, once (`DisputeDesk.sol:428-434`), so it cannot be paid on both sides.\n- HiddenList uses a bitmap per round and one side per wallet (`HiddenList.sol:190-192,256-275`).\n\n**5. COLLUSION VS PREDICTORS — info, no new issue**\n- The cheapest plan: the author leaves the WILL DO side empty, self-submits, and","treeHash":null,"usage":{"cachedInputTokens":1075625,"inputTokens":32,"model":"claude-opus-5-5","outputTokens":14640,"runtime":"claude","turns":16,"wallClockMs":678760}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"e02bec63aa9dcaa5","findings":[{"citation":"resolved","description":"CaseDesk.step, Phase.Appealed timeout: when a filed appeal was not asked within swarmTimeout (6 h) it is voided and the submission goes back to Decided, but `callFrom` (the start of the reopened 24 h appeal window) is set to block.timestamp, the moment somebody happens to poke, not to the void moment `appealedAt + swarmTimeout`. Every other lazy transition anchors the next window to its own deterministic moment (vote end: `callFrom = voteEnd`; silence: `callFrom = at` = end of the grace), and docs/MECHANICS.md states that 'the outcome never depends on who pokes when' and that an unasked appeal 'is voided with a full refund and reopens the 24 h appeal window'. Consequences (R5, R8 no keeper): (1) the submission is not final at appealedAt + 6 h + 24 h; a poke at or after that moment still leaves it Decided for a further full 24 h; (2) the appellant (a rejected doer appeals for free: the fee comes from his already-lost bond, DisputeDesk._fundCall) can wait any time after the void and then poke + appeal in one transaction (appeal() plays the void via advanced() and then files), pushing finality back by (idle time + 24 h) per void instead of 24 h, up to three voids (MAX_SWARM_CALLS), at zero cost (full refund of bond and fee); (3) stepDeadline(subId) in Phase.Appealed reports appealedAt + 6 h, but the end of the reopened window cannot be known from the chain before the poke. Loss at a 100 USDC reward and a 10,000 YOLO prediction pool: no money is taken, but the pot, the doer/disputer bonds and the prediction market (which resolves only with the task) stay locked for 3 x (idle + 24 h) beyond the documented rule; the author's refund (rejected fake) or the doer's payout (accepted honest submission appealed by a stranger) is delayed by the unpoked time, and a defender has to poke at the exact void second to keep the documented timing. Smallest fix: `s.callFrom = uint64(uint256(s.appealedAt) + p.swarmTimeout);` (the void moment), which also makes _advance finalize in one call once voidAt + appealWindow has passed. Merged from three specialist reports (permissions medium, math low, flow low): same line, same mechanism, same fix. Reproduced: the attached Foundry proof fails on this commit (3/3) and passes with that one-line change (verified locally, source restored).","line":209,"path":"contracts/src/desks/CaseDesk.sol","reproduction":"1. author creates a 100 USDC task; doer submits; dave disputes; a holder of 20 NFTs votes reject (instant Decided at T0). 2. doer calls appeal(subId, max, noSwap) at T0 (Phase.Appealed, appealedAt = T0, zero IMD/USDC outlay: fee from the lost bond) and never calls askAppealSwarm. 3. Nobody pokes until T0 + 6 h + 24 h; then anyone calls poke(taskId). Expected: the void happened at T0 + 6 h and the reopened 24 h window ended at T0 + 30 h, so the submission is Final (Rejected). Actual: phase is Decided with callFrom = T0 + 30 h and stepDeadline(subId) = T0 + 54 h (test output: 1800180000 != 1800108000, phase 5 != 8). 4. Variant: nobody pokes for 10 days; the doer calls appeal() at T0 + 6 h + 10 d. Expected: revert WrongPhase (final). Actual: the appeal is accepted and the submission stays open for another 6 h + 24 h at least.","severity":"medium","snippet":"            s.callFrom = uint64(block.timestamp);","title":"Voided appeal reopens the 24 h appeal window from the poke time, not from the void moment: finality of a decided submission is pushed back by the unpoked gap per void, re-appeal possible at any later "},{"citation":"resolved","description":"_fundCall: a side of the dispute calling for itself posts no bond and its first fee comes from its own dispute bond (fromBond = min(fee, bondImd)); with fee 0.5 IMD and bondFixedImd 0.5 IMD, pay = 0. For a doer the NFT decided against (or a disputer the NFT decided against), that 0.5 IMD is already lost, so appeal(subId, 0, noSwap) costs nothing and succeeds with maxImdIn = 0. Every rational losing side therefore appeals: it delays finality by up to 24 h (appeal filed at the end of the window) + 6 h (ask) + 7 h (answer + grace), and _settleDecided subtracts _sideSpent of the losing side from penaltyImd (line 637), so after a confirming swarm answer the disputer and the correct NFT voters get 0 IMD (they keep only the reward-currency part of the loser bond). R5 says a dispute side pays the fee from its bond, so this is the documented rule; the unstated consequence is that the losing side's IMD bond is in practice never paid to the winners after an appeal and that the appeal itself is free griefing. Loss at a 100 USDC reward: winners lose the 0.5 IMD share (keep 3 USDC), the author's refund/payout is delayed about 37 h, the doer pays 0 extra. Smallest fix: charge the fee from the wallet when the calling side's bond is already lost (NFT decided against it), or exclude the from-bond part from the penalty reduction when the appealing side loses. Reproduced by executing a scratch Foundry test on this commit: appeal(s, 0, noSwap) by the rejected doer succeeds with an unchanged IMD balance, and after the swarm confirms the rejection getSettlement(s).voterBond.imd == 0 and disputerReward.imd == 0 (rew parts 1.5e6 each). No fail-then-pass test since this is the documented rule (low).","line":306,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"100 USDC task, doer submits (bond 0.5 IMD + 3 USDC), dave disputes, 20 NFTs vote reject (instant Decided). vm.prank(doer); vault.appeal(s, 0, noSwap) succeeds with maxImdIn = 0; getAppeal(s) = {bond 0/0, funds 0, fromBond 0.5e18}; the doer's IMD and USDC balances are unchanged. askAppealSwarm(s, rt); the swarm answers false; poke. Expected (fair): winners share the loser's 0.5 IMD + 3 USDC. Actual: getSettlement(s).voterBond.imd == 0, disputerReward.imd == 0, voterBond.rew == 1.5e6, disputerReward.rew == 1.5e6.","severity":"low","snippet":"        uint256 pay = fee - fromBond;","title":"A side of the dispute decided against appeals at zero cost, and its free appeal removes the IMD penalty the winners (disputer, correct voters) would share"},{"citation":"resolved","description":"appeal() prepays the live fee F0 (a.funds + a.fromBond); the ask is a later transaction (askAppealSwarm). If the Intake price at ask time is above F0, _askAppeal takes the void branch: dropCaller, back to Decided, ++asks. There is no way for the appellant or anyone else to add the difference, and while the appeal sits unasked nobody else can appeal. An appellant on the side the NFT decision favours (e.g. a friend of a doer whose 20 own NFTs won an instant accept) can file at the old price just before a known price change; three such voids reach MAX_SWARM_CALLS and make the NFT decision final (_endUncalled) without any swarm check, each void giving a full refund, so the cost is gas only. Success depends on Intake price movements the attacker does not control, hence low. Fix: let askAppealSwarm accept extra funds from its caller (collect price - funds from msg.sender) instead of voiding, or do not count a price-void toward MAX_SWARM_CALLS. Reproduced by executing a scratch Foundry test on this commit: instant Accept by 20 NFTs, bob appeals at price 0.5 IMD, the mock Intake price is set to 0.5 IMD + 1 wei, askAppealSwarm leaves the submission Decided with asks == 1 and credits bob 1 IMD (bond 0.5 + fee 0.5 back).","line":407,"path":"contracts/src/desks/CaseDesk.sol","reproduction":"1) Submission in Decided(Accept). 2) Bob calls appeal(subId, max, payIn) with the swarm price at 10 IMD, so funds = 10 IMD. 3) The Intake price goes to 11 IMD. 4) Anyone calls askAppealSwarm(subId, rt). Expected: the swarm is asked, or at least the honest party can add the difference. Actual: AppealVoided(subId, 11e18), phase back to Decided, asks = 1, a new 24 h window from now. After 3 rounds _endUncalled finalizes Accepted and a fake proof is paid (100 USDC minus the 10% fee to the doer).","severity":"low","snippet":"        if (price != 0 && price <= funds) {","title":"A prepaid appeal is voided, and uses one of the 3 swarm calls, when the Intake price rises between appeal() and askAppealSwarm(); nobody can top it up"},{"citation":"resolved","description":"_swarmStatus: when the Intake reports the request completed without a recorded answer (status 1/2), the moment of silence is block.timestamp of whoever pokes, and step() sets callFrom = at, so the new 24 h window to appeal / call the swarm starts at the poke, not at the Intake's completion. Same class as the Appealed-void finding (a lazy timer anchored to the poke) but bounded: after askedAt + swarmTimeout + SWARM_DELIVERY_GRACE the moment becomes lastDelivery, so the shift is at most 7 h, and whoever wants the window to start early can poke. Fix: anchor to a deterministic moment (lastDelivery, or record the completion time), or document the <= 7 h drift. Reproduced by reading lines 435-446 and step() line 226. Merged from two specialist reports.","line":444,"path":"contracts/src/desks/CaseDesk.sol","reproduction":"100 USDC task, doer submits, dave disputes, no votes -> NoDecision; dave calls the swarm at T; the Intake marks the request completed with status 2 (no consensus) at T+10 min. Nobody pokes until T+5 h; poke. Expected (deterministic timers): callFrom = T+10 min (or lastDelivery = T+7 h); actual: callFrom = T+5 h, stepDeadline = T+29 h. A poke at T+10 min would have given stepDeadline = T+24 h 10 min: the end of the window depends on who poked when.","severity":"low","snippet":"            if (completed) return (SwarmStatus.Silent, false, uint64(block.timestamp));","title":"Silence declared from an Intake 'completed' report starts the next 24 h call window at the poke time (bounded by the 7 h grace)"},{"citation":"resolved","description":"Item 9 (knowing before paying). _fundCall charges a side's first call from its bond only while its SideSpent is 0 (line 299); later calls are paid wholly from the wallet. claimDisputer (line 348), the doer's credit (lines 569, 696) and the penalty (line 637) subtract _sideSpent. Neither getDispute, getDisputer, getAppeal nor getSettlement return it, there is no getter in DisputeDesk, DeskApi or ITaskVault, _charge emits no event, after the ask getAppeal().fromBond is zeroed and after silence/void the Appeal is deleted. So from views alone a side cannot compute (a) the IMD its next appeal/callSwarm will pull (fee, or fee minus fromBond), nor (b) the IMD it will get back; only the event trail (SwarmCalled/Appealed + SwarmAsked.paid) lets a reader reconstruct it. ITaskVault.quoteDisputeBond's note 'Appealing or calling the swarm costs the same bond plus quoteSwarmFee' (line 130) is also wrong for a side (no bond, fee from bond) and for IMD tasks (maxImdIn must also cover bond.rew). Smallest fix: add `sideSpent(uint256 subId) external view returns (SideSpent memory)` routed in TaskVault._route (or include it in getDisputer/getSettlement) and correct the quote note. Merged from three specialist reports (permissions info, math low, flow low). Reproduced by grep: no 'sideSpent'/'SideSpent' in ITaskVault.sol or DeskApi.sol.","line":30,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"USDC task reward 100e6, fee 0.5e18 IMD. Disputer D disputes (bond 0.5e18 IMD + 1e6 USDC). NFT decides Accept at vote end. D appeals: _fundCall takes fromBond = 0.5e18, pay = 0; askAppealSwarm spends 0.5e18 -> _sideSpent.disputer = 0.5e18. Swarm answers reject: D wins. Views: getDisputer(sub, D).bond.imd = 0.5e18, getSettlement(sub).disputerReward.imd = 0.5e18 (doer bond). Expected from views: claimDisputer pays 1.0e18 IMD. Actual: payout.imd = 0.5e18 - 0.5e18 + 0.5e18 = 0.5e18. Likewise before a second call by D the wallet IMD needed is the full fee (0.5e18) while the views suggest 0 (bond still shows 0.5e18); callSwarm reverts CostAboveMax if maxImdIn was set from the first call's 0.","severity":"low","snippet":"    mapping(uint256 => SideSpent) internal _sideSpent;","title":"No view exposes _sideSpent: a dispute side cannot learn from the chain what its next swarm call costs or what claimDisputer / its credit will return"},{"citation":"resolved","description":"Item 7. R7 allows the dev wallet to set the prices and the buyback wallet, so this is a documented trust assumption, not a bypass; the money consequences are not stated in the docs. (1) _stakeBps compares the doer's YoloLockHub power with the author's reward converted to YOLO at the dev-set price (DevPrices.yoloValue). DevPrices.setYoloPriceUsd has no timelock and only a 10x-per-call step (MAX_STEP = 10, unlimited calls), so the dev wallet can raise yoloPriceUsd by any factor in a few transactions right before a friend's submission, making r tiny so that any lock (1 YOLO for 1 week) reaches the full 400 bps, then move the price back. (2) BuybackBurner.setBuybackWallet (dev wallet only, no timelock) names where TaskVault._burn (line 670) sends the ETH/USDC/IMD 'burn' shares of a staked acceptance (half of the stakers' 5% part plus the NFT 5% nobody earned) directly at payout; the dev wallet may name itself. Both come out of the 10% fee; no author, contributor, disputer, voter or predictor loses anything, the treasury does. Fix: document as trust assumptions; optionally read the YOLO price with a delay/floor and timelock the buyback wallet. Merged from three specialist reports (permissions info, math low, math info). Reproduced by reading DevPrices.setYoloPriceUsd (line 68, MAX_STEP line 25), CaseDesk._stakeBps (lines 504-521), DisputeDesk._payReward (lines 717-739), TaskVault._burn (lines 665-679), BuybackBurner.setBuybackWallet (line 75).","line":520,"path":"contracts/src/desks/CaseDesk.sol","reproduction":"Setup: USDC task, author reward 100e6, DevPrices yoloPriceUsd = 0.0001e18 (reward = 1,000,000 YOLO). Doer locks 1e18 YOLO for 1 week (power 1e18/52). Dev calls setYoloPriceUsd 10x seven times (0.0001 -> 1000 USD/YOLO): r = 1e17 < power -> stakeBps = 400. Doer submits, is accepted undisputed: _payReward credits the doer 90e6 + 4e6, treasury gets 0.5e6 and 0.5e6 goes to the dev-set buyback wallet instead of treasury 10e6. Expected under an honest price: stakeBps 0 (floor) or 7 bps, treasury 5e6 + 2.5e6.","severity":"low","snippet":"        return (uint16(power >= r ? MAX_STAKE_BONUS_BPS : MAX_STAKE_BONUS_BPS * power / r), true);","title":"Dev wallet trust surface beyond cancel: the hand-set YOLO price moves up to 4% of any accepted reward from the treasury fee to a friendly staked doer, and the 'burn' share of ETH/USDC/IMD goes to a wa"},{"citation":"resolved","description":"The view returns askedAt + 6 h + 1 h, while _swarmStatus (line 443-444) declares silence at once when the Intake's requests(rid) reports completed with no recorded answer; a frontend showing the view's time as 'nothing moves before' is wrong by up to 7 h. Text/view drift only; no money moves differently. Fix: have stepDeadline return block.timestamp (or min(now, lastDelivery)) when the Intake reports completed, or document the drift. Merged from two specialist reports.","line":329,"path":"contracts/src/desks/CaseDesk.sol","reproduction":"Ask the swarm at T; the Intake completes with status 2 at T+5 min; stepDeadline(subId) = T+7 h; a poke at T+6 min already moves the submission to NoDecision/Decided with a new window.","severity":"info","snippet":"        if (ph == Phase.Asked) return uint256(_bank().requests(s.request).askedAt) + p.swarmTimeout + SWARM_DELIVERY_GRACE;","title":"stepDeadline in Phase.Asked reports the end of the delivery grace even when a poke now would already declare silence (Intake reports the request completed)"},{"citation":"resolved","description":"CaseDesk._doerBond uses _pctBond(base, doerBondBps) where base is the author's own share only; VaultTypes.sol:97 says 'the $YOLO add-on are not part of any bond' and Bond has no YOLO field. The interface text a frontend relies on says otherwise. ITaskVault.sol:130 'Appealing or calling the swarm costs the same bond plus quoteSwarmFee' is wrong for the doer or the disputer calling for their own side (no bond, fee from the bond) and for IMD tasks (maxImdIn must also cover bond.rew). Fix: correct both comments. Items 9/11.","line":56,"path":"contracts/src/interfaces/ITaskVault.sol","reproduction":"Task with 100 USDC reward and a 10,000 YOLO add-on: vault.quoteSubmission(taskId) returns {imd 0.5e18, rew 3e6}; the comment promises an extra 3% of 10,000 YOLO. Expected per comment: a YOLO part; actual: none.","severity":"info","snippet":"    /// @notice The bond a new submission to `taskId` would need now: 0.5 IMD fixed + 3% R (and 3% of a $YOLO","title":"Doc drift: quoteSubmission and quoteDisputeBond comments promise a bond the code does not charge (3% of the $YOLO add-on; 'same bond plus fee' for a dispute side)"},{"citation":"resolved","description":"vote() reads only instantDelta/minWinnerWallets from the task's version; eligibility comes from the immutable `votePower` (line 35) whose nft and delegateRegistry are fixed at deployment. ParamsRegistry.sol:18-19 tells users 'a new version may name other counterparties (intake, nft, delegateRegistry, devPrices), which then judge every task created under it'. For nft/delegateRegistry that is false (good: the params owner cannot swap the electorate, but the documented trust surface is wider than reality and VaultParams validates two fields nothing reads). Fix: drop the two fields from the doc/validation or wire them. Items 1/7 (params owner). Reproduced by grep: no `.nft`/`delegateRegistry` read in contracts/src/desks.","line":185,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"Propose and activate a version 2 with nft = another ERC-721 and delegateRegistry = 0; create a task under v2; dispute; a holder of the new NFT calls vault.vote -> NotTokenHolder; a holder of the original NFT votes fine. Expected per the ParamsRegistry comment: the new NFT judges v2 tasks; actual: the original one does.","severity":"info","snippet":"            (,, uint32 instantDelta, uint32 minWallets) = ParamsRegistry(registry).voteParams(version);","title":"Params.nft / Params.delegateRegistry per version are dead: votes always use the immutable VotePower, contrary to the registry's trust note"},{"citation":"resolved","description":"_settleDecided refunds a winning caller only bond + unspent funds (lines 699-705) plus bondFixedImd off the top for a stranger appellant (641-645); a winning side (doer/disputer) is never reimbursed `spent` beyond its share of the split, and when the loser's IMD part was itself consumed by the loser's own free call the fee is simply gone. `a.spent` is written (line 325) and never read (grep). The struct comment and docs disagree with the code on what a swarm call costs a winner. Fix: correct the comment (or implement the reimbursement). Items 9/11.","line":245,"path":"contracts/src/VaultTypes.sol","reproduction":"USDC 100 task, NoDecision, disputer D calls callSwarm(sidesDoer=false) from bond (fromBond 0.5e18, spent 0.5e18), swarm answers false. claimDisputer pays D bond.imd - 0.5e18 + share of the doer penalty (0.5e18 fixed part + 3e6): D nets its fee back only through the loser's bond, not as a reimbursement; expected per comment: spent reimbursed from the loser.","severity":"info","snippet":"    uint256 spent; // paid to the Intake for his question (reimbursed from the loser if he wins)","title":"Appeal.spent comment promises reimbursement of a winning caller's fee; no code path reads it and only a stranger appellant gets 0.5 IMD back"},{"citation":"resolved","description":"TaskVault sets `ever` in createTask/topUp and exposes isContributor(); no desk calls it (grep: declared in DeskApi.IVault and TaskVault only), and _settleDecided/_splitCurrency explicitly pay a winning contributor like anyone else ('Every winner gets their share, also one who put money into the task'). The comment states the opposite rule. No behavioural effect. Fix: correct the comment or drop the field.","line":181,"path":"contracts/src/VaultTypes.sol","reproduction":"A contributor who disputes and wins receives the pro-rata share (DisputeDesk._splitCurrency lines 777-781), contrary to the comment; `.ever` is only written (TaskVault.sol:302, 322) and read by the unused isContributor view.","severity":"info","snippet":"    bool ever; // ever contributed to this task: as a winner they get only their own bond back","title":"Text drift: Contribution.ever / isContributor are written but never read by any desk; the field comment describes a rule that does not exist"},{"citation":"resolved","description":"R3 says the dispute bond is 1% of the author's own share. The code freezes `base` at submission; an author top-up during the check (allowed by R1, joins the reward at once) raises the share and the doer's payout but not the dispute/appeal bond (quoteDisputeBond reads s.base). Wording drift only; freezing is the safer behaviour (no bond can be moved under a disputer between quote and transaction).","line":111,"path":"contracts/src/desks/CaseDesk.sol","reproduction":"Author reward 100e6 USDC, doer submits (base 100e6). Author tops up 900e6 during the Review window. Dispute bond rew = 1e6 (1% of 100e6), while 1% of the author's share now is 10e6.","severity":"info","snippet":"        s.base = uint128(t.base); // a part of the pot","title":"R3 text drift: the dispute/appeal bond base is the author's share at submission time, not at dispute time"},{"citation":"resolved","description":"Item 9: there is a view for the resolution, but none for the amount a predictor will receive. A frontend has to rebuild the claim from markets, splits, positionsOf and paidIn, and the result shifts as other winners claim (mulDiv on poolLeft/pointsLeft, last claimant takes the dust). No loss, only a gap in what a user can learn from views before calling. Fix: add `claimable(taskId, who)` mirroring claim()'s arithmetic.","line":260,"path":"contracts/src/YoloPredict.sol","reproduction":"Task Completed, 10,000 YOLO pool. Calling claim through eth_call is the only way to learn the exact payout; no claimable(taskId, who) view exists (grep of YoloPredict's functions).","severity":"info","snippet":"    function resolution(uint256 taskId) external view returns (Resolution) {","title":"YoloPredict has no view that returns what a position can claim"},{"citation":"resolved","description":"getTask exposes queueHead/queueTail but not the queued submission ids; a frontend must rebuild the order from Submitted(queued = true) events (ids are increasing, so the order is derivable). RPC-only frontends can do this (item 10 holds); it is a convenience gap for item 9. Fix: add `queuedAt(taskId, index)` or `queueOf(taskId)`.","line":106,"path":"contracts/src/TaskVault.sol","reproduction":"Task with active sub 1 and queued subs 2, 3: getTask(t).queueHead = 0, queueTail = 2; no view returns [2, 3].","severity":"info","snippet":"    mapping(uint256 => mapping(uint256 => uint64)) internal _queue; // taskId => index => subId","title":"The submission queue order is not readable from views (no getter for _queue)"},{"citation":"resolved","description":"appeal() fixes sidesDoer = (decision == Reject). The disputer appealing a Reject stands for the doer, so _fundCall treats them as an outsider (full bond, full fee from the wallet: `sender == d.disputer && !sidesDoer` is false), but _settleDecided's appellantIsStranger excludes d.disputer from the 0.5 IMD off-the-top refund a winning stranger gets. Inconsistent role handling, no abuse path (they can only lose their own bond or win the normal split). Role-walk drift (item 1).","line":631,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"100 USDC task, dave disputes, 20 NFTs reject, dave appeals (sidesDoer = true, charged bond 0.5 IMD + 1 USDC + fee 0.5 IMD), askAppealSwarm, swarm answers accept: dave wins the appeal and gets appPart of the penalty but not the 0.5 IMD stranger fee a third party would get.","severity":"info","snippet":"        bool appellantIsStranger = hasCaller && a.appellant != f.doer && a.appellant != d.disputer;","title":"The one disputer appealing against its own winning Reject decision is charged a full stranger bond + fee but is excluded from the stranger fee refund"}],"hash":"f51fe8183052302a79aca1c72b34d614624c905268046a4ff01a60bcf29a9a71","nodeId":"238e41c7-715a-4f0f-abd3-37a600cd2ca7","outcome":"completed","summary":"The review is complete and the findings file is written. Nothing in the tracked tree was changed; only `contracts/test/scratch/` (not kept) was added.\n\n**Outcome.** 22 specialist findings were reduced to 15 after reproduction and merging. One medium, five low, nine info. No critical or high. Every path, line and snippet in `.imd-findings.json` was checked against the tree by script.\n\n**The one real logic finding (medium, with proof).** The void of an unasked appeal in `CaseDesk.step` anchors the reopened 24 h appeal window to the poke time instead of the void moment. Three specialists reported it under three severities. I kept it as medium: the losing side appeals for free, nobody is obliged to poke, and each of the three allowed voids extends the hold by the idle gap plus 24 h, so the pot and the prediction market stay locked past the documented 30 h. I rewrote the specialist's proof as a self-contained file importing only `src/` contracts and the mocks under `contracts/test/mocks/`. It fails 3 of 3 on this commit, passes with the one-line fix (applied temporarily, then restored with git), and passes `forge fmt --check`.\n\n**Low findings, each merged and reproduced.**\n- A dispute side decided against appeals at zero cost, and its free appeal zeroes the IMD penalty for the disputer and voters. Reproduced by a scratch test.\n- A swarm price rise between appeal and ask voids the prepaid appeal and burns one of the three calls, with no way to top up. Reproduced by a scratch test.\n- Silence from an Intake \"completed\" report is anchored to the poke time, bounded by the 7 h grace. Confirmed from code.\n- No view exposes the IMD a side already spent from its bond, so the next call's cost and the claim amount are unreadable from views. Three duplicates merged. Confirmed by grep.\n- Dev wallet trust surface: the hand-set YOLO price can move up to 4% of a reward from the treasury fee to a friendly staked doer, and the burn share of ETH, USDC and IMD goes to a wallet it names. Three duplicates merged, treasury-only loss.\n\n**Info findings.** stepDeadline overstating the Asked step, five comment or doc drifts, no claimable view in YoloPredict, no queue getter, and the disputer-appeals-own-Reject role inconsistency.\n\n**Not kept as separate items.** Every specialist finding reproduced in substance. No finding was dropped outright, but duplicates were collapsed, and severities were set from the brief's scale rather than the specialists' labels.\n\n**Checks run.** The proof under `forge test --match-path` fails on this code and passes with the fix. `forge fmt --check` on the proof passes. The full project compiled as part of those runs. I did not run the full project test suite given the time budget.","treeHash":null,"usage":{"cachedInputTokens":2387444,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":30880,"runtime":"claude","turns":29,"wallClockMs":568373}}],"verification":[]}