{"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":"0c5450ce-5174-4618-b72b-30fa84631fdc","kind":"audit","nodes":[{"acceptedSubmissionHash":"c1e73852281e234ce57e5190bedeccfc0e7928a937d8e73b381d51cadb717fd9","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":"aa33a0c72784830b0583803f351df6d8eb4298e90e5f474667d3bd2034090cf4","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":"66718e0a66d7d3642d783f9d64406212a90a744258617c12a44dcdcbb9db8bc8","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":"888bbe64ca50de9c72676652ffec4931c66447253ccaad7701785426f9abb9d0","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":"1e5bbc0df51d413c116ed6356d596e3d3172d5eed52752c255491c616ccdcfa1","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 every payout and the token economics of moneycanbuy.fun. Repository at commit d9c24e3b2d199548faf236d7bb5e80e4a12b6ada. Run forge at the repo root, import as \"src/...\". Modify no existing file; new files only under contracts/test/audit/.\n\nSOURCE OF TRUTH. The RULES below are the money rules; docs/MECHANICS.md, YOLO.md and TOKENOMICS.md restate them. Code or docs off the RULES = a finding. Do not re-report what contracts/test/audit/ proves fixed.\n\nSCOPE. Amounts: who gets how much in every outcome, and who can game it. Code security and role scenarios are other orders; one info line each.\n\nRULES. R = the reward in its currency (ETH, USDC, IMD or YOLO). A = the author's own share of it (with own top-ups; no others' top-ups, no YOLO add-on). Payouts only from money the paying contract holds; token funds pay nothing. A user's tokens leave their wallet only in their own call (from = caller); anyone else, owners and deployer too, spending a user's allowance = critical.\n- Fee 10% of R, only on acceptance. Of it 5% to the right NFT votes if the NFT decided right, else \"unearned NFT 5%\"; the other 5% = stakers' part.\n- No stake: doer 90%, treasury 10% (5% if the NFT 5% was earned).\n- Staked doer: b = 4% x min(1, power / A in YOLO at the price parameter), floored to whole bps, no threshold. Doer 90% + b; (5% - b) half burn, half treasury; unearned NFT 5% burn. $100, no dispute, doer/burn/treasury: full power 94/5.50/0.50; half 92/6.50/1.50; 5% of A 90.20/7.40/2.40. b is paid on all R and the add-on.\n- Power = amount x weeks / 52 (1-52 weeks), fixed at the lock, never falling; add or extend: max(old + added x weeks left / 52, total x weeks left / 52). Read before the submission block; the lock must still run at payout or b = 0.\n- Burn: YOLO to the BuybackBurner, anyone's burnYolo sends it to 0x...dEaD; ETH, USDC, IMD straight to the buyback wallet at payout (a failed send waits in the burner for forward). Treasury YOLO income also goes to the burner.\n- Doer bond 0.5 IMD + 3% of A in R's currency; dispute bond 0.5 IMD + 1% of A. Never doubled; each currency splits alone.\n- Loser's bond: NFT decided and the outcome matches: 50% winning side (pro rata to bonds), 50% right NFT votes; NFT wrong or undecided: 100% winning side. The winner's own bond comes back.\n- NFT undecided and no swarm call: accepted, the disputer's bond 100% to the doer.\n- Rejected: R stays in the task, open for the next submission.\n- Appeal: a dispute party pays the 0.5 IMD fee from its bond (again: from its wallet); an outsider posts a dispute bond + fee and, winning, first gets 0.5 IMD off the loser's bond. Max 3 swarm calls per submission; a silent call's fee is gone; a failed ask or an appeal not asked within 6 h counts, bond and fee back; after the third, outcome as without appeal.\n- Top-ups and YOLO add-ons are locked to the deadline. Nobody accepted by the deadline: each funder takes back their own part. A top-up or add-on during review joins R; none into a resolved task. Submitted before the deadline, accepted after = done.\n- Cancel only by the dev wallet (the DevPrices owner): all back to who put it in, predictions 100%; swarm fees paid stay gone; the dev wallet gets nothing.\n- YoloPredict, YOLO only, irrevocable, until the deadline, paused during review. Entry 100: 5 burn, 5 deferred, 90 pool. Points = 90 x ((deadline - now)/(deadline - creation))^2. Winners: their 90 back + the losing pool by points. Deferred: done -> 2.5% author, 2.5% doer; else burn. Done with nobody on WILL DO: the losing pool half author, half doer; not done with nobody on WON'T DO: all to author. Cancel = 100% back, no burn.\n- Trophy: 7,777 YOLO, burned.\n- Token: 1,000,000,000 YOLO, split per docs/TOKENOMICS.md. Buy tax 30% -> 0 linear in minutes 30-60 after openAt; minutes 0-30 only snapshot wallets buy, 500,000 YOLO per NFT, untaxed. Sell tax 30% in minutes 0-30, 30% -> 0 in 30-60, on top (sell 1,000: 700 to the pool, 300 tax). Taxes in YOLO to the treasury wallet.\n\nPrices (no network; 19.10.2026 10:13 UTC): IMD $5.42, ETH $2,434, YOLO price parameter $0.001, swarm call 0.5 IMD = $2.71, gas 0.045 gwei; in code every price is a dev-wallet parameter, USDC = $1.\n\nREPORT FORMAT, MANDATORY. One entry per item, same order, even \"no issue\"; a missing item = INCOMPLETE. Entry 0: VERDICT, max 8 lines: payouts vs RULES, every money blocker, top 3 changes in numbers. Items 1-15: what you checked, file:line, severity (critical = theft, permanent loss or freeze; high = a RULES amount broken; medium = bounded loss or profitable gaming; low = dust, edge case; info = drift), loss in tokens and $ at a stated R, smallest fix, a Foundry PoC (full source, contracts/test/audit/, exact amounts, fails now, passes once fixed, no fork, no ffi) if real, else one line why not.\n\n1. ACCEPTED, NO DISPUTE. DisputeDesk._payReward, TaskVault burn/addTreasury: the three $100 examples in USDC, ETH, IMD, YOLO; b = 0.\n2. DISPUTE, NFT RIGHT. Accepted and rejected: reward split, loser's bond 50/50, winner's bond back, voter shares exact.\n3. NFT WRONG OR UNDECIDED. Swarm overturns or decides; undecided, no call: unearned NFT 5%, 100% of the loser's bond.\n4. REJECTED. R after a fake, the next submission's bond and payout, claimVoidSubmission.\n5. APPEAL MONEY. _penaltyAndWinnerBonds, _splitCurrency, _sideSpent, _charge: party vs outsider, second call, three silences, failed ask. Each party's net in IMD and R per case.\n6. BONDS. Base A, rounding at the minimum rewards (5 USDC, 0.002 ETH, 1 IMD, 1,000 YOLO), who keeps dust.\n7. STAKE BONUS. CaseDesk._stakeBps, YoloLockHub, _lockRuns: base A not R, floor to bps, add/extend after submission, lock ended before payout.\n8. YOLO ADD-ON. YoloSweetener: same split as R, voters only after a right NFT decision, refunds, add-on during review.\n9. EXPIRY AND CANCEL. withdrawContribution, cancelTask amounts: each funder exactly their part.\n10. YOLOPREDICT. settle, claim, _split, _points: claims + burned + deferred = paid in, empty sides, a winning side with zero points, void.\n11. BURNS. Every burn path to its end; YOLO reaching a wallet instead; monthly burn vs unlock at 200 tasks/month, R $100.\n12. TOKENOMICS VS CODE. token/, LaunchPlan: split = 1e27, dust, unlock table, both taxes at minutes 0, 15, 30, 45, 60, tax recipient.\n13. INCENTIVES. Net $ per action at each minimum reward and at R $100: disputing a fake vs its gas, an NFT vote vs its gas, an outsider appeal, stake recycling via own top-ups, griefing cost, bond dust. Collusion: other order.\n14. DOCS VS CODE. Every amount in the three docs, one line each.\n15. COVERAGE. Files read in full, what was not reached, forge build and test result.\n\nNot findings: test/mocks; placeholder wallets; legal; brand/, landing/; Intake, Uniswap, IMD factory; the name and size of the \"Stabilizer / MM fund\". By design unless worse than stated: self-dispute from a second wallet; one late position taking the other side; many wallets in predictions; losers get nothing when the winning side is empty; an author profiting from a task nobody does; junk submissions next to a big prediction pool; one lock for all submissions; a stale price moving only the stake bonus; no task limits; ERC-6909 buys before openAt; untaxed v2/v3 pools; fees of silent swarm calls kept.\n","parentJobId":null,"planHash":"ea15ae04abf5ad60f5c4d83df473a84d16844fafe63c61918c24adb0f41ef682","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"0c5450ce-5174-4618-b72b-30fa84631fdc","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":"52182","feedbackHash":"8268ab9d1b4b2be545de089d7a1af84e35d05eb251680b3f53c4f5dafbf36860","nodeKey":"audit_economics","submissionHash":"c1e73852281e234ce57e5190bedeccfc0e7928a937d8e73b381d51cadb717fd9","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52680","feedbackHash":"3eb607ddb48ff065289a14d81b7abd2364bdb3d2d4f1a7e7983be2b059da9baa","nodeKey":"audit_flow","submissionHash":"aa33a0c72784830b0583803f351df6d8eb4298e90e5f474667d3bd2034090cf4","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52190","feedbackHash":"106f4b7f6bcc09fce3fcf20665ab0648d1faf54a7d577502f97573e940cd7b29","nodeKey":"audit_judge","submissionHash":"66718e0a66d7d3642d783f9d64406212a90a744258617c12a44dcdcbb9db8bc8","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51558","feedbackHash":"b011f37ab20c97dcc8d6de83bd29541c20cb4c0b903d86f15456b77b3c46f7aa","nodeKey":"audit_math","submissionHash":"888bbe64ca50de9c72676652ffec4931c66447253ccaad7701785426f9abb9d0","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51882","feedbackHash":"4db3f23ead78654679eeb556503b54dc0c4ade27a56415bb9f2638dea2f5660d","nodeKey":"audit_permissions","submissionHash":"1e5bbc0df51d413c116ed6356d596e3d3172d5eed52752c255491c616ccdcfa1","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"0b3438e488be7ecc146a3b58df9eafd05ea73c6feb1643b43d948818f83ae407","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"5c1e4764d4e20ef5","findings":[{"citation":"resolved","description":"VERDICT (8 lines). 1) Every payout amount traced in DisputeDesk._payReward/_settleDecided/_splitCurrency, CaseDesk._stakeBps, YoloSweetener.payout, YoloPredict.settle/claim, TaskVault.withdrawContribution/cancelTask, YoloToken taxes, YoloSplitter/Vestings matches the RULES at the launch parameters (VaultParams.LaunchParams.initial): the three $100 examples give doer/burn/treasury 94/5.50/0.50, 92/6.50/1.50, 90.20/7.40/2.40 and 90/0/10 in every currency. 2) No critical: every transferFrom uses the caller (vault _sender(), sweetener/predict/hub/trophy msg.sender); nothing is paid from token funds; no payout path can revert permanently (failed pushes become credits). 3) No high: no RULES amount is broken at the launch parameters. 4) Low: DisputeDesk._splitCurrency line 765 sends the winning side's 100% share to the treasury when the winning side has no bond in that currency; unreachable with the launch bonds but reachable if the registry sets doerBondBps = 0 (ParamsBounds allows 0..2000), then a disputer's 1% rew penalty goes to the treasury instead of the doer. 5) Info drifts: YoloLockHub.stakePower accepts a lock that lapsed and was re-locked in the same epoch (tokens never left, b still paid); dust (1 wei) of a 50/50 bond split goes to the voters' half, odd unit of (5%-b) to the treasury, both documented. 6) Top 3 changes in numbers: (a) make _splitCurrency credit the doer directly when doerWins and winnerBonds == 0 (0 tokens lost today, 1% of A per disputed-and-accepted task if doerBondBps is ever set to 0); (b) in YoloLockHub.stakePower also require that the lock never lapsed since `since` (b up to 4% of R, $4 at R=$100, paid on a lock that ended and was renewed); (c) docs: none off. 7) Code security and role scenarios: one info line each in items 15. 8) Build: forge build succeeded on the second attempt (the first was stopped at a 10-minute limit; the via-IR compile takes about 20 minutes on this shared machine); forge test --match-path contracts/test/MoneyRules.t.sol: 43 passed, 0 failed, including test_WorkedExamples and test_YoloReward_TreasuryShareBurned. No new test was added: no high or critical finding needs a PoC.","line":711,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"No failing input for the verdict itself; see the items below.","severity":"info","snippet":"    function _payReward(uint256 subId, Facts calldata f, bool nftRight, ParamsCore memory p) private {","title":"0. VERDICT: payouts match the RULES; no money blocker found; one low param-dependent drift"},{"citation":"resolved","description":"Checked DisputeDesk._payReward (lines 711-733) with TaskVault.burn/addTreasury (lines 576-585, 665-679). fee = r*1000/1e4, nftPart = r*500/1e4, toVoters = 0 without a right NFT decision. No stake: toTreasury = fee (10%), doer r - fee (90%); for YOLO addTreasury routes to the burner (line 577), so the whole 10% burns, as RULES say. Stake with bps b: stakers = 5%, bonus = r*b/1e4 capped at stakers, half = (stakers-bonus)/2 burned plus the unearned nftPart, treasury = stakers - bonus - half (odd unit to the treasury). R = $100 in USDC (100e6), ETH (100e18/2434), IMD (100e18/5.42), YOLO (100_000e18): b=400 -> doer 94, burn 0.5+5, treasury 0.5; b=200 -> 92 / 6.5 / 1.5; b=20 -> 90.20 / 7.40 / 2.40. The sum bonus+treasury+burn+toVoters = fee exactly, no dust. Burn in ETH/USDC/IMD goes straight to the buyback wallet (TaskVault._burn 670-676) and only to the BuybackBurner when that send fails. Existing tests MoneyRules.t.sol test_WorkedExamples and YoloLockHub.t.sol cover the numbers. Loss: none. No PoC: no defect.","line":732,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"Input: task 100e6 USDC, undisputed, stakeBps 0: doer credited 90e6, treasuryPot[usdc] += 10e6; stakeBps 400 and a running lock: doer 94e6, buyback wallet 5.5e6, treasuryPot 0.5e6. Expected == actual.","severity":"info","snippet":"        bank.credit(f.currency, f.doer, r - fee + bonus);","title":"1. ACCEPTED, NO DISPUTE: 90/10 and staked 94/5.50/0.50, 92/6.50/1.50, 90.20/7.40/2.40 verified"},{"citation":"resolved","description":"Checked DisputeDesk.settle (494-510), _settleDecided (611-701), _splitCurrency (746-777), _claimVoter (412-442), claimDisputer (331-362). Accepted after NFT Accept: _payReward with nftRight gives voters 5% of R (voterFee), treasury 5% (or with a stake: bonus, half/half of 5%-b, nothing of the NFT part burned). Penalty = disputer bond (0.5 IMD, 1% of A) minus the disputer's own swarm spend; toVoters = penalty - floor(penalty*5000/1e4) (the 1-wei dust goes to the voters' half), the other 50% to the doer directly; the doer's own bond (0.5 IMD + 3% A, minus its own swarm spend) is credited back (691-692). Rejected after NFT Reject: penalty = doer bond; 50% to voters, 50% bucketed pro rata for disputers (one disputer: all) via claimDisputer, which also returns the disputer's own bond. Voter shares: voterBond.imd * n / left with left = votes on the final side, remaining/remaining, so the last claimant takes the exact rest; payTo pools are honoured. At R = $100 USDC: voters get 5 USDC + 0.25 IMD + 0.5 USDC (accepted) or 0.25 IMD + 1.5 USDC (rejected). Loss: none. No PoC: no defect.","line":760,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"Input: 100e6 USDC task, doer bond (0.5e18 IMD, 3e6), dispute bond (0.5e18 IMD, 1e6), 10 accept votes, final Accepted: voterFee 5e6, voterBond (0.25e18, 0.5e6), doer credit 0.25e18 IMD + 0.5e6 + bond back (0.5e18, 3e6) + 90e6 - stake rule. Expected == actual (DisputeDesk 644-676, 690-693).","severity":"info","snippet":"        toVoters = nftRight ? penalty - penalty * winnerBondBps / BPS : 0;","title":"2. DISPUTE, NFT RIGHT: 5% of R to right votes, loser's bond 50/50, winner's bond back, voter shares remaining/remaining"},{"citation":"resolved","description":"Checked CaseDesk._endUncalled (358-361: undecided and no call -> Accepted), CaseDesk.step silent/third-call paths (214-231), DisputeDesk.settle nftRight = decision != None && decision agrees (505), _payReward with nftRight false (the unearned 5% to the treasury without a stake, burned with one), _splitCurrency with nftRight false (toVoters 0, winnerShare = whole penalty). Swarm overturn (NFT Accept, swarm Reject): penalty = doer bond (+ a losing caller's bond), 100% to disputers plus a winning stranger's 0.5 IMD first. Undecided, nobody called: the disputer's whole bond (0.5 IMD + 1% A) to the doer. All match RULES. Drift: line 765 sends winnerShare to the treasury when winnerBonds == 0. With the launch parameters winnerBonds is never 0 (the fixed IMD part is 0.5 and 3% / 1% of any reward >= its minimum is > 0), but ParamsBounds.validate (VaultParams.sol 180) allows doerBondBps = 0; then a doer who wins against a disputer with the NFT undecided gets 0 of the disputer's 1% rew penalty, the treasury gets it. Loss at R = $100 USDC with that parameter: 1 USDC per such case to the treasury instead of the doer. Smallest fix: when doerWins credit `doer` with winnerShare directly whenever winnerBonds == 0 (the doer is a known single winner); keep the treasury fallback only for the disputers' side. No PoC attached: not reachable at the launch parameters (a 48h-timelocked parameter change by the registry owner is a precondition), severity low.","line":765,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"State: registry version with doerBondBps = 0 (allowed by ParamsBounds), task 100e6 USDC, submission, dispute (bond 0.5e18 IMD + 1e6 USDC), no NFT decision, nobody calls the swarm, appeal window passes. Expected (RULES 'NFT wrong or undecided: 100% winning side'): doer credited 1e6 USDC and 0.5e18 IMD. Actual: _splitCurrency(rew) sees winnerBonds = doerBond.rew (0) + 0 == 0 and books 1e6 USDC into treasuryPot[usdc]; the doer gets only the IMD half.","severity":"low","snippet":"        if (winnerBonds == 0) return (toVoters, winnerShare, 0, 0);","title":"3. NFT WRONG OR UNDECIDED: 100% to the winning side holds, except the winning side's share goes to the treasury when it has no bond in that currency (reachable only with doerBondBps = 0)"},{"citation":"resolved","description":"Checked TaskVault.closeSubmission (430-448): on Rejected the pot is untouched, activeSub moves to the queue head (activated by CaseDesk._finalize 475-476), on Accepted pot -= reward where reward = potOf (the whole pot incl. top-ups during review, CaseDesk 461). The next submission's bond was fixed at its own submit (0.5 IMD + 3% of the author's base then; no escalation, Desk._doerBond) and its payout reads the pot at its finalize. CaseDesk.claimVoidSubmission (140-155) pays a Queued submission's bond back only when the task is Completed or Unwound; a queued submission can never sit on an Expired task because the queue is non-empty only while a submission is active and a rejection activates the next one before expiry can run (TaskVault._advance 640-641). Loss: none. No PoC: no defect.","line":442,"path":"contracts/src/TaskVault.sol","reproduction":"Input: 100e6 USDC task, sub A rejected, sub B queued then activated and accepted: B's doer gets 90e6 of the untouched pot; A's doer bond went to the penalty; a sub C queued behind B claims its (0.5e18 IMD, 3e6) back after Completed. Expected == actual.","severity":"info","snippet":"            ++t.rejectedCount;","title":"4. REJECTED: R stays in the pot, next submission's bond and payout unchanged, claimVoidSubmission returns queued bonds"},{"citation":"resolved","description":"Checked _fundCall (278-308), _charge (311-321), _refundCaller (574-578), dropCaller (487-490), _penaltyAndWinnerBonds (584-601), _settleNeutral (562-571), CaseDesk._askAppeal (403-422), step Appealed timeout (194-213) and Asked silence (214-231), MAX_SWARM_CALLS = 3 counted in _ask, void appeal and timeout. Party (doer for accept, disputer for reject): no second bond, min(fee, bond IMD) from the bond once (SideSpent), the rest and every later call from the wallet; a void call spends nothing so the next is still the first. Outsider: dispute bond + whole fee from the wallet; winning: 0.5 IMD off the loser's IMD penalty first, then a pro-rata share by bond and own bond back. Nets at R = $100 USDC, fee 0.5 IMD: doer appeals a rejection and wins: -0.5 IMD (from bond) +100% or 50% of the disputer's (0.5 IMD, 1 USDC); loses: bond gone (penalty already net of the 0.5 spent); outsider wins against the doer: +0.5 IMD, +share of 3 USDC pro rata with the disputer (1:1 by bond -> 1.5 USDC), bond back; outsider loses: -0.5 IMD fee, -(0.5 IMD + 1 USDC) bond. Three silences: each caller's fee gone, bond back (dropCaller), after the third the outcome is as uncalled. Failed ask (callSwarm) reverts whole; a void lazy appeal or one not asked within 6h refunds bond and fee and counts. Cancel: bonds back minus the side's spent fee. Loss: none. No PoC: no defect.","line":297,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"Input: NFT Reject, doer appeals (fee 0.5e18 taken from its 0.5e18 bond IMD, SideSpent.doer = 0.5e18), swarm Accept: doer bond refund = 0.5e18 - 0.5e18 = 0 IMD + 3e6 USDC, penalty from the disputer 100% to the doer. Expected == actual (DisputeDesk 632, 691).","severity":"info","snippet":"                fromBond = fee < bondImd ? fee : bondImd;","title":"5. APPEAL MONEY: party pays its first call from its bond, later calls and outsiders from the wallet; silence keeps the fee; void/failed asks refund"},{"citation":"resolved","description":"Checked Desk._pctBond/_doerBond/_disputeBond (118-137), TaskVault.submitTerms base (409: the author's own active contribution, own top-ups included, others' and the add-on excluded), CaseDesk.submit (113-118). At the minimums: 5 USDC -> 150000 / 50000 units; 0.002 ETH -> 6e13 / 2e13 wei; 1 IMD -> 0.03 / 0.01 IMD; 1000 YOLO -> 30 / 10 YOLO; none rounds to 0, so _splitCurrency's winnerBonds == 0 treasury branch is unreachable at launch parameters (see item 3 for the parameter case). Dust: a 50/50 split gives the odd wei to the voters (penalty - floor), the pro-rata shares are remaining/remaining so the last claimant keeps the rest; nothing is stuck. Loss: dust only. No PoC: no defect.","line":120,"path":"contracts/src/desks/Desk.sol","reproduction":"Input: 5e6 USDC task: quoteSubmission -> (0.5e18, 150000); quoteDisputeBond -> (0.5e18, 50000). Expected == actual.","severity":"info","snippet":"        b = amount * bps / BPS;","title":"6. BONDS: base = author's active share, floor division, no zero bond at any minimum, dust to the voters' half or the treasury"},{"citation":"resolved","description":"Checked CaseDesk._stakeBps (502-515): powerAt(doer, block.timestamp) is the point strictly before now (YoloLockHub._pointBefore), r = DevPrices.yoloValue(currency, t.base) (A only), bps = floor(400*power/r) capped at 400, 0 without power or price; the bonus is then paid on the whole R and the add-on (DisputeDesk 725, YoloSweetener 148). Add/extend after submission: stakePower returns the point before `since`, so a later add never raises b; a withdraw bumps the epoch and voids it; a lock that ended before payout returns 0 (YoloLockHub 111). YoloLockHub.lock power = max(old + add*left/52, amount*left/52) with left rounded up, never above amount, never falling while running. Drift (info): a lock that ended between submission and payout and was set again without withdrawing keeps its epoch, so stakePower returns the old power and b is paid although the lock did not run continuously; the tokens never left the hub and the doer had to re-lock for at least one more week, so no theft. Loss: at most b of R moves from burn/treasury to the doer (<= $4 at R = $100). Smallest fix if wanted: in stakePower also require that no point after `since` has power 0 / end <= its at. No PoC: within the stated rule's intent.","line":514,"path":"contracts/src/desks/CaseDesk.sol","reproduction":"Input: lock 100000e18 YOLO for 1 week (power 1923e18), submit on day 6 against A = 100e6 USDC (r = 100000e18 YOLO at $0.001): bps = floor(400*1923/100000) = 7. Dispute, votes, windows push the payout to day 8; lock ended day 7; doer calls lock(0, 1) on day 7.5: stakePower != 0 -> 7 bps of R paid (0.07 USDC). Expected by a strict reading: 0.","severity":"info","snippet":"        return uint16(power >= r ? MAX_STAKE_BONUS_BPS : MAX_STAKE_BONUS_BPS * power / r);","title":"7. STAKE BONUS: base A not R, floor to bps, power read strictly before the block, lock must run at payout; a lapsed-and-renewed lock still counts"},{"citation":"resolved","description":"Checked YoloSweetener.sweeten (92-104: Open, before the deadline, after poke, so an add-on during review joins and none enters a resolved task), withdraw (108-121: Expired/Unwound only, each their own givenBy), payout (125-156), claimVoter (160-172). payout: fee 10%, nftPart 5% to the accept votes only when the accepted submission's nftDecision is Accept (i.e. right), stake extra = bonus*Settlement.stakeBps/1e4 capped at 5% (the same bps the vault paid, 0 when the lock did not run), everything else burned: that includes the treasury's half of (5%-b), which for YOLO goes to the burner by RULES ('Treasury YOLO income also goes to the burner'). Doer gets bonus - fee + extra. Voter pool remaining/remaining via mulDiv. Loss: none. No PoC: no defect.","line":151,"path":"contracts/src/YoloSweetener.sol","reproduction":"Input: add-on 100000e18 YOLO, accepted after NFT Accept, stakeBps 400: doer 94000e18, voters 5000e18, burner 1000e18 (0.5% burn + 0.5% treasury half). Expected == actual.","severity":"info","snippet":"        uint256 burned = fee - toVoters - extra;","title":"8. YOLO ADD-ON: same split as R, voters only after a right NFT Accept, refunds on expiry/unwind, add-on during review joins"},{"citation":"resolved","description":"Checked TaskVault.withdrawContribution (334-346): nothing while Open (before or after the deadline), 0 after Completed (the pot went to the doer), c.active after Expired/Unwound, pot -= amount, paid or credited. cancelTask (364-375): only the DevPrices owner, only while Open; unwind -> DisputeDesk._settleNeutral (doer bond minus its spent fee, appellant bond + unspent fee), disputers via claimDisputer (neutral: bond minus spent), queued via claimVoidSubmission, add-on via YoloSweetener.withdraw, predictions 100% via YoloPredict.claim (Void: paidIn incl. the held burn and deferred); the dev wallet receives nothing; swarm fees already paid stay gone. Loss: none. No PoC: no defect.","line":340,"path":"contracts/src/TaskVault.sol","reproduction":"Input: author 100e6 + contributor 50e6 USDC, expired: author withdraws 100e6, contributor 50e6, pot 0. Cancel during a dispute with a 0.5e18 IMD swarm call by the doer: doer credited 0 IMD + 3e6, disputer claims (0.5e18, 1e6). Expected == actual.","severity":"info","snippet":"        uint256 amount = st == TaskStatus.Completed ? 0 : c.active;","title":"9. EXPIRY AND CANCEL: each funder takes back exactly their active share; cancel returns every bond, prediction and add-on"},{"citation":"resolved","description":"Checked predict (134-169: Open, before the deadline, activeSub == 0 so paused during review; 5% burnHeld, 5% deferred, 90% pool; points = eff*(remaining/duration)^2 via two mulDiv), settle (177-222: burn to 0xdEaD unless Void; deferred half/half author-doer when Completed, burned otherwise, kept inside paidIn when Void; NoWinner: WILL DO won with no points -> losing pool half author half doer, WON'T DO won with no points -> all to the author), claim (226-257: winners eff + poolLeft*points/pointsLeft remaining/remaining, losers 0, Void paidIn). Sums: every wei is in exactly one of burnHeld, deferred, a pool or paidIn, and each leaves once. A winner whose points round to 0 (needs duration^2 > eff, i.e. a task longer than ~100 years at the 1 YOLO floor) gets its eff back and no share. Loss: none. No PoC: no defect.","line":147,"path":"contracts/src/YoloPredict.sol","reproduction":"Input: A 100e18 WILL DO at creation (points 90e18), B 100e18 WON'T DO at half time (points 22.5e18), task Completed: burn 10e18, deferred 5e18 -> 2.5e18 author + 2.5e18 doer, A claims 90e18 + 90e18 = 180e18, B 0; contract ends at 0. Expected == actual.","severity":"info","snippet":"        uint256 effPosition = amount - burned - deferred;","title":"10. YOLOPREDICT: claims + burned + deferred = paid in; empty sides, zero-point winners and void handled"},{"citation":"resolved","description":"Checked TaskVault._burn (665-679) and addTreasury (576-579): YOLO -> BuybackBurner (pay or credit, pulled by BuybackBurner.pull) -> burnYolo to 0xdEaD; ETH/USDC/IMD -> buybackWallet with 30k gas, fallback to the burner's forward. YoloSweetener burned -> vault.burner. YoloPredict -> DEAD directly. DareTrophy PRICE 7777e18 -> DEAD directly. No YOLO burn path reaches a wallet (the buyback wallet receives only ETH/USDC/IMD, as RULES say; the first-hour tax is a wallet by design). Burn vs unlock, 200 tasks/month at R = $100: if every task were a YOLO reward with no stake and no dispute: 10% x 100000 YOLO x 200 = 2,000,000 YOLO/month ($2,000); with staked doers 5.5-7.5% -> 1.1-1.5M; a USDC task burns no YOLO at all (its burn share is USDC to the buyback wallet). Month-1 unlock from the vestings: incentives 187.5M/24 + treasury 148.148M/12 + dev 77.78M/12 + team 50M/12 = 30.8M YOLO/month, 15x the maximal task burn. Loss: none. No PoC: no defect.","line":667,"path":"contracts/src/TaskVault.sol","reproduction":"Input: YOLO task 100000e18 accepted undisputed, no stake: burner balance += 10000e18, burnYolo moves it to 0xdEaD. Expected == actual.","severity":"info","snippet":"        if (token == address(yolo)) {","title":"11. BURNS: every path ends at 0xdEaD (YOLO) or the buyback wallet (ETH/USDC/IMD); monthly burn far below the unlock"},{"citation":"resolved","description":"Checked YoloSplitter (250M + 150M + 77,777,777 + 50M + rest 222,222,223 = 750M; + factory pool 15% + swarm 10% = 1e27; dust to the treasury vesting), Vestings (incentives 62.5M at once + 730 d linear; treasury 74,074,074 at once + 365 d; dev, team 365 d), ProjectVesting.vestedAmount (free = min(total, unlocked), linear on the rest from the split's clock). Unlock: day 0 = 150 + 100 + 150 + 62.5 + 74.07 = 536.6M (54%), month 6 ~721.4M, month 12 ~906.3M, month 24 1e9: matches TOKENOMICS.md. Taxes: buy 0% in 0-30 (whitelist 500,000e18 per NFT via NftGate.consume), 30% at minute 30, 15% at 45, 0 at 60 (line 151); sell 30% of the total the seller parts with in 0-30 (value*3/7 on top: 700 in, 300 tax), 15% at 45, 0 at 60; all tax to taxRecipient (immutable, the treasury wallet); the launcher and the PoolManager are exempt. LaunchPlan: YOLO_POOL_BPS 1500, supply 1e27, YOLO_PRICE_USD 0.001e18, trophy 7777e18. Loss: none. No PoC: no defect.","line":119,"path":"contracts/src/token/YoloToken.sol","reproduction":"Input: at openAt + 45 min a buy of 1000e18 from the PoolManager: tax 150e18 to taxRecipient, buyer 850e18; a sell putting 700e18 into the pool at minute 10: seller parts with 1000e18, 300e18 to taxRecipient. Expected == actual.","severity":"info","snippet":"        return value * num / (10_000 * (TAX_END_SECONDS - WHITELIST_SECONDS) - num);","title":"12. TOKENOMICS VS CODE: split sums to 1e27, unlock table matches, taxes at minutes 0/15/30/45/60 match, tax recipient fixed"},{"citation":"resolved","description":"Prices: IMD $5.42, ETH $2,434, YOLO $0.001, gas 0.045 gwei (a 150k-gas tx = $0.016). R = $100 USDC: doer bond $2.71 + $3 = $5.71; dispute bond $2.71 + $1 = $3.71. Disputing a fake: NFT right -> bond back + $2.86 (50% of the doer bond) = +$2.85 net of gas; NFT undecided and the disputer calls the swarm from its bond (0.5 IMD) and wins -> +$5.71 - $2.71 = +$3.00. One NFT vote: share of $5 (fee) + $2.86 (bond half) over >= 10 votes -> >= $0.79 per vote vs $0.016 gas. Outsider appeal: pays $6.42, wins +0.5 IMD + ~$1.50 = +$4.21 gross, +$1.50 net of the fee; loses -$6.42. At the minimum 5 USDC (0.002 ETH, 1 IMD, 1000 YOLO are the same $5 scale): doer bond $2.86, dispute bond $2.76; disputer wins +$1.43 (NFT right) or +$2.86 - $2.71 = +$0.15 (own swarm call); a vote earns >= ($0.25 + $1.43)/10 = $0.17 > gas. Stake recycling via own top-ups: an author-doer pays 100% of A and gets back 90% + b <= 94%, a loss of >= 6%; top-ups during review raise the fee base, not the bond base. Griefing a legit doer: a dispute costs $3.71 and delays by 24-48 h; the griefer loses the bond when the NFT or the swarm sides with the doer. Bond dust: 1% of the minimum rewards is $0.05, never 0. Collusion: other order. No defect. No PoC.","line":136,"path":"contracts/src/desks/Desk.sol","reproduction":"Numbers above are derived from LaunchParams.initial (bondFixedImd 0.5e18, doerBondBps 300, disputeBondBps 100, feeBps 1000, nftFeeBps 500, winnerBondBps 5000, endDelta 10) and the stated prices.","severity":"info","snippet":"        b.rew = _pctBond(base, p.disputeBondBps);","title":"13. INCENTIVES: disputing a fake, voting and an outsider appeal stay positive at R $100 and at every minimum; griefing costs the griefer its bond"},{"citation":"resolved","description":"One line each. MECHANICS.md: minimums 5 USDC / 0.002 ETH / 1 IMD / 1000 YOLO = LaunchParams; doer bond 0.5 IMD + 3% A, dispute 0.5 IMD + 1% A = Desk; 10% fee, 5% NFT, treasury rest = _payReward; staked split line 190 = code incl. the odd unit; 50/50 and 100% bond splits (195-196) = _splitCurrency; 0.5 IMD stranger refund (150, 194) = line 636-640; examples 201-205 = item 1; 300 USDC example 209 (9 / 3 USDC) = 3% / 1%; first hour (231) = YoloToken; shares (232) = YoloSplitter/Vestings; stake bps rounded down (256) = _stakeBps. YOLO.md: fee table 146, sweetener table 175, predict table 225-232 (5/5/90, w squared, half/half, all-to-author, void), lock table 242-248 (power formula, examples), burner 256-259, first hour 112-114, distribution 84-90: all equal the code. TOKENOMICS.md: split table (sums to 1e9), unlock table (54/72/91/100%), burn sources (5.5% to almost 7.5%, 0.5% to almost 2.5%), YOLO income 10%-b / 5%-b, predict 5+5, trophy 7,777, first hour: all equal the code. No drift found. No PoC.","line":190,"path":"docs/MECHANICS.md","reproduction":"Each doc amount compared with the RULES and the constant or formula in code; no mismatch to reproduce.","severity":"info","snippet":"| ...the rest, staked doer | of the stakers' 5%: the share fixed at the submission, b = 4% × min(1, power / the author's reward in $YOLO) of R, to the doer (at most the 5%); what is left (5% − b) half to the burn, half to the treasury (an odd unit to the treasury) | `desks/DisputeDesk.sol`; `VaultTypes.sol` |","title":"14. DOCS VS CODE: every amount in MECHANICS.md, YOLO.md and TOKENOMICS.md matches the RULES and the code"},{"citation":"resolved","description":"Read in full: TaskVault.sol, desks/DisputeDesk.sol, desks/CaseDesk.sol, desks/Desk.sol, VaultParams.sol, YoloSweetener.sol, YoloPredict.sol, YoloLockHub.sol, BuybackBurner.sol, DevPrices.sol, token/YoloToken.sol, token/YoloSplitter.sol, token/NftGate.sol, token/ProjectVesting.sol, token/Vestings.sol, docs/TOKENOMICS.md, docs/YOLO.md, the money lines of docs/MECHANICS.md, VaultTypes.sol structs and constants, the LaunchPlan constants (YOLO_POOL_BPS, YOLO_PRICE_USD, supply) and the existing audit test names. Not reached in full: ParamsRegistry.sol, ProtocolDirectory.sol, HiddenList.sol, DareTrophy.sol beyond the burn path, TrophyCatalog.sol, helpers/*, adapters/*, OracleAttestation.sol, docs/LAUNCH-TASK.md, brand/, landing/ (out of scope or not money). forge build: the first attempt was stopped at a 10-minute background limit; the second completed (about 20 minutes of solc, via-IR with v4-core). forge test --match-path contracts/test/MoneyRules.t.sol: 43 passed, 0 failed, 0 skipped. The full suite was not re-run for time; no new test was written since no finding reached high. Code security: one info line: every state-changing entry point is either routed through the vault with the original sender appended, onlyDesk, only(vault/caseDesk/disputeDesk), DevPrices-owner, registry-owner or permissionless-by-design; no user allowance is spent by anyone but its owner. Role scenarios: one info line: the dev wallet (cancel any open task, buyback wallet, prices moving only the stake bonus), the registry owner (future parameters under a 48h timelock incl. feeBps <= 15% and doerBondBps >= 0, attester set, pause creation, bindSweetener once) and the NftGate writer (the snapshot) are trust assumptions, not bypasses.","line":67,"path":"contracts/src/TaskVault.sol","reproduction":"Not a defect; coverage statement.","severity":"info","snippet":"contract TaskVault {","title":"15. COVERAGE: all protocol sources read in full; forge build ok, MoneyRules suite 43/43 passed, no new test added"}],"hash":"1e5bbc0df51d413c116ed6356d596e3d3172d5eed52752c255491c616ccdcfa1","nodeId":"f806b7c8-990b-4502-a58b-b292e83d6b6b","outcome":"completed","summary":"The review is complete and the report is in `.imd-findings.json` at the repository root with all 16 entries (verdict plus items 1 to 15, in order).\n\n**Outcome.** The payouts match the RULES at the launch parameters. I traced every amount through the reward split, bond penalties, appeal funding, stake bonus, YOLO add-on, expiry and cancel refunds, YoloPredict settlement, every burn path, the token taxes and the vesting split, and compared the three docs line by line. No critical or high finding, so no proof test was needed. The build succeeded on the second attempt and the money-rules suite passed.\n\n| Check | Result |\n|---|---|\n| forge build | ok, about 20 minutes of via-IR compile |\n| MoneyRules.t.sol | 43 passed, 0 failed |\n| Findings | 1 low, 15 info |\n\n**The one low finding.** In `DisputeDesk._splitCurrency`, the winning side's share of a loser's bond goes to the treasury when the winning side has no bond in that currency. This cannot happen with the launch bonds, but the parameter bounds allow the registry owner to set the doer bond percentage to zero. In that state a doer who wins with the NFT undecided loses the disputer's 1% penalty to the treasury, against the rule that gives 100% to the winning side. The smallest fix credits the doer directly in that branch.\n\n**Info drifts worth knowing.** A doer stake lock that lapsed between submission and payout and was renewed without withdrawing still earns the bonus, because the hub only checks the epoch and that a lock runs now. Dust of a 50/50 bond split goes to the voters' half, and the odd unit of the stakers' remainder goes to the treasury, both as documented.\n\n**Not done.** The full test suite was not re-run for time. Collusion scenarios, code security and role powers are other orders and have one info line each in the coverage entry. No existing file was modified.","treeHash":null,"usage":{"cachedInputTokens":2391178,"inputTokens":454,"model":"claude-fable-5-1","outputTokens":56664,"runtime":"claude","turns":36,"wallClockMs":2058915}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3beb94a990617d46","findings":[{"citation":"resolved","description":"The NFT 5% of the fee goes to the votes on the final side whenever the NFT decided and the outcome agrees (nftRight), and _splitCurrency gives those votes 50% of the loser's bond. Nothing ties the disputer and the voters apart. One actor holding >= instantDelta (20) NFTs (or >= endDelta 10 at the vote end; minWinnerWallets = 1) disputes an obviously honest submission from a second wallet, votes 'accept' with their NFTs and is the only right voter. They lose 50% of their own dispute bond to the doer, get the other 50% back as the voter, and take the whole NFT 5% of R that otherwise goes to the treasury. The doer is not harmed (90% + half the bond, delayed by vote + appeal windows), so nobody appeals. Net for the actor = 0.05*A - 0.5*(0.5 IMD + 0.01*A); at IMD $5.42 the break-even is A ~ $30; at R = $100 the pair nets about +$3.15 and the treasury receives 5 USDC instead of 10. Reproduced with the PoC (fails now: the pair profits). This is not doer self-dispute and needs no second party. Smallest fix (design decision): pay the NFT 5% and the voters' bond half only when the dispute was contested (the losing side had at least one vote), or exclude the disputer's own wallet / payTo from the voter pool.","line":715,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"Launch params, IMD $5.42. Task R = A = 100 USDC, honest submission. Holder W with 20 NFTs; wallet W2 disputes (bond 0.5 IMD + 1 USDC). W votes accept with 20 ids -> instant NFT decision Accept; no appeal; after 24h poke -> Accepted, nftRight. claimVoterReward(subId, W) pays W 0.25 IMD + 0.5 USDC (half of W2's bond) + 5 USDC (NFT fee). Expected: a frivolous dispute costs its maker; actual: W+W2 net +$3.15 (-$3.71 paid, +$6.86 received), treasuryPot[usdc] = 5e6 instead of 10e6. PoC contracts/test/scratch/FrivolousDisputeNftHolder.t.sol: forge test --match-path contracts/test/scratch/FrivolousDisputeNftHolder.t.sol fails with 'a frivolous dispute by an NFT holder must not profit: 3145000000000000000 > 0' (run on this commit).","severity":"medium","snippet":"        uint256 toVoters = nftRight ? nftPart : 0;","title":"An NFT holder disputing an honest submission from a second wallet takes the NFT 5% of R from the treasury at a profit"},{"citation":"resolved","description":"RULES: a staked doer gets b = 4% x min(1, power/A) floored to whole bps, 'no threshold'; the rest of the stakers' 5% is (5% - b) half burn, half treasury, and the unearned NFT 5% is burned. _payReward keys the staked branch on f.stakeBps != 0, so a doer with a running lock whose power is below A/400 (b floors to 0; CaseDesk._stakeBps line 514 returns 0) is settled as if there were no lock: the whole 10% goes to the treasury and nothing is burned. The doer's 90% is right; 7.5% of R that the RULES send to the burn (buyback wallet) lands in the treasury pot. For YOLO rewards both buckets reach the burner, so the drift is in ETH/USDC/IMD only. Loss at R = $100: 7.5 USDC per such task moved from burn to treasury. Smallest fix: carry a 'lock present' flag next to stakeBps (or let _stakeBps return a non-zero marker when power > 0) and key the split on it.","line":722,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"YOLO price parameter $0.001. USDC task A = R = 100 USDC (A = 100,000 YOLO; 1 bp needs power >= 250 YOLO). Doer locks 200 YOLO for 52 weeks (power 200e18), next block submits: Submission.stakeBps = 0. Nobody disputes; poke after 24h. Expected (RULES): doer 90 USDC, 7.5 USDC to the buyback wallet, treasuryPot 2.5 USDC. Actual: doer 90 USDC, buyback wallet 0, treasuryPot 10 USDC. PoC contracts/test/scratch/StakeFloorZeroSplit.t.sol: forge test --match-path contracts/test/scratch/StakeFloorZeroSplit.t.sol fails with 'burn 7.5% of R: 0 != 7500000' (run on this commit; the doer's 93 USDC credit = 90% + the 3 USDC bond back is as the RULES say).","severity":"medium","snippet":"        if (bps != 0 && _lockRuns(f)) {","title":"A staked doer whose bonus floors to 0 bps is settled by the no-stake split: treasury 10% instead of burn 7.5% / treasury 2.5%"},{"citation":"resolved","description":"settle() tests the winning side's emptiness with its points total, not its pool. _points() = mulDiv(mulDiv(eff, rem, dur), rem, dur) rounds to 0 whenever eff * rem^2 < dur^2, which a small position taken shortly before the deadline of a very long task reaches (deadline is any future uint64; with the 1 YOLO floor it needs a task longer than about 30 years). Then the only WILL DO holder is a winner by the RULES ('their 90 back + the losing pool by points') but the whole losing pool goes to the author and the doer; claim() (points == 0 skips the split) returns only the 90%. The symmetric WON'T DO branch at line 216 has the same test. Edge case: low. Smallest fix: test the pool (m.willDoPool == 0 / m.wontDoPool == 0) for the no-winner branch and, when pointsTotal == 0 but the pool is not, split the losing pool pro rata to amounts (or floor every position's points at 1).","line":209,"path":"contracts/src/YoloPredict.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\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {YoloPredict} from \"src/YoloPredict.sol\";\nimport {Task, Submission, TaskStatus, Outcome} from \"src/VaultTypes.sol\";\n\ncontract Yolo is ERC20 {\n    constructor() ERC20(\"YOLO\", \"YOLO\") {\n        _mint(msg.sender, 1e27);\n    }\n}\n\n/// @dev the narrow vault view YoloPredict reads\ncontract VaultStub {\n    Task internal _task;\n    Submission internal _sub;\n    IERC20 public immutable yolo;\n\n    constructor(IERC20 y, address author, address doer, uint64 createdAt, uint64 deadline) {\n        yolo = y;\n        _task.author = author;\n        _task.deadline = deadline;\n        _task.createdAt = createdAt;\n        _task.status = TaskStatus.Open;\n        _task.activeSub = 0;\n        _sub.doer = doer;\n        _sub.outcome = Outcome.Accepted;\n    }\n\n    function complete() external {\n        _task.status = TaskStatus.Completed;\n        _task.activeSub = 1;\n    }\n\n    function poke(uint256) external {}\n\n    function getTask(uint256) external view returns (Task memory) {\n        return _task;\n    }\n\n    function getSubmission(uint256) external view returns (Submission memory) {\n        return _sub;\n    }\n\n    function registry() external view returns (address) {\n        return address(this);\n    }\n}\n\n/// Finding: YoloPredict.settle treats a winning side whose positions all rounded to zero points as \"nobody on the\n/// winning side\" (it tests willDoPointsTotal == 0, not willDoPool == 0) and gives the whole losing pool to the\n/// author and the doer; the real winner gets only their 90% back. Fails now, passes once the empty-side test uses\n/// the pool (and the pool is split pro rata to amounts, or points are floored at 1).\ncontract PredictZeroPointsTest is Test {\n    Yolo yolo;\n    VaultStub vault;\n    YoloPredict predict;\n    address author = makeAddr(\"author\");\n    address doer = makeAddr(\"doer\");\n    address a = makeAddr(\"A\"); // WILL DO, 1 YOLO, one second before the deadline of a 40-year task\n    address b = makeAddr(\"B\"); // WON'T DO, 1,000 YOLO, at creation\n\n    function setUp() public {\n        vm.warp(1_700_000_000);\n        yolo = new Yolo();\n        uint64 createdAt = uint64(block.timestamp);\n        uint64 deadline = createdAt + 1_261_440_000; // 40 years\n        vault = new VaultStub(IERC20(address(yolo)), author, doer, createdAt, deadline);\n        predict = new YoloPredict(address(vault), 1e18);\n        yolo.transfer(a, 1e18);\n        yolo.transfer(b, 1_000e18);\n        vm.prank(b);\n        yolo.approve(address(predict), type(uint256).max);\n        vm.prank(a);\n        yolo.approve(address(predict), type(uint256).max);\n    }\n\n    function test_SoleWinnerWithZeroPointsGetsTheLosingPool() public {\n        vm.prank(b);\n        predict.predict(1, false, 1_000e18); // WON'T DO at creation: 900 YOLO in the pool\n        vm.warp(1_700_000_000 + 1_261_440_000 - 1);\n        vm.prank(a);\n        predict.predict(1, true, 1e18); // WILL DO: eff 0.9 YOLO, points round to 0\n        (,,,, uint256 willDoPoints,,) = predict.markets(1);\n        assertEq(willDoPoints, 0, \"the only WILL DO position has zero points\");\n\n        vault.complete(); // the task got done: WILL DO wins\n        predict.settle(1);\n        vm.prank(a);\n        predict.claim(1);\n\n        // RULES: \"Winners: their 90 back + the losing pool by points\" and \"Done with nobody on WILL DO: the losing\n        // pool half author, half doer\". A is on WILL DO, so A gets 0.9 + 900 YOLO and the author/doer get nothing\n        // of the losing pool (they only get the deferred 2.5% + 2.5% = 0.05 YOLO each).\n        assertEq(yolo.balanceOf(a), 0.9e18 + 900e18, \"the sole winner takes the losing pool\");\n        assertEq(yolo.balanceOf(author), 0.025e18 + 0.025e18, \"author: only the deferred part\");\n        assertEq(yolo.balanceOf(doer), 0.025e18 + 0.025e18, \"doer: only the deferred part\");\n    }\n}","reproduction":"Task created at t0, deadline t0 + 1,261,440,000 s (40 years). B predicts WON'T DO 1,000 YOLO at t0 (points 900e18). A predicts WILL DO 1 YOLO at deadline - 1 s: eff 0.9e18, mid = 0.9e18 * 1 / 1,261,440,000 = 713,470,319, points = 713,470,319 / 1,261,440,000 = 0. Task Completed. settle(): willDoPointsTotal == 0, wontDoPool = 900e18 -> 450 YOLO to the author, 450 to the doer; A.claim() pays 0.9 YOLO. Expected: A gets 0.9 + 900 YOLO. PoC contracts/test/scratch/PredictZeroPoints.t.sol (vault stub, no fork): forge test --match-path contracts/test/scratch/PredictZeroPoints.t.sol fails with 'the sole winner takes the losing pool: 900000000000000000 != 900900000000000000000' (run on this commit; the specialist's supplied proof, unchanged).","severity":"low","snippet":"        if (res == Resolution.WillDoWon && m.willDoPointsTotal == 0 && m.wontDoPool != 0) {","title":"YoloPredict.settle treats a winning side whose positions all have zero points as empty and pays the losing pool to the author and doer"},{"citation":"resolved","description":"RULES: an outsider who posts bond + fee and wins 'first gets 0.5 IMD off the loser's bond'. _settleDecided first subtracts the losing side's own swarm spending (_sideSpent, line 632) from penaltyImd and only then carves out the stranger fee, capped at what is left. With bondFixedImd = 0.5 IMD and a swarm fee of 0.5 IMD, one silent call paid by the losing party from its bond empties the IMD penalty, so the winning outsider gets no fee back. Two rules compete for the same 0.5 IMD and the loser's own fee wins; the honest outsider ends the case at a loss. Bounded: 0.5 IMD ($2.71) per case. Fix: take the stranger fee from the reward-currency penalty when the IMD penalty is short, or document the priority.","line":636,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"Task R = A = 100 USDC. Doer submits (bond 0.5 IMD + 3 USDC), D disputes (0.5 IMD + 1 USDC), the NFT vote ends undecided -> NoDecision. Doer calls callSwarm(sidesDoer = true): _fundCall takes 0.5 IMD from the doer bond, _charge sets _sideSpent.doer = 0.5e18. The swarm stays silent -> dropCaller, NoDecision again. Outsider S calls callSwarm(sidesDoer = false) posting 0.5 IMD + 1 USDC and 0.5 IMD fee; the swarm answers Reject -> Rejected. _settleDecided: penaltyImd = 0.5e18 (doer bond) - 0.5e18 (sideSpent.doer) = 0, the strangerFee branch is skipped; S is credited its bond (0.5 IMD + 1 USDC) and 1.5 USDC (half of the doer's 3 USDC, pro rata with D). Expected per RULES: S also gets 0.5 IMD first. Actual: S nets -0.5 IMD + 1.5 USDC = -$1.21 despite winning.","severity":"low","snippet":"        if (appellantIsStranger && callerWins && penaltyImd != 0) {","title":"A winning outsider swarm caller gets no 0.5 IMD fee back when the losing party already spent its IMD bond on its own silent call"},{"citation":"resolved","description":"RULES: add or extend = max(old + added x weeks left / 52, total x weeks left / 52). The code rounds the weeks left up to a whole week, so an amount added right after a week boundary of the lock's end (end - now = k weeks + 1 s gives left = k + 1) earns added/52 of power it is not locked for. For a short remaining term this is large: with 1 week + 1 s left an add is credited two weeks, twice the power of an honest 1-week lock of the same amount. Power read at the next block's submission raises b = 4% x min(1, power/A); the lock still has to run at payout (>= 24 h review), which a 1-week term does. Bounded to 1/52 of the added amount per add. Fix: compute the added power on the exact seconds left (add * (end - now) / MAX_LOCK) or round the weeks left down.","line":79,"path":"contracts/src/YoloLockHub.sol","reproduction":"t0: lock(100e18, 2) -> end = t0 + 2 weeks, power = 100e18 * 2 / 52 = 3.846e18. At t0 + 1 week - 1 s (1 week + 1 s left) call lock(100e18, 0): left = (1 week + 1 s + 1 week - 1 s) / 1 week = 2, pw = 3.846e18 + 100e18 * 2 / 52 = 7.692e18 (all = 200e18 * 2 / 52 = 7.692e18). Expected with 1.0000017 weeks left: 3.846e18 + 1.923e18 = 5.769e18. Reproduced: contracts/test/scratch/LockRoundUp.t.sol (check, not a proof) prints power after the add = 7692307692307692307 vs exact-seconds 5769233948921448920; a submission in the next block against A = 7,692 YOLO gets 400 bps instead of 300.","severity":"low","snippet":"        uint256 left = (end - block.timestamp + WEEK - 1) / WEEK; // whole weeks, rounded up","title":"YoloLockHub rounds the weeks left up when adding or extending, crediting an add with up to one week it is not locked for"},{"citation":"resolved","description":"RULES: 'NFT wrong or undecided: 100% winning side'. When the winning side has no bond in a currency, the whole share goes to the treasury instead. At the launch parameters the doer bond's reward part is never 0 (3% of any reward >= its minimum), so the branch is unreachable today; ParamsBounds.validate (VaultParams.sol line 180) allows doerBondBps = 0 and a registry version with it makes every doer who wins against a disputer with the NFT undecided lose the disputer's 1% of A to the treasury, although the doer is a known single winner. Low: a 48h-timelocked parameter change by the registry owner is the precondition. Fix: when doerWins credit `doer` with winnerShare directly whenever winnerBonds == 0; keep the treasury fallback for the disputers' side only.","line":765,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"Registry version with doerBondBps = 0 (accepted by ParamsBounds.validate). Task 100e6 USDC, submission (bond 0.5e18 IMD, 0 USDC), dispute (0.5e18 IMD + 1e6 USDC), no NFT decision, nobody calls the swarm, appeal window passes -> Accepted, nftRight false. _splitCurrency(usdc): penalty 1e6, winnerBonds = doerBond.rew (0) + 0 = 0 -> treasuryPot[usdc] += 1e6. Expected: the doer is credited 1e6 USDC. Actual: the doer gets only the IMD half (0.5e18).","severity":"low","snippet":"        if (winnerBonds == 0) return (toVoters, winnerShare, 0, 0);","title":"_splitCurrency sends the winning side's 100% share to the treasury when the winner has no bond in that currency (doerBondBps = 0 is an allowed parameter)"},{"citation":"resolved","description":"RULES define A as the author's own share with own top-ups, and allow top-ups until the deadline, also while a submission is queued or under review (TaskVault.topUp). The code fixes A once, at submit: the doer bond is 3% of t.base at submit (also for a queued submission activated much later), DisputeDesk.dispute (line 111) sizes the dispute bond from the same frozen s.base, and CaseDesk._stakeBps (line 115) fixes the bonus share against that A while DisputeDesk._payReward (line 725) then pays those bps on the whole pot at finalization (potOf, top-ups included). After an author top-up both bonds are below 3% / 1% of A and a $5 stake keeps 400 bps on a $10,000 pot. docs/MECHANICS.md lines 183 and 190 document 'the same for every try' / 'fixed at the submission', so this is RULES-vs-docs drift as much as a code defect; it needs the author's own top-up after a submission exists. Fix: size a queued submission's bond at activation and the dispute bond from the author's share at dispute time; cap the bonus base to the pot at submission plus the add-on, or recompute the share at finalization.","line":111,"path":"contracts/src/desks/CaseDesk.sol","reproduction":"(a) 100 USDC task; doer 1 submits (active), doer 2 submits (queued, bond 0.5 IMD + 3 USDC). Author tops up 900 USDC during doer 1's review (allowed: Open, before the deadline). Doer 1 is rejected; doer 2 activates with s.base = 100e6: its bond stays 3 USDC on a 1,000 USDC pot (RULES 30 USDC) and a disputer posts 1 USDC (RULES 10 USDC). (b) 5 USDC task, doer locks 5,000 YOLO ($5, power 5,000e18 = A at $0.001) and submits: stakeBps = 400; the author tops up 9,995 USDC during review; accepted undisputed: _payReward r = 10,000e6, bonus = 400e6 USDC to the doer, treasury 50e6, burn 550e6. With A = 10,000 USDC the RULES give b = 0 bps, treasury 1,000e6.","severity":"low","snippet":"        s.base = uint128(t.base); // a part of the pot","title":"A (the author's share) is frozen at submission for the bonds and the stake share while author top-ups until the deadline keep changing it"},{"citation":"resolved","description":"Submission.addon is copied from the sweetener at submit time (CaseDesk line 112). An add-on sweetened during the review joins the bonus (YoloSweetener.payout reads bonusOf at payout, as the RULES say) and funds a voter pool, but s.addon stays 0, so the batch claim never calls YoloSweetener.claimVoter for that task. No money is lost: YoloSweetener.claimVoter(taskId, payTo) still pays it when called directly; only the single-call path the docs describe misses it. Merged from three specialists. Fix: set bonusTask whenever outcome == Accepted and let the try/catch skip tasks without a voter pool.","line":441,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"Task in USDC, no add-on at submission (s.addon = 0). During the review a third party sweetens 10,000 YOLO. Dispute, NFT votes accept (right), accepted after the appeal window. YoloSweetener.payout: voterPool = 500 YOLO. A voter calls TaskVault.claimVoterRewards([subId], payTo): receives the USDC fee share and bond share, not the 500 YOLO; YoloSweetener.claimVoter(taskId, payTo) afterwards pays them.","severity":"info","snippet":"        if (s.addon != 0 && s.outcome == Outcome.Accepted) bonusTask = s.taskId;","title":"claimVoterRewards skips the YoloSweetener voter claim when the add-on arrived after the submission (s.addon snapshot is 0)"},{"citation":"resolved","description":"The epoch only changes on withdraw(). lock(0, weeks) on a lock whose end passed (same epoch) starts a new term; stakePower(doer, since) then finds the old point (same epoch, p.end > since) and l.end > now, and returns the old power. The RULES say 'the lock must still run at payout or b = 0'; the tokens never left the hub, so this is a defensible reading, but a doer whose 1-week lock expired during a long dispute restores b with lock(0, 1) just before the payout, which a doer who withdrew cannot. At most b of R (<= $4 at R = $100) moves from burn/treasury to the doer. Fix, if the strict reading is wanted: bump the epoch in lock() when the previous term had ended.","line":111,"path":"contracts/src/YoloLockHub.sol","reproduction":"Doer locks 10,000 YOLO for 1 week on day 0 and submits on day 0 (stakeBps 400 on a 10,000 YOLO task... at the price parameter). Dispute, NFT vote, appeal: payout on day 9. On day 8 the doer calls lock(0, 1): old = 0 (ended), all = 10,000e18 * 1 / 52, same epoch, new end day 15. On day 9 _lockRuns -> stakePower returns the day-0 power and b is paid; a doer who had withdrawn and re-locked gets 0.","severity":"info","snippet":"        if (p.epoch != l.epoch || l.end <= block.timestamp || p.end <= since) return 0;","title":"A lock that ended and was set again without withdrawing keeps its epoch, so stakePower still pays b for a submission made under the old term"},{"citation":"resolved","description":"CaseDesk._stakeBps fixes b at submission (power read before the submission block, price parameter at submission) and DisputeDesk._payReward only checks that the lock still runs and pays the fixed b. The doc line implies a price or power change after submission moves b, which it does not (RULES: fixed at the lock, read before the submission block). Docs drift only.","line":61,"path":"docs/YOLO.md","reproduction":"Lock power worth 50% of A at submission (b = 200 bps); before the payout the dev raises the YOLO price x10 so the power is now worth 5x A. Payout pays 2% (fixed at submission), not 4% as the doc line implies.","severity":"info","snippet":"   to the treasury. Computed by the DisputeDesk at payout and booked by the vault from the fee it holds.","title":"docs/YOLO.md says the stake bonus is computed by the DisputeDesk at payout; the code fixes it in CaseDesk at submission"},{"citation":"resolved","description":"docs/YOLO.md says every vesting clock runs in months of 30.4 days (12 months = 364.8 days); contracts/src/token/Vestings.sol uses 365 days for the 12-month vestings and 730 days for the 24-month one. Drift of 0.2 days per year between the doc and the code; amounts unaffected.","line":79,"path":"docs/YOLO.md","reproduction":"Compare docs/YOLO.md line 79 with Vestings.sol lines 17, 23, 29, 37 (ProjectVesting(..., 730 days) / (..., 365 days)). Fix: say '12 months = 365 days, 24 months = 730 days' in YOLO.md.","severity":"info","snippet":"Every vesting clock starts when `YoloSplitter.split` pays it, right after launch 1 (months of 30.4 days); no cliff","title":"docs/YOLO.md states vesting months of 30.4 days while the vestings are compiled with 365 and 730 days"}],"hash":"66718e0a66d7d3642d783f9d64406212a90a744258617c12a44dcdcbb9db8bc8","nodeId":"71592fdc-bb57-4bbd-b165-8c88cca3306e","outcome":"completed","summary":"The review is done. `.imd-findings.json` holds 11 verified findings, every snippet matches the tree, and three carry Foundry proofs that fail on this commit.\n\n**What I did.** Rebuilt the project (via-IR, about 25 minutes), reproduced each specialist finding by reading the code, and wrote or ran PoCs in `contracts/test/scratch/`. The existing MoneyRules suite passes 43/43 on this commit.\n\n**Kept, with proofs (all three fail now for the stated reason):**\n- **Medium, DisputeDesk.sol:715.** An NFT holder with 20 NFTs disputes an honest submission from a second wallet, votes accept alone, and takes the NFT 5% of R from the treasury. The PoC shows the pair nets +$3.145 at R = $100 and IMD $5.42. The doer is unharmed, so nobody appeals.\n- **Medium, DisputeDesk.sol:722.** A staked doer whose bonus floors to 0 bps is settled by the no-stake split. PoC: burn 0 and treasury 10 USDC where the RULES give burn 7.5 and treasury 2.5. Specialist audit_flow cited a PoC file that does not exist in the tree, so I wrote my own.\n- **Low, YoloPredict.sol:209.** A sole winner whose points round to zero is treated as nobody on the winning side. The specialist's supplied proof fails as stated (0.9 vs 900.9 YOLO).\n\n**Kept without proof (reproduced by code trace, one by a check test):**\n- Low: winning outsider loses the 0.5 IMD fee refund when the loser's IMD bond was already spent on its own silent call (DisputeDesk.sol:636).\n- Low: YoloLockHub rounds weeks left up. My check test confirms an add with 1 week + 1 s left gets power 7.69e18 instead of 5.77e18 (YoloLockHub.sol:79). Two specialist entries merged.\n- Low: winner share goes to the treasury when the winner has no bond in that currency, reachable only with doerBondBps = 0 (DisputeDesk.sol:765).\n- Low: A is frozen at submission for the doer bond, the dispute bond and the stake share while author top-ups keep changing it. Two audit_math entries merged into one (CaseDesk.sol:111).\n- Info: batch voter claim skips the sweetener share for a late add-on (three specialists merged), a lapsed-then-relocked lock keeps paying b, and two docs drifts in YOLO.md.\n\n**Dropped.** None of the specialist findings failed to reproduce. The audit_permissions \"no issue\" items 0 to 15 are coverage statements, not defects, so they are not in the file.\n\n**Severity note.** I rated the stake-floor split medium rather than high because the doer's 90% is correct and only the burn-versus-treasury allocation of the protocol's own fee deviates; for YOLO rewards both buckets reach the burner anyway.","treeHash":null,"usage":{"cachedInputTokens":3801876,"inputTokens":740,"model":"claude-fable-5-1","outputTokens":45648,"runtime":"claude","turns":41,"wallClockMs":1181662}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"323aec17bfc8d652","findings":[{"citation":"resolved","description":"settle() tests the winning side's emptiness with its points total, not its pool. _points() = mulDiv(mulDiv(eff, rem, dur), rem, dur) rounds to 0 whenever eff * rem^2 < dur^2, which a position taken shortly before the deadline of a long task reaches (deadline is a free uint64). Then the only WILL DO holder is a winner by the RULES ('their 90 back + the losing pool by points') but the whole losing pool is sent to the author and the doer, and the holder gets only their 90% back (claim(): points == 0 skips the split). The symmetric WON'T DO branch at line 216 has the same test. Smallest fix: test m.willDoPool == 0 (resp. m.wontDoPool == 0) for the no-winner branch and, when pointsTotal == 0 but the pool is not, split the losing pool pro rata to amounts (or floor every position's points at 1).","line":209,"path":"contracts/src/YoloPredict.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\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {YoloPredict} from \"src/YoloPredict.sol\";\nimport {Task, Submission, TaskStatus, Outcome} from \"src/VaultTypes.sol\";\n\ncontract Yolo is ERC20 {\n    constructor() ERC20(\"YOLO\", \"YOLO\") {\n        _mint(msg.sender, 1e27);\n    }\n}\n\n/// @dev the narrow vault view YoloPredict reads\ncontract VaultStub {\n    Task internal _task;\n    Submission internal _sub;\n    IERC20 public immutable yolo;\n\n    constructor(IERC20 y, address author, address doer, uint64 createdAt, uint64 deadline) {\n        yolo = y;\n        _task.author = author;\n        _task.deadline = deadline;\n        _task.createdAt = createdAt;\n        _task.status = TaskStatus.Open;\n        _task.activeSub = 0;\n        _sub.doer = doer;\n        _sub.outcome = Outcome.Accepted;\n    }\n\n    function complete() external {\n        _task.status = TaskStatus.Completed;\n        _task.activeSub = 1;\n    }\n\n    function poke(uint256) external {}\n\n    function getTask(uint256) external view returns (Task memory) {\n        return _task;\n    }\n\n    function getSubmission(uint256) external view returns (Submission memory) {\n        return _sub;\n    }\n\n    function registry() external view returns (address) {\n        return address(this);\n    }\n}\n\n/// Finding: YoloPredict.settle treats a winning side whose positions all rounded to zero points as \"nobody on the\n/// winning side\" (it tests willDoPointsTotal == 0, not willDoPool == 0) and gives the whole losing pool to the\n/// author and the doer; the real winner gets only their 90% back. Fails now, passes once the empty-side test uses\n/// the pool (and the pool is split pro rata to amounts, or points are floored at 1).\ncontract PredictZeroPointsTest is Test {\n    Yolo yolo;\n    VaultStub vault;\n    YoloPredict predict;\n    address author = makeAddr(\"author\");\n    address doer = makeAddr(\"doer\");\n    address a = makeAddr(\"A\"); // WILL DO, 1 YOLO, one second before the deadline of a 40-year task\n    address b = makeAddr(\"B\"); // WON'T DO, 1,000 YOLO, at creation\n\n    function setUp() public {\n        vm.warp(1_700_000_000);\n        yolo = new Yolo();\n        uint64 createdAt = uint64(block.timestamp);\n        uint64 deadline = createdAt + 1_261_440_000; // 40 years\n        vault = new VaultStub(IERC20(address(yolo)), author, doer, createdAt, deadline);\n        predict = new YoloPredict(address(vault), 1e18);\n        yolo.transfer(a, 1e18);\n        yolo.transfer(b, 1_000e18);\n        vm.prank(b);\n        yolo.approve(address(predict), type(uint256).max);\n        vm.prank(a);\n        yolo.approve(address(predict), type(uint256).max);\n    }\n\n    function test_SoleWinnerWithZeroPointsGetsTheLosingPool() public {\n        vm.prank(b);\n        predict.predict(1, false, 1_000e18); // WON'T DO at creation: 900 YOLO in the pool\n        vm.warp(1_700_000_000 + 1_261_440_000 - 1);\n        vm.prank(a);\n        predict.predict(1, true, 1e18); // WILL DO: eff 0.9 YOLO, points round to 0\n        (,,,, uint256 willDoPoints,,) = predict.markets(1);\n        assertEq(willDoPoints, 0, \"the only WILL DO position has zero points\");\n\n        vault.complete(); // the task got done: WILL DO wins\n        predict.settle(1);\n        vm.prank(a);\n        predict.claim(1);\n\n        // RULES: \"Winners: their 90 back + the losing pool by points\" and \"Done with nobody on WILL DO: the losing\n        // pool half author, half doer\". A is on WILL DO, so A gets 0.9 + 900 YOLO and the author/doer get nothing\n        // of the losing pool (they only get the deferred 2.5% + 2.5% = 0.05 YOLO each).\n        assertEq(yolo.balanceOf(a), 0.9e18 + 900e18, \"the sole winner takes the losing pool\");\n        assertEq(yolo.balanceOf(author), 0.025e18 + 0.025e18, \"author: only the deferred part\");\n        assertEq(yolo.balanceOf(doer), 0.025e18 + 0.025e18, \"doer: only the deferred part\");\n    }\n}","reproduction":"Task created at t0 with deadline t0 + 40 years (1,261,440,000 s). B predicts WON'T DO 1,000 YOLO at t0 (points 900e18). A predicts WILL DO 1 YOLO at deadline - 1 s: eff = 0.9e18, mid = 0.9e18 * 1 / 1,261,440,000 = 713,470,319, points = 713,470,319 / 1,261,440,000 = 0. A submission made before the deadline is accepted: Completed. settle(): willDoPointsTotal == 0 and wontDoPool == 900e18, so 450 YOLO go to the author and 450 to the doer; A.claim() pays 0.9 YOLO. Expected by the RULES: A, the only winner, gets 0.9 + 900 YOLO (loss 900 YOLO = $0.90 at $0.001; scales with the losing pool, e.g. a 1,000,000 YOLO losing pool = $900). Proof: contracts/test/scratch/PredictZeroPoints.t.sol, forge test --match-path contracts/test/scratch/PredictZeroPoints.t.sol fails now with 'the sole winner takes the losing pool: 0.9e18 != 900.9e18'.","severity":"low","snippet":"        if (res == Resolution.WillDoWon && m.willDoPointsTotal == 0 && m.wontDoPool != 0) {","title":"YoloPredict: a winning side whose positions all have zero points is paid as 'nobody on the winning side'"},{"citation":"resolved","description":"_stakeBps compares the doer's lock power with t.base (the author's own share) at submission and freezes the bps; DisputeDesk._payReward (line 725, bonus = r * bps / BPS) then pays those bps on f.reward = potOf(taskId) at finalization, which includes top-ups the author made during the review. RULES: b = 4% x min(1, power / A), A = the author's own share with own top-ups. With the top-up A is far larger than the power, so the rule's b is near 0 while the code pays the full 4%. The treasury and the burn lose up to 4% of every top-up made after the submission; it needs the author's cooperation (an author who wants to tip the doer at the treasury's expense, or an author who is the doer's second wallet). docs/MECHANICS.md line 190 documents 'fixed at the submission', so this is also docs-vs-RULES drift. Smallest fix: recompute the share at finalization against the author's share then (SubmitTerms.base is still available), or cap the bonus base to the pot at submission (pay b on s.reward recorded at submit plus the add-on, not on later top-ups).","line":115,"path":"contracts/src/desks/CaseDesk.sol","reproduction":"YOLO price parameter $0.001. Author creates a 5 USDC task (A = 5 USDC = 5,000 YOLO). Doer locks 5,000 YOLO ($5) for 52 weeks (power 5,000e18) and submits: _stakeBps = 400. During the 24 h review the author tops up 9,995 USDC (topUp allows it while Open and before the deadline). Nobody disputes; accepted. _payReward: r = 10,000 USDC, fee 1,000, bonus = 10,000 * 400 / 10,000 = 400 USDC to the doer, burn 5.5% = 550, treasury 50. Expected by the RULES with A = 10,000 USDC: b = 4% x 5,000 / 10,000,000 = 0.002% -> 0 bps after flooring, doer 9,000, treasury 1,000. Actual doer 9,400, treasury 50 + burn 550: $400 of treasury/burn income moved to the doer against a $5 stake.","severity":"low","snippet":"        s.stakeBps = _stakeBps(t, sender, p);","title":"Stake bonus share is fixed against A at submission but paid on the whole pot at payout: an author top-up during review pays b on money the stake never covered"},{"citation":"resolved","description":"The doer bond is collected from t.base at submit (also for a queued submission that activates much later) and DisputeDesk.dispute() sizes the dispute bond from s.base, the same frozen value. The RULES define A as the author's own share including own top-ups; top-ups are allowed until the deadline, also while a submission is queued or under review (TaskVault.topUp). After a top-up both bonds are below the RULES' 3% and 1% of A, so a fake submission or a fake dispute on a now-large task risks only the small old bond, and the winner's half-bond share shrinks accordingly. Documented in docs/MECHANICS.md (line 183, 'the same for every try') as fixed at submission, which conflicts with the RULES' definition of A. Smallest fix: size the doer bond of a queued submission when it activates (collect the difference, or recompute the bond from the author's share at activation) and size the dispute bond from the author's share at dispute time.","line":111,"path":"contracts/src/desks/CaseDesk.sol","reproduction":"100 USDC task by the author alone. Doer 1 submits (active), doer 2 submits (queued, bond 0.5 IMD + 3 USDC). Author tops up 900 USDC during doer 1's review (allowed: Open, before the deadline). Doer 1's submission is rejected after a dispute; doer 2's activates with s.base = 100 USDC: its bond stays 0.5 IMD + 3 USDC on a 1,000 USDC pot (RULES: 0.5 IMD + 30 USDC), and a disputer of it posts 0.5 IMD + 1 USDC (RULES: 0.5 IMD + 10 USDC). If doer 2 is a fake that the NFT rejects, the disputer's bond share is 50% of 3 USDC = 1.50 USDC instead of 15 USDC; a fake on a $1,000 pot risks $3.00 + 0.5 IMD instead of $30 + 0.5 IMD.","severity":"low","snippet":"        s.base = uint128(t.base); // a part of the pot","title":"Doer and dispute bonds use A frozen at the submission (s.base), so a queued submission activated after author top-ups, and disputes after a top-up during review, post less than 3% / 1% of A"},{"citation":"resolved","description":"Submission.addon is copied from the sweetener at submit time. An add-on sweetened during the review (allowed, and paid with the bonus by YoloSweetener.payout) leaves s.addon == 0, so the batch claim never calls YoloSweetener.claimVoter for that task. No money is lost: anyone can call YoloSweetener.claimVoter(taskId, payTo) directly, but the single-call path the docs describe does not pay it. Smallest fix: set bonusTask whenever outcome == Accepted and let the try/catch skip tasks without a voter pool, or read bonusOf at finalization.","line":441,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"Task in USDC, no add-on at submission (s.addon = 0). During the review a third party sweetens 10,000 YOLO. Dispute, NFT votes accept (right), accepted. YoloSweetener.payout: voterPool = 500 YOLO. A voter calls TaskVault.claimVoterRewards([subId], payTo): they receive the USDC fee share and bond share, but not the 500 YOLO; YoloSweetener.claimVoter(taskId, payTo) still pays it when called separately.","severity":"info","snippet":"        if (s.addon != 0 && s.outcome == Outcome.Accepted) bonusTask = s.taskId;","title":"claimVoterRewards skips the YoloSweetener voter claim when the add-on arrived after the submission (s.addon snapshot is 0)"},{"citation":"resolved","description":"RULES: add = old + added x weeks left / 52. The code rounds the weeks left up, so an amount added with one second left is credited with a full week of power. Power never exceeds the locked amount and the lock must still run at payout, so no bonus can be taken with it; it only overstates the Locked event / powerAt for the remaining seconds. Smallest fix: compute the added power on the exact seconds left (add * (end - now) / MAX_LOCK).","line":79,"path":"contracts/src/YoloLockHub.sol","reproduction":"lock(1e18, 1) at t0 (power 1e18/52). At t0 + 7 days - 1 s call lock(5200e18, 0): left = 1, pw = 1e18/52 + 100e18 (RULES: 1e18/52 + 5200e18 * 1 s / (52 weeks) = 0.0194e18 + 0.00017e18). The lock ends one second later, so stakePower() is 0 at any payout; no payout changes.","severity":"info","snippet":"        uint256 left = (end - block.timestamp + WEEK - 1) / WEEK; // whole weeks, rounded up","title":"YoloLockHub counts a partial last week as a whole week when adding to a running lock"},{"citation":"resolved","description":"The epoch only changes on withdraw(). lock() on a lock whose end passed (same epoch) starts a new term; stakePower(doer, since) then finds the old point (same epoch, p.end > since) and the new l.end > now, and returns the old power. The RULES say 'the lock must still run at payout or b = 0'; the tokens never left the hub, so this is a defensible reading, but a doer whose 1-week lock expired during a long dispute can restore b with lock(0, 1) just before the payout, which a doer who withdrew cannot. Smallest fix, if the strict reading is wanted: bump epoch in lock() when the previous term had ended.","line":111,"path":"contracts/src/YoloLockHub.sol","reproduction":"Doer locks 10,000 YOLO for 1 week on day 0, submits on day 0 (stakeBps 400 on a 10,000 YOLO task). Dispute, NFT vote, appeal: payout on day 9. On day 8 the doer calls lock(0, 1): old = 0 (ended), all = 10,000e18 * 1 / 52, same epoch. On day 9 _lockRuns -> stakePower returns the day-0 power 10,000e18 * 1 / 52 and b = 400 * (192e18 / 10,000e18) = 7 bps is paid; a doer who had withdrawn and re-locked gets 0.","severity":"info","snippet":"        if (p.epoch != l.epoch || l.end <= block.timestamp || p.end <= since) return 0;","title":"A lock that ended and was re-locked without withdraw keeps its epoch, so stakePower() still pays b for a submission made under the old term"}],"hash":"888bbe64ca50de9c72676652ffec4931c66447253ccaad7701785426f9abb9d0","nodeId":"0a5de36b-7e42-4915-bc62-fbf338ef1513","outcome":"completed","summary":"The findings file is written with six entries, one carrying a failing proof. Here is the report in the mandated format.\n\n**0. VERDICT.** Every payout in `DisputeDesk._payReward`, `_settleDecided`, `_splitCurrency`, `YoloSweetener.payout` and `YoloPredict` matches the RULES at $100 in all four currencies, with and without stake, NFT right, wrong or undecided. No money blocker: no theft path, no freeze, no spend of a user's allowance outside the user's own call. Top three changes, by $ impact at R $100: (1) YoloPredict pays the losing pool to author and doer when the sole winner's points round to zero (loss = whole losing pool, needs a multi-decade deadline, proof attached); (2) stake share b frozen against A at submission but paid on top-ups made during review ($400 of treasury income to a $5 stake on a 5 USDC task topped up to 10,000); (3) bonds frozen at the submission's A, so a queued submission or a dispute after an author top-up posts 3 USDC or 1 USDC instead of 30 or 10 on a 1,000 USDC pot.\n\n**1. Accepted, no dispute.** `DisputeDesk.sol:711-733`, `TaskVault.sol:576-585`. At R 100 USDC: fee 10, nftPart 5, no stake: doer 90, treasury 10. Full power: bonus 4, half 0.5 burn, treasury 0.5, burn also the 5 unearned (5.5 total). Half: 92 / 6.5 / 1.5. 5% of A: 20 bps, 90.20 / 7.40 / 2.40. Same arithmetic for 0.041 ETH, 18.45 IMD, 100,000 YOLO. YOLO treasury part is redirected to the burner by `addTreasury`. No issue.\n\n**2. Dispute, NFT right.** `DisputeDesk.sol:611-701, 746-777`. Loser bond split per currency: `toVoters = penalty - penalty*5000/BPS`, winner share pro rata to bonds, winner's own bond credited back at lines 690-699, voters pro rata via `_claimVoter` with the last claimant taking dust. Rejected: disputer gets 50% of 0.5 IMD + 3% A, voters the rest, via `claimDisputer`. No issue.\n\n**3. NFT wrong or undecided.** `nftRight` false forces `toVoters = 0` and the whole penalty to the winning side; the 5% NFT fee goes to treasury without a stake, burns with one (`_payReward` line 729). Undecided and no call: `CaseDesk._endUncalled` accepts, disputer bond 100% to the doer. No issue.\n\n**4. Rejected.** `TaskVault.closeSubmission` lines 438-447: pot untouched, next queued activated. `claimVoidSubmission` returns the bond only on Completed/Unwound; a queued submission cannot exist on an Expired task. Low finding 3: the next submission's bond is 3% of A at its own submit time, not at activation.\n\n**5. Appeal money.** `_fundCall` lines 278-308, `_charge` 311-321, `_settleDecided`. Party: first call 0.5 IMD out of its bond, later calls from the wallet. Outsider: bond + fee, winning gets 0.5 IMD off the loser's IMD first (line 636-640), then a pro-rata share. Silence: bond and unspent fee back, fee gone, counts toward 3. Failed ask or 6 h timeout: full refund, counts. Net per case at R $100 USDC: outsider wins +$1.50 (0.5 IMD + half of 3 USDC, minus the $2.71 fee), loses −$6.42; party silent call −$2.71. No issue.\n\n**6. Bonds.** `Desk.sol:118-137`. At the minimums: 0.15 / 0.05 USDC, 6e13 / 2e13 wei, 0.03 / 0.01 IMD, 30 / 10 YOLO, all exact; sub-unit dust stays with the payer; odd penalty wei goes to voters, odd stakers' unit to treasury (matches MECHANICS line 190). No issue.\n\n**7. Stake bonus.** `CaseDesk._stakeBps` lines 502-515 reads `powerAt(doer, block.timestamp)` (strictly before the block) against `t.base`, floors to bps; `_lockRuns` needs the same epoch and a running lock at payout. Low finding 2 (A frozen, b paid on later top-ups), info findings 5 and 6 (week rounding, re-lock without withdraw).\n\n**8. YOLO add-on.** `YoloSweetener.payout` lines 125-156: 10% fee, 5% to accept votes only when `nftDecision == Accept`, same `stakeBps` from the settlement, rest to the burner. Withdraw on Expired/Unwound/past deadline not busy. Info finding 4: batch claim skips the sweetener when the add-on came during review.\n\n**9. Expiry and cancel.** `withdrawContribution` lines 334-346 returns `c.active` per funder; `cancelTask` unwinds the a","treeHash":null,"usage":{"cachedInputTokens":2131063,"inputTokens":452,"model":"claude-fable-5-1","outputTokens":45099,"runtime":"claude","turns":35,"wallClockMs":1271926}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"d7e6877fd9e388bb","findings":[{"citation":"resolved","description":"RULES: a staked doer gets b = 4% x min(1, power/A) floored to whole bps, 'no threshold'; the rest of the stakers' 5% is (5% - b) half burn, half treasury, and the unearned NFT 5% is burned. _payReward decides 'staked' by `f.stakeBps != 0`, so any running lock whose power is below A/400 (b floors to 0) is settled as if there were no lock: the whole 10% goes to the treasury and nothing burns. The doer's amount is right (90%); the treasury receives 7.5% of R that the RULES send to the burn. CaseDesk._stakeBps (line 514) and docs ('any power counts, no threshold') promise the staked path from any power. Smallest fix: carry a 'lock present' flag next to stakeBps (or return min 1 bp in _stakeBps when power > 0) and key the split on it, so a running lock with b = 0 takes the staked branch.","line":722,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"USDC task, A = R = 100 USDC, YOLO price parameter $0.001 (A = 100,000 YOLO; 1 bp needs power >= 250 YOLO). Doer locks 200 YOLO for 52 weeks (power 200 > 0, 400*200/100000 = 0.8 -> stakeBps 0), submits, nobody disputes, poke after 24h. Expected (RULES): doer 90 USDC, burn 2.5 + 5 = 7.5 USDC to the buyback wallet, treasury 2.5 USDC. Actual: doer 90 USDC, treasury 10 USDC, buyback wallet 0. Loss: 7.5 USDC per $100 task moved from burn to treasury. PoC: contracts/test/audit/StakeFloorZero.t.sol (Base fixture: YOLO $0.0001, lock 2,000 YOLO -> 0.8 bp -> 0).","severity":"low","snippet":"        if (bps != 0 && _lockRuns(f)) {","title":"Staked doer whose bonus floors to 0 bps is paid by the no-stake split (treasury 10%, burn 0) instead of the staked split (burn 7.5%, treasury 2.5%)"},{"citation":"resolved","description":"RULES: add or extend = max(old + added x weeks left / 52, total x weeks left / 52). The code rounds the weeks left up to a whole week, so the added amount is credited with up to one week it is not locked for. A doer can time every add to the moment just after a week boundary of the lock end (end - now = k weeks + 1 second gives left = k + 1) and gain added/52 of power per add; with repeated adds (each a new, later checkpoint) the lock's power exceeds amount x weeks/52 by up to 1/52 of the amount, which raises b. Bounded: at most ~1.9% more power. Smallest fix: compute power with the exact seconds left (amount * (end - now) / MAX_LOCK) or round the weeks left down.","line":79,"path":"contracts/src/YoloLockHub.sol","reproduction":"t0: lock(100e18, 2) -> end = t0 + 2 weeks, power = 100*2/52 = 3.846e18. At t0 + 1 week - 1 second (1 week + 1 s left) call lock(100e18, 0): left = (1 week + 1 s + 1 week - 1 s)/1 week = 2 -> power = 3.846e18 + 100e18*2/52 = 7.692e18. Expected with 1.0000017 weeks left: 3.846e18 + 1.923e18 = 5.769e18. The lock holds 200 YOLO for 1 more week: 'amount x weeks/52' for the whole 200 over the term it actually runs is at most 200*2/52 = 7.69 only if all 200 had been locked from t0; the second 100 were not. Power is overstated by 1.92e18 (1/52 of the added amount).","severity":"low","snippet":"        uint256 left = (end - block.timestamp + WEEK - 1) / WEEK; // whole weeks, rounded up","title":"YoloLockHub counts the weeks left rounded UP when adding or extending, so an add one second before a week boundary earns a full extra week of power"},{"citation":"resolved","description":"claimVoterRewards bundles the sweetener's voter share only when `s.addon` (the add-on at submission time) is non-zero. RULES: an add-on during review joins R, and YoloSweetener.payout pays voters on the whole add-on at payout. A task sweetened only after the submission (during review) has s.addon = 0, so the batch skips the sweetener claim; the voter must call YoloSweetener.claimVoter directly. No money is lost (the pool waits), only the convenience path misses it. Fix: read the sweetener's voterVotesLeft/bonus instead of s.addon, or drop the s.addon guard (the try/catch already tolerates nothing-to-claim).","line":441,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"Task with no add-on; doer submits; during review someone sweetens 1,000 YOLO; dispute, NFT decides Accept, appeal window runs out, accepted; YoloSweetener.payout(task) sets voterPool = 50 YOLO. Voter calls claimVoterRewards([subId], payTo): vault shares are paid, the 50 YOLO pool is untouched (bonusTask = 0). Expected: the sweetener share is claimed in the same call as documented ('an accepted submission with a $YOLO add-on also claims payTo's share').","severity":"info","snippet":"        if (s.addon != 0 && s.outcome == Outcome.Accepted) bonusTask = s.taskId;","title":"Batched voter claim skips the YoloSweetener bonus share when the add-on arrived after the submission (s.addon fixed at submission, bonus paid on the whole add-on)"},{"citation":"resolved","description":"Drift between the doc and the code: CaseDesk._stakeBps fixes b (power read before the submission block, price parameter at submission); DisputeDesk._payReward only verifies the lock still runs (stakePower) and pays the fixed b. A reader of this line would expect a price or power change after submission to move b, which it does not (RULES: fixed at the lock, read before the submission block).","line":61,"path":"docs/YOLO.md","reproduction":"Lock power worth 50% of A at submission (b = 200 bps); before the payout the dev raises the YOLO price x10 (A in YOLO falls to 10% of its former value, power now worth 5x A). Payout pays 2% (fixed at submission), not 4% as the doc line implies.","severity":"info","snippet":"   to the treasury. Computed by the DisputeDesk at payout and booked by the vault from the fee it holds.","title":"docs/YOLO.md says the stake bonus is 'computed by the DisputeDesk at payout'; the code fixes it in CaseDesk at submission and the DisputeDesk only checks the lock"}],"hash":"aa33a0c72784830b0583803f351df6d8eb4298e90e5f474667d3bd2034090cf4","nodeId":"b1f23df7-8fef-4300-8dee-bf5bbfd0e647","outcome":"completed","summary":"Review complete. The findings file holds 4 entries (2 low, 2 info); the PoC fails on the current code as stated, and the full suite passes.\n\n**0. VERDICT**\nPayouts match the RULES in every outcome I traced: fee 10%, NFT 5% only when the NFT decided right, stakers' 5% with b, loser's bond 50/50 or 100%, bonds never doubled, YoloPredict 5/5/90, trophy burn, token split = 1e27, taxes 30% to 0. No money blocker, no theft path, no freeze. One rule deviation: a running lock whose b floors to 0 bps is settled as \"no stake\" (treasury 10% instead of burn 7.5% + treasury 2.5%); low. Top 3 changes in numbers: (1) staked split keyed on the lock, not on bps != 0: moves 7.5% of R per such task from treasury to burn; (2) YoloLockHub weeks-left rounded down or exact seconds: removes up to 1/52 of an added amount of over-credited power; (3) claimVoterRewards reads the sweetener pool rather than s.addon: no money, convenience only. Full suite: 664 passed, 0 failed, 1 skipped.\n\n**Items**\n\n1. ACCEPTED, NO DISPUTE. `DisputeDesk._payReward` (contracts/src/desks/DisputeDesk.sol:711-733), `TaskVault.burn/addTreasury` (contracts/src/TaskVault.sol:576-585, 665-679). R=100: no stake doer 90, treasury 10; b=400: doer 94, burn 0.5+5, treasury 0.5; b=200: 92 / 6.5 / 1.5; b=20: 90.20 / 7.40 / 2.40; the odd unit goes to the treasury (line 728). Same arithmetic in USDC (6 dec), ETH, IMD; in YOLO every treasury amount is sent to the burner (line 577). Confirmed by `MoneyRules.test_WorkedExamples` passing. No issue.\n\n2. DISPUTE, NFT RIGHT. `_settleDecided` (DisputeDesk.sol:611-701), `_splitCurrency` (746-777). Accepted: voters get 5% of R (line 714-717) plus `penalty - penalty*5000/10000` of each currency of the disputer's bond; doer gets the floor half and his own bond back (691-692). Rejected: the doer's bond split the same way, disputer's own bond back and the winner share via `claimDisputer` (331-362) pro rata. Voter shares remaining/remaining (431-437), exact, last claimant takes the dust. No issue.\n\n3. NFT WRONG OR UNDECIDED. `settle` line 505 defines nftRight; wrong or none gives `toVoters = 0` (760) and 100% to the winning side; in `_payReward` the NFT 5% goes to the treasury without a stake and is burned with one (729). Undecided, no call: `CaseDesk._endUncalled` (CaseDesk.sol:358-361) accepts, disputer's bond 100% to the doer (770). No issue.\n\n4. REJECTED. `TaskVault.closeSubmission` (430-448) leaves the pot untouched; the next submission's bond is recomputed from the same base (CaseDesk.sol:113); `claimVoidSubmission` (140-155) returns the queued bond after Completed/Unwound; a queue can never reach Expired because expiry needs `activeSub == 0` (TaskVault.sol:640). No issue.\n\n5. APPEAL MONEY. `_fundCall` (278-308), `_charge` (311-321), `_sideSpent`, `_penaltyAndWinnerBonds` (584-601). Party: first call min(fee, 0.5 IMD) out of its bond, rest and every later call from the wallet, no second bond; net on a win: bond back minus spent (691, 343); on a loss: penalty reduced by spent (632). Outsider: bond + fee; win: 0.5 IMD first off the loser's IMD penalty (636-640) then pro rata share and own bond back; loss: bond lost, unspent funds back (695). Silence: `dropCaller` returns bond and leftover, fee gone; three silences or voids end as uncalled (CaseDesk 211, 228, 421). Failed ask or not asked within swarmTimeout: full refund, counts (CaseDesk 206-212, 415-421). No issue.\n\n6. BONDS. `Desk._pctBond/_doerBond/_disputeBond` (contracts/src/desks/Desk.sol:118-137). Minimums: 5 USDC gives 0.15 / 0.05 USDC; 0.002 ETH gives 6e13 / 2e13 wei; 1 IMD gives 0.03 / 0.01 IMD (merged into one IMD pull, CaseDesk.sol:123); 1,000 YOLO gives 30 / 10 YOLO. Nothing rounds to 0; buckets split remaining/remaining; a currency with no winner bond goes to the treasury (765), unreachable at these minimums. No issue.\n\n7. STAKE BONUS. `CaseDesk._stakeBps` (502-515), `YoloLockHub.powerAt/stakePower` (101-113), `_lockRuns` (DisputeDesk 737-741). Base is A not R (510), floor to bps (514),","treeHash":null,"usage":{"cachedInputTokens":1926548,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":43988,"runtime":"claude","turns":38,"wallClockMs":910227}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"d11ea2b5e05fa7a8","findings":[{"citation":"resolved","description":"The NFT 5% of the fee (nftFeeBps) is paid to the votes on the final side whenever the NFT decided and the outcome agrees (nftRight), and the loser bond is split 50% winning side / 50% those votes (_splitCurrency). Nothing ties the disputer and the voters apart: one actor holding >= endDelta (10) NFTs (>= instantDelta 20 for an instant decision; minWinnerWallets = 1) can dispute an obviously honest submission from a second wallet, vote 'accept' with their NFTs, and be the only correct voter. They lose 50% of their own dispute bond to the doer and receive the other 50% back as the voter plus the whole NFT 5% of R, which otherwise went to the treasury. The doer is not harmed (gets 90% + half the bond), so nobody appeals; the treasury's fee share halves on every task where this is run. Net for the actor = 0.05*A - 0.5*(0.5 IMD + 0.01*A) + gas; at IMD $5.42 the break-even is A ~ $30.1. This is not doer self-dispute and needs no second party, so it is outside the 'by design' list.","line":715,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"Prices of the brief (IMD $5.42). Task R = A = 100 USDC, no stake, honest submission. Actor W holds 20 NFTs; from wallet W2 calls dispute(subId) posting 0.5 IMD + 1 USDC. W calls vote(subId, SIDE_ACCEPT, W, 20 ids) -> instant NFT decision Accept; nobody appeals; after appealWindow poke -> Accepted, nftRight = true. Expected per RULES: treasury 5 USDC, voters 5 USDC - the rule is followed literally, but the economics: W pays 2.71 + 1.00 = 3.71 USD, claimVoterReward pays W 0.25 IMD + 0.50 USDC (50% of own bond) + 5.00 USDC (fee) = 6.86 USD -> W nets +3.15 USD per $100 task, gas ~400k * 0.045 gwei * $2,434 = $0.04. Treasury receives 5 USDC instead of 10. At A = 5 USDC: W nets 0.25 - 0.5*(2.71+0.05) = -1.13 (unprofitable), so the attack starts around A > $30. Smallest fix (design decision): pay the NFT 5% only when the dispute was contested (e.g. the loser side had at least one vote, or the disputer's own payTo/wallet votes are excluded from the voter pool), or route an uncontested disputer's bond share 100% to the doer instead of 50% to the votes.","severity":"medium","snippet":"        uint256 toVoters = nftRight ? nftPart : 0;","title":"Frivolous dispute by an NFT holder is profitable above ~$30 of author reward: the treasury's 5% is diverted to the disputer's own votes"},{"citation":"resolved","description":"RULES: an outsider who posts bond + fee and wins 'first gets 0.5 IMD off the loser's bond'. The code first subtracts the loser side's own swarm spending (_sideSpent) from penaltyImd (line 632) and only then carves out the stranger fee, capped at what is left. With bondFixedImd = 0.5 IMD and a swarm price of 0.5 IMD, one silent call by the losing party empties its IMD bond, so penaltyImd is 0 and the outsider gets no fee back although they won. Two rules compete for the same 0.5 IMD; the losing party's fee wins. Bounded loss: 0.5 IMD ($2.71) per case, borne by the honest outsider; the doer or disputer who called first and lost is effectively subsidised.","line":636,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"Task R = A = 100 USDC. Doer submits (bond 0.5 IMD + 3 USDC). Disputer D disputes (0.5 IMD + 1 USDC). NFT vote ends tied -> Phase.NoDecision. Doer calls callSwarm(sidesDoer = true): _fundCall takes fromBond = 0.5 IMD from the doer bond (sideSpent.doer = 0.5 IMD). Swarm silent -> dropCaller, NoDecision again. Outsider S calls callSwarm(sidesDoer = false) posting bond 0.5 IMD + 1 USDC and paying fee 0.5 IMD from their wallet. Swarm answers Reject -> Rejected. _settleDecided: penaltyImd = 0.5 (doer bond) - 0.5 (sideSpent.doer) = 0 -> strangerFee branch skipped; S is credited only their bond back (0.5 IMD + 1 USDC) plus 1.5 USDC of the doer's 3 USDC rew bond (pro rata with D). Expected per RULES: S also gets 0.5 IMD first. Actual: S nets -0.5 IMD (-$2.71) + 1.5 USDC = -$1.21 despite winning. Fix: take the stranger fee before deducting the loser's own spending, or when penaltyImd is short, take the remainder from the reward-currency penalty, or document that the loser's own fee has priority.","severity":"low","snippet":"        if (appellantIsStranger && callerWins && penaltyImd != 0) {","title":"A winning outsider swarm caller loses their 0.5 IMD fee when the losing side's IMD bond was already spent on the loser's own silent swarm call"},{"citation":"resolved","description":"Submission.addon is the add-on as it stood at submission (SubmitTerms.addon). A sweetener added during the review window joins the bonus (YoloSweetener.payout reads bonusOf at payout, per RULES 'an add-on during review joins R'), and voters do get a voterPool there; but the batch claim path only tries YoloSweetener.claimVoter when s.addon != 0, so for a late add-on the voters' YOLO share waits until someone calls YoloSweetener.claimVoter(taskId, payTo) directly. No loss, only an extra call; the drift is between the batch helper and the add-on rule.","line":441,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"Task in USDC, no add-on at submit time. Doer submits; D disputes; NFT decides Accept; during the 24 h appeal window Alice sweetens 10,000 YOLO. Final Accepted, nftRight. YoloSweetener.payout(taskId): voterPool = 500 YOLO. Voter calls vault.claimVoterRewards([subId], voter): s.addon == 0 -> bonusTask = 0 -> the 500 YOLO are not claimed in this call; YoloSweetener.claimVoter(taskId, voter) afterwards pays them. Fix: read the add-on at finalize (bonusOf(taskId) at settle) or always try claimVoter when the task has a bonus paid out.","severity":"info","snippet":"        if (s.addon != 0 && s.outcome == Outcome.Accepted) bonusTask = s.taskId;","title":"claimVoterRewards does not auto-claim the YOLO add-on voter share when the add-on arrived after the submission"},{"citation":"resolved","description":"docs/YOLO.md 'Distribution' says every clock runs in 'months of 30.4 days' (12 months = 364.8 days); Vestings.sol uses 365 days for the 12-month vestings and 730 days for the 24-month one, as docs/TOKENOMICS.md and MECHANICS.md state. Drift of 0.2 days per year between two docs; amounts unaffected (the unlock table is computed on 365/730).","line":23,"path":"contracts/src/token/Vestings.sol","reproduction":"Compare docs/YOLO.md line 79 ('months of 30.4 days') with Vestings.sol lines 17, 23, 29, 37 and TOKENOMICS.md line 32. Fix: say '12 months = 365 days, 24 months = 730 days' in YOLO.md.","severity":"info","snippet":"    constructor(address beneficiary_) ProjectVesting(beneficiary_, 0, 365 days) {}","title":"Docs: YOLO.md states vesting months of 30.4 days while the vestings are compiled with 365 and 730 days"}],"hash":"c1e73852281e234ce57e5190bedeccfc0e7928a937d8e73b381d51cadb717fd9","nodeId":"25616ad3-e4f1-4ce3-aa4e-73076d59f89d","outcome":"completed","summary":"The build-wait watcher was also stopped at its time limit, consistent with the build itself never completing. Nothing changes in the deliverable: the report above and `.imd-findings.json` stand as final, with item 15 recording that `forge build` did not finish and no tests ran.","treeHash":null,"usage":{"cachedInputTokens":232454,"inputTokens":4,"model":"claude-fable-5-1","outputTokens":89,"runtime":"claude","turns":1,"wallClockMs":2744636}}],"verification":[]}