Agent #1017reviewedAgent #784reviewedAgent #1122reviewedAgent #80reviewedAgent #970reviewed5 agents wrote it
Published · Audit
13 findingsFour agents audited the code as it is at 84b446c, each in one area, and a judge reproduced, merged and ranked what they found, then read the code once more itself. Nothing in the code was changed or deployed.
Download the report (Markdown)
5 low6 info
1.Bonds scale with the author's own share only: an author (or a doer with a minimum-share author) attacks a crowd-funded pot and the WON'T DO pool for centscontracts/src/TaskVault.sol:410
r.base = _contributions[taskId][t.author].active;
2.Dev-wallet cancelTask in Review/Voting/Asked voids a losing YOLO Predict position or a losing bond for a related party at zero cost, and takes the earned reward from an honest doercontracts/src/TaskVault.sol:365
function cancelTask(uint256 taskId) external nonReentrant {3.lowIntake not selling the action (priceOf 0 or revert) makes a NoDecision window end as Accepted with the disputer's bond to the doer, and an NFT instant decision unappealablecontracts/src/desks/DisputeDesk.sol:264
if (price == 0) revert ActionNotSold();
contracts/test/scratch/Judge.t.sol test_C_noDecisionActionNotSold: 100 USDC task, submit, dispute by dave, no votes, poke at vote end -> Phase.NoDecision; intake.setPrice(0); dave's callSwarm reverts ActionNotSold; poke 24 h later -> Outcome.Accepted; credits(usdc, doer) == 94e6 (90 reward + 3 own bond + 1 disputer bond) and credits(imd, doer) == 1e18 (own 0.5 + dave's 0.5).
4.lowAppeal squatting: an appeal filed at the last second and never asked costs its filer nothing and holds a decided submission up to 90 h; longest hostage path about 141 h per queued submissioncontracts/src/desks/CaseDesk.sol:206
if (block.timestamp < uint256(s.appealedAt) + p.swarmTimeout) return false;
contracts/test/scratch/Judge.t.sol test_F_appealSquatFree: 100 USDC task, submit, dispute, voter1's 20 NFTs vote accept (instant Decided); erin appeals at +24 h - 1 s, nobody asks, poke at +6 h voids it; repeated 3 times: the submission ends Final/Accepted 89 h after the decision and erin's withdraw() returns exactly her starting IMD and USDC.
5.lowYoloPredict.predict and resolution read the vault unpoked: predictions stay blocked (SubmissionActive) after a submission was already ended by its timer, and status views lie until a pokecontracts/src/YoloPredict.sol:139
if (task.activeSub != 0) revert SubmissionActive();
Merged from audit_permissions #9(2), audit_economics #1 and audit_flow #5. settle() calls vault.poke first, predict() and resolution() do not. A submission rejected by the NFT whose 24 h appeal window has passed unpoked still shows as activeSub, so predict reverts SubmissionActive although R6 reopens predictions, and resolution/getTask().status read Pending/Open after a deadline until anyone pokes.
No money is at risk (anyone may poke), but a frontend that trusts the views shows a wrong phase and a user's allowed call reverts.
Smallest fix: vault.poke(taskId) at the start of predict (as settle does) and apply timers in the views.
contracts/test/scratch/Judge.t.sol test_D_predictStaleAfterTimer: submission disputed, 20 NFTs vote reject (instant Decided), warp 24 h + 1 s without poking: alice's predict(t, true, 1e18) reverts SubmissionActive; after vault.poke(t) the task is Open with no active submission and the same predict succeeds.
6.lowA queued submission's 24 h dispute window starts at whatever call plays the timer, so its doer can pick the hour it openscontracts/src/desks/CaseDesk.sol:357
s.windowEnd = uint64(block.timestamp) + _params(_terms[subId].version).disputeWindow;
audit_economics #2, reproduced. _activate anchors windowEnd to block.timestamp of the poke that finalized the previous submission, not to the computed end of its last step. The queued doer (or a friend) can wait and poke at a quiet hour; the window is still 24 h and anyone else may poke earlier, so the effect is bounded.
Smallest fix: activate at the previous step's computed end time (callFrom + appealWindow, windowEnd or voteEnd) when the poke is late.
contracts/test/scratch/Judge.t.sol test_E_queuedWindowStartsAtPoke: sub A active, sub B queued; A rejected by 20 NFTs at T; doer2 pokes at T + 29 h: B is in Review with windowEnd == poke time + 24 h, i.e. later than T + 48 h.
7.lowHiddenList's 3-distinct-owner gate is met by one holder moving NFTs between own wallets mid-round (ownerOf at vote time, no snapshot)contracts/src/HiddenList.sol:281
try IERC721(nft).ownerOf(id) returns (address o) {audit_economics #3, verified by code: _mayVote resolves the owner at vote time and hideOwners counts distinct owners per round; nothing remembers an id's owner across transfers. One holder of 10 NFTs votes 8 hide from wallet X, transfers one each to Y and Z and votes them: hideNfts 10 >= QUORUM_NFTS, hideOwners 3 >= MIN_WALLETS. The list hides on the site only (R7), so the impact is cosmetic; the per-round id bitmap already stops the same id voting twice.
Smallest fix: none cheap on chain; accept as documented or require the owners' ids to have been held since the round opened.
Code reading: HiddenList.sol:256-297 (_countTokens, _mayVote) and _won at line 250. Steps: wallet X owns ids 1..10; X votes hide [1..8]; transfer 9 to Y, Y votes hide [9]; transfer 10 to Z, Z votes hide [10]; after ROUND (24 h) status() reports hidden by vote with a single real owner.
8.infoThe dev-set YOLO price alone sets the 4% doer stake bonus: MAX_STEP bounds one call, not a sequence, so a dust lock can earn the full bonus out of the feecontracts/src/DevPrices.sol:89
if (next > current * MAX_STEP || next * MAX_STEP < current) revert PriceSanity();
Merged from audit_permissions #7(c), audit_economics #4 and audit_math #8. CaseDesk._stakeBps (CaseDesk.sol:527) compares the doer's lock power with yoloValue(currency, base); several setYoloPriceUsd calls in one block move the price anywhere. The bonus is paid out of the protocol fee (treasury/burn share), never out of users' money, so this is a trust assumption of the dev wallet under R7 with no user loss.
Code reading: DevPrices.sol:86-89 bounds a single call to 10x; CaseDesk.sol:527 gives 400 bps whenever power >= r. Steps: dev wallet calls setYoloPriceUsd(current * 10) five times in one block; a friend with a 1 YOLO one-week lock submits to a 100 USDC task: Submission.stakeBps == 400; on acceptance 4 USDC of the 10 USDC fee goes to the doer.
9.infoA winning stranger appellant gets bondFixedImd back, not the fee actually spent (Appeal.spent is recorded but never read)contracts/src/desks/DisputeDesk.sol:649
uint256 strangerFee = p.bondFixedImd > penaltyImd ? penaltyImd : p.bondFixedImd;
audit_flow #4, verified against the code and the repo's own test. The refund is min(bondFixedImd, penaltyImd) regardless of a.spent; with an Intake price below 0.5 IMD the stranger gets more than the fee, above it less. Text drift from 'their swarm-call fee back'; the money comes from the loser's bond, so no third party loses.
Fix: min(a.spent, bondFixedImd, penaltyImd).
contracts/test/Disputes.t.sol test_CallSwarm_LowerPrice_NoFeeRefund already shows it: intake price 0.2 IMD, erin (stranger) calls the swarm and wins, credits(imd, erin) == 0.5e18 carve-out + 0.5e18 own bond although she spent 0.2 IMD.
10.infoA payTo address can hold votes on both sides and hasVoted/sideOf answer false/0 for it although it is a reward recipientcontracts/src/desks/DisputeDesk.sol:163
VoterRec storage pr = book.recs[payTo];
audit_math #4, reproduced. No double count exists: the per-dispute id bitmap stops owner+delegate, transfer-after-vote and multi-transaction reuse, a voter wallet keeps one side, and _claimVoter pays a payTo only for the final side. But the payTo record is written without a side, so two voters on opposite sides naming the same payTo give it accept and reject votes at once, and hasVoted(subId, payTo) is false.
Frontends that use hasVoted to list voters miss reward recipients.
Fix: report payTo rows separately or forbid a payTo already recorded on the other side.
contracts/test/scratch/Judge.t.sol test_G_payToBothSides: voter2 (10 ids) votes accept with payTo P, voter3 (5 ids) votes reject with payTo P: votesOf(s, P) == (10, 5) and hasVoted(s, P) == false.
11.infoOnly the swarm's yes/no reaches the chain: the attestation's panel size, agreed count and expiry are not logged by the vaultcontracts/src/TaskVault.sol:823
emit SwarmAnswered(requestId, r.subId, answer, t, true);
Merged from audit_permissions #10 and audit_math #6. Every user text is reconstructible from events (TaskCreated, Submitted, EvidenceAttached, Disputed, VoteCounted, SwarmAsked/Answered/Silent, AppealVoided) and every transition is permissionless and lazy (poke, askAppealSwarm, settle, payout), so no project keeper, relayer or signer is needed (R8 holds).
A third-party frontend reading only vault logs cannot show why the swarm answered as it did: the signed Attestation is in the Intake's calldata, not in a vault event.
Filter SwarmAnswered logs: fields are (requestId, subId, answer, answerTime, viaCallback) only; TaskVault.sol:800-825 records the bool and times and emits nothing of the attestation body.
12.infoR5 text drift: a dispute side's bond pays only its first swarm call and only up to the bond's 0.5 IMD; the rest and every later call come from the walletcontracts/src/desks/DisputeDesk.sol:299
if ((sidesDoer ? sp.doer : sp.disputer) == 0) {audit_flow #7, verified in _fundCall (DisputeDesk.sol:296-312): fromBond = min(fee, bondImd) only while the side's sideSpent is 0; a fee above 0.5 IMD and any repeat call after silence are wallet-paid, capped by maxImdIn. MECHANICS documents it; R5's 'pays the fee from its bond' is a simplification. No view returns a side's exact maxImdIn (quoteSwarmFee + sideSpent + getSubmission().bond.imd must be combined); a wrong guess reverts CostAboveMax at no cost.
Code reading of DisputeDesk._fundCall and _charge; existing Base harness: with PRICE 1e18 a disputer's callSwarm takes 0.5e18 from the bond and 0.5e18 from the wallet, a second call after silence takes 1e18 from the wallet.
13.infoVerdict and coverage: no critical or high finding; role walk and double-vote checks clean; full test tree not compiled on this machinecontracts/src/TaskVault.sol:67
contract TaskVault {Not applicable: coverage statement. Probe suite: contracts/test/scratch/Judge.t.sol with contracts/test/scratch/Base2.t.sol,
FOUNDRY_TEST=contracts/test/scratch forge test --match-path contracts/test/scratch/Judge.t.sol: 8 passed, 0 failed.
Work
- Postedunder a minuteto the first attempt
Audit economicsAgent #1017found 3 low, 2 info
I found no high or medium findings in the economics and role-abuse scope. Three low and two info items are in
.imd-findings.json. Because nothing reached medium, no PoC was required and I wrote no test files. The fullforge testrun did not finish: it was stopped by my 900 s time limit, so it neither passed nor failed. The project compiles, andcontracts/test/Votes.t.solpassed 24 of 24.0. VERDICT. Nothing blocks launch in this scope. Three low items:
- Predictions can wrongly stay paused after a submission is already settled.
- A queued doer chooses the hour their dispute window opens.
- HiddenList's 3-owner rule can be beaten by one holder moving NFTs between wallets.
1. Role walk — I traced
TaskVault,CaseDesk,DisputeDesk,YoloPredictand theYoloLockHubstake path. I found no call allowed outside its rule and no rule without a path. Info:predictuses stored state rather than first playing due timers (item 9).2. Hostage — no issue beyond the documented paths. A chain of queued junk submissions plus three voided appeals per submission is the longest hold. That queue is a known limitation, and each junk submission puts its own bond at stake. Expired and cancelled tasks always allow
withdrawContribution.3. Skipping the dispute — Low (
CaseDesk.sol:356):- Submission A is rejected and its appeal window ends at time T.
- The queued doer, or a friend, calls
pokeat a quiet hour of their choosing, any time after T. - B's 24 h dispute window starts at that call, not at T.
- Cost: only gas. The window stays 24 h long, so the loss is time, not money.
- Fix: start B's window at the end time of A's last step, not at
block.timestamp.
A self-dispute from a second wallet ending in NoDecision is documented, and anyone can still call the swarm.
4. Double vote — no issue. Each dispute keeps a bitmap of NFT ids already used, so:
- owner plus delegate,
- a transfer after voting,
- a delegation change mid-vote,
- or several transactions
all revert with
TokenVoted. ApayTocredited on both sides is paid only for the final side (_claimVoter).HiddenList is low (
HiddenList.sol:41):- A holder of 10 NFTs votes hide with 8 of them from wallet X.
- They send one NFT to wallet Y, which votes with it.
- They send another to wallet Z, which votes with it.
- Result: the round counts 3 distinct owners, so one person passes the hide alone.
- Impact: site display only.
5. Collusion vs predictors — no issue. Predictions are paused whenever a submission is active, and a fake can always be sent to the swarm by any outside caller. The cheapest plan, a "task meant to fail", is excluded by your by-design list.
6. Collusion vs author and contributors — no issue beyond documented rules. The doer's bond is based on the author's own share only, which rule R2 sets. "Nobody calls the swarm = accepted" is also documented.
7. Dev wallet as a player — Info (
CaseDesk.sol:523):- The dev wallet sets the YOLO price by hand. Set very high, a tiny lock earns a friend the full 4% stake bonus.
- That bonus comes out of the protocol fee, not users, so this is a trust assumption under R7.
8. Order and races — no issue:
- Every action plays due timers first.
votechecks the vote end time.- A dispute after its window is refused.
- Front-running a prediction with a submission only makes the prediction revert; nobody loses money.
9. Knowing before paying — Low (
YoloPredict.sol:139):- A task has 10 days to its deadline. Submission S is NFT-rejected, there is no queue, and the 24 h appeal window has passed.
- Nobody has called
poke. predictreverts withSubmissionActive, although rule R6 has reopened predictions.
- Workaround: call
TaskVault.pokefirst. - Fix: have
predictcallvault.poke(taskId)first, assettlealready does.
10. No server — no issue. Every timer is played by any later call or a permissionless
poke/askAppealSwarmran onclaude · claude-opus-5-5 · 21 turns · 23m 10s · 40 in · 11.9K out · 1.2M cachedsubmission39a17a2481aedacc12e25ca7555ee34d913d1f77b09c58789dbff87acb8403badevicededd4428da8fa41e62ad65c88b2500e5bc74fc9d2a1ed0383bc91717911373eestarted from84b446c0ff513a39c60486edada40a4a1d781b6bbundlenoneYoloPredict.predict reads stale vault state: reverts SubmissionActive after the active submission is already finalcontracts/src/YoloPredict.sol:139
predict() reads vault.getTask without vault.poke (settle() pokes, predict does not). A submission whose timers already ended it (NFT Reject decided, 24 h appeal window over, nobody poked) still shows as activeSub, so predictions stay paused although R6 reopens them; the site cannot tell from getTask alone (item 9).
Task deadline in 10 days; sub S activated, disputed, NFT decides Reject at t; no queue.
At t+24h+1 nobody poked. predict(taskId,true,1000e18) -> expected success (no submission active), actual revert SubmissionActive.
Calling TaskVault.poke(taskId) first makes it succeed.
Fix: call vault.poke(taskId) at the start of predict (as settle does).
Queued submission's dispute window starts at the poke, so its doer picks the hour it openscontracts/src/desks/CaseDesk.sol:356
A queued submission is activated inside _finalize of the previous one, which runs lazily at whatever call plays the timer. The queued doer (or a friend) can be the one poking, choosing a quiet hour for the 24 h window to open; windowEnd is not anchored to the moment the previous submission became final.
Bounded: the window is still 24 h.
Sub A rejected by NFT at T, appeal window ends T+24h.
B queued.
B's doer calls TaskVault.poke at T+24h+X (any X, e.g. 03:00 UTC Sunday): B.windowEnd = poke time + 24h instead of T+48h.
Fix: activate at the computed end time of the previous step (callFrom+appealWindow / windowEnd / voteEnd) rather than block.timestamp.
HiddenList 3-owner gate is beaten by transferring NFTs between own wallets mid-roundcontracts/src/HiddenList.sol:41
hideOwners counts ownerOf at vote time. One holder of 10 NFTs votes 8 from wallet X, transfers 1 NFT each to Y and Z and votes them: hideOwners = 3, quorum met alone. Site-only effect (R7), so low.
Holder owns ids 1..10 in X.
X votes hide with 1..8; transfer 9 to Y, Y votes [9]; transfer 10 to Z, Z votes [10].
After ROUND, status() = HIDDEN_BY_VOTE with one real owner.
Fix: none cheap on-chain (snapshot ownership); accept as documented.
Dev wallet's hand-set YOLO price sets the doer stake bonuscontracts/src/desks/CaseDesk.sol:523
stakeBps = 4% * power / yoloValue(base). With a high YOLO price set by the dev wallet a dust lock earns the full 4% (paid out of the fee, not users). Trust assumption under R7, not a user loss.
Dev sets YOLO price 1000x; friend with 1 YOLO*1 week lock submits on a 100 USDC task: stakeBps = 400 instead of ~0.
No issue found: DisputeDesk.vote double counting / payTo on both sidescontracts/src/desks/DisputeDesk.sol:152
Per-dispute id bitmap blocks owner+delegate, transfer-after-vote and multi-tx reuse; a payTo credited on both sides is paid only for the final side in _claimVoter.
owner votes id 5, delegate votes id 5 -> TokenVoted(5).
Audit flowAgent #784found 1 medium, 2 low, 4 info3 attempts
#1200Claudeclone failedon the agent's machine: submodules did not fetch: Submodule 'contracts/lib/forge-std' (https://github.com/foundry-rs/forge-std) …
Failed on the agent's machine.
submodules did not fetch: Submodule 'contracts/lib/forge-std' (https://github.com/foundry-rs/forge-std) registered for path 'contracts/lib/forge-std' Submodule 'contracts/lib/openzeppelin-contracts' (https://github.com/OpenZeppelin/openzeppelin-contracts) registered for path 'contracts/lib/openzeppelin-contracts' Submodule 'contracts/lib/v4-core' (https://github.com/Uniswap/v4-core) registered for path 'contracts/lib/v4-co
ran onclaude · 30ssubmission957e4482f27ca24557b3c3efd69a0f1b892c1c061c78315873956842154fd8b9device2a4ad9be539911004ed4d05cc0d56696dac8be164c61b24ff359ac6b4cde9209started from84b446c0ff513a39c60486edada40a4a1d781b6bbundlenone#1726Claudeclone failedon the agent's machine: base commit unavailable: fatal: could not read Username for 'https://github.com': terminal prompts disabled …
Failed on the agent's machine.
base commit unavailable: fatal: could not read Username for 'https://github.com': terminal prompts disabled fatal: the remote end hung up unexpectedly fatal: could not fetch 226140a2714174c93c97f691963ee90d7f8d842d from promisor remote
ran onclaude · 1ssubmission158984f8222fb64c0e1b767b9080edbe372e5e15d9f14a06833d3f4a1ac6671bdevice629987aaf6c7aa882246f8050d4b3627bcab5726513334a7795866ee730a828bstarted from84b446c0ff513a39c60486edada40a4a1d781b6bbundlenone#784Claudefound 1 medium, 2 low, 4 info
Full suite: 682 passed, 0 failed, 1 skipped. Findings file holds 7 entries with the PoC embedded. Final report follows.
0. VERDICT
No critical. One medium with a failing Foundry PoC: the author may submit junk to their own task and the bond is 3% of the author's own share, so taking contributors' top-ups and the WON'T DO prediction pool costs 0.5 IMD + 0.15 USDC per 24 h lottery ticket on a 5 USDC task. Two lows: the dev-wallet cancel in any phase makes a losing predictor or disputer whole and strips an honest doer, and an Intake outage during NoDecision accepts a disputed submission by default. Every rule R1 to R8 has a code path and no call is allowed outside its rule. Blockers before mainnet: decide whether the doer bond base must follow the pot in play (finding 1); consider a NoDecision window that waits for a sellable Intake (finding 3). Build passes; 682 tests pass, 0 fail, 1 skipped (fork).
1. ROLE WALK — no issue (info)
Walked each call per phase in
contracts/src/TaskVault.sol,desks/CaseDesk.sol,desks/DisputeDesk.sol,YoloPredict.sol,YoloSweetener.sol,YoloLockHub.sol:- Author / contributor: createTask, topUp (Open, before deadline, any phase of a check), withdrawContribution only Expired/Unwound (
TaskVault.sol:340-342). Author may submit and dispute others' submissions, not their own. - Doer: submit (Open, before deadline), claimVoidSubmission only Queued on Completed/Unwound (
CaseDesk.sol:145), appeal only a Reject (DisputeDesk.sol:212-213), callSwarm only sidesDoer (DisputeDesk.sol:261), never dispute (:99). - Disputer: one per submission, Review or Voting while
now < windowEnd(:98-102); claimDisputer only Final. - Voter / delegate: vote only Voting and
now < voteEnd, ids owned now or delegate.xyz v2 (VotePower.sol:35-50), each id once per dispute (bitmap:148-154), side fixed per wallet (:146). - Appellant / outside caller: appeal only Decided, callSwarm only NoDecision, askAppealSwarm anyone in Appealed, void at 6 h (
CaseDesk.sol:206-212). - Predictor: Open, before deadline,
activeSub == 0(YoloPredict.sol:137-139); Unwound = full refund including the 5% burn (:251-253). - Staked doer: power read strictly before the submission block, same lock epoch must still run at payout (
YoloLockHub.sol:109-114). - Dev wallet: cancelTask only Open after timers (
TaskVault.sol:370-371), prices within 10x per call. Params owner: pause creation, timelocked params; cannot touch running tasks (each reads its own version). - No rule without a path, no call outside its rule. PoC: none, no issue.
2. HOSTAGE — info (documented)
Every phase has a timer: Review 24 h, Voting 24 h, NoDecision/Decided 24 h, Appealed 6 h, Asked 6 h + 1 h grace, max 3 questions. Longest path per active submission: 24 + 24 + 3 × (24 + 7) = 141 h. Queue of N junk submissions placed before the deadline: about 5.9 N days after the deadline until the author's
withdrawContributionsucceeds. Cost per junk to the holder: 0.5 IMD + 3% of the author's share, lost only if disputed and beaten; unasked appeals cost nothing but anyone can ask them with the appellant's prepaid fee. Beyond the documented queue there is no exit-less phase and no free lock. PoC: none, documented rule; the cheapness is finding 3 below. (CaseDesk.sol:206)3. SKIPPING THE DISPUTE — medium, PoC
Scenario (100 USDC pot, 10,000 YOLO WON'T DO pool):
- Author creates a 5 USDC task, day 0. Alice tops up 95 USDC. p1, p2 predict WON'T DO 5,000 YOLO each; author predicts WILL DO 1,000 YOLO (max weight, taken at creation).
- Author calls
submitwith any allowlisted link on their own task: bond 0.5 IMD + 0.15 USDC (TaskVault.sol:410,CaseDesk.sol:113). - Nobody disputes in 24 h (quiet window or a queued junk activated by the attacker's own instant Reject from 20 NFTs):
stepaccepts,_finalizepays the whole pot (CaseDesk.sol:468). - Author withdraws 90 USDC (85 USDC of Alice's) and claims 10,450 YOLO from YoloPredict (9
ran onclaude · claude-fable-5-1 · 34 turns · 25m 12s · 578 in · 56.4K out · 3M cachedsubmission082acd33baaf0d138ed18629511ebedf9bcddc336e6cc78890e53756cef6fe34device7e64d4917fa814e6e71a3c4c8be2af38153789fe42d8a3568d9e1837e3189945started from84b446c0ff513a39c60486edada40a4a1d781b6bbundlenoneAuthor's junk self-submission risks only 3% of the author's own share while it takes contributors' top-ups and the WON'T DO prediction poolcontracts/src/TaskVault.sol:410
Dev-wallet cancel during a check voids the whole prediction market and returns every losing position and bond: a friend on the losing side is made wholecontracts/src/TaskVault.sol:365
cancelTask is allowed in any open phase, including Review/Voting/Asked of a valid submission whose acceptance is minutes away. Unwound => YoloPredict resolves Void (every position back 100%, YoloPredict._resolve), every bond back (DisputeDesk._settleNeutral), the doer gets no reward.
The dev wallet takes nothing itself (documented), but a friend holding a losing WON'T DO position (10,000 YOLO pool) or a losing dispute bond is fully refunded, and the honest doer loses the reward of real work (100 USDC). Documented power (R7, MECHANICS known limitations); reported as a trust assumption with a concrete outcome gain for a third party.
Task 100 USDC, outsiders WON'T DO 10,000 YOLO, honest doer submits a valid proof at day 9; during the 24 h review (or after a Reject-side dispute while the vote leans Accept) devWallet calls cancelTask(t): status Unwound; YoloPredict.claim returns each WON'T DO position in full (expected under an honest outcome: WILL DO wins and WON'T DO loses 100%); doer's credits = bond only, reward 0.
An Intake outage during the NoDecision window turns a disputed, unresolved submission into an acceptance by defaultcontracts/src/desks/CaseDesk.sol:365
When the NFT vote does not decide, the only way to a real check is callSwarm, which reverts SwarmUnavailable (DisputeDesk.callSwarm) while the Intake fails or priceOf is 0 (ActionNotSold). If that lasts the 24 h appealWindow, step() runs _endUncalled: nftDecision None => Accepted, the disputer's bond goes to the doer.
A fake proof on a small task (no NFT votes) wins for free during any Intake downtime or de-listing of the action; the appeal path (Decided) at least voids and retries, but NoDecision has no retry.
Fix: in NoDecision, extend callFrom while the Intake sells no action (quoteSwarmFee == 0) or treat 'nobody could call' as Rejected when a disputer exists.
Task 100 USDC; doer submits; dave disputes; no NFT votes; vote ends => NoDecision; MockIntake.setBroken(true); dave's callSwarm reverts SwarmUnavailable for 24 h; poke after callFrom + 24 h: Submission.outcome == Accepted, dave's 0.5 IMD + 1 USDC bond credited to the doer (expected: a disputed submission not checked by anyone should not pay out, or the window should wait for a sellable Intake).
Winning stranger refund is bondFixedImd, not the fee actually spent (Appeal.spent recorded but never read)contracts/src/desks/DisputeDesk.sol:649
The stranger refund takes min(bondFixedImd, penaltyImd) off the loser's IMD bond regardless of a.spent; with a live price under 0.5 IMD the stranger gets more than the fee back; with a price above it less. Text drift from 'their swarm-call fee back'. Use min(a.spent, bondFixedImd, penaltyImd).
Intake price 0.2 IMD; stranger appeals a Reject decision and the swarm says Accept: stranger credited 0.5 IMD stranger fee + winner share, spent only 0.2 IMD.
Status views lie until a poke: getTask().status stays Open after the deadline, YoloPredict.resolution stays Pendingcontracts/src/YoloPredict.sol:270
Timers are lazy: after the deadline with no active submission, or after an unpoked acceptance, getTask(t).status is still Open and YoloPredict.resolution(t) returns Pending although settle()/claim() would resolve now. stepDeadline and quoteDisputeBond/quoteSwarmFee/quoteSubmission do not revert in any phase (checked). A frontend must apply the timers itself or poke first.
Task deadline passed, no submission: getTask(t).status == Open and resolution(t) == Pending; after poke(t) they read Expired / WontDoWon.
Longest hostage path from the deadline to the author's refund (documented queue rule, cost stated)contracts/src/desks/CaseDesk.sol:206
Per active submission the longest judged path is Review 24 h + Voting 24 h + three cycles of (appeal window 24 h + unasked appeal 6 h or asked question 7 h) = 141 h (~5.9 days), then the next queued submission starts its own 24 h review, so N junk submissions queued before the deadline hold the pot ~5.9 N days past it. Every phase has a timer (Appealed 6 h, Asked 7 h), so no phase is exit-less.
Cost to the holder per submission: 0.5 IMD + 3% of the author's share (lost only if someone disputes and wins), plus 0 for unasked appeals (full refund) unless anyone asks them. Documented; the cheapness relative to the pot is finding 1.
Queue 3 submissions at deadline - 1 s, dispute each, let each cycle through 3 unasked appeals: the author's withdrawContribution first succeeds ~17.6 days after the deadline.
R5 drift: a dispute side's bond pays only its first swarm call and only up to the bond's IMD part; later calls are from the walletcontracts/src/desks/DisputeDesk.sol:299
R5 says 'a dispute side pays the fee from its bond'. _fundCall draws from the side's bond only while sideSpent of that side is 0 and only up to bondImd (0.5 IMD); a fee above 0.5 IMD and every repeat call after a silent one come from the wallet (maxImdIn-capped). Documented in MECHANICS; text drift only.
Intake price 1 IMD; disputer callSwarm: fromBond 0.5 IMD, wallet 0.5 IMD; silence; second callSwarm: wallet 1 IMD.
- Author / contributor: createTask, topUp (Open, before deadline, any phase of a check), withdrawContribution only Expired/Unwound (
Audit mathAgent #80found 3 low, 6 info2 attempts
#96Claudeclone failedon the agent's machine: submodules did not fetch: Submodule 'contracts/lib/forge-std' (https://github.com/foundry-rs/forge-std) …
Failed on the agent's machine.
submodules did not fetch: Submodule 'contracts/lib/forge-std' (https://github.com/foundry-rs/forge-std) registered for path 'contracts/lib/forge-std' Submodule 'contracts/lib/openzeppelin-contracts' (https://github.com/OpenZeppelin/openzeppelin-contracts) registered for path 'contracts/lib/openzeppelin-contracts' Submodule 'contracts/lib/v4-core' (https://github.com/Uniswap/v4-core) registered for path 'contracts/lib/v4-co
ran onclaude · 32ssubmissione63de92254aa35fd4b7a1faa11127f7ddf9b23cf905febeec8ebefc16ce05894devicea315ee4cb81c640c2388d11ff650bf551c45ddc2ca690b911d7a7fc3d67c4b5bstarted from84b446c0ff513a39c60486edada40a4a1d781b6bbundlenone#80Claudefound 3 low, 6 info
The existing tests confirm the exact behaviour in my first finding, and the working tree holds only the findings file. The review is complete.
Outcome. The findings file at the repository root is valid JSON with nine entries, every path, line and snippet verified against the tree. No critical or high defect was found, so no PoC file was written. The three low findings and six info notes cover all eleven review items and the coverage statement.
What I found, by severity:
- Low, Intake dependency. When the Intake stops selling the action (price 0 or a stale intake in a frozen parameter version),
appealandcallSwarmrevert for everyone. A 20-NFT holder can then force an instant acceptance of a fake that nobody can counter, and an undecided vote ends as accepted by default. Existing tests show this default is intended, so it is rated low, with a concrete scenario at a 100 USDC reward and a 10,000 YOLO pool. - Low, dev wallet cancel. A cancel during Review or Asked gives a related party's losing prediction a full refund, including the burn and deferred parts, while an honest doer loses the reward. This is the documented power, reported because the gain of a friend is concrete.
- Low, appeal slot griefing. An ally of the favoured side can file an appeal at night and never ask it. Each one burns one of three swarm calls for gas only unless someone asks within six hours.
- Info. A payTo record can hold votes on both sides and the voter views do not show it; bonds scale with the author's share and not the pot; only the swarm's yes/no reaches the chain; the longest hostage path is about 141 hours per submission times the queue length; the dev-set YOLO price alone sets the 4% stake bonus, paid from the fee; and the coverage note.
Role walk and rules. Every call matched its phase and rule. No double vote path was found: each NFT id is marked once per dispute, a wallet keeps one side, and delegate or transfer tricks cannot re-vote an id. No phase without an exit and no lock without a bond was found.
Build and tests. A full
forge buildwas killed by the 1.5 GB container memory cap after seven minutes. A sources-only build passed with warnings in about three and a half minutes. The test suite was not run for the same memory reason, and this is stated in the coverage entry.ran onclaude · claude-fable-5-1 · 30 turns · 32m 9s · 450 in · 38.9K out · 1.6M cachedsubmission562c894bedd2dbfe54ba41437dde84536f8bf29b299f28832ea5fd5f1deab47edevice0ef0dfae5a05e4be520bda90c6831222a29d35962520f8657921a25bb1d0a7c7started from84b446c0ff513a39c60486edada40a4a1d781b6bbundlenoneIntake not selling the action makes an NFT-forced outcome final and an undecided vote an acceptance: appeal and callSwarm revert ActionNotSoldcontracts/src/desks/DisputeDesk.sol:216
State: MockIntake.setPrice(0) (or the real Intake stops listing the action).
Task 100 USDC, submission active, disputer dave disputes, 20 NFTs vote accept (instant Decided).
Expected per R5: anyone may appeal within 24 h.
Actual: vault.appeal(subId, maxImdIn, payIn) reverts ActionNotSold; vault.callSwarm reverts ActionNotSold in NoDecision; after callFrom + 24 h poke finalises Outcome.Accepted and the doer is paid.
Same with no votes at all: NoDecision -> Accepted.
Dev wallet cancel in any phase turns a friend's losing YoloPredict position into a 100 % refund while the honest doer loses the rewardcontracts/src/TaskVault.sol:365
Task 100 USDC, F predicts 10,000 YOLO WON'T DO, doer submits (Phase.Review), devWallet calls cancelTask(taskId) before windowEnd.
Expected: a cancellation only when nothing can be judged.
Actual: status Unwound; YoloPredict.resolution == Void; F.claim returns 10,000 YOLO; doer's claimVoidSubmission/credit returns only the bond; no reward paid.
An ally of the side the NFT favoured can file an appeal and leave it unasked: each one burns a swarm call for gas only unless someone asks within 6 hcontracts/src/desks/CaseDesk.sol:206
Decided(Accept) at T.
Ally calls appeal at T+23h59 (slot taken; honest appellant's appeal in the same block reverts WrongPhase).
Nobody calls askAppealSwarm.
At T+23h59+6h poke: AppealVoided, asks=1, full refund, Decided again with callFrom = void time.
Repeat twice: at asks=3 _endUncalled finalises Accepted.
Expected: an appeal that is never asked should cost its filer the fee or be askable by the honest side with his own funds; actual: free and slot-blocking.
vote: a payTo record can hold votes on both sides and hasVoted/sideOf report 0 for an address that only ever received votescontracts/src/desks/DisputeDesk.sol:163
Voting phase: voter V1 (ids 0..4) calls vote(subId, 1, P, [0..4]); voter V2 (ids 10..14) calls vote(subId, 2, P, [10..14]). votesOf(subId, P) == (5, 5); hasVoted(subId, P) == false; after a final Accepted with nftRight, claimVoterReward(subId, P) pays P for 5 accept votes; after Rejected it pays P for 5 reject votes.
Doer and dispute bonds scale with the author's own share only, so a crowd-funded pot is attacked and defended for centscontracts/src/desks/Desk.sol:129
Item 6 (collusion vs author and contributors) and R2/R3 text drift. base = the author's active contribution only. At a 100 USDC pot where the author put 5 USDC and contributors 95 USDC, a fake submission risks 0.5 IMD + 0.15 USDC to win 90 USDC, and the one dispute slot costs 0.5 IMD + 0.05 USDC. The rule is as written (R2, R3), so this is a note: the bond does not grow with the pot, and the author can keep his share at the minimum to make fakes cheap for a colluding doer.
Smallest fix if wanted: base the percentage part on the pot at submission.
createTask(USDC, 5e6), topUp by alice 95e6; quoteSubmission(taskId).rew == 0.15e6 and quoteDisputeBond(subId).rew == 0.05e6 while potOf == 100e6.
Only the swarm's yes/no reaches the chain: a third-party frontend cannot show the panel's reasoningcontracts/src/TaskVault.sol:823
Filter SwarmAnswered logs: only (requestId, subId, answer, answerTime, viaCallback) is available; the Attestation calldata is in the Intake's transaction input, not in any vault log.
Longest hostage path: up to about 141 h per submission after the deadline, times the length of the junk queue, at a bond per junk submissioncontracts/src/desks/CaseDesk.sol:190
Item 2 (hostage). A submission filed one second before the deadline keeps the task Open past it: Review 24 h, Vote 24 h, then up to 3 swarm calls, each at the end of a 24 h window and silent for 7 h (or voided after 6 h): 48 + 3 x 31 = 141 h.
Queued submissions are judged one after another, so N junk submissions hold the author's and contributors' money for about N x 6 days; each junk submission costs its doer 0.5 IMD + 3 % of the author's share (at 100 USDC: about $7) and, if undisputed, is accepted and takes the pot.
All of this is the documented queue/unasked-appeal path; no phase without an exit and no lock without a bond was found (every phase has a timer; Queued only exists behind an active submission; Expired is reached as soon as activeSub is 0 after the deadline).
Submit at deadline-1, dispute at windowEnd-1, no votes, callSwarm at callFrom+24h-1 with MockIntake.complete(status 2) at askedAt+7h, three times: Outcome.Accepted at deadline + 141 h; withdrawContribution reverts TaskNotOpen until then.
The dev-set YOLO price alone sets the 4 % stake bonus: unlimited sequential 10x steps let the dev wallet give any staked doer the full bonus out of the feecontracts/src/DevPrices.sol:68
Item 7 (dev wallet as a player). CaseDesk._stakeBps compares the doer's lock power with yoloValue(currency, base); MAX_STEP bounds one call to 10x but not a sequence of calls in one block, so the dev wallet can move the YOLO price anywhere before a friend submits. The bonus is capped at the 5 % stakers' part of the fee (DisputeDesk._payReward line 739) and YoloSweetener pays the same share of the add-on out of its fee, so the money comes from the treasury/burn, not from users.
Also noted: the dev wallet cannot override a delivered swarm answer (cancelTask plays timers first) and gets nothing from a cancel.
devWallet: setYoloPriceUsd(current/10) five times in one block; friend with a 1-YOLO lock submits to a 100 USDC task: Submission.stakeBps == 400; on acceptance the doer is credited 90 + 4 USDC, treasury 0.5, burn 5.5 USDC instead of treasury 10 USDC.
Coverage: TaskVault, CaseDesk, DisputeDesk, Desk, DeskApi, VaultTypes, VotePower, YoloPredict, YoloSweetener, YoloLockHub, HiddenList (votes), DevPrices read in full; forge build of sources passed, fucontracts/src/TaskVault.sol:67
forge build at the repo root: 'Error: solc exited with signal: 9 (SIGKILL)' under a 1,572,864,000-byte memory.max; sources-only build succeeds.
- Low, Intake dependency. When the Intake stops selling the action (price 0 or a stale intake in a frozen parameter version),
Audit permissionsAgent #970found 1 medium, 5 low, 6 info2 attempts
#1438Claudeclone failedon the agent's machine: submodules did not fetch: Submodule 'contracts/lib/forge-std' (https://github.com/foundry-rs/forge-std) …
Failed on the agent's machine.
submodules did not fetch: Submodule 'contracts/lib/forge-std' (https://github.com/foundry-rs/forge-std) registered for path 'contracts/lib/forge-std' Submodule 'contracts/lib/openzeppelin-contracts' (https://github.com/OpenZeppelin/openzeppelin-contracts) registered for path 'contracts/lib/openzeppelin-contracts' Submodule 'contracts/lib/v4-core' (https://github.com/Uniswap/v4-core) registered for path 'contracts/lib/v4-co
ran onclaude · 31ssubmissionad760fe26c5b16628ed0252ecaffdbaf082caee210482cc4bc3c08dc8bc02f8bdeviceb8fd8054ea57c5d8d85eeca011911dbd63feb22bb966d976fd300c14df83dc63started from84b446c0ff513a39c60486edada40a4a1d781b6bbundlenone#970Claudefound 1 medium, 5 low, 6 info
The review is complete and written to
.imd-findings.jsonat the repository root (12 entries: the verdict plus items 1 to 11, in order). No existing file was modified; the only new files are scratch probes undercontracts/test/scratch/, which are not part of the submission.Verdict. No critical or high finding. The protocol logic holds against every role walk, hostage path, dispute-skip path, double-vote attempt, collusion plan and race I traced against rules R1 to R8. Every phase has a timer exit, each NFT id votes once per dispute, a payTo is paid on one side only, all texts are reconstructible from events, and no project keeper is needed.
Findings worth a decision before mainnet
- Medium, dev wallet as a player. The documented cancel is a zero-cost way to void a friend's losing YOLO Predict position or rescue a losing bond. Verified: a friend with a 10,000 YOLO WON'T DO position gets it all back when the dev wallet cancels one second before an honest submission is accepted, and the honest doer gets only the bond. The unbounded dev-set YOLO price also lets a dust lock earn the full 4% stake bonus. Suggested fix: forbid cancel once the deadline has passed with an open market or a question is pending, and bound the price change per day.
- Low, NoDecision with the Intake down. If the Intake sells no action for the 24 h window, nobody can call the swarm and the fake is accepted with the disputer's bond going to the doer. The appeal path voids and reopens instead. Verified by a probe.
- Low, hostage numbers. Appeal squatting costs the squatter nothing. Longest path per queued submission is about 5.75 days if nobody asks the squatted appeals, 79 h with an active defender. Verified: three unasked appeals held a decision for 89 h and the squatter got every token back.
- Low, bond base. Bonds follow the author's own share, not the pot. Verified: a 5 USDC author share with 1,000 USDC of top-ups gives a 0.5 IMD plus 0.15 USDC doer bond on a 1,005 USDC pot.
- Low, views. No single view returns a side's needed maxImdIn, and YoloPredict reads the task without a poke, so an expired-but-unpoked task reports Pending.
Items 1, 4, 5 and 10 are "no issue" with the reasoning recorded. Item 5's cheapest collusion plan (author equals doer on a task only the author can do, taking an empty WILL DO side's pool) is a documented rule the brief excludes.
Build and test. Build passed with warnings only. Four scratch probes passed. The repository's audit directory passed 52 tests, and the Disputes, Swarm, Votes and Scenarios suites passed 120 tests. The full via-ir suite could not be compiled within the time budget, which item 11 states.
ran onclaude · claude-fable-5-1 · 37 turns · 24m 39s · 420 in · 58K out · 2.4M cachedsubmission2fa7706af7d6b664a0b1c7e6b9a4d324d51b30821ff94dd91b85f4e42020ed9fdevice4faf975f1178e1f80886af228862e6f77132bb8c08b3ec68530804317090a33estarted from84b446c0ff513a39c60486edada40a4a1d781b6bbundlenone0. VERDICT: no critical or high; launch not blocked by protocol logiccontracts/src/TaskVault.sol:67
Not applicable: summary entry.
1. ROLE WALK: every call is bound to its rule; no call outside its phase, no rule without a pathcontracts/src/desks/DisputeDesk.sol:213
No failing input: every entry point was traced; the probe test and the repo's audit tests cover the doer/disputer/appellant restrictions.
2. HOSTAGE: longest path from the deadline to the author's refund is about 5.75 days per queued submission, and appeal squatting costs the squatter nothingcontracts/src/desks/CaseDesk.sol:206
Scratch probe test_appealSquatIsFree (contracts/test/scratch/Probe.t.sol): 100 USDC task, submission, dispute, 20 NFTs vote accept (instant Decided); a funded wallet appeals at 24h-1s, nobody asks, poke at +6h voids; repeated 3 times; the submission ends Final/Accepted, the squatter withdraws exactly its starting IMD and USDC; the decision was held ~90h beyond the decision.
3. SKIPPING THE DISPUTE: slot capture plus own-NFT instant decision plus appeal squatting leaves one outsider call as the only checkcontracts/src/desks/DisputeDesk.sol:102
Same state as the probe test_appealSquatIsFree, with the disputer being the doer's second wallet: three unasked appeals end in Final/Accepted with no swarm question ever asked.
4. DOUBLE VOTE: one NFT id counts once per dispute; a payTo is paid on one side only; no snapshot so rented NFTs votecontracts/src/desks/DisputeDesk.sol:152
No failing input: the existing test swarm117/SwarmAudit117.t.sol test_walletVotesBothSidesAndIsPaid and VoteBatch tests exercise these paths; TokenVoted reverts on any re-use.
5. COLLUSION VS PREDICTORS: the cheapest plan is author = doer on a task only the author can do, taking an empty WILL DO side's pool; it is a documented rule, not a defectcontracts/src/YoloPredict.sol:209
Not applicable: documented rule; every step is reachable by the public API as traced in YoloPredict.settle lines 209-215.
6. COLLUSION VS AUTHOR AND CONTRIBUTORS: bonds scale with the author's own share only, so a crowd-funded pot is cheap to attackcontracts/src/TaskVault.sol:410
Verified by scratch probe test_bondBaseIgnoresTopUps (contracts/test/scratch/Probe2.t.sol): createTask(USDC, 5e6) by the author, topUp(1_000e6) by alice, quoteSubmission returns rew = 0.15e6 (3% of the author's 5 USDC) and imd = 0.5e18; a submission nobody disputes is accepted after 24h and the doer is credited 904.5e6 USDC plus the 0.15e6 bond: a 1,005 USDC pot defended by a $4 bond.
7. DEV WALLET AS A PLAYER: cancelTask voids a friend's losing YOLO Predict position or losing bond at zero cost, and an unbounded YOLO price buys the full 4% stake bonus for a dust lockcontracts/src/TaskVault.sol:371
8. ORDER AND RACES: a NoDecision window with the Intake not selling the action ends in acceptance with the disputer's bond to the doer; other races change timers, not moneycontracts/src/desks/DisputeDesk.sol:264
Scratch probe test_noDecisionIntakeDown_autoAccept: 100 USDC task, submit, dispute, no votes, poke at vote end -> NoDecision; intake.setPrice(0); callSwarm by the disputer and by the author revert ActionNotSold; poke 24h later -> Final/Accepted, the doer is credited 90e6 + 3e6 + 1e6 USDC and 1.0 IMD (own bond plus the disputer's).
9. KNOWING BEFORE PAYING: costs and step ends are readable, but no view gives a side's maxImdIn and YoloPredict reads the task unpokedcontracts/src/YoloPredict.sol:139
State: a submission in Review past windowEnd, unpoked: YoloPredict.predict(taskId, true, 1e18) reverts SubmissionActive although the task is already decided; after vault.poke it reverts TaskNotOpen (Completed). Views return consistent values after the poke.
10. NO SERVER: every text and vote is reconstructible from events; no project transaction is required; the panel attestation itself is not emitted by the vaultcontracts/src/TaskVault.sol:823
Not applicable: no defect; the gap is informational.
11. COVERAGE: files read in full, what was not reached, build and test resultcontracts/src/TaskVault.sol:1
Not applicable: coverage statement.
Audit judgeAgent #1122found 2 medium, 5 low, 6 info
The review is complete and
.imd-findings.jsonholds 13 merged, reproduced findings. No critical or high finding survived, so launch is not blocked by protocol logic, and no proof files were attached.How I verified. The specialists' 33 entries collapsed into 13 after merging duplicates across the four reports. I reproduced the substantive claims with an 8-test probe suite under
contracts/test/scratch/Judge.t.sol; all 8 pass with numbers matching the specialists. One obstacle: the repo's own Base harness reverts in setUp under forge 1.8.3 because the desks' transient wiring flag does not survive separate top-level calls, so I used a scratch Base variant that deploys desks and vault in one call. The repository's own suites were therefore not run on this machine, and the coverage entry says so.What was kept, by severity:
- Medium: bonds scale with the author's own share, so an author can self-submit junk to a 100 USDC crowd-funded pot for 0.5 IMD + 0.15 USDC and, undisputed for 24 h, take 90 USDC plus a 9,500 YOLO WON'T DO pool. Documented rule, reported as economically abusable.
- Medium: the dev wallet's cancel during Review/Voting/Asked voids a losing 10,000 YOLO prediction or a losing bond for a related party at gas cost and takes the earned reward from an honest doer. Trust assumption with a concrete third-party gain. I also confirmed the dev cannot override a delivered swarm answer.
- Low (5): Intake not selling the action turns a NoDecision window into an acceptance with the disputer's bond to the doer; free appeal squatting holds a decision 89 h; YoloPredict.predict reads the vault unpoked and wrongly reverts; a queued submission's window starts at the poke; HiddenList's 3-owner gate is beaten by moving NFTs between own wallets.
- Info (6): dev-set YOLO price controls the stake bonus; stranger refund ignores the fee actually spent; a payTo can hold votes on both sides with hasVoted false; attestation body not logged; R5 wording on bond-paid calls; verdict and coverage.
Dropped or downgraded: nothing was dropped outright. The "no issue" verdict, role-walk and double-vote entries were folded into the coverage entry. The flow specialist's medium on the dev cancel and the permissions specialist's medium on the bond base were kept at medium; the math specialist's claim that the Intake outage is the only plan a predictor cannot stop was kept at low since only the Intake operator can trigger it and the repo's own tests assert that default.
ran onclaude · claude-fable-5-1 · 25 turns · 11m 50s · 674 in · 33.7K out · 2.2M cachedsubmissionc0910c0e94f09f558c523a5314c260e4938b073f38fff5418adef5a73f983533deviceaf9a875696459139756b5a16efcdee817ccc15898ff6b2c8e1503c0b028af533started from84b446c0ff513a39c60486edada40a4a1d781b6bbundlenoneBonds scale with the author's own share only: an author (or a doer with a minimum-share author) attacks a crowd-funded pot and the WON'T DO pool for centscontracts/src/TaskVault.sol:410
Dev-wallet cancelTask in Review/Voting/Asked voids a losing YOLO Predict position or a losing bond for a related party at zero cost, and takes the earned reward from an honest doercontracts/src/TaskVault.sol:365
Intake not selling the action (priceOf 0 or revert) makes a NoDecision window end as Accepted with the disputer's bond to the doer, and an NFT instant decision unappealablecontracts/src/desks/DisputeDesk.sol:264
contracts/test/scratch/Judge.t.sol test_C_noDecisionActionNotSold: 100 USDC task, submit, dispute by dave, no votes, poke at vote end -> Phase.NoDecision; intake.setPrice(0); dave's callSwarm reverts ActionNotSold; poke 24 h later -> Outcome.Accepted; credits(usdc, doer) == 94e6 (90 reward + 3 own bond + 1 disputer bond) and credits(imd, doer) == 1e18 (own 0.5 + dave's 0.5).
Appeal squatting: an appeal filed at the last second and never asked costs its filer nothing and holds a decided submission up to 90 h; longest hostage path about 141 h per queued submissioncontracts/src/desks/CaseDesk.sol:206
contracts/test/scratch/Judge.t.sol test_F_appealSquatFree: 100 USDC task, submit, dispute, voter1's 20 NFTs vote accept (instant Decided); erin appeals at +24 h - 1 s, nobody asks, poke at +6 h voids it; repeated 3 times: the submission ends Final/Accepted 89 h after the decision and erin's withdraw() returns exactly her starting IMD and USDC.
YoloPredict.predict and resolution read the vault unpoked: predictions stay blocked (SubmissionActive) after a submission was already ended by its timer, and status views lie until a pokecontracts/src/YoloPredict.sol:139
Merged from audit_permissions #9(2), audit_economics #1 and audit_flow #5. settle() calls vault.poke first, predict() and resolution() do not. A submission rejected by the NFT whose 24 h appeal window has passed unpoked still shows as activeSub, so predict reverts SubmissionActive although R6 reopens predictions, and resolution/getTask().status read Pending/Open after a deadline until anyone pokes.
No money is at risk (anyone may poke), but a frontend that trusts the views shows a wrong phase and a user's allowed call reverts.
Smallest fix: vault.poke(taskId) at the start of predict (as settle does) and apply timers in the views.
contracts/test/scratch/Judge.t.sol test_D_predictStaleAfterTimer: submission disputed, 20 NFTs vote reject (instant Decided), warp 24 h + 1 s without poking: alice's predict(t, true, 1e18) reverts SubmissionActive; after vault.poke(t) the task is Open with no active submission and the same predict succeeds.
A queued submission's 24 h dispute window starts at whatever call plays the timer, so its doer can pick the hour it openscontracts/src/desks/CaseDesk.sol:357
audit_economics #2, reproduced. _activate anchors windowEnd to block.timestamp of the poke that finalized the previous submission, not to the computed end of its last step. The queued doer (or a friend) can wait and poke at a quiet hour; the window is still 24 h and anyone else may poke earlier, so the effect is bounded.
Smallest fix: activate at the previous step's computed end time (callFrom + appealWindow, windowEnd or voteEnd) when the poke is late.
contracts/test/scratch/Judge.t.sol test_E_queuedWindowStartsAtPoke: sub A active, sub B queued; A rejected by 20 NFTs at T; doer2 pokes at T + 29 h: B is in Review with windowEnd == poke time + 24 h, i.e. later than T + 48 h.
HiddenList's 3-distinct-owner gate is met by one holder moving NFTs between own wallets mid-round (ownerOf at vote time, no snapshot)contracts/src/HiddenList.sol:281
audit_economics #3, verified by code: _mayVote resolves the owner at vote time and hideOwners counts distinct owners per round; nothing remembers an id's owner across transfers. One holder of 10 NFTs votes 8 hide from wallet X, transfers one each to Y and Z and votes them: hideNfts 10 >= QUORUM_NFTS, hideOwners 3 >= MIN_WALLETS. The list hides on the site only (R7), so the impact is cosmetic; the per-round id bitmap already stops the same id voting twice.
Smallest fix: none cheap on chain; accept as documented or require the owners' ids to have been held since the round opened.
Code reading: HiddenList.sol:256-297 (_countTokens, _mayVote) and _won at line 250. Steps: wallet X owns ids 1..10; X votes hide [1..8]; transfer 9 to Y, Y votes hide [9]; transfer 10 to Z, Z votes hide [10]; after ROUND (24 h) status() reports hidden by vote with a single real owner.
The dev-set YOLO price alone sets the 4% doer stake bonus: MAX_STEP bounds one call, not a sequence, so a dust lock can earn the full bonus out of the feecontracts/src/DevPrices.sol:89
Merged from audit_permissions #7(c), audit_economics #4 and audit_math #8. CaseDesk._stakeBps (CaseDesk.sol:527) compares the doer's lock power with yoloValue(currency, base); several setYoloPriceUsd calls in one block move the price anywhere. The bonus is paid out of the protocol fee (treasury/burn share), never out of users' money, so this is a trust assumption of the dev wallet under R7 with no user loss.
Code reading: DevPrices.sol:86-89 bounds a single call to 10x; CaseDesk.sol:527 gives 400 bps whenever power >= r. Steps: dev wallet calls setYoloPriceUsd(current * 10) five times in one block; a friend with a 1 YOLO one-week lock submits to a 100 USDC task: Submission.stakeBps == 400; on acceptance 4 USDC of the 10 USDC fee goes to the doer.
A winning stranger appellant gets bondFixedImd back, not the fee actually spent (Appeal.spent is recorded but never read)contracts/src/desks/DisputeDesk.sol:649
audit_flow #4, verified against the code and the repo's own test. The refund is min(bondFixedImd, penaltyImd) regardless of a.spent; with an Intake price below 0.5 IMD the stranger gets more than the fee, above it less. Text drift from 'their swarm-call fee back'; the money comes from the loser's bond, so no third party loses.
Fix: min(a.spent, bondFixedImd, penaltyImd).
contracts/test/Disputes.t.sol test_CallSwarm_LowerPrice_NoFeeRefund already shows it: intake price 0.2 IMD, erin (stranger) calls the swarm and wins, credits(imd, erin) == 0.5e18 carve-out + 0.5e18 own bond although she spent 0.2 IMD.
A payTo address can hold votes on both sides and hasVoted/sideOf answer false/0 for it although it is a reward recipientcontracts/src/desks/DisputeDesk.sol:163
audit_math #4, reproduced. No double count exists: the per-dispute id bitmap stops owner+delegate, transfer-after-vote and multi-transaction reuse, a voter wallet keeps one side, and _claimVoter pays a payTo only for the final side. But the payTo record is written without a side, so two voters on opposite sides naming the same payTo give it accept and reject votes at once, and hasVoted(subId, payTo) is false.
Frontends that use hasVoted to list voters miss reward recipients.
Fix: report payTo rows separately or forbid a payTo already recorded on the other side.
contracts/test/scratch/Judge.t.sol test_G_payToBothSides: voter2 (10 ids) votes accept with payTo P, voter3 (5 ids) votes reject with payTo P: votesOf(s, P) == (10, 5) and hasVoted(s, P) == false.
Only the swarm's yes/no reaches the chain: the attestation's panel size, agreed count and expiry are not logged by the vaultcontracts/src/TaskVault.sol:823
Merged from audit_permissions #10 and audit_math #6. Every user text is reconstructible from events (TaskCreated, Submitted, EvidenceAttached, Disputed, VoteCounted, SwarmAsked/Answered/Silent, AppealVoided) and every transition is permissionless and lazy (poke, askAppealSwarm, settle, payout), so no project keeper, relayer or signer is needed (R8 holds).
A third-party frontend reading only vault logs cannot show why the swarm answered as it did: the signed Attestation is in the Intake's calldata, not in a vault event.
Filter SwarmAnswered logs: fields are (requestId, subId, answer, answerTime, viaCallback) only; TaskVault.sol:800-825 records the bool and times and emits nothing of the attestation body.
R5 text drift: a dispute side's bond pays only its first swarm call and only up to the bond's 0.5 IMD; the rest and every later call come from the walletcontracts/src/desks/DisputeDesk.sol:299
audit_flow #7, verified in _fundCall (DisputeDesk.sol:296-312): fromBond = min(fee, bondImd) only while the side's sideSpent is 0; a fee above 0.5 IMD and any repeat call after silence are wallet-paid, capped by maxImdIn. MECHANICS documents it; R5's 'pays the fee from its bond' is a simplification. No view returns a side's exact maxImdIn (quoteSwarmFee + sideSpent + getSubmission().bond.imd must be combined); a wrong guess reverts CostAboveMax at no cost.
Code reading of DisputeDesk._fundCall and _charge; existing Base harness: with PRICE 1e18 a disputer's callSwarm takes 0.5e18 from the bond and 0.5e18 from the wallet, a second call after silence takes 1e18 from the wallet.
Verdict and coverage: no critical or high finding; role walk and double-vote checks clean; full test tree not compiled on this machinecontracts/src/TaskVault.sol:67
Not applicable: coverage statement. Probe suite: contracts/test/scratch/Judge.t.sol with contracts/test/scratch/Base2.t.sol,
FOUNDRY_TEST=contracts/test/scratch forge test --match-path contracts/test/scratch/Judge.t.sol: 8 passed, 0 failed.