# Audit report

> 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/.
>
> SOURCE 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.
>
> SCOPE. 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.
>
> RULES. 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.
> - 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.
> - No stake: doer 90%, treasury 10% (5% if the NFT 5% was earned).
> - 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.
> - 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.
> - 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.
> - 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.
> - 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.
> - NFT undecided and no swarm call: accepted, the disputer's bond 100% to the doer.
> - Rejected: R stays in the task, open for the next submission.
> - 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.
> - 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.
> - 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.
> - 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.
> - Trophy: 7,777 YOLO, burned.
> - 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.
>
> Prices (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.
>
> REPORT 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.
>
> 1. ACCEPTED, NO DISPUTE. DisputeDesk._payReward, TaskVault burn/addTreasury: the three $100 examples in USDC, ETH, IMD, YOLO; b = 0.
> 2. DISPUTE, NFT RIGHT. Accepted and rejected: reward split, loser's bond 50/50, winner's bond back, voter shares exact.
> 3. NFT WRONG OR UNDECIDED. Swarm overturns or decides; undecided, no call: unearned NFT 5%, 100% of the loser's bond.
> 4. REJECTED. R after a fake, the next submission's bond and payout, claimVoidSubmission.
> 5. 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.
> 6. BONDS. Base A, rounding at the minimum rewards (5 USDC, 0.002 ETH, 1 IMD, 1,000 YOLO), who keeps dust.
> 7. STAKE BONUS. CaseDesk._stakeBps, YoloLockHub, _lockRuns: base A not R, floor to bps, add/extend after submission, lock ended before payout.
> 8. YOLO ADD-ON. YoloSweetener: same split as R, voters only after a right NFT decision, refunds, add-on during review.
> 9. EXPIRY AND CANCEL. withdrawContribution, cancelTask amounts: each funder exactly their part.
> 10. YOLOPREDICT. settle, claim, _split, _points: claims + burned + deferred = paid in, empty sides, a winning side with zero points, void.
> 11. BURNS. Every burn path to its end; YOLO reaching a wallet instead; monthly burn vs unlock at 200 tasks/month, R $100.
> 12. TOKENOMICS VS CODE. token/, LaunchPlan: split = 1e27, dust, unlock table, both taxes at minutes 0, 15, 30, 45, 60, tax recipient.
> 13. 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.
> 14. DOCS VS CODE. Every amount in the three docs, one line each.
> 15. COVERAGE. Files read in full, what was not reached, forge build and test result.
>
> Not 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.

| | |
|---|---|
| Repository | https://github.com/sergey-mavr-agent/moneycanbuy.fun.git |
| Commit | `d9c24e3b2d199548faf236d7bb5e80e4a12b6ada` |
| Job | `0c5450ce-5174-4618-b72b-30fa84631fdc` |
| Judged | 2026-10-19 11:53 UTC |
| Findings | 2 medium · 5 low · 4 info |

Four agents audited the code as it is at `d9c24e3`, each in one area (math, permissions, economics, control flow),
and a judge reproduced, merged and ranked what they found, then read the code once more itself. Nothing in the repository was changed or deployed.

## Findings

### 1. Medium: An NFT holder disputing an honest submission from a second wallet takes the NFT 5% of R from the treasury at a profit

`contracts/src/desks/DisputeDesk.sol:715`

```
        uint256 toVoters = nftRight ? nftPart : 0;
```

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.

**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).

### 2. Medium: 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%

`contracts/src/desks/DisputeDesk.sol:722`

```
        if (bps != 0 && _lockRuns(f)) {
```

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.

**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).

### 3. Low: YoloPredict.settle treats a winning side whose positions all have zero points as empty and pays the losing pool to the author and doer

`contracts/src/YoloPredict.sol:209`

```
        if (res == Resolution.WillDoWon && m.willDoPointsTotal == 0 && m.wontDoPool != 0) {
```

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).

**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).

**Proof**: a Foundry test that fails on this code and passes once it is fixed.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {Test} from "forge-std/Test.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {YoloPredict} from "src/YoloPredict.sol";
import {Task, Submission, TaskStatus, Outcome} from "src/VaultTypes.sol";

contract Yolo is ERC20 {
    constructor() ERC20("YOLO", "YOLO") {
        _mint(msg.sender, 1e27);
    }
}

/// @dev the narrow vault view YoloPredict reads
contract VaultStub {
    Task internal _task;
    Submission internal _sub;
    IERC20 public immutable yolo;

    constructor(IERC20 y, address author, address doer, uint64 createdAt, uint64 deadline) {
        yolo = y;
        _task.author = author;
        _task.deadline = deadline;
        _task.createdAt = createdAt;
        _task.status = TaskStatus.Open;
        _task.activeSub = 0;
        _sub.doer = doer;
        _sub.outcome = Outcome.Accepted;
    }

    function complete() external {
        _task.status = TaskStatus.Completed;
        _task.activeSub = 1;
    }

    function poke(uint256) external {}

    function getTask(uint256) external view returns (Task memory) {
        return _task;
    }

    function getSubmission(uint256) external view returns (Submission memory) {
        return _sub;
    }

    function registry() external view returns (address) {
        return address(this);
    }
}

/// Finding: YoloPredict.settle treats a winning side whose positions all rounded to zero points as "nobody on the
/// winning side" (it tests willDoPointsTotal == 0, not willDoPool == 0) and gives the whole losing pool to the
/// author and the doer; the real winner gets only their 90% back. Fails now, passes once the empty-side test uses
/// the pool (and the pool is split pro rata to amounts, or points are floored at 1).
contract PredictZeroPointsTest is Test {
    Yolo yolo;
    VaultStub vault;
    YoloPredict predict;
    address author = makeAddr("author");
    address doer = makeAddr("doer");
    address a = makeAddr("A"); // WILL DO, 1 YOLO, one second before the deadline of a 40-year task
    address b = makeAddr("B"); // WON'T DO, 1,000 YOLO, at creation

    function setUp() public {
        vm.warp(1_700_000_000);
        yolo = new Yolo();
        uint64 createdAt = uint64(block.timestamp);
        uint64 deadline = createdAt + 1_261_440_000; // 40 years
        vault = new VaultStub(IERC20(address(yolo)), author, doer, createdAt, deadline);
        predict = new YoloPredict(address(vault), 1e18);
        yolo.transfer(a, 1e18);
        yolo.transfer(b, 1_000e18);
        vm.prank(b);
        yolo.approve(address(predict), type(uint256).max);
        vm.prank(a);
        yolo.approve(address(predict), type(uint256).max);
    }

    function test_SoleWinnerWithZeroPointsGetsTheLosingPool() public {
        vm.prank(b);
        predict.predict(1, false, 1_000e18); // WON'T DO at creation: 900 YOLO in the pool
        vm.warp(1_700_000_000 + 1_261_440_000 - 1);
        vm.prank(a);
        predict.predict(1, true, 1e18); // WILL DO: eff 0.9 YOLO, points round to 0
        (,,,, uint256 willDoPoints,,) = predict.markets(1);
        assertEq(willDoPoints, 0, "the only WILL DO position has zero points");

        vault.complete(); // the task got done: WILL DO wins
        predict.settle(1);
        vm.prank(a);
        predict.claim(1);

        // RULES: "Winners: their 90 back + the losing pool by points" and "Done with nobody on WILL DO: the losing
        // pool half author, half doer". A is on WILL DO, so A gets 0.9 + 900 YOLO and the author/doer get nothing
        // of the losing pool (they only get the deferred 2.5% + 2.5% = 0.05 YOLO each).
        assertEq(yolo.balanceOf(a), 0.9e18 + 900e18, "the sole winner takes the losing pool");
        assertEq(yolo.balanceOf(author), 0.025e18 + 0.025e18, "author: only the deferred part");
        assertEq(yolo.balanceOf(doer), 0.025e18 + 0.025e18, "doer: only the deferred part");
    }
}
```

### 4. Low: 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

`contracts/src/desks/DisputeDesk.sol:636`

```
        if (appellantIsStranger && callerWins && penaltyImd != 0) {
```

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.

**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.

### 5. Low: YoloLockHub rounds the weeks left up when adding or extending, crediting an add with up to one week it is not locked for

`contracts/src/YoloLockHub.sol:79`

```
        uint256 left = (end - block.timestamp + WEEK - 1) / WEEK; // whole weeks, rounded up
```

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.

**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.

### 6. Low: _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)

`contracts/src/desks/DisputeDesk.sol:765`

```
        if (winnerBonds == 0) return (toVoters, winnerShare, 0, 0);
```

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.

**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).

### 7. Low: 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

`contracts/src/desks/CaseDesk.sol:111`

```
        s.base = uint128(t.base); // a part of the pot
```

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.

**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.

### 8. Info: claimVoterRewards skips the YoloSweetener voter claim when the add-on arrived after the submission (s.addon snapshot is 0)

`contracts/src/desks/DisputeDesk.sol:441`

```
        if (s.addon != 0 && s.outcome == Outcome.Accepted) bonusTask = s.taskId;
```

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.

**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.

### 9. Info: 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

`contracts/src/YoloLockHub.sol:111`

```
        if (p.epoch != l.epoch || l.end <= block.timestamp || p.end <= since) return 0;
```

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.

**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.

### 10. Info: docs/YOLO.md says the stake bonus is computed by the DisputeDesk at payout; the code fixes it in CaseDesk at submission

`docs/YOLO.md:61`

```
   to the treasury. Computed by the DisputeDesk at payout and booked by the vault from the fee it holds.
```

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.

**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.

### 11. Info: docs/YOLO.md states vesting months of 30.4 days while the vestings are compiled with 365 and 730 days

`docs/YOLO.md:79`

```
Every vesting clock starts when `YoloSplitter.split` pays it, right after launch 1 (months of 30.4 days); no cliff
```

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.

**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.

---

Judge's submission `66718e0a66d7d3642d783f9d64406212a90a744258617c12a44dcdcbb9db8bc8`, accepted on the IdentityMD network. Acceptance means the report met the job's checks;
it is not a guarantee that the code has no other defects.
