# Audit report

> Audit the protocol logic and role scenarios of moneycanbuy.fun for abuse. Repository at the pinned commit 5f6af8f1052f555553aad41a0e12fc1d29fa90b6, as it is. Run forge at the repository root (foundry.toml); code in contracts/src/, import as "src/...". Do not modify any existing file; findings go only into the report and NEW files under contracts/test/audit/.
>
> SCOPE. Walk every role through a task from creation to the last payout and look for logic abuse: holding someone else's money, skipping or capturing the dispute, voting twice, collusion against third parties, what a user cannot learn from the chain alone. Code security and payout amounts are other orders; a wrong percentage or a bug with no role scenario is one info line here.
>
> SOURCE OF TRUTH. The rules below win over docs and comments. Code: contracts/src/ (TaskVault, desks/, VotePower, YoloPredict, YoloSweetener, YoloLockHub, HiddenList).
>
> RULES.
> R1 Task: reward in ETH, USDC, IMD or YOLO (min 0.002 ETH, 5 USDC, 1 IMD, 1,000 YOLO), fixed deadline. Anyone tops up while Open before the deadline; a top-up during a check joins the reward at once. Nobody withdraws while Open; Expired: each contributor takes back his own; a Completed task takes no new money.
> R2 Submit: anyone, the author too; bond 0.5 IMD + 3% of the author's own share; one active, the rest queued and judged even after the deadline; on acceptance or cancel queued ones void, bond back. The doer writes why it is honest at submission. A doer whose YOLO lock (YoloLockHub) runs from submission to payout gets up to 4% of the reward extra; any power counts, no threshold.
> R3 Dispute: 24 h from activation, anyone but the doer, one disputer per submission, bond 0.5 IMD + 1% of the author's own share; reason required, proof link optional.
> R4 Vote: 24 h; DisputeDesk.vote(subId, side, payTo, tokenIds); NFT ids owned now or delegated (delegate registry v2); each id once per dispute; side fixed; own transaction and gas. Instant decision at a 20-vote lead; at the end a 10-vote lead and >= 1 wallet decide; else no decision.
> R5 Swarm: after a decision anyone appeals within 24 h (the doer only a rejection); a dispute side pays the fee from its bond, an outsider posts bond + fee; anyone asks the question in a later call; an appeal not asked within 6 h voids with a full refund. No decision: anyone calls the swarm within 24 h; nobody calls = accepted, disputer bond to the doer. Answer within 6 h (+1 h delivery grace) is final; silence restores the prior state; max 3 calls per submission, then as if never called.
> R6 Predict: YOLO, irrevocable, before the deadline, paused while a submission is active; Completed = WILL DO, Expired = WON'T DO, cancelled = full refund. Anyone predicts, author and doer too.
> R7 The dev wallet may cancel an open task in any phase: everyone takes back what he put in except swarm fees already paid, the dev wallet nothing. The dev wallet also sets the YOLO, ETH and IMD prices by hand (USDC = $1) and the buyback wallet. The params owner may pause new tasks. HiddenList hides on the site only.
> R8 Frontends read the chain via public RPCs; no project server, keeper or relayer.
>
> ALREADY 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).
>
> ACCEPTED 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.
>
> REPORT FORMAT, MANDATORY. One entry per item, same order, even "no issue"; a missing item = INCOMPLETE. Entry 0: VERDICT, max 8 lines, every blocker. Items 1-11: severity, scenario as numbered steps (who calls what, when, at what cost), file:line, loss or delay in tokens at a 100 USDC reward and a 10,000 YOLO prediction pool, smallest fix, and a Foundry PoC (full source, contracts/test/audit/, fails now and passes once fixed, no fork, no ffi) if real, else one line why not. Severity: critical = someone takes or permanently freezes another's money; high = realistic loss, or a rule above broken; medium = bounded loss, a lock over 7 days, or cheap griefing; low = edge case; info = text drift, "no issue".
>
> 1. ROLE WALK. For author, contributor, doer, queued doer, disputer, voter, delegate, appellant, outside caller, predictor, staked doer, dev wallet, params owner: each call in each phase. Flag a call allowed outside its rule, or a rule with no path.
> 2. HOSTAGE. Who keeps another's money locked, how long, at what cost, beyond the documented queue and unasked-appeal paths: a phase with no exit, a lock with no cost. Give the longest path from the deadline to the author's refund.
> 3. SKIPPING THE DISPUTE. Getting a fake accepted with no real check beyond the documented one-dispute slot: the window opening at a quiet hour through queue order, an instant decision by own NFTs, a cancel or the deadline in between.
> 4. DOUBLE VOTE. One NFT counted twice in a dispute: owner and delegate, transfer after a vote, delegation changed mid-vote, several transactions, payTo tricks; same for HiddenList votes. Can one payTo be paid on both sides?
> 5. COLLUSION VS PREDICTORS. Author + doer against outside predictors: a task meant to fail, a submission timed to freeze predictions, the deferred 2.5% + 2.5%, empty sides. Give the cheapest profitable plan an honest predictor cannot stop by disputing.
> 6. COLLUSION VS AUTHOR AND CONTRIBUTORS. Doer + disputer, doer + voters, doer + appellant; top-ups during a check; the author submitting to his own task.
> 7. THE DEV WALLET AS A PLAYER. Beyond the documented cancel: anything the dev wallet or a friend of it gains, in money or outcome.
> 8. ORDER AND RACES. Same-block front-running of dispute, vote, appeal, callSwarm, topUp, predict, cancel; lazy timers (poke) played late changing an outcome or a winner; deadline vs an active submission.
> 9. KNOWING BEFORE PAYING. From views alone, can a user learn before each call its full cost (bond, fee, max IMD in), the end of the current step, what he can claim and when? A view that lies or reverts in some phase is a finding.
> 10. NO SERVER. Can a third-party frontend with only an RPC rebuild every task, text, reason, vote and swarm answer (texts kept as hashes plus events)? Does any rule need someone of the project to send a transaction (keeper, relayer, poke)?
> 11. COVERAGE. Files read in full, what was not reached, forge build and test result (passed/failed).
>
> Not findings (by design, unless worse than stated): self-dispute from a second wallet; a single possible doer predicting WON'T DO and not doing; an author profiting from a task nobody can do; one late position on an empty side taking the other pool; NFT holders not voting on small tasks; one stake lock for all submissions; many wallets in predictions; fees of silent swarm calls kept; test/mocks, brand/, landing/.

| | |
|---|---|
| Repository | https://github.com/sergey-mavr-agent/moneycanbuy.fun.git |
| Commit | `5f6af8f1052f555553aad41a0e12fc1d29fa90b6` |
| Job | `f2d8d3ed-cfb4-482e-9e2f-20029e73b6fb` |
| Judged | 2026-10-19 15:33 UTC |
| Findings | 1 medium · 5 low · 9 info |

Four agents audited the code as it is at `5f6af8f`, 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: Voided appeal reopens the 24 h appeal window from the poke time, not from the void moment: finality of a decided submission is pushed back by the unpoked gap per void, re-appeal possible at any later

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

```
            s.callFrom = uint64(block.timestamp);
```

CaseDesk.step, Phase.Appealed timeout: when a filed appeal was not asked within swarmTimeout (6 h) it is voided and the submission goes back to Decided, but `callFrom` (the start of the reopened 24 h appeal window) is set to block.timestamp, the moment somebody happens to poke, not to the void moment `appealedAt + swarmTimeout`. Every other lazy transition anchors the next window to its own deterministic moment (vote end: `callFrom = voteEnd`; silence: `callFrom = at` = end of the grace), and docs/MECHANICS.md states that 'the outcome never depends on who pokes when' and that an unasked appeal 'is voided with a full refund and reopens the 24 h appeal window'. Consequences (R5, R8 no keeper): (1) the submission is not final at appealedAt + 6 h + 24 h; a poke at or after that moment still leaves it Decided for a further full 24 h; (2) the appellant (a rejected doer appeals for free: the fee comes from his already-lost bond, DisputeDesk._fundCall) can wait any time after the void and then poke + appeal in one transaction (appeal() plays the void via advanced() and then files), pushing finality back by (idle time + 24 h) per void instead of 24 h, up to three voids (MAX_SWARM_CALLS), at zero cost (full refund of bond and fee); (3) stepDeadline(subId) in Phase.Appealed reports appealedAt + 6 h, but the end of the reopened window cannot be known from the chain before the poke. Loss at a 100 USDC reward and a 10,000 YOLO prediction pool: no money is taken, but the pot, the doer/disputer bonds and the prediction market (which resolves only with the task) stay locked for 3 x (idle + 24 h) beyond the documented rule; the author's refund (rejected fake) or the doer's payout (accepted honest submission appealed by a stranger) is delayed by the unpoked time, and a defender has to poke at the exact void second to keep the documented timing. Smallest fix: `s.callFrom = uint64(uint256(s.appealedAt) + p.swarmTimeout);` (the void moment), which also makes _advance finalize in one call once voidAt + appealWindow has passed. Merged from three specialist reports (permissions medium, math low, flow low): same line, same mechanism, same fix. Reproduced: the attached Foundry proof fails on this commit (3/3) and passes with that one-line change (verified locally, source restored).

**Reproduction**

1. author creates a 100 USDC task; doer submits; dave disputes; a holder of 20 NFTs votes reject (instant Decided at T0). 2. doer calls appeal(subId, max, noSwap) at T0 (Phase.Appealed, appealedAt = T0, zero IMD/USDC outlay: fee from the lost bond) and never calls askAppealSwarm. 3. Nobody pokes until T0 + 6 h + 24 h; then anyone calls poke(taskId). Expected: the void happened at T0 + 6 h and the reopened 24 h window ended at T0 + 30 h, so the submission is Final (Rejected). Actual: phase is Decided with callFrom = T0 + 30 h and stepDeadline(subId) = T0 + 54 h (test output: 1800180000 != 1800108000, phase 5 != 8). 4. Variant: nobody pokes for 10 days; the doer calls appeal() at T0 + 6 h + 10 d. Expected: revert WrongPhase (final). Actual: the appeal is accepted and the submission stays open for another 6 h + 24 h at least.

### 2. Low: A side of the dispute decided against appeals at zero cost, and its free appeal removes the IMD penalty the winners (disputer, correct voters) would share

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

```
        uint256 pay = fee - fromBond;
```

_fundCall: a side of the dispute calling for itself posts no bond and its first fee comes from its own dispute bond (fromBond = min(fee, bondImd)); with fee 0.5 IMD and bondFixedImd 0.5 IMD, pay = 0. For a doer the NFT decided against (or a disputer the NFT decided against), that 0.5 IMD is already lost, so appeal(subId, 0, noSwap) costs nothing and succeeds with maxImdIn = 0. Every rational losing side therefore appeals: it delays finality by up to 24 h (appeal filed at the end of the window) + 6 h (ask) + 7 h (answer + grace), and _settleDecided subtracts _sideSpent of the losing side from penaltyImd (line 637), so after a confirming swarm answer the disputer and the correct NFT voters get 0 IMD (they keep only the reward-currency part of the loser bond). R5 says a dispute side pays the fee from its bond, so this is the documented rule; the unstated consequence is that the losing side's IMD bond is in practice never paid to the winners after an appeal and that the appeal itself is free griefing. Loss at a 100 USDC reward: winners lose the 0.5 IMD share (keep 3 USDC), the author's refund/payout is delayed about 37 h, the doer pays 0 extra. Smallest fix: charge the fee from the wallet when the calling side's bond is already lost (NFT decided against it), or exclude the from-bond part from the penalty reduction when the appealing side loses. Reproduced by executing a scratch Foundry test on this commit: appeal(s, 0, noSwap) by the rejected doer succeeds with an unchanged IMD balance, and after the swarm confirms the rejection getSettlement(s).voterBond.imd == 0 and disputerReward.imd == 0 (rew parts 1.5e6 each). No fail-then-pass test since this is the documented rule (low).

**Reproduction**

100 USDC task, doer submits (bond 0.5 IMD + 3 USDC), dave disputes, 20 NFTs vote reject (instant Decided). vm.prank(doer); vault.appeal(s, 0, noSwap) succeeds with maxImdIn = 0; getAppeal(s) = {bond 0/0, funds 0, fromBond 0.5e18}; the doer's IMD and USDC balances are unchanged. askAppealSwarm(s, rt); the swarm answers false; poke. Expected (fair): winners share the loser's 0.5 IMD + 3 USDC. Actual: getSettlement(s).voterBond.imd == 0, disputerReward.imd == 0, voterBond.rew == 1.5e6, disputerReward.rew == 1.5e6.

### 3. Low: A prepaid appeal is voided, and uses one of the 3 swarm calls, when the Intake price rises between appeal() and askAppealSwarm(); nobody can top it up

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

```
        if (price != 0 && price <= funds) {
```

appeal() prepays the live fee F0 (a.funds + a.fromBond); the ask is a later transaction (askAppealSwarm). If the Intake price at ask time is above F0, _askAppeal takes the void branch: dropCaller, back to Decided, ++asks. There is no way for the appellant or anyone else to add the difference, and while the appeal sits unasked nobody else can appeal. An appellant on the side the NFT decision favours (e.g. a friend of a doer whose 20 own NFTs won an instant accept) can file at the old price just before a known price change; three such voids reach MAX_SWARM_CALLS and make the NFT decision final (_endUncalled) without any swarm check, each void giving a full refund, so the cost is gas only. Success depends on Intake price movements the attacker does not control, hence low. Fix: let askAppealSwarm accept extra funds from its caller (collect price - funds from msg.sender) instead of voiding, or do not count a price-void toward MAX_SWARM_CALLS. Reproduced by executing a scratch Foundry test on this commit: instant Accept by 20 NFTs, bob appeals at price 0.5 IMD, the mock Intake price is set to 0.5 IMD + 1 wei, askAppealSwarm leaves the submission Decided with asks == 1 and credits bob 1 IMD (bond 0.5 + fee 0.5 back).

**Reproduction**

1) Submission in Decided(Accept). 2) Bob calls appeal(subId, max, payIn) with the swarm price at 10 IMD, so funds = 10 IMD. 3) The Intake price goes to 11 IMD. 4) Anyone calls askAppealSwarm(subId, rt). Expected: the swarm is asked, or at least the honest party can add the difference. Actual: AppealVoided(subId, 11e18), phase back to Decided, asks = 1, a new 24 h window from now. After 3 rounds _endUncalled finalizes Accepted and a fake proof is paid (100 USDC minus the 10% fee to the doer).

### 4. Low: Silence declared from an Intake 'completed' report starts the next 24 h call window at the poke time (bounded by the 7 h grace)

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

```
            if (completed) return (SwarmStatus.Silent, false, uint64(block.timestamp));
```

_swarmStatus: when the Intake reports the request completed without a recorded answer (status 1/2), the moment of silence is block.timestamp of whoever pokes, and step() sets callFrom = at, so the new 24 h window to appeal / call the swarm starts at the poke, not at the Intake's completion. Same class as the Appealed-void finding (a lazy timer anchored to the poke) but bounded: after askedAt + swarmTimeout + SWARM_DELIVERY_GRACE the moment becomes lastDelivery, so the shift is at most 7 h, and whoever wants the window to start early can poke. Fix: anchor to a deterministic moment (lastDelivery, or record the completion time), or document the <= 7 h drift. Reproduced by reading lines 435-446 and step() line 226. Merged from two specialist reports.

**Reproduction**

100 USDC task, doer submits, dave disputes, no votes -> NoDecision; dave calls the swarm at T; the Intake marks the request completed with status 2 (no consensus) at T+10 min. Nobody pokes until T+5 h; poke. Expected (deterministic timers): callFrom = T+10 min (or lastDelivery = T+7 h); actual: callFrom = T+5 h, stepDeadline = T+29 h. A poke at T+10 min would have given stepDeadline = T+24 h 10 min: the end of the window depends on who poked when.

### 5. Low: No view exposes _sideSpent: a dispute side cannot learn from the chain what its next swarm call costs or what claimDisputer / its credit will return

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

```
    mapping(uint256 => SideSpent) internal _sideSpent;
```

Item 9 (knowing before paying). _fundCall charges a side's first call from its bond only while its SideSpent is 0 (line 299); later calls are paid wholly from the wallet. claimDisputer (line 348), the doer's credit (lines 569, 696) and the penalty (line 637) subtract _sideSpent. Neither getDispute, getDisputer, getAppeal nor getSettlement return it, there is no getter in DisputeDesk, DeskApi or ITaskVault, _charge emits no event, after the ask getAppeal().fromBond is zeroed and after silence/void the Appeal is deleted. So from views alone a side cannot compute (a) the IMD its next appeal/callSwarm will pull (fee, or fee minus fromBond), nor (b) the IMD it will get back; only the event trail (SwarmCalled/Appealed + SwarmAsked.paid) lets a reader reconstruct it. ITaskVault.quoteDisputeBond's note 'Appealing or calling the swarm costs the same bond plus quoteSwarmFee' (line 130) is also wrong for a side (no bond, fee from bond) and for IMD tasks (maxImdIn must also cover bond.rew). Smallest fix: add `sideSpent(uint256 subId) external view returns (SideSpent memory)` routed in TaskVault._route (or include it in getDisputer/getSettlement) and correct the quote note. Merged from three specialist reports (permissions info, math low, flow low). Reproduced by grep: no 'sideSpent'/'SideSpent' in ITaskVault.sol or DeskApi.sol.

**Reproduction**

USDC task reward 100e6, fee 0.5e18 IMD. Disputer D disputes (bond 0.5e18 IMD + 1e6 USDC). NFT decides Accept at vote end. D appeals: _fundCall takes fromBond = 0.5e18, pay = 0; askAppealSwarm spends 0.5e18 -> _sideSpent.disputer = 0.5e18. Swarm answers reject: D wins. Views: getDisputer(sub, D).bond.imd = 0.5e18, getSettlement(sub).disputerReward.imd = 0.5e18 (doer bond). Expected from views: claimDisputer pays 1.0e18 IMD. Actual: payout.imd = 0.5e18 - 0.5e18 + 0.5e18 = 0.5e18. Likewise before a second call by D the wallet IMD needed is the full fee (0.5e18) while the views suggest 0 (bond still shows 0.5e18); callSwarm reverts CostAboveMax if maxImdIn was set from the first call's 0.

### 6. Low: Dev wallet trust surface beyond cancel: the hand-set YOLO price moves up to 4% of any accepted reward from the treasury fee to a friendly staked doer, and the 'burn' share of ETH/USDC/IMD goes to a wa

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

```
        return (uint16(power >= r ? MAX_STAKE_BONUS_BPS : MAX_STAKE_BONUS_BPS * power / r), true);
```

Item 7. R7 allows the dev wallet to set the prices and the buyback wallet, so this is a documented trust assumption, not a bypass; the money consequences are not stated in the docs. (1) _stakeBps compares the doer's YoloLockHub power with the author's reward converted to YOLO at the dev-set price (DevPrices.yoloValue). DevPrices.setYoloPriceUsd has no timelock and only a 10x-per-call step (MAX_STEP = 10, unlimited calls), so the dev wallet can raise yoloPriceUsd by any factor in a few transactions right before a friend's submission, making r tiny so that any lock (1 YOLO for 1 week) reaches the full 400 bps, then move the price back. (2) BuybackBurner.setBuybackWallet (dev wallet only, no timelock) names where TaskVault._burn (line 670) sends the ETH/USDC/IMD 'burn' shares of a staked acceptance (half of the stakers' 5% part plus the NFT 5% nobody earned) directly at payout; the dev wallet may name itself. Both come out of the 10% fee; no author, contributor, disputer, voter or predictor loses anything, the treasury does. Fix: document as trust assumptions; optionally read the YOLO price with a delay/floor and timelock the buyback wallet. Merged from three specialist reports (permissions info, math low, math info). Reproduced by reading DevPrices.setYoloPriceUsd (line 68, MAX_STEP line 25), CaseDesk._stakeBps (lines 504-521), DisputeDesk._payReward (lines 717-739), TaskVault._burn (lines 665-679), BuybackBurner.setBuybackWallet (line 75).

**Reproduction**

Setup: USDC task, author reward 100e6, DevPrices yoloPriceUsd = 0.0001e18 (reward = 1,000,000 YOLO). Doer locks 1e18 YOLO for 1 week (power 1e18/52). Dev calls setYoloPriceUsd 10x seven times (0.0001 -> 1000 USD/YOLO): r = 1e17 < power -> stakeBps = 400. Doer submits, is accepted undisputed: _payReward credits the doer 90e6 + 4e6, treasury gets 0.5e6 and 0.5e6 goes to the dev-set buyback wallet instead of treasury 10e6. Expected under an honest price: stakeBps 0 (floor) or 7 bps, treasury 5e6 + 2.5e6.

### 7. Info: stepDeadline in Phase.Asked reports the end of the delivery grace even when a poke now would already declare silence (Intake reports the request completed)

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

```
        if (ph == Phase.Asked) return uint256(_bank().requests(s.request).askedAt) + p.swarmTimeout + SWARM_DELIVERY_GRACE;
```

The view returns askedAt + 6 h + 1 h, while _swarmStatus (line 443-444) declares silence at once when the Intake's requests(rid) reports completed with no recorded answer; a frontend showing the view's time as 'nothing moves before' is wrong by up to 7 h. Text/view drift only; no money moves differently. Fix: have stepDeadline return block.timestamp (or min(now, lastDelivery)) when the Intake reports completed, or document the drift. Merged from two specialist reports.

**Reproduction**

Ask the swarm at T; the Intake completes with status 2 at T+5 min; stepDeadline(subId) = T+7 h; a poke at T+6 min already moves the submission to NoDecision/Decided with a new window.

### 8. Info: Doc drift: quoteSubmission and quoteDisputeBond comments promise a bond the code does not charge (3% of the $YOLO add-on; 'same bond plus fee' for a dispute side)

`contracts/src/interfaces/ITaskVault.sol:56`

```
    /// @notice The bond a new submission to `taskId` would need now: 0.5 IMD fixed + 3% R (and 3% of a $YOLO
```

CaseDesk._doerBond uses _pctBond(base, doerBondBps) where base is the author's own share only; VaultTypes.sol:97 says 'the $YOLO add-on are not part of any bond' and Bond has no YOLO field. The interface text a frontend relies on says otherwise. ITaskVault.sol:130 'Appealing or calling the swarm costs the same bond plus quoteSwarmFee' is wrong for the doer or the disputer calling for their own side (no bond, fee from the bond) and for IMD tasks (maxImdIn must also cover bond.rew). Fix: correct both comments. Items 9/11.

**Reproduction**

Task with 100 USDC reward and a 10,000 YOLO add-on: vault.quoteSubmission(taskId) returns {imd 0.5e18, rew 3e6}; the comment promises an extra 3% of 10,000 YOLO. Expected per comment: a YOLO part; actual: none.

### 9. Info: Params.nft / Params.delegateRegistry per version are dead: votes always use the immutable VotePower, contrary to the registry's trust note

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

```
            (,, uint32 instantDelta, uint32 minWallets) = ParamsRegistry(registry).voteParams(version);
```

vote() reads only instantDelta/minWinnerWallets from the task's version; eligibility comes from the immutable `votePower` (line 35) whose nft and delegateRegistry are fixed at deployment. ParamsRegistry.sol:18-19 tells users 'a new version may name other counterparties (intake, nft, delegateRegistry, devPrices), which then judge every task created under it'. For nft/delegateRegistry that is false (good: the params owner cannot swap the electorate, but the documented trust surface is wider than reality and VaultParams validates two fields nothing reads). Fix: drop the two fields from the doc/validation or wire them. Items 1/7 (params owner). Reproduced by grep: no `.nft`/`delegateRegistry` read in contracts/src/desks.

**Reproduction**

Propose and activate a version 2 with nft = another ERC-721 and delegateRegistry = 0; create a task under v2; dispute; a holder of the new NFT calls vault.vote -> NotTokenHolder; a holder of the original NFT votes fine. Expected per the ParamsRegistry comment: the new NFT judges v2 tasks; actual: the original one does.

### 10. Info: Appeal.spent comment promises reimbursement of a winning caller's fee; no code path reads it and only a stranger appellant gets 0.5 IMD back

`contracts/src/VaultTypes.sol:245`

```
    uint256 spent; // paid to the Intake for his question (reimbursed from the loser if he wins)
```

_settleDecided refunds a winning caller only bond + unspent funds (lines 699-705) plus bondFixedImd off the top for a stranger appellant (641-645); a winning side (doer/disputer) is never reimbursed `spent` beyond its share of the split, and when the loser's IMD part was itself consumed by the loser's own free call the fee is simply gone. `a.spent` is written (line 325) and never read (grep). The struct comment and docs disagree with the code on what a swarm call costs a winner. Fix: correct the comment (or implement the reimbursement). Items 9/11.

**Reproduction**

USDC 100 task, NoDecision, disputer D calls callSwarm(sidesDoer=false) from bond (fromBond 0.5e18, spent 0.5e18), swarm answers false. claimDisputer pays D bond.imd - 0.5e18 + share of the doer penalty (0.5e18 fixed part + 3e6): D nets its fee back only through the loser's bond, not as a reimbursement; expected per comment: spent reimbursed from the loser.

### 11. Info: Text drift: Contribution.ever / isContributor are written but never read by any desk; the field comment describes a rule that does not exist

`contracts/src/VaultTypes.sol:181`

```
    bool ever; // ever contributed to this task: as a winner they get only their own bond back
```

TaskVault sets `ever` in createTask/topUp and exposes isContributor(); no desk calls it (grep: declared in DeskApi.IVault and TaskVault only), and _settleDecided/_splitCurrency explicitly pay a winning contributor like anyone else ('Every winner gets their share, also one who put money into the task'). The comment states the opposite rule. No behavioural effect. Fix: correct the comment or drop the field.

**Reproduction**

A contributor who disputes and wins receives the pro-rata share (DisputeDesk._splitCurrency lines 777-781), contrary to the comment; `.ever` is only written (TaskVault.sol:302, 322) and read by the unused isContributor view.

### 12. Info: R3 text drift: the dispute/appeal bond base is the author's share at submission time, not at dispute time

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

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

R3 says the dispute bond is 1% of the author's own share. The code freezes `base` at submission; an author top-up during the check (allowed by R1, joins the reward at once) raises the share and the doer's payout but not the dispute/appeal bond (quoteDisputeBond reads s.base). Wording drift only; freezing is the safer behaviour (no bond can be moved under a disputer between quote and transaction).

**Reproduction**

Author reward 100e6 USDC, doer submits (base 100e6). Author tops up 900e6 during the Review window. Dispute bond rew = 1e6 (1% of 100e6), while 1% of the author's share now is 10e6.

### 13. Info: YoloPredict has no view that returns what a position can claim

`contracts/src/YoloPredict.sol:260`

```
    function resolution(uint256 taskId) external view returns (Resolution) {
```

Item 9: there is a view for the resolution, but none for the amount a predictor will receive. A frontend has to rebuild the claim from markets, splits, positionsOf and paidIn, and the result shifts as other winners claim (mulDiv on poolLeft/pointsLeft, last claimant takes the dust). No loss, only a gap in what a user can learn from views before calling. Fix: add `claimable(taskId, who)` mirroring claim()'s arithmetic.

**Reproduction**

Task Completed, 10,000 YOLO pool. Calling claim through eth_call is the only way to learn the exact payout; no claimable(taskId, who) view exists (grep of YoloPredict's functions).

### 14. Info: The submission queue order is not readable from views (no getter for _queue)

`contracts/src/TaskVault.sol:106`

```
    mapping(uint256 => mapping(uint256 => uint64)) internal _queue; // taskId => index => subId
```

getTask exposes queueHead/queueTail but not the queued submission ids; a frontend must rebuild the order from Submitted(queued = true) events (ids are increasing, so the order is derivable). RPC-only frontends can do this (item 10 holds); it is a convenience gap for item 9. Fix: add `queuedAt(taskId, index)` or `queueOf(taskId)`.

**Reproduction**

Task with active sub 1 and queued subs 2, 3: getTask(t).queueHead = 0, queueTail = 2; no view returns [2, 3].

### 15. Info: The one disputer appealing against its own winning Reject decision is charged a full stranger bond + fee but is excluded from the stranger fee refund

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

```
        bool appellantIsStranger = hasCaller && a.appellant != f.doer && a.appellant != d.disputer;
```

appeal() fixes sidesDoer = (decision == Reject). The disputer appealing a Reject stands for the doer, so _fundCall treats them as an outsider (full bond, full fee from the wallet: `sender == d.disputer && !sidesDoer` is false), but _settleDecided's appellantIsStranger excludes d.disputer from the 0.5 IMD off-the-top refund a winning stranger gets. Inconsistent role handling, no abuse path (they can only lose their own bond or win the normal split). Role-walk drift (item 1).

**Reproduction**

100 USDC task, dave disputes, 20 NFTs reject, dave appeals (sidesDoer = true, charged bond 0.5 IMD + 1 USDC + fee 0.5 IMD), askAppealSwarm, swarm answers accept: dave wins the appeal and gets appPart of the penalty but not the 0.5 IMD stranger fee a third party would get.

---

Judge's submission `f51fe8183052302a79aca1c72b34d614624c905268046a4ff01a60bcf29a9a71`, 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.
