{"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":"532b2ea7-88fc-4027-8e59-f7d22aa83d47","kind":"audit","nodes":[{"acceptedSubmissionHash":"629dbf17ed95c33ce37c7f9a8e5221298c8d2de6d6166db233f34c62483320a6","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":"88cf090a1ad15690cc8c150698b0e8deb6b12def32578ee002b3f195d2899efb","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":"fac8a29213827dfd683d693c4466b2289a82cd73f9c9131a5ad780f97ce84e99","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":"1c62bf3a5f86ad4dd49e59acad5a767cdc7ea2e784b7fc3b97cd8acca67208a5","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":"1944d9f8427af0dbaefc6c0687db71c321286d80abe0e87f941112c6afafb027","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 5f6af8f1052f555553aad41a0e12fc1d29fa90b6. 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 (own top-ups in; others' top-ups and YOLO add-ons out). Payouts only from money the paying contract holds; token funds pay nothing. A user's tokens leave their wallet only in their own call; anyone else 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. 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, then burnYolo to 0x...dEaD; ETH, USDC, IMD to the buyback wallet at payout.\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, 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: each funder takes back their own. A top-up during review joins R. Submitted before the deadline, accepted after = done.\n- Cancel only by the dev wallet, any open phase: all back to who put it in, predictions 100%; swarm fees paid stay gone.\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 per docs/TOKENOMICS.md. Buy tax 30% -> 0 in minutes 30-60 after openAt; minutes 0-30 only snapshot wallets buy, 500,000 YOLO per NFT, untaxed. Sell tax 30% in 0-30, 30% -> 0 in 30-60, on top, to the treasury wallet.\n\nPrices (no network): IMD $5.42, ETH $2,434, YOLO $0.001 (parameter), swarm call 0.5 IMD = $2.71, gas 0.045 gwei; in code every price is a dev-wallet parameter, USDC = $1.\n\nALREADY FIXED IN THIS COMMIT (passing PoC in contracts/test/audit/): maxImdIn caps the IMD bond part too (MaxImdInCapTest); a late askAppealSwarm reverts (LateAppealAskTest); a 0 bps stake keeps the staked split (StakeFloorZeroSplitTest); stepDeadline while Appealed (StepDeadlineAppealedTest).\n\nACCEPTED BY DESIGN, documented in docs/MECHANICS.md Known limitations. Not findings unless the code differs from the documented rule: 20+ NFTs disputing from a second wallet; undisputed in 24 h = accepted, WON'T DO predictors dispute; an appeal not asked in 6 h void, full refund, new 24 h window; the queue judged in turn; one dispute per submission; cancelTask in any open phase, spent swarm fees kept; a started YoloLockHub week counted whole.\n\nREPORT FORMAT, MANDATORY. One entry per item, same order, even \"no issue\"; a missing item = INCOMPLETE. Entry 0: VERDICT, max 8 lines: payouts vs RULES, every money blocker. 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: the $100 examples in USDC, ETH, IMD, YOLO; b = 0.\n2. DISPUTE, NFT RIGHT. Accepted and rejected: loser's bond 50/50, winner's bond back, voter shares.\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, 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.\n9. EXPIRY AND CANCEL. withdrawContribution, cancelTask: each funder 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; 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 the minimum rewards and R $100: disputing a fake, an NFT vote, an outsider appeal vs gas; stake recycling via own top-ups, griefing cost.\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; an author profiting from a task nobody does; 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":"9c9310233199f6700daedce95c6853dcad337471e5a9d2ce3ef0b7cf48574f81","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"532b2ea7-88fc-4027-8e59-f7d22aa83d47","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":"51476","feedbackHash":"76d8fc8bf56ce0563143ec991e947fb8f496943bc4d4b6226e32e04fe0974fd5","nodeKey":"audit_economics","submissionHash":"629dbf17ed95c33ce37c7f9a8e5221298c8d2de6d6166db233f34c62483320a6","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52272","feedbackHash":"2ad3fc9670ee37129ed3f1c23c61aaec2eec562cb7b1183fef838a3c6b2c5647","nodeKey":"audit_flow","submissionHash":"88cf090a1ad15690cc8c150698b0e8deb6b12def32578ee002b3f195d2899efb","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51027","feedbackHash":"4c5b2b1431abf53ff5cce59f78c56eaf8438fd6cfa2652ebd3f049ac24888f89","nodeKey":"audit_judge","submissionHash":"fac8a29213827dfd683d693c4466b2289a82cd73f9c9131a5ad780f97ce84e99","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52685","feedbackHash":"db5b6ec8f80b04cf8f57ee954c0e43c1b5634c763540b38d31909f7f1fc0077c","nodeKey":"audit_math","submissionHash":"1c62bf3a5f86ad4dd49e59acad5a767cdc7ea2e784b7fc3b97cd8acca67208a5","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51512","feedbackHash":"22fa0d861a381b2bc29137cce354edc392fe0c4a18718ef677e58f1e0e83f84f","nodeKey":"audit_permissions","submissionHash":"1944d9f8427af0dbaefc6c0687db71c321286d80abe0e87f941112c6afafb027","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"33db29dfea3cf9717a3e9dfddfb520b1cfe1e38b971fea7374ec210c8fda2a7c","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"8c9c4986ca881107","findings":[{"citation":"resolved","description":"settle() decides that nobody was on the winning side by `willDoPointsTotal == 0` (and `wontDoPointsTotal == 0` at line 216), not by the side's pool. Points are `floor(floor(eff*rem/dur)*rem/dur)` (_points, line 285), so a position placed shortly before a far deadline has 0 points while its 90% sits in the pool: with duration > ~1e9 s (a deadline 32+ years out) a 1 YOLO position 1 s before the deadline gets 0 points. The market then pays the whole losing pool to the author and the doer (NoWinner) and claim() pays that winner only their effPosition (points == 0 skips the split). RULES: 'Winners: their 90 back + the losing pool by points' and 'Done with nobody on WILL DO' only when nobody is on it; docs/YOLO.md line 229: 'a lone late winner takes the whole losing pool'. Loss at the PoC's amounts: the winner loses 90 YOLO ($0.09 at $0.001; scales with the losing pool) to the author and doer. Bounded, needs a decades-long deadline, so low. Smallest fix: test `willDoPool == 0` / `wontDoPool == 0` for the no-winner branch, and in claim() when `sp.pointsLeft == 0` share the pool pro rata to the winning amounts (or floor points at 1 wei in _points).","line":209,"path":"contracts/src/YoloPredict.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.28;\n\nimport {Base} from \"../Base.t.sol\";\nimport {YoloPredict} from \"../../src/YoloPredict.sol\";\nimport \"../../src/VaultTypes.sol\";\n\n/// @notice YoloPredict.settle decides \"nobody on the winning side\" by `willDoPointsTotal == 0`, not by the pool.\n///         A WILL DO position whose points floor to 0 (1 YOLO placed 1 s before a far deadline: points =\n///         floor(floor(eff * rem / dur) * rem / dur) = 0 once dur > ~1e9 s) is a real winner with money in the\n///         pool, yet the whole losing pool is paid to the author and the doer and the winner gets only their 90% back.\n///         RULES: \"Winners: their 90 back + the losing pool by points\"; \"Done with nobody on WILL DO\" only when\n///         nobody is on it. Fails on this code; passes once the no-winner test uses the pool (and zero-point\n///         winners share the pool, e.g. by amount).\ncontract PredictZeroPointsWinnerTest is Base {\n    YoloPredict internal predict;\n\n    function setUp() public override {\n        super.setUp();\n        predict = new YoloPredict(address(vault), 1e18);\n        vm.prank(alice);\n        yolo.approve(address(predict), type(uint256).max);\n        vm.prank(bob);\n        yolo.approve(address(predict), type(uint256).max);\n    }\n\n    function test_ZeroPointWinner_LosesWholePoolToAuthorAndDoer() public {\n        // a 40-year dare: duration 1.26e9 s\n        uint64 deadline = uint64(_now() + 40 * 365 days);\n        uint256 t = _createWithDeadline(author, address(usdc), 100e6, deadline);\n\n        // bob: WON'T DO 100 YOLO at creation (90 to the pool); alice is the lone WILL DO winner below\n        vm.prank(bob);\n        predict.predict(t, false, 100e18);\n        uint256 losingPool = 90e18;\n\n        // 1 s before the deadline: alice takes WILL DO with 1 YOLO -> points = 0\n        vm.warp(deadline - 1);\n        vm.prank(alice);\n        predict.predict(t, true, 1e18);\n        (, uint256 willDoPoints,,) = predict.positionsOf(t, alice);\n        assertEq(willDoPoints, 0, \"points floor to 0\");\n        (,, uint256 willDoPool,,,,) = predict.markets(t);\n        assertEq(willDoPool, 0.9e18, \"but alice IS on WILL DO\");\n\n        // the doer submits in the same second, nobody disputes: done\n        uint256 s = _submit(doer, t);\n        _warp(24 hours);\n        vault.poke(t);\n        assertEq(uint8(vault.getTask(t).status), uint8(TaskStatus.Completed));\n        assertEq(uint8(_sub(s).outcome), uint8(Outcome.Accepted));\n\n        uint256 authorBefore = yolo.balanceOf(author);\n        uint256 doerBefore = yolo.balanceOf(doer);\n        uint256 aliceBefore = yolo.balanceOf(alice);\n        vm.prank(alice);\n        predict.claim(t);\n\n        // RULES: the lone winner gets her 90% back plus the whole losing pool; the deferred 5% of both positions\n        // (2.5% author, 2.5% doer) is all the author and the doer should receive from the market\n        uint256 deferred = (100e18 + 1e18) * 500 / 10_000;\n        assertEq(yolo.balanceOf(author) - authorBefore, deferred / 2, \"author: only the deferred 2.5%\");\n        assertEq(yolo.balanceOf(doer) - doerBefore, deferred - deferred / 2, \"doer: only the deferred 2.5%\");\n        assertEq(yolo.balanceOf(alice) - aliceBefore, 0.9e18 + losingPool, \"winner: 90% back + the losing pool\");\n    }\n}","reproduction":"Task deadline = now + 40 years (USDC 100e6). bob predicts WON'T DO 100 YOLO at creation (pool 90). At deadline-1 alice predicts WILL DO 1 YOLO: positionsOf(alice).willDoPoints == 0 while markets.willDoPool == 0.9e18. doer submits in the same second, 24 h later poke accepts: task Completed. alice claim(): expected alice +90.9 YOLO, author +2.525 (deferred 2.5%), doer +2.525. Actual: author +47.525 (45 of the losing pool + 2.525), doer +47.525, alice +0.9 only.","severity":"low","snippet":"        if (res == Resolution.WillDoWon && m.willDoPointsTotal == 0 && m.wontDoPool != 0) {","title":"YoloPredict: a winner whose points floor to 0 is treated as 'nobody on the winning side'; the losing pool goes to the author and the doer instead of the winner"}],"hash":"1944d9f8427af0dbaefc6c0687db71c321286d80abe0e87f941112c6afafb027","nodeId":"b691b9e6-642d-4995-9bf7-63e899ceb6ca","outcome":"completed","summary":"The audit is complete. The findings file holds one substantiated defect with a failing PoC; everything else checked matches the RULES.\n\n**Entry 0: VERDICT**\nEvery payout path in DisputeDesk, CaseDesk, TaskVault, YoloSweetener, YoloLockHub, BuybackBurner and the token matches the RULES at the stated amounts. No money blocker, no theft, no freeze. One low finding in YoloPredict: a winner whose points floor to 0 is treated as \"nobody on the winning side\" and the losing pool goes to the author and doer. The 670 non-fork tests pass. The four items marked fixed in this commit were not re-reported.\n\n**1. Accepted, no dispute.** `contracts/src/desks/DisputeDesk.sol:717` `_payReward`: fee = R x 1000/10000, nftPart 5%, no stake: doer R - fee, treasury fee. $100: USDC 90/10; ETH 0.036977/0.004108 of 0.041085; IMD 16.605/1.845 of 18.450; YOLO 90,000 doer, 10,000 to the BuybackBurner via `addTreasury` (`TaskVault.sol:576`). No issue. No PoC: matches.\n\n**2. Dispute, NFT right.** `DisputeDesk.sol:616-706` and `_splitCurrency:752`: toVoters = penalty - penalty x 5000/10000, winnerShare pro rata to winner bonds, winner's own bond credited at 695-704, voters claim pro rata at `_claimVoter:436-438`, odd unit to voters. No issue.\n\n**3. NFT wrong or undecided.** `settle:510` nftRight false → `toVoters = 0`, treasury gets the full 10% (or burn 5% + split with a stake), `_splitCurrency:766` gives 100% to the winning side; undecided and no call → `CaseDesk._endUncalled:358` Accepted, disputer bond 100% to the doer. No issue.\n\n**4. Rejected.** `TaskVault.closeSubmission:438-447`: pot untouched, next queued activated with its own fixed bond and `base`; `CaseDesk.claimVoidSubmission:140` pays a Queued bond back only on Completed/Unwound; an Expired task never has a queue. No issue.\n\n**5. Appeal money.** `_fundCall:282`: a party's first call from its bond (fromBond = min(fee, bondImd)), later calls from the wallet; an outsider posts a dispute bond + fee; `_charge:316` books sideSpent; stranger fee 0.5 IMD off penaltyImd at 641-645; void/silent → `dropCaller` refunds bond and unspent funds, fee gone. Nets (R $100 USDC): party first call -0.5 IMD of bond; second call -0.5 IMD wallet; outsider wins: +0.5 IMD back + share of the loser's rew bond (e.g. $0.25 of a $1 disputer bond). No issue.\n\n**6. Bonds.** `Desk.sol:118-137`: 0.5 IMD + 3% / 1% of `base` (author's active share, `TaskVault.submitTerms:409`). At the minimums: 0.15/0.05 USDC, 6e13/2e13 wei, 0.03/0.01 IMD, 30/10 YOLO. Dust with no winner bond goes to the treasury (`_splitCurrency:771`), unreachable since base ≥ minReward. No issue.\n\n**7. Stake bonus.** `CaseDesk._stakeBps:504` uses `t.base`, floors bps, reads `powerAt(doer, block.timestamp)` (strictly earlier points, `YoloLockHub.sol:101`); `stakePower:108` ignores later adds, returns 0 after withdraw or end; `_payReward:728` then uses the no-stake split. Power math `YoloLockHub.sol:79-82` = max(old + add x left/52, all x left/52). No issue (info: a lock that ended at payout falls to the treasury split, as docs/MECHANICS.md line 191 states).\n\n**8. YOLO add-on.** `YoloSweetener.payout:137-152`: 10% fee, 5% voters only when `nftDecision == Accept`, extra = bonus x Settlement.stakeBps capped at fee - nftPart, rest burned; `withdraw:108` each funder their own. No issue.\n\n**9. Expiry and cancel.** `TaskVault.withdrawContribution:334` pays `c.active` on Expired/Unwound, 0 on Completed; `cancelTask:364` dev only, any open phase, `_settleNeutral:567` bond - own spent fees. No issue.\n\n**10. YoloPredict.** `YoloPredict.sol:145-165` 5/5/90 split; `claim:234-250` winners eff + pool by points, last takes dust; void pays `paidIn`. Finding (low): `settle:209,216` test `*PointsTotal == 0` not the pool. PoC in the findings file fails now (author +47.525 YOLO instead of +2.525) and passes once the no-winner test uses the pool and zero-point winners share by amount. Fix: one condition change plus a fallback split.\n\n**11. Burns.** YOLO: `TaskVault._burn:665` → burner","treeHash":null,"usage":{"cachedInputTokens":2464204,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":39844,"runtime":"claude","turns":38,"wallClockMs":897880}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"776c025db24644e8","findings":[{"citation":"resolved","description":"Submission.addon is fixed at submission (CaseDesk.submit: s.addon = t.addon). A bonus sweetened during the review (allowed: task Open, before the deadline) is paid out by YoloSweetener.payout with the reward and books a 5% voter pool (nftDecision == Accept), but _claimVoter decides whether to call YoloSweetener.claimVoter from the stale s.addon == 0, so the documented behaviour of claimVoterRewards ('An accepted submission with a $YOLO add-on also claims payTo's share of its paid-out bonus') does not hold for that add-on. No money is lost: the voter can still call YoloSweetener.claimVoter(taskId, payTo) directly. Smallest fix: read the live add-on (`_bank().sweetener()` -> `bonusOf`/`voterVotesLeft`) or drop the `s.addon != 0` guard and rely on the try/catch, since claimVoter reverts NothingToPay when there is no pool.","line":446,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"USDC task R=100e6 by author; doer submits (Submission.addon = 0); alice sweetens 10,000e18 YOLO during the review; dave disputes; voter1 votes 20 NFTs accept (instant decision); +24h poke -> Completed. YoloSweetener.payout(t) books voterPool = 500e18 (5% of the add-on). vault.claimVoterRewards([s], voter1): expected the voter's YOLO share (500e18) to be claimed in the same batch; actual: sweetener.voterPool(t) is still 500e18 and voter1's YOLO balance is unchanged; a direct sweetener.claimVoter(t, voter1) then pays it. Verified in contracts/test/scratch/AuditLeads.t.sol test_AddonAfterSubmission_BatchClaimSkipsSweetener (passes = behaviour reproduced).","severity":"low","snippet":"        if (s.addon != 0 && s.outcome == Outcome.Accepted) bonusTask = s.taskId;","title":"claimVoterRewards skips the sweetener voter share when the $YOLO add-on arrived after the submission"},{"citation":"resolved","description":"The RULES state the sell tax as '30% in 0-30, 30% -> 0 in 30-60, on top'. The code takes tax = value * num / (1800e4 - num), i.e. the tax is 30% of (intoPool + tax): a swap settling 70 YOLO into the pool pulls 30 YOLO more from the seller (42.86% of what enters the pool). sellTaxBps() returns 3000 while the on-top rate against the swap amount is 4286 bps. docs/YOLO.md and TOKENOMICS.md describe this ('selling 100 YOLO at 30% sends 70 into the pool and 30 to the treasury'), so this is a wording drift between the RULES ('30% on top') and the implemented semantic, not a loss; a wallet quoting sellTaxBps() against the swap input under-estimates the tax by 1286 bps at minute 0-30 and 265 bps at minute 45 (num=2.7M: 17.65% on top vs 15% displayed).","line":119,"path":"contracts/src/token/YoloToken.sol","reproduction":"At openAt + 10 min a sell that settles 70e18 YOLO into the PoolManager: expected (per RULES '30% on top') tax = 21e18; actual tax = 70e18 * 5_400_000 / (18_000_000 - 5_400_000) = 30e18, pulled from the seller on top (test_SellTaxNumbers in contracts/test/scratch/AuditLeads.t.sol). sellTaxBps() = 3000 at that moment.","severity":"info","snippet":"        return value * num / (10_000 * (TAX_END_SECONDS - WHITELIST_SECONDS) - num);","title":"sellTaxBps() reports 30% of the seller's gross while the amount taken on top of what enters the pool is 42.86%"},{"citation":"resolved","description":"7.777777% of 1,000,000,000 is 77,777,770 YOLO; YoloSplitter.DEV is 77_777_777e18 (7.7777777%). The amount column and the code agree; the percentage is truncated by one digit. Same truncation in docs/YOLO.md and script/LaunchPlan.sol comments ('dev 7.777777%'). Documentation drift only: 7 YOLO.","line":16,"path":"docs/TOKENOMICS.md","reproduction":"YoloSplitter.DEV = 77_777_777e18; 1e27 * 7.777777 / 100 = 77_777_770e18; difference 7e18 YOLO. Expected the doc percentage to match the amount (7.7777777%).","severity":"info","snippet":"| Dev | 7.777777% | 77,777,777 | linear over 12 months |","title":"TOKENOMICS dev share percentage does not equal the coded 77,777,777 YOLO"}],"hash":"1c62bf3a5f86ad4dd49e59acad5a767cdc7ea2e784b7fc3b97cd8acca67208a5","nodeId":"adb86e6a-12bc-4924-867e-990ccf7a1347","outcome":"completed","summary":"The findings file is valid and every quoted line matches the tree. No existing file was modified. Report follows.\n\n**0. VERDICT.** Every payout traced in `DisputeDesk._payReward`, `_settleDecided`, `_splitCurrency`, `claimDisputer`, `_claimVoter`, `YoloSweetener.payout`, `YoloPredict.settle/claim` and the token/vesting split matches the RULES at the amounts stated. No critical or high. No money blocker found: every bucket is pull-claimable, every refund path (expiry, cancel, void appeal, silence, queued void) returns each funder exactly their own, and the three fixes listed as already landed hold. One low (batch voter claim misses a late add-on's YOLO share, recoverable by a direct call) and two info drifts (sell-tax wording, a doc percentage). Findings are in `.imd-findings.json`; the scratch reproduction is `contracts/test/scratch/AuditLeads.t.sol`.\n\n**1. Accepted, no dispute.** `DisputeDesk.sol:717-739`. $100 in USDC (100e6), ETH (0.0411e18), IMD (18.45e18), YOLO (100,000e18): fee = R×1000/1e4, nftPart = R×500/1e4, voters 0, treasury = fee, doer = R − fee → 90/0/10 in every currency; YOLO's treasury part goes to `addTreasury` → `_burn` → BuybackBurner (`TaskVault.sol:576-579`). Odd units floor into the doer's share. No issue.\n\n**2. Dispute, NFT right.** `_settleDecided` (`DisputeDesk.sol:616-706`) + `_splitCurrency` (`:752-783`). Accepted: penalty = disputer bond (0.5 IMD + 1% A) minus the disputer's own swarm spend; `toVoters = penalty − penalty×5000/1e4` (voters take the odd unit), winners 50% pro rata to bonds, doer bond credited back minus their own spend. Rejected: mirror with the doer bond (0.5 IMD + 3% A), disputers bucketed for `claimDisputer` pro rata. Voter shares `voterBond×n/left` with the last claimant taking the rest (`:436-442`). Fee 5% to voters only in this case (`:721`). No issue.\n\n**3. NFT wrong or undecided.** `settle` (`:499-515`): `nftRight` false when `nftDecision == None` or the final outcome disagrees → `voterVotesLeft` stays 0, `_splitCurrency` sends 100% to the winning side, unearned NFT 5% to treasury (no stake) or burn (`:735`, staked). `_endUncalled` (`CaseDesk.sol:358-361`) accepts an undecided, uncalled case; the disputer's bond then goes whole to the doer. No issue.\n\n**4. Rejected.** `closeSubmission` (`TaskVault.sol:430-448`): pot untouched, `rejectedCount++`, next queued activates with the same bond base (`Desk._doerBond`, no doubling). `claimVoidSubmission` (`CaseDesk.sol:140-155`) pays a queued doer both bond parts on Completed/Unwound; no queue can survive to Expired (expiry requires `activeSub == 0`, and a rejection always pops the queue). No issue.\n\n**5. Appeal money.** `_fundCall` (`DisputeDesk.sol:282-313`), `_charge` (`:316-326`), `_penaltyAndWinnerBonds` (`:589-606`), `_sideSpent`. Party first call: `fromBond = min(fee, 0.5 IMD)`, wallet pays the rest, no second bond; second call wholly from the wallet; silence/void refunds bond + unspent via `dropCaller`, the asked fee stays spent; three silences or voids → `_endUncalled`. Outsider: bond + fee; winning, `strangerFee = min(0.5 IMD, penaltyImd)` first (`:641-645`), then the pro-rata share. Net per case at R $100 USDC, fee 0.5 IMD: doer appeals a reject and wins: −0 IMD net (bond paid the fee), +50% or 100% of 0.5 IMD + 1 USDC; outsider appeals against the doer and wins: fee back, then shares 3 USDC with the disputer pro rata (1:1 bonds → +1.5 USDC); outsider siding with the doer and winning: +0.25 USDC (1 USDC split 3:1 with the doer's bond). Info: when the losing party already spent its whole 0.5 IMD on its own call, penaltyImd is 0 and the winning outsider gets no fee back; this follows the rule's wording \"off the loser's bond\". No RULES amount broken.\n\n**6. Bonds.** `Desk._pctBond` (`Desk.sol:118-122`), `submitTerms.base` (`TaskVault.sol:409`) = author's own active share. Minimums: USDC 5e6 → 150,000 / 50,000 units; ETH 0.002e18 → 6e13 / 2e13 wei; IMD 1e18 → 0.03 / 0.01; YOLO 1000e18 → 30 / 10. All 50/50 splits of these are exac","treeHash":null,"usage":{"cachedInputTokens":2256757,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":52316,"runtime":"claude","turns":36,"wallClockMs":1428441}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"e9049e5b12ba53ba","findings":[{"citation":"resolved","description":"RULES: 'a failed ask or an appeal not asked within 6 h counts, bond and fee back'. The appeal path (CaseDesk._askAppeal) does this: dropCaller refunds, then ++s.asks. The NoDecision escalation path (callSwarm -> askEscalation) instead reverts the whole transaction when the Intake request fails. The caller loses nothing (the revert undoes the bond and fee collection), but the failed ask is not counted in Submission.asks, and the submission stays in NoDecision. Nothing is mis-paid: the window still ends at callFrom + 24 h and _endUncalled settles it as accepted. So this is drift between the rule text and the code, not a money loss.","line":270,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"Submission in Phase.NoDecision, an Intake whose request() reverts while priceOf() returns 0.5e18. A stranger calls vault.callSwarm(subId, true, 2e18, payIn, rt). Expected per RULES: bond and fee credited back and s.asks == 1. Actual: the call reverts SwarmUnavailable, s.asks stays 0, no credit is booked, and the IMD stays in the wallet. Net money is the same in both cases (0 loss).","severity":"info","snippet":"        if (!ok) revert SwarmUnavailable();","title":"A failed ask in callSwarm reverts instead of counting toward the 3 swarm calls with bond and fee back"}],"hash":"629dbf17ed95c33ce37c7f9a8e5221298c8d2de6d6166db233f34c62483320a6","nodeId":"063375d8-d6c8-44d6-be11-c334e477048c","outcome":"completed","summary":"The build has now finished (exit 0), so I ran the audit test folder: **11 passed and 12 failed.**\n\nEvery failure I can see stops in contract setup, with `WiringClosed()` or `BindingClosed()` raised in `setUp()` or just after it. This includes `SwarmAudit117Test` and both tests in `VestingClockPreStart`; I did not look at the full list of all 12. Both errors come from one-time binding checks that read a transient-storage flag written in the constructor (`Desk.wire`, `ProjectVesting.bindStarter`). That points to how this forge version handles transient storage across calls inside a test setup, not to any payout rule. I have not confirmed that, and I did not rerun with `--isolate` or another forge version.\n\nSo the \"already fixed\" proofs (`MaxImdInCapTest`, `LateAppealAskTest`, `StakeFloorZeroSplitTest`, `StepDeadlineAppealedTest`) are not confirmed passing on this machine.\n\nMy audit conclusions are unchanged: no high or critical payout defect, and one info-level finding in `.imd-findings.json`. Item 15 (coverage) should now read: forge build succeeds; the audit tests give 11 passed and 12 failed, all failures seen being setup errors.","treeHash":null,"usage":{"cachedInputTokens":250735,"inputTokens":6,"model":"claude-opus-5-5","outputTokens":903,"runtime":"claude","turns":2,"wallClockMs":1077788}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ef31844bb462de78","findings":[{"citation":"resolved","description":"RULES: the lock behind b must still run at payout or b = 0. `stakePower(who, since)` checks only `p.epoch == l.epoch && l.end > now && p.end > since`, and `epoch` grows only in `withdraw`. A lock that ended (`end <= now`) can be set again with `lock(0, weeks)` without a withdrawal; the comment on line 65-66 says such a lock 'starts afresh' (old power 0), but it keeps the epoch, so at payout `stakePower` returns the power of the point before the submission and `DisputeDesk._lockRuns` pays the staked split. The lock behind the submission had expired; a new, possibly 1-week lock stands in for it. Loss: on R = $100 USDC with stakeBps 7 the doer gets $0.07 more and the treasury $7.535 less than the no-stake split it would otherwise settle with (treasury 10 -> 2.465, burn 0 -> 7.465); bounded by 4% of R. Smallest fix: in `lock`, when the previous lock is not running (`l.end <= block.timestamp`) start a new epoch: `uint32 ep = l.end > block.timestamp ? l.epoch : l.epoch + 1;` and store/checkpoint `ep`, so a re-lock after the end is another lock, as the comment states and as `withdraw` already does.","line":83,"path":"contracts/src/YoloLockHub.sol","reproduction":"Base fixture (YOLO price 0.001). doer: hub.lock(100_000e18, 1) (power 1,923 YOLO); warp 1s; author creates 100e6 USDC task; doer submits -> stakeBps 7, staked. dave disputes. warp 8 days (lock ended): hub.stakePower(doer, submittedAt) == 0. doer: hub.lock(0, 1) -> stakePower(doer, submittedAt) == 1,923 again. poke (NoDecision), warp 24h, poke -> Accepted. Expected (lock ended before payout, b = 0, no-stake split): treasuryPot(usdc) 10e6, buyback 0, doer credit 94e6 (90 + 3 bond + 1 dave). Actual: getSettlement(s).stakeBps == 7, treasuryPot 2.465e6, buybackWallet 7.465e6, doer credit 94.07e6. Run with: forge test --no-isolate --no-dynamic-test-linking --match-path contracts/test/scratch/Probe.t.sol --match-test test_C_RelockAfterEnd -vv","severity":"low","snippet":"        locks[msg.sender] = Lock(uint96(amount), uint64(end), l.epoch);","title":"Stake bonus paid although the lock behind it ended before payout: re-locking in the same epoch revives stakePower"},{"citation":"resolved","description":"RULES: an outsider who posts bond + fee and wins 'first gets 0.5 IMD off the loser's bond'. `penaltyImd` is reduced by the loser's own `_sideSpent` before this line (line 637), so when the losing side (the disputer or the doer) paid its first swarm call out of its 0.5 IMD bond and that call stayed silent, the loser's IMD penalty is 0 and the outsider's fee refund is 0: the outsider nets -0.5 IMD (-$2.71) on a won call instead of 0. docs/MECHANICS.md line 150 ('gets 0.5 IMD from the loser's IMD bond before the split') and line 194 do not mention this case. Consistent with 'off the loser's bond' when nothing is left, so info: either document 'if the loser's IMD bond still holds it' or, if the refund is meant to be guaranteed, take it from the loser's reward-currency bond part next (design decision).","line":641,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"Base fixture, 100e6 USDC task, doer submits, dave disputes, no votes -> NoDecision after 24h. dave calls the swarm against the doer (party: fee 0.5 IMD taken from his bond, imd balance unchanged); Intake completes with no answer (silent); poke -> NoDecision again. erin (outsider) calls the swarm siding the doer: pays 0.5 IMD bond + 1e6 USDC + 0.5 IMD fee. Swarm answers yes -> Accepted. Expected per RULES wording: erin credited 1.0 IMD (bond + 0.5 fee refund). Actual: erin credited 0.5 IMD (bond only) + 1.25e6 USDC; doer 0.5 IMD + 93.75e6 USDC. Run: forge test --no-isolate --no-dynamic-test-linking --match-path contracts/test/scratch/Probe.t.sol --match-test test_B_StrangerAfterPartySpentBond -vv","severity":"info","snippet":"        if (appellantIsStranger && callerWins && penaltyImd != 0) {\n            uint256 strangerFee = p.bondFixedImd > penaltyImd ? penaltyImd : p.bondFixedImd;","title":"Outsider swarm caller who wins gets no 0.5 IMD when the losing party already spent its IMD bond on its own silent call; docs state the refund unconditionally"},{"citation":"resolved","description":"Not a contract defect. The desks' wiring relies on EIP-1153 transient storage surviving from `new CaseDesk(...)` to the vault constructor's `wire` call inside the same setUp. With forge 1.8.5 on this machine every top-level call of a test runs isolated (transient storage reset) and the dynamic-test-linking preprocessor turns `new X()` into `vm.deployCode`, so `wire` sees `open == 0` and every test built on test/Base.t.sol, including contracts/test/audit/*, fails in setUp. The suite passes with `forge test --no-isolate --no-dynamic-test-linking`. Worth pinning in docs (README line 92 says plain `forge test`) or in CI, since the audit PoCs listed as 'already fixed' cannot be re-run with the documented command.","line":62,"path":"contracts/src/desks/Desk.sol","reproduction":"forge test --match-path contracts/test/audit/StakeFloorZeroSplit.t.sol -> [FAIL: WiringClosed()] setUp(). forge test --no-isolate --no-dynamic-test-linking --match-path contracts/test/audit/StakeFloorZeroSplit.t.sol -> 2 passed.","severity":"info","snippet":"        if (open == 0 || vault != address(0)) revert WiringClosed();","title":"Whole test suite reverts in setUp (WiringClosed) under forge 1.8.5 defaults; it needs --no-isolate (and --no-dynamic-test-linking)"}],"hash":"88cf090a1ad15690cc8c150698b0e8deb6b12def32578ee002b3f195d2899efb","nodeId":"83679f3a-4a4c-4848-9369-3c314cc67df2","outcome":"completed","summary":"The findings file is written and validated. Full suite: 673 pass, 3 fail (two gas-measurement tests and one Desks test, outside the money scope). Final report follows.\n\n**Entry 0: VERDICT.** Every payout traced in `DisputeDesk._payReward`, `_settleDecided`, `_splitCurrency`, `YoloSweetener.payout`, `YoloPredict.settle/claim`, `YoloToken._update` and the token split matches the RULES at the stated amounts; six probe scenarios reproduced the exact figures. No critical or high finding. One low: a lock that ended before payout still pays the stake bonus if re-extended in the same epoch (YoloLockHub.sol:83). Two info items: the outsider's 0.5 IMD refund is 0 when the losing party's IMD bond already went to a silent swarm call (DisputeDesk.sol:641, docs say it unconditionally), and the test suite only runs with `--no-isolate --no-dynamic-test-linking` on forge 1.8.5 (Desk.sol:62). No money blocker: token funds pay nothing, every bucket is pull-based, every user transfer is `msg.sender`-sourced. Code security and role scenarios: other orders, one info line each in items 15.\n\n1. **ACCEPTED, NO DISPUTE** (`DisputeDesk.sol:717-739`). b = 0, $100: USDC doer 90e6 + bond back, treasury 10e6; ETH 0.041: doer 0.0369, treasury 0.0041; IMD 18.45: doer 16.605, treasury 1.845; YOLO 100,000: doer 93,000 incl. bond, 10,000 to BuybackBurner, treasuryPot 0. Severity none; no PoC (probe `test_F_Examples` passes).\n2. **DISPUTE, NFT RIGHT** (`_settleDecided` :616-706, `_splitCurrency` :752). Accepted: disputer 0.5 IMD + 1% A split ceil-half voters, floor-half doer; doer bond credited back :696. Rejected: doer 0.5 IMD + 3% A split the same, half to `disputerReward` pro rata :780, disputer's own bond back in `claimDisputer` :347. Voter shares remaining/remaining :436-442. None; no PoC needed.\n3. **NFT WRONG OR UNDECIDED** (`settle` :510, `_endUncalled` CaseDesk.sol:358). nftRight false sets toVoters 0 :721 and winnerBondBps path skipped :766: unearned 5% to treasury (no stake) or burned (staked); loser's bond 100% to the winner. Probe A: NFT rejects, swarm accepts: doer 93.75 USDC + 0.5 IMD, treasury 10. None.\n4. **REJECTED** (`closeSubmission` TaskVault.sol:430-447, `claimVoidSubmission` CaseDesk.sol:140). Pot untouched, next queued activates with its own 0.5 IMD + 3% A bond fixed at submission :113; acceptance re-reads the pot :461. Void bond back in full :153. None.\n5. **APPEAL MONEY** (`_fundCall` :282, `_charge` :316, `_penaltyAndWinnerBonds` :589, :637, :641). Party: first call from its bond, second from wallet, no second bond; silence refunds bond + unspent, fee gone. Outsider wins after wrong NFT: +0.5 IMD stranger fee +0.25% A (probe A: erin 1.0 IMD, 1.25 USDC). Outsider wins after the losing party spent its IMD on a silent call: 0 IMD refund (probe B: erin 0.5 IMD). Info, finding 2. Three silences / void ask: `s.asks` reaches 3, outcome as uncalled :211,:228,:421. Each party's net at $100: doer appeal won +1% A −0 IMD; doer lost −3% A −0.5 IMD; outsider won +0.25% A (+$1.50 if the doer lost) −gas; outsider lost −1% A −1 IMD.\n6. **BONDS** (`Desk.sol:118-137`). Base A at minimums: 5 USDC → 0.15/0.05; 0.002 ETH → 6e13/2e13 wei; 1 IMD → 0.03/0.01; 1,000 YOLO → 30/10. No rounding to 0; `winnerBonds == 0` dust-to-treasury branch :771 unreachable at the minimums. None.\n7. **STAKE BONUS** (`CaseDesk._stakeBps` :504-521, `_lockRuns` :743, `YoloLockHub.stakePower` :108). Base A not R :516, floor :520, add/extend after submission ignored (point before `since`), lock ended no relock → no-stake split (probe D: treasury 10). Lock ended then `lock(0,1)` → bonus paid (probe C). **Low**, finding 1; loss ≤ 4% of R ($0.07 at stakeBps 7, treasury −$7.54 on $100); fix: new epoch on a re-lock after the end.\n8. **YOLO ADD-ON** (`YoloSweetener.payout` :125-156). Same split as R, voters only when `nftDecision == Accept` on an accepted submission :140, stake share via `Settlement.stakeBps` :146, refunds on Expired/Unwound :108. None.\n9. **EXPIRY AND CANCEL** (`withdrawC","treeHash":null,"usage":{"cachedInputTokens":4417100,"inputTokens":706,"model":"claude-fable-5-1","outputTokens":58469,"runtime":"claude","turns":47,"wallClockMs":1780262}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"61b4050710026370","findings":[{"citation":"resolved","description":"settle() decides that nobody was on the winning side by willDoPointsTotal == 0 (line 209) / wontDoPointsTotal == 0 (line 216), not by that side's pool. _points (line 285) is floor(floor(eff*rem/dur)*rem/dur), so a position placed shortly before a far deadline has 0 points while its 90% sits in the pool: with duration above ~1e9 s (a deadline 30+ years out, which TaskVault.createTask allows, it only requires deadline > now) a 1 YOLO position 1 s before the deadline gets 0 points. The market then pays the whole losing pool to the author and the doer (NoWinner) and claim() pays that winner only their effPosition (points == 0 skips the split). RULES: 'Winners: their 90 back + the losing pool by points' and 'Done with nobody on WILL DO' only when nobody is on it; docs/YOLO.md line 229: 'a lone late winner takes the whole losing pool'. Loss at the reproduction's amounts: the winner loses 90 YOLO ($0.09 at $0.001; it scales with the losing pool). Bounded and needs a decades-long deadline, so low. Smallest fix: test willDoPool == 0 / wontDoPool == 0 in the no-winner branches, and in claim() share the pool pro rata to the winning amounts when the winning side's points are 0 (or floor points at 1 wei in _points). Merged from the audit_permissions specialist; reproduced here with their proof.","line":209,"path":"contracts/src/YoloPredict.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.28;\n\nimport {Base} from \"../Base.t.sol\";\nimport {YoloPredict} from \"../../src/YoloPredict.sol\";\nimport \"../../src/VaultTypes.sol\";\n\n/// @notice YoloPredict.settle decides \"nobody on the winning side\" by `willDoPointsTotal == 0`, not by the pool.\ncontract PredictZeroPointsWinnerTest is Base {\n    YoloPredict internal predict;\n\n    function setUp() public override {\n        super.setUp();\n        predict = new YoloPredict(address(vault), 1e18);\n        vm.prank(alice);\n        yolo.approve(address(predict), type(uint256).max);\n        vm.prank(bob);\n        yolo.approve(address(predict), type(uint256).max);\n    }\n\n    function test_ZeroPointWinner_LosesWholePoolToAuthorAndDoer() public {\n        uint64 deadline = uint64(_now() + 40 * 365 days);\n        uint256 t = _createWithDeadline(author, address(usdc), 100e6, deadline);\n\n        vm.prank(bob);\n        predict.predict(t, false, 100e18);\n        uint256 losingPool = 90e18;\n\n        vm.warp(deadline - 1);\n        vm.prank(alice);\n        predict.predict(t, true, 1e18);\n        (, uint256 willDoPoints,,) = predict.positionsOf(t, alice);\n        assertEq(willDoPoints, 0, \"points floor to 0\");\n        (,, uint256 willDoPool,,,,) = predict.markets(t);\n        assertEq(willDoPool, 0.9e18, \"but alice IS on WILL DO\");\n\n        uint256 s = _submit(doer, t);\n        _warp(24 hours);\n        vault.poke(t);\n        assertEq(uint8(vault.getTask(t).status), uint8(TaskStatus.Completed));\n        assertEq(uint8(_sub(s).outcome), uint8(Outcome.Accepted));\n\n        uint256 authorBefore = yolo.balanceOf(author);\n        uint256 doerBefore = yolo.balanceOf(doer);\n        uint256 aliceBefore = yolo.balanceOf(alice);\n        vm.prank(alice);\n        predict.claim(t);\n\n        uint256 deferred = (100e18 + 1e18) * 500 / 10_000;\n        assertEq(yolo.balanceOf(author) - authorBefore, deferred / 2, \"author: only the deferred 2.5%\");\n        assertEq(yolo.balanceOf(doer) - doerBefore, deferred - deferred / 2, \"doer: only the deferred 2.5%\");\n        assertEq(yolo.balanceOf(alice) - aliceBefore, 0.9e18 + losingPool, \"winner: 90% back + the losing pool\");\n    }\n}","reproduction":"Base fixture. Task deadline = now + 40 years (USDC 100e6). bob predicts WON'T DO 100 YOLO at creation (pool 90e18). At deadline-1 alice predicts WILL DO 1 YOLO: positionsOf(alice).willDoPoints == 0 while markets.willDoPool == 0.9e18. doer submits in the same second, 24 h later poke accepts: task Completed. alice claim(): expected alice +90.9 YOLO, author +2.525 (deferred 2.5%), doer +2.525. Actual (forge test --match-path contracts/test/scratch/Proof_2420c475f563.t.sol): author +47.525 (45 of the losing pool + 2.525), doer +47.525, alice +0.9 only. Test fails 'author: only the deferred 2.5%: 47525000000000000000 != 2525000000000000000'.","severity":"low","snippet":"        if (res == Resolution.WillDoWon && m.willDoPointsTotal == 0 && m.wontDoPool != 0) {","title":"YoloPredict: a winner whose points floor to 0 counts as 'nobody on the winning side'; the losing pool goes to the author and the doer instead of the winner"},{"citation":"resolved","description":"RULES: the lock behind b must still run at payout or b = 0. stakePower(who, since) (line 108-113) only checks p.epoch == l.epoch && l.end > now && p.end > since, and epoch grows only in withdraw(). A lock that ended (end <= now) can be set again with lock(0, weeks) without a withdrawal; the comment on lines 65-66 says such a lock 'starts afresh' (old power 0), but line 83 keeps l.epoch, so at the payout stakePower returns the power of the point before the submission and DisputeDesk._lockRuns (line 743) pays the staked split with the old bps. The lock behind the submission had expired; a new, possibly 1-week, lock stands in for it. Loss on R = $100 USDC with stakeBps 7: doer +$0.07 (94.07 instead of 94.00), treasury 2.465 instead of 10, buyback wallet 7.465 instead of 0; bounded by 4% of R to the doer and 10% of R shifted from the treasury to the burn. Low: the doer could have extended the lock one second before its end at the same cost and kept b legitimately, so the gaming value is small; it is a drift from the stated rule. Smallest fix: in lock(), when the previous lock is not running (l.end <= block.timestamp) start a new epoch (uint32 ep = l.end > block.timestamp ? l.epoch : l.epoch + 1) and store/checkpoint ep, so a re-lock after the end is another lock, as withdraw() already does. Merged from the audit_flow specialist; reproduced here.","line":83,"path":"contracts/src/YoloLockHub.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.28;\n\nimport {Base} from \"../Base.t.sol\";\nimport \"../../src/VaultTypes.sol\";\n\n/// @notice A lock that ended before the payout is set again (lock(0, 1)) without a withdrawal: same epoch, so\n///         YoloLockHub.stakePower returns the pre-submission power again and the staked split is paid.\ncontract RelockAfterEndTest is Base {\n    function test_RelockAfterEnd_RevivesStakeBonus() public {\n        // YOLO $0.0001 in Base: $100 = 1,000,000 YOLO. 1,000,000 YOLO for 1 week: power 19,230 -> 7 bps.\n        vm.prank(doer);\n        hub.lock(1_000_000e18, 1);\n        _warp(1);\n        uint256 t = _create(author, address(usdc), 100e6);\n        uint256 s = _submit(doer, t);\n        assertEq(_sub(s).stakeBps, 7, \"b = 7 bps\");\n        assertTrue(_sub(s).staked);\n        uint64 since = _sub(s).submittedAt;\n        _dispute(dave, s);\n\n        _warp(8 days); // the lock ended\n        assertEq(hub.stakePower(doer, since), 0, \"lock ended: no stake power\");\n        vm.prank(doer);\n        hub.lock(0, 1); // re-lock, no withdrawal in between\n        assertEq(hub.stakePower(doer, since), uint256(1_000_000e18) / 52, \"revived without a withdrawal\");\n\n        vault.poke(t); // vote ended undecided -> NoDecision -> nobody called: accepted, disputer's bond to the doer\n        assertEq(uint8(_sub(s).outcome), uint8(Outcome.Accepted));\n\n        // RULES: the lock must still run at payout or b = 0 -> no-stake split: treasury 10, burn 0\n        assertEq(vault.getSettlement(s).stakeBps, 0, \"b = 0: the lock behind the submission ended\");\n        assertEq(vault.treasuryPot(address(usdc)), 10e6, \"treasury 10%\");\n        assertEq(usdc.balanceOf(buybackWallet), 0, \"no burn\");\n        assertEq(vault.credits(address(usdc), doer), 90e6 + 3e6 + 1e6, \"90 + own bond + disputer's bond\");\n    }\n}","reproduction":"Base fixture (YOLO $0.0001: $100 = 1,000,000 YOLO). doer: hub.lock(1_000_000e18, 1) (power 19,230 YOLO); warp 1 s; author creates a 100e6 USDC task; doer submits -> Submission.stakeBps == 7, staked == true; dave disputes. warp 8 days (lock ended): hub.stakePower(doer, submittedAt) == 0. doer: hub.lock(0, 1) -> hub.stakePower(doer, submittedAt) == 19,230e18 again. vault.poke: vote undecided -> NoDecision -> nobody called -> Accepted. Expected (lock ended before payout, b = 0, no-stake split): getSettlement(s).stakeBps == 0, treasuryPot(usdc) == 10e6, buyback wallet 0, doer credit 94e6. Actual (forge test --match-path contracts/test/scratch/Relock.t.sol): getSettlement(s).stakeBps == 7 (assertion '7 != 0'), treasuryPot 2.465e6, buybackWallet 7.465e6, doer credit 94.07e6.","severity":"low","snippet":"        locks[msg.sender] = Lock(uint96(amount), uint64(end), l.epoch);","title":"Stake bonus paid although the lock behind it ended before the payout: re-locking in the same epoch revives stakePower"},{"citation":"resolved","description":"Submission.addon is fixed at submission (CaseDesk.submit line 112: s.addon = t.addon). YoloSweetener.sweeten (line 97) allows a bonus while the task is Open and before the deadline, so an add-on can arrive during the review. YoloSweetener.payout then pays it by the reward's rule and books a 5% voter pool when nftDecision == Accept (lines 140-144), but _claimVoter decides whether to call YoloSweetener.claimVoter from the stale s.addon == 0, so the documented behaviour of claimVoterRewards (lines 382-383: 'An accepted submission with a $YOLO add-on also claims payTo's share of its paid-out bonus') does not hold for that add-on. No money is lost: the voter can still call YoloSweetener.claimVoter(taskId, payTo) directly, so low. Smallest fix: drop the s.addon != 0 guard and rely on the existing try/catch (claimVoter reverts NothingToPay when there is no pool), or read the live bonus from the sweetener. Merged from the audit_math specialist; reproduced here.","line":446,"path":"contracts/src/desks/DisputeDesk.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.28;\n\nimport {Base} from \"../Base.t.sol\";\nimport {VoteLib} from \"../../src/libraries/VoteLib.sol\";\nimport \"../../src/VaultTypes.sol\";\n\n/// @notice An add-on sweetened during the review: claimVoterRewards skips the sweetener voter share (s.addon == 0).\ncontract AddonAfterSubmissionTest is Base {\n    function test_AddonAfterSubmission_BatchClaimSkipsSweetener() public {\n        uint256 t = _create(author, address(usdc), 100e6);\n        uint256 s = _submit(doer, t);\n        assertEq(_sub(s).addon, 0);\n        vm.prank(alice);\n        sweetener.sweeten(t, 10_000e18);\n        _dispute(dave, s);\n        _vote(s, 1, VoteLib.SIDE_ACCEPT, 120, 20); // instant decision: accept\n        assertEq(uint8(_phase(s)), uint8(Phase.Decided));\n        _warp(24 hours);\n        vault.poke(t);\n        assertEq(uint8(vault.getTask(t).status), uint8(TaskStatus.Completed));\n        sweetener.payout(t);\n        assertEq(sweetener.voterPool(t), 500e18, \"5% of the add-on to the right votes\");\n\n        uint256[] memory ids = new uint256[](1);\n        ids[0] = s;\n        uint256 before = yolo.balanceOf(voters[1]);\n        vault.claimVoterRewards(ids, voters[1]);\n        assertEq(yolo.balanceOf(voters[1]) - before, 500e18, \"the batch claim pays the add-on share too\");\n        assertEq(sweetener.voterPool(t), 0);\n    }\n\n    /// @dev Control: the add-on before the submission is claimed in the batch.\n    function test_AddonBeforeSubmission_BatchClaimPaysSweetener() public {\n        uint256 t = _create(author, address(usdc), 100e6);\n        vm.prank(alice);\n        sweetener.sweeten(t, 10_000e18);\n        uint256 s = _submit(doer, t);\n        assertEq(_sub(s).addon, 10_000e18);\n        _dispute(dave, s);\n        _vote(s, 1, VoteLib.SIDE_ACCEPT, 120, 20);\n        _warp(24 hours);\n        vault.poke(t);\n        sweetener.payout(t);\n        uint256[] memory ids = new uint256[](1);\n        ids[0] = s;\n        uint256 before = yolo.balanceOf(voters[1]);\n        vault.claimVoterRewards(ids, voters[1]);\n        assertEq(yolo.balanceOf(voters[1]) - before, 500e18);\n    }\n}","reproduction":"Base fixture. USDC task R = 100e6 by author; doer submits (Submission.addon == 0); alice sweetens 10,000e18 YOLO during the review; dave disputes; voter1 votes 20 NFTs accept (instant decision); +24 h poke -> Completed. sweetener.payout(t) books voterPool(t) == 500e18. vault.claimVoterRewards([s], voter1): expected voter1 +500e18 YOLO in the same batch and voterPool(t) == 0; actual (forge test --match-path contracts/test/scratch/AddonAfterSub.t.sol): voter1's YOLO balance unchanged ('0 != 500000000000000000000'), voterPool(t) still 500e18. Control in the same file: the same add-on placed before the submission is paid by the batch claim.","severity":"low","snippet":"        if (s.addon != 0 && s.outcome == Outcome.Accepted) bonusTask = s.taskId;","title":"claimVoterRewards skips the sweetener voter share when the $YOLO add-on arrived after the submission"},{"citation":"resolved","description":"RULES: an outsider who posts bond + fee and wins 'first gets 0.5 IMD off the loser's bond'. penaltyImd is reduced by the loser's own _sideSpent before this line (line 637), so when the losing side (the disputer or the doer) paid its first swarm call out of its 0.5 IMD bond and that call stayed silent, the loser's IMD penalty is 0 and the outsider's fee refund is 0: the outsider nets -0.5 IMD (-$2.71) on a won call instead of 0. docs/MECHANICS.md line 150 ('gets 0.5 IMD from the loser's IMD bond before the split') and line 194 do not mention this case. Consistent with 'off the loser's bond' when nothing is left, so info: either document 'if the loser's IMD bond still holds it' or, if the refund is meant to be guaranteed, take it from the loser's reward-currency bond part next (a design decision). Merged from the audit_flow specialist; reproduced here.","line":641,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"Base fixture, 100e6 USDC task, doer submits, dave disputes, no votes -> NoDecision after 24 h. dave calls the swarm against the doer (party: 0.5 IMD fee taken from his bond, his IMD balance unchanged); the Intake completes with no answer (silent); poke -> NoDecision again. erin (outsider) calls the swarm siding with the doer: pays 0.5 IMD bond + 1e6 USDC + 0.5 IMD fee. The swarm answers yes -> Accepted. Expected per the RULES wording: erin credited 1.0 IMD (bond + 0.5 fee refund). Actual (forge test --match-path contracts/test/scratch/StrangerFee.t.sol -vv): erin credited 0.5 IMD (bond only) + 1.25e6 USDC; doer 0.5 IMD + 93.75e6 USDC. Net money: nothing is lost by the protocol; the fee went to the swarm.","severity":"info","snippet":"        if (appellantIsStranger && callerWins && penaltyImd != 0) {","title":"An outsider swarm caller who wins gets no 0.5 IMD when the losing party already spent its IMD bond on its own silent call; docs state the refund unconditionally"},{"citation":"resolved","description":"RULES: 'a failed ask or an appeal not asked within 6 h counts, bond and fee back'. The appeal path (CaseDesk._askAppeal lines 403-422) does this: dropCaller refunds, then ++s.asks. The NoDecision escalation path (callSwarm -> askEscalation) instead reverts the whole transaction when the Intake request fails. The caller loses nothing (the revert undoes the bond and fee collection), but the failed ask is not counted in Submission.asks and the submission stays in NoDecision. Nothing is mis-paid: the window still ends at callFrom + 24 h and _endUncalled settles it as accepted. Drift between the rule text and the code, no money loss. Merged from the audit_economics specialist; confirmed by reading the code paths.","line":270,"path":"contracts/src/desks/DisputeDesk.sol","reproduction":"Submission in Phase.NoDecision, an Intake whose request() reverts while priceOf() returns 0.5e18. A stranger calls vault.callSwarm(subId, true, 2e18, payIn, rt). Expected per RULES: bond and fee credited back and s.asks == 1. Actual: the call reverts SwarmUnavailable (DisputeDesk.sol line 270), s.asks stays 0, no credit is booked, and the IMD stays in the wallet. Net money is the same in both cases (0 loss).","severity":"info","snippet":"        if (!ok) revert SwarmUnavailable();","title":"A failed ask in callSwarm reverts instead of counting toward the 3 swarm calls with bond and fee back"},{"citation":"resolved","description":"7.777777% of 1,000,000,000 is 77,777,770 YOLO; YoloSplitter.DEV (contracts/src/token/YoloSplitter.sol line 33) is 77_777_777e18 (7.7777777%). The amount column and the code agree; the percentage is truncated by one digit. Same truncation in docs/YOLO.md line 89, contracts/src/token/Vestings.sol line 20 and contracts/script/LaunchPlan.sol line 85 ('dev 7.777777%'). Documentation drift only: 7 YOLO ($0.007). Fix: write 7.7777777%. Merged from the audit_math specialist; confirmed against the files.","line":16,"path":"docs/TOKENOMICS.md","reproduction":"YoloSplitter.DEV = 77_777_777e18; 1e27 * 7.777777 / 100 = 77_777_770e18; difference 7e18 YOLO. Expected the doc percentage to match the amount (7.7777777%).","severity":"info","snippet":"| Dev | 7.777777% | 77,777,777 | linear over 12 months |","title":"docs/TOKENOMICS.md dev share percentage (7.777777%) does not equal the coded 77,777,777 YOLO"}],"hash":"fac8a29213827dfd683d693c4466b2289a82cd73f9c9131a5ad780f97ce84e99","nodeId":"a17396b7-e6af-4960-a6f1-95c886f7babc","outcome":"completed","summary":"All six kept findings are written and their snippets verify against the tree. Wrapping up with a recap.\n\nThe review is complete. `.imd-findings.json` holds six verified findings, merged from the four specialists' eight claims.\n\n**Kept and reproduced** (all with failing tests under `contracts/test/scratch/`, run on forge 1.8.3 with the repo's default settings):\n\n- **Low, YoloPredict line 209.** A winner whose points floor to 0 is treated as nobody on the winning side. The losing pool goes to the author and doer. The specialist's proof fails as stated. It needs a deadline over 30 years, which the vault allows, so low.\n- **Low, YoloLockHub line 83.** Re-locking an expired lock without withdrawing keeps the epoch, so stakePower revives and the staked split pays at payout. My test shows stakeBps 7 settled after the lock ended. The doer could have extended before expiry at the same cost, so low.\n- **Low, DisputeDesk line 446.** The batch voter claim skips the sweetener share for an add-on placed after the submission. The direct sweetener claim still pays, so no loss.\n- **Info, DisputeDesk line 641.** An outsider who wins gets no 0.5 IMD refund when the loser already spent its IMD bond on a silent call. Reproduced. The docs state the refund unconditionally.\n- **Info, DisputeDesk line 270.** A failed Intake request in callSwarm reverts instead of counting as a call with refund. No money moves either way.\n- **Info, docs/TOKENOMICS.md line 16.** The dev percentage is truncated by one digit against the coded 77,777,777 YOLO.\n\n**Dropped:**\n\n- The sell tax wording claim. The code, the docstring and the docs agree that 30% is of the seller's gross, so nothing is off.\n- The WiringClosed suite failure. It is specific to forge 1.8.5. On this machine the audit tests pass with plain `forge test`.\n\nThree low findings carry proofs. They build on the repo's own `contracts/test/Base.t.sol` fixture, as every test in `contracts/test/audit/` does. The scratch directory is untracked and nothing else in the repository was modified.","treeHash":null,"usage":{"cachedInputTokens":1199087,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":25460,"runtime":"claude","turns":33,"wallClockMs":410442}}],"verification":[]}