# Audit report

> moneycanbuy.fun audit round 2: fix check, HiddenList, full repeat, business risks, mainnet verdict. Audit this repository at the pinned commit 7714a243271eeb1560e472d06167594e2f731ec0, as it is, for a mainnet launch: contracts/src and contracts/script/LaunchPlan.sol, and the business logic in README.md, docs/MECHANICS.md, docs/TOKENOMICS.md, docs/LAUNCH-TASK.md, docs/AUDIT-INTERNAL.md. Foundry project is contracts/: run forge from there, import as "src/...". Do not modify, delete or reformat any existing file. Findings go only into the report and into NEW test files under contracts/test/scratch/.
>
> Second audit of the same board of paid dares. Since audit 117cf05a (commit d9a1ee5: 3 medium, 5 low, 3 info, no critical/high), the owner fixed all 11 findings and added a standalone HiddenList contract (launch 3's 4th) so the site stops rendering a hidden task or proof; NFT holders can also vote one hidden; a questionPrefix line tells the swarm to answer "no" on illegal content. Both already in this commit. Treat every file as unaudited, a full second pass, not a diff.
>
> REPORT FORMAT, MANDATORY. One finding-style entry per numbered item below, same order, even where nothing is wrong (info severity, "no issue", one line of evidence). A missing item makes the report INCOMPLETE. Entry 0 first, regardless of severity: the VERDICT, ready for mainnet or not, every blocker, at most 8 lines. Entries 1-11 FIX CHECKS: closed yes/no, new issue opened yes/no (file:line, PoC if so). Entries 12-20 CODE: severity, file:line, impact, external call sequence, concrete input/state with expected vs actual, reachable at launch values or not, smallest fix, a Foundry PoC (full source, under contracts/test/scratch/, fails on this code and passes once fixed, no fork/ffi). Entries 21-24 BUSINESS/CONCEPT RISKS: who gains, who loses, cost, recommendation. Entry 25 COVERAGE: what you read in full, what you could not reach, the forge test result.
>
> Severity: critical = theft, permanent loss or bricked launch; high = loss under realistic conditions or a core promise broken; medium = bounded loss, cheap griefing or a documented guarantee that fails; low = edge cases; info = docs drift, hardening, "no issue".
>
> FIX CHECKS, audit 117cf05a, each reproduced, not assumed:
> 1. YoloPoolSamples: one-block swap-sample-swap-back stores nothing; highest-of-2h, $0.01 cap, 30,000 YOLO floor hold.
> 2. ProjectVesting.bindStarter: only YoloSplitter (bound once, its own deploy tx) starts a clock; no 1-wei pre-start path.
> 3. DisputeDesk MAX_SHORTFALL_BPS = 20%: winners and correct voters keep >= 80% of the penalty, any relayer side.
> 4. DisputeDesk: doer cannot appeal or callSwarm against their own accepted submission.
> 5. VoteLib: a wallet voting both sides has its second ballot skipped, unpaid.
> 6. YoloSweetener NO_PRICE_WAIT (7 days): no-price payout defers, pays doer in full after 7 days still no price.
> 7. CaseDesk SWARM_DELIVERY_GRACE (signed time + 1h): outcome does not depend on poke timing, either order.
> 8. TaskVault announceWithdraw: 1h notice plus a 24h window on an Open task's active share; Expired/Unwound/pending top-ups need none.
> 9. AttestationChecker: an expired signed answer is rejected (code 6, expiresAt).
> 10. ParamsRegistry: the last attester cannot be removed; MECHANICS.md "Trust assumptions" matches the code.
> 11. LaunchPlan.sol: single INTAKE constant = Intake v2 (0xa43e6f75ee006411f79ac1c84120606c2330de82) for every launch; v1 stays only in a comment.
>
> CODE (12-14 new, 15-20 a full repeat, same rigor as audit 1). Report anything missed at d9a1ee5, introduced by the 11 fixes, or from HiddenList touching the core (should be none):
> 12. HIDDENLIST MONEY. No payable/receive/token call; cannot hold, freeze, delay or redirect money; plain ETH reverts. TaskVault, CaseDesk, DisputeDesk, ParamsRegistry, ProtocolDirectory, NftGate bytecode unchanged except the prefix string.
> 13. HIDDENLIST VOTE. NFT ownership or delegate.xyz delegation, one vote per tokenId per round, 24h round from the first hide vote, passes at hideNfts >= 10 AND > keepNfts AND >= 3 distinct wallets, 7-day cooldown on a failed round, KEPT blocks all rounds, owner setState closes/overrides both ways. Any double-count, vote-after-close, or cheap grief of an innocent item.
> 14. HIDDENLIST GAS. Launch 3 (ProtocolDirectory, YoloSweetener, DareTrophy, HiddenList) gas against the 16,777,216 cap. The new questionPrefix text: registry byte bound still holds (worst case under 16,384), questionHash changes with it, nothing else depends on the old hash.
> 15. REPEAT MONEY. Every outcome (accepted, rejected, tie, no decision, swarm yes/no/silence, appeal won/lost/voided, expired, unwound, queued void) and every split's rounding: steal, double pay, or stuck money?
> 16. REPEAT STATE MACHINE. Lazy timers, the queue, pending top-ups, busyEpoch, deadlines, emergencyExit: ordering, gas or DoS blocking settlement or letting one party pick the outcome.
> 17. REPEAT SWARM PATH. AttestationChecker panel/quorum/EIP-712 domain, timeouts, silence, prepayment refunds, voided appeals, Intake price/callback-gas changes.
> 18. REPEAT NFT VOTING. VoteLib signatures, ERC-1271 gas cap, delegation, one count per token, relayer reimbursement abuse.
> 19. REPEAT PRICES/MEV. ChainlinkUniV3PriceOracle TWAP, YoloPoolSamples, the swap adapter: sandwich/front-run cost against launch values.
> 20. REPEAT ROLES/DOCS VS CODE. ParamsRegistry, ProtocolDirectory, HiddenList owner powers over live tasks or deposits; every promise in README.md, docs/MECHANICS.md, docs/TOKENOMICS.md, docs/LAUNCH-TASK.md or NatSpec that the code does not hold.
>
> BUSINESS/CONCEPT RISKS:
> 21. BOARD ABUSE. Author-doer collusion to extract a sweetener/trophy; cheap Sybil cluster swinging a dispute or HiddenList vote; cheap spam griefing honest users. Price each in IMD, IMD = $7.65.
> 22. DEPOSIT/FEE ECONOMICS. Bond/fee sizes (10% settlement fee, dispute and relayer bonds) vs realistic task sizes: too cheap to deter abuse, or too costly for a small task.
> 23. TOKENOMICS. 54% unlocked day 0; the 15% "Stabilizer / MM fund" is an owner decision, kept (do not recommend removing it, comment on risk/disclosure only); what gives $YOLO demand; do fees fund the project; vesting vs docs/TOKENOMICS.md.
> 24. HARMFUL DARES. Contracts accept any condition text. Do questionPrefix (14) and the HiddenList hide layer (12-13; site itself out of scope) together form an adequate, cheap, enforceable gate against an illegal dare being posted and paid? What is missing, cheapest addition?
> 25. COVERAGE, as set out in Report format above.
>
> Not findings: placeholder wallets (0xF001-0xF005, <TREASURY_WALLET>, HiddenList's owner arg); test/mocks; items acknowledged in docs/AUDIT-INTERNAL.md incl. "Swarm audit 117cf05a" unless the reason is wrong or a fix regressed; the Stabilizer/MM fund's existence and size; brand/. External systems (Intake, ProjectFactory, Uniswap, Chainlink, delegate.xyz, identity.md NFT) are out of scope except how this code trusts them.

| | |
|---|---|
| Repository | https://github.com/sergey-mavr-agent/moneycanbuy.fun.git |
| Commit | `7714a243271eeb1560e472d06167594e2f731ec0` |
| Job | `b88083a7-f45f-4cab-b143-31293f2c56e4` |
| Judged | 2026-10-15 15:12 UTC |
| Findings | 1 medium · 5 low · 22 info |

Four agents audited the code as it is at `7714a24`, 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

### 15. Medium: 13b. HIDDENLIST VOTE: one NFT opens an uncontested round on the voter's own item and the 7-day cooldown then drops every honest hide vote; repeated every 8 days it keeps the item un-hideable by vote f

`contracts/src/HiddenList.sol:126`

```
                if (block.timestamp < s.roundEnd + COOLDOWN) return; // cooldown after a round that did not pass
```

The first hide vote opens a round whatever its size, and a round that ends under quorum starts a 7-day cooldown in which every hide vote returns silently (no event, no revert, line 126). The author of a harmful dare who holds or is delegated one identity.md NFT votes hide on his own task the moment it is created; the round ends 24h later with 1 NFT; for the next 7 days the honest hide votes of any number of holders are no-ops; at day 8 he votes once more. Cost: 1 NFT and about 50k gas per 8 days. Effect: the NFT-vote layer of the harmful-content gate is defeated by exactly the party it targets; only the owner's setState hides the item. Display only (no money), but cheap, repeatable and silent, so medium ('cheap griefing of a documented guarantee'). Smallest fix: start the cooldown only after a contested round (keepNfts > 0), or let a round that ended under quorum be reopened at once by a hide vote from a wallet that did not vote in it; or shorten COOLDOWN for uncontested rounds. The launch 3 text forbids changing the cooldown window 'unless a reproduced finding requires it'; this is such a finding.

**Reproduction**

contracts/test/scratch/Proof_5d8c4edc729c.t.sol (fails on this commit: '0 != 3'): griefer with 1 NFT votes hide at t0; at t0+2d ten honest holders in ten wallets vote hide with 10 NFTs: all dropped (roundEnd < now, cooldown until t0+8d); at t0+3d status() is VISIBLE with hideNfts 1 / hideWallets 1; expected HIDDEN_BY_VOTE after the honest round.

**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 {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {HiddenList} from "src/HiddenList.sol";

contract GriefNft is ERC721("identity.md", "IMDID") {
    function mint(address to, uint256 id) external {
        _mint(to, id);
    }
}

/// @notice One NFT holder (the author of a harmful dare, for instance) opens a hide round on his own item the moment
///         it exists, with one hide vote and nobody watching. The round fails after 24 h (1 of 10 NFTs) and the 7-day
///         cooldown then blocks every honest hide vote: 10 honest holders in 10 wallets voting on day 2 count nothing
///         and the item stays VISIBLE for the whole week. Fails on this code; passes once an uncontested round that
///         fell short of quorum (no keep votes) starts no cooldown, or once hide votes may reopen a round during it.
contract HiddenListCooldownGriefTest is Test {
    HiddenList list;
    GriefNft nft;

    address constant OWNER = address(0x7EA5);
    address constant VAULT = address(0xBA07);
    address griefer = address(0xBAD);

    function setUp() public {
        vm.warp(1_800_000_000);
        nft = new GriefNft();
        list = new HiddenList(OWNER, address(nft), address(0));
        nft.mint(griefer, 1);
    }

    function _ids1(uint256 id) internal pure returns (uint256[] memory a) {
        a = new uint256[](1);
        a[0] = id;
    }

    function test_oneNftBlocksHonestHidersForAWeek() public {
        // day 0: the griefer opens a round alone
        vm.prank(griefer);
        list.vote(VAULT, 1, 1, true, _ids1(1));

        // day 2: ten honest holders, ten wallets, try to hide the item
        vm.warp(block.timestamp + 2 days);
        for (uint256 i; i < 10; ++i) {
            address w = address(uint160(0x1000 + i));
            nft.mint(w, 100 + i);
            vm.prank(w);
            list.vote(VAULT, 1, 1, true, _ids1(100 + i));
        }
        (uint8 s, uint32 hideNfts,, uint16 hideWallets, uint64 roundEnd) = list.status(VAULT, 1, 1);
        assertLt(roundEnd, block.timestamp, "no new round opened: cooldown");

        // day 3: still visible although 10 NFTs in 10 wallets asked to hide it
        vm.warp(block.timestamp + 1 days);
        (s,,,,) = list.status(VAULT, 1, 1);
        assertEq(hideNfts + hideWallets, 2, "honest votes were dropped (1 NFT, 1 wallet of the griefer only)");
        assertEq(s, list.HIDDEN_BY_VOTE(), "ten honest holders could not hide the item within the week");
    }
}
```

### 14. Low: 13a. HIDDENLIST VOTE: the '>= 3 distinct hide wallets' rule counts callers, so one holder of 10 NFTs hides any item alone through two delegate.xyz delegations or two transfers

`contracts/src/HiddenList.sol:159`

```
            if (hide) ++s.hideWallets;
```

hideWallets is incremented once per msg.sender that first votes hide in a round, and _mayVote accepts a delegate.xyz delegate of the token's current owner (line 208), so three addresses controlled by one person satisfy MIN_WALLETS. One holder of 10 identity.md NFTs calls delegateAll on two hot wallets (no transfer, gas only) and casts hide with 8/1/1 tokenIds from three addresses: hideNfts 10, hideWallets 3, and after 24h status() returns HIDDEN_BY_VOTE for an honest task or proof until the owner's setState. The NatSpec (line 15), MECHANICS.md:34 and the launch 3 text present the wallet rule as a second, independent bar; it adds no Sybil resistance over the 10-NFT quorum. Display only: the task keeps running and no money moves, so low. Merged from three specialists (economics, flow, math #13). Smallest fix: count distinct current owners (the ownerOf result) instead of msg.sender for hideWallets, which forces the real transfers the docs imply; or state in the docs that the only real bar is 10 NFTs and that the owner's KEPT is the remedy.

**Reproduction**

contracts/test/scratch/Proof_476db268aaec.t.sol (fails on this commit: '3 != 0'): MockNFT mints ids 1..10 to holder; holder.delegateAll(hot1), delegateAll(hot2); holder votes hide [1..8], hot1 votes hide [9], hot2 votes hide [10]; warp 24h; status() expected VISIBLE (one holder), actual HIDDEN_BY_VOTE (3).

**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 {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {HiddenList} from "src/HiddenList.sol";
import {IDelegateRegistry} from "src/interfaces/IDelegateRegistry.sol";

contract SybilNft is ERC721("identity.md", "IMDID") {
    function mint(address to, uint256 id) external {
        _mint(to, id);
    }
}

/// @dev delegate.xyz v2 subset: wallet-level delegation (`delegateAll`), as the real registry offers.
contract SybilDelegates is IDelegateRegistry {
    mapping(address => mapping(address => bool)) public delegated;

    function delegateAll(address to, bool on) external {
        delegated[msg.sender][to] = on;
    }

    function checkDelegateForERC721(address to, address from, address, uint256, bytes32) external view returns (bool) {
        return delegated[from][to];
    }
}

/// @notice HiddenList's "3 distinct hide wallets" rule counts msg.sender, not the NFT holder. One holder of 10 NFTs
///         delegates (delegate.xyz, free, no transfer) to two hot wallets and votes from three addresses: the round
///         passes with a single real holder behind it. Fails on this code (status == HIDDEN_BY_VOTE); passes once
///         distinct wallets are counted per NFT holder (ownerOf) instead of per caller.
contract HiddenListDelegateSybilTest is Test {
    HiddenList list;
    SybilNft nft;
    SybilDelegates delegates;

    address constant OWNER = address(0x7EA5);
    address constant VAULT = address(0xBA07);
    address holder = address(0xA11CE);
    address hot1 = address(0xD001);
    address hot2 = address(0xD002);

    function setUp() public {
        vm.warp(1_800_000_000);
        nft = new SybilNft();
        delegates = new SybilDelegates();
        list = new HiddenList(OWNER, address(nft), address(delegates));
        for (uint256 i = 1; i <= 10; ++i) {
            nft.mint(holder, i);
        }
        vm.startPrank(holder);
        delegates.delegateAll(hot1, true);
        delegates.delegateAll(hot2, true);
        vm.stopPrank();
    }

    function _ids(uint256 from, uint256 n) internal pure returns (uint256[] memory a) {
        a = new uint256[](n);
        for (uint256 i; i < n; ++i) {
            a[i] = from + i;
        }
    }

    function test_oneHolderThreeAddressesPassesTheThreeWalletRule() public {
        vm.prank(holder);
        list.vote(VAULT, 1, 1, true, _ids(1, 8));
        vm.prank(hot1);
        list.vote(VAULT, 1, 1, true, _ids(9, 1));
        vm.prank(hot2);
        list.vote(VAULT, 1, 1, true, _ids(10, 1));
        (, uint32 hideNfts,, uint16 hideWallets,) = list.status(VAULT, 1, 1);
        assertEq(hideNfts, 10, "all ten NFTs of one holder counted");
        assertEq(hideWallets, 3, "three 'wallets' from one holder");

        vm.warp(block.timestamp + 24 hours);
        (uint8 state,,,,) = list.status(VAULT, 1, 1);
        // the documented rule ">= 3 distinct hide wallets" is meant to need three holders; one holder passes it
        assertEq(state, list.VISIBLE(), "a single NFT holder must not hide an item alone");
    }
}
```

### 16. Low: 13c. HIDDENLIST VOTE: the round end is fixed at the first hide vote, so a last-block hide swing of 9 NFTs cannot be answered; after an owner setState(VISIBLE) no cooldown applies and the same cluster

`contracts/src/HiddenList.sol:120`

```
        bool activeRound = s.roundEnd != 0 && block.timestamp < s.roundEnd;
```

Keep votes are accepted only while block.timestamp < roundEnd and roundEnd never moves. A cluster of 10 NFTs in 3 wallets opens the round with 1 NFT, so keepers see a weak hide side (and need keepNfts >= hideNfts to block, which they must pre-commit blind); in the round's last block the cluster adds the other 9 and nobody can reply; the passed round stands until the owner acts (line 125). If the owner restores VISIBLE, setState zeroes roundEnd (line 102), so the cooldown check at line 126 does not apply and the next hide vote reopens a round immediately; only KEPT ends it. Cost: 10 identity.md NFTs (the same cluster as 13a; delegate.xyz lets borrowed or delegated NFTs count). Display only, the owner's KEPT is a full remedy, so low. The specialist's medium is reduced: it needs the 10-NFT quorum, and 13b is both cheaper and stronger. Smallest fix: extend roundEnd by a short reply window (e.g. 1h) when a hide vote lands in the last hour and flips _won, and apply COOLDOWN after an owner setState(VISIBLE) (or document that restored items should be set KEPT).

**Reproduction**

contracts/test/scratch/Proof_ced07852b14f.t.sol (fails on this commit: '3 != 0'): w1 votes hide [1] at t0 (round end t0+24h); keeper votes keep [100,101]; at end-1 w0, w1, w2 vote hide with the remaining 9 NFTs; at end status() returns 3 (HIDDEN_BY_VOTE); expected: a reply window for the keep side.

**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 "forge-std/Test.sol";
import {HiddenList} from "src/HiddenList.sol";

contract Nft { mapping(uint256 => address) public ownerOf; function set(uint256 id, address o) external { ownerOf[id] = o; } }

/// A 10-NFT cluster opens a round with 1 NFT, waits, and adds 9 in the round's last second: defenders who saw
/// 1 hide NFT for 24h are outvoted with no chance to answer, and the innocent item is hidden until the owner acts.
/// After the owner restores it (VISIBLE), the same cluster re-hides it in the next 24h: no cooldown applies.
contract HiddenListSnipeTest is Test {
    Nft nft = new Nft();
    HiddenList hl;
    address owner = address(0xA11CE);
    address[3] w = [address(0x1), address(0x2), address(0x3)];
    address keeper = address(0x4);
    address vault = address(0xBEEF);

    function setUp() public {
        hl = new HiddenList(owner, address(nft), address(0));
        for (uint256 i = 1; i <= 10; ++i) nft.set(i, w[i % 3]);
        nft.set(100, keeper); nft.set(101, keeper);
    }

    function _ids(uint256 a, uint256 b, uint256 m) internal pure returns (uint256[] memory r) {
        uint256 n; for (uint256 i = a; i <= b; ++i) if (i % 3 == m) ++n;
        r = new uint256[](n); n = 0; for (uint256 i = a; i <= b; ++i) if (i % 3 == m) r[n++] = i;
    }

    function _snipe() internal {
        vm.prank(w[1]); uint256[] memory one = new uint256[](1); one[0] = 1; hl.vote(vault, 1, 7, true, one);
        // keeper defends with 2 NFTs > 1 hide NFT
        uint256[] memory k = new uint256[](2); k[0] = 100; k[1] = 101;
        vm.prank(keeper); hl.vote(vault, 1, 7, false, k);
        (,,,, uint64 end) = hl.status(vault, 1, 7);
        vm.warp(end - 1);
        vm.prank(w[0]); hl.vote(vault, 1, 7, true, _ids(2, 10, 0));
        vm.prank(w[1]); hl.vote(vault, 1, 7, true, _ids(2, 10, 1));
        vm.prank(w[2]); hl.vote(vault, 1, 7, true, _ids(2, 10, 2));
        vm.warp(end);
    }

    function test_lastSecondSnipeHidesWithoutReply() public {
        _snipe();
        (uint8 st,,,,) = hl.status(vault, 1, 7);
        // expected (fixed, e.g. round extended on a late swing): still VISIBLE at `end`; actual: HIDDEN_BY_VOTE
        assertEq(st, 0, "hidden by a last-second swing nobody could answer");
    }
}
```

### 19. Low: 16. REPEAT STATE MACHINE: a live withdrawal notice is renewed by a new announceWithdraw, so an author who re-announces every 24h always has a due notice and can pull the pot in the block a proof appea

`contracts/src/TaskVault.sol:307`

```
        withdrawNoticeAt[taskId][msg.sender] = at;
```

announceWithdraw overwrites withdrawNoticeAt unconditionally and may be called while a submission is active. An author announces at creation and again every 24h: from hour 1 on there is always a notice that is due and valid, so withdrawContribution works at any idle moment and can still front-run a doer's submit (the submit then reverts RewardTooSmall / RewardChanged after the work was done). The hour of warning promised in MECHANICS.md:53 holds only if the doer reads the standing notice (withdrawNoticeAt / WithdrawAnnounced) and refuses the task; the site must show it. A variant: a notice filed during an active submission is due the instant the busy period ends, before any new doer can submit. YoloSweetener.announceWithdraw (line 118-119) has the same renewal rule. The rest of the state machine is clean: all windows are anchored to signed or recorded times, the 8-transition loop reaches the same state whoever pokes, the queue activates inside the finalizing call with a gas reserve, emergencyExit needs 3M gas and only unwinds a step 30 days past its deadline. Merged from the economics (low) and flow (info) specialists. Smallest fix: revert announceWithdraw while a notice is live (at != 0 && block.timestamp <= at + WITHDRAW_NOTICE_VALID) and while activeSub != 0, so every 25h cycle shows a visible gap; same in YoloSweetener.

**Reproduction**

contracts/test/scratch/RollingNotice.t.sol (judge's test, fails on this commit: 'next call did not revert as expected'): create a 0.01 ETH task; announceWithdraw at t0; announceWithdraw again at t0+24h (inside the live notice, succeeds); at t0+30h withdrawContribution succeeds, although the first notice expired at t0+25h. Expected with a non-renewable notice: NoNotice.

### 22. Low: 19. REPEAT PRICES/MEV: submit to every ETH/USDC task reverts 'OLD' while the USDC/IMD v3 pool's observation ring cannot cover 30 minutes (cardinality 1 on mainnet today; the doc lists the fix after la

`contracts/src/desks/Desk.sol:148`

```
            imdPerEth = o.imdValue(ETH, 1e18);
```

Desk._priceReward calls the oracle's imdValue/usdValue without try/catch for ETH and USDC; ChainlinkUniV3PriceOracle.imdWeiPerUsdc calls pool.observe([1800, 0]), which reverts 'OLD' whenever the pool's oldest stored observation is younger than 30 minutes. The pool 0x894D...c520 has observationCardinality 1 today (the specialist read slot0 live; the fork test raises cardinality itself at PriceOracleFork.t.sol:28, so the suite never sees it), so until increaseObservationCardinalityNext(180) is called and the ring holds a 30-minute-old observation, createTask in ETH/USDC works (Chainlink only) but no submission can be made and a doer racing a deadline cannot hand in proof. This is acknowledged in docs/LAUNCH-TASK.md:130 and AUDIT-INTERNAL B-06, so it is an ordering and hardening item, not a new medium: the step is listed under 'Outside the launches' after launch 3 instead of as a precondition of launch 2's first ETH/USDC task. The specialist's second claim, that an attacker refills the 180-slot ring with 181 dust swaps inside 30 minutes, does not hold: Uniswap v3 writes at most one observation per block (Oracle.write returns early when last.blockTimestamp == blockTimestamp, and only on a tick change), so 180 slots always span at least 180 blocks, about 36 minutes at 12 s, longer than the 30-minute window. Smallest fix: (a) operational, do the cardinality call and wait 30 minutes of swaps before launch 2 and verify observe([1800,0]) succeeds; (b) code, in Desk._priceReward wrap the ETH/USDC oracle calls in try/catch and on failure set large = true and rewardImd = max(rewardImd, largeRewardImd) exactly as _priceYolo does, or in the oracle fall back to the oldest available observation when it is younger than twapWindow. Either makes the attached test pass.

**Reproduction**

contracts/test/scratch/Proof_e41b25ec00a3.t.sol (fails on this commit with 'OLD'): real oracle, registry, desks and vault; a stub v3 pool at the live tick whose observe reverts 'OLD' once setOld(true); author creates a 0.01 ETH task (succeeds); setOld(true); doer submits with 100 IMD approved: reverts OLD. Expected: the submission is taken (as 'large' with bonds at the threshold) or priced over the available window.

**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 {TaskVault} from "src/TaskVault.sol";
import {ParamsRegistry} from "src/ParamsRegistry.sol";
import {CaseDesk} from "src/desks/CaseDesk.sol";
import {DisputeDesk} from "src/desks/DisputeDesk.sol";
import {TextRules} from "src/helpers/TextRules.sol";
import {SwarmQuestionBuilder} from "src/helpers/SwarmQuestion.sol";
import {AttestationChecker} from "src/helpers/AttestationChecker.sol";
import {BallotChecker} from "src/libraries/VoteLib.sol";
import {ChainlinkUniV3PriceOracle, IAggregatorV3, IUniswapV3PoolObserve} from "src/oracles/ChainlinkUniV3PriceOracle.sol";
import {ITaskVault} from "src/interfaces/ITaskVault.sol";
import {Evidence, ImdPay, PROOF_LINK} from "src/VaultTypes.sol";

/// @dev Audit round 2, finding: an unavailable IMD TWAP (Uniswap v3 `observe` reverting "OLD": the pool's
///      observation ring holds no entry older than the 30-minute window, which is the live state of
///      0x894D4e6d3d2Abc64Fc0de0e4e2Ea63A93BC9c520 today with observationCardinality = 1, and happens again after
///      `increaseObservationCardinalityNext(180)` whenever more than 180 tick-moving swaps land within 30 minutes)
///      makes `submit` to any ETH / USDC task revert (createTask only needs Chainlink and survives), because
///      `Desk._priceReward` calls `imdValue` without the try/catch it gives $YOLO. A doer can be shut out of a
///      deadline by anyone making small tick-moving swaps in that pool. Expected: the vault degrades (the submission
///      counts as large / bonds at the threshold, as for an unpriced $YOLO task) or the oracle falls back to the
///      longest available window; actual: the user call reverts with "OLD".
///      Fails on the audited code, passes once the vault tolerates an oracle revert for ETH / USDC.
contract Coin is ERC20 {
    uint8 private immutable _d;

    constructor(string memory n, uint8 d) ERC20(n, n) {
        _d = d;
    }

    function decimals() public view override returns (uint8) {
        return _d;
    }

    function mint(address to, uint256 a) external {
        _mint(to, a);
    }
}

contract Feed is IAggregatorV3 {
    int256 public answer;
    uint8 public dec;

    constructor(int256 a, uint8 d) {
        answer = a;
        dec = d;
    }

    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {
        return (0, answer, 0, block.timestamp, 0);
    }

    function decimals() external view returns (uint8) {
        return dec;
    }
}

/// @dev A USDC/IMD v3 pool whose observation ring cannot serve the window: `observe` reverts "OLD" exactly as
///      Uniswap v3's Oracle.getSurroundingObservations does when the oldest stored observation is younger than
///      `secondsAgo` (cardinality 1 and a tick-moving swap in the last 30 minutes, or the ring exhausted).
contract V3Pool is IUniswapV3PoolObserve {
    int24 public constant TICK = 258_947; // the live tick of the mainnet pool at the time of the audit
    bool public old;

    function setOld(bool v) external {
        old = v;
    }

    function observe(uint32[] calldata ago) external view returns (int56[] memory cum, uint160[] memory spl) {
        if (old) revert("OLD");
        cum = new int56[](ago.length);
        spl = new uint160[](ago.length);
        for (uint256 i; i < ago.length; ++i) {
            cum[i] = int56(TICK) * int56(uint56(10_000_000 - ago[i]));
        }
    }
}

contract PoolManagerStub {
    function extsload(bytes32) external pure returns (bytes32) {
        return bytes32(0); // the YOLO pool is not open: YOLO has no price (irrelevant here)
    }
}

/// @dev Deploys the two desks and the vault in one frame, as the launch factory does in one transaction.
contract CoreDeployer {
    TaskVault public vault;

    constructor(address registry, address imd, address usdc, address yolo) {
        TextRules text = new TextRules();
        CaseDesk cases = new CaseDesk(registry, imd, usdc, address(text), address(new SwarmQuestionBuilder()));
        DisputeDesk disputes = new DisputeDesk(registry, imd, usdc, address(text), address(new BallotChecker()));
        vault = new TaskVault(
            registry,
            imd,
            usdc,
            yolo,
            address(new AttestationChecker(registry)),
            address(text),
            address(cases),
            address(disputes)
        );
    }
}

contract TwapUnavailableBlocksEthTasksTest is Test {
    Coin imd;
    Coin usdc;
    Coin yolo;
    V3Pool pool;
    ChainlinkUniV3PriceOracle oracle;
    ParamsRegistry registry;
    TaskVault vault;

    address author = makeAddr("author");
    address doer = makeAddr("doer");

    function setUp() public {
        vm.warp(1_800_000_000);
        imd = new Coin("IMD", 18);
        usdc = new Coin("USDC", 6);
        yolo = new Coin("YOLO", 18);
        pool = new V3Pool();
        oracle = new ChainlinkUniV3PriceOracle(
            address(usdc),
            address(imd),
            new Feed(2000e8, 8),
            new Feed(1e8, 8),
            pool,
            30 minutes,
            2 hours,
            48 hours,
            address(yolo),
            address(new PoolManagerStub()),
            address(0xB00C),
            12_500,
            60,
            0.01e18,
            15_000e18 * 1e18 / 1_000_000_000e18
        );
        registry = new ParamsRegistry(
            address(this),
            address(0x1234),
            address(oracle),
            address(0),
            address(0xABCD),
            address(0),
            address(this),
            address(0x5555)
        );
        vault = new CoreDeployer(address(registry), address(imd), address(usdc), address(yolo)).vault();
        vm.deal(author, 1 ether);
        vm.deal(doer, 1 ether);
        imd.mint(doer, 100e18);
        vm.prank(doer);
        imd.approve(address(vault), type(uint256).max);
    }

    function _pay() internal pure returns (ImdPay memory p) {
        p.tokenIn = address(0);
    }

    function _createEthTask() internal returns (uint256 taskId) {
        vm.prank(author);
        taskId = vault.createTask{value: 0.0105 ether}(
            address(0), 0.01 ether, 0, "dare", "post a photo of the sign", 0, PROOF_LINK, "", 0.0005 ether, _pay()
        );
    }

    /// @notice A doer with the bond in hand cannot submit to an open ETH task while the TWAP cannot be read.
    function test_submitToEthTaskSurvivesAnUnavailableTwap() public {
        uint256 taskId = _createEthTask();
        assertEq(oracle.usdValue(address(0), 1 ether), 2000e18, "the oracle works while the ring can serve 30 min");

        // a tick-moving swap landed in the USDC/IMD pool less than 30 minutes ago (or the ring is exhausted)
        pool.setOld(true);

        vm.prank(doer);
        uint256 subId = ITaskVault(address(vault))
            .submit(taskId, "https://example.com/proof", Evidence(bytes32(0), false, ""), 0, type(uint256).max, _pay());
        assertEq(subId, 1, "the submission must be taken: the vault has to degrade, not revert");
    }
}
```

### 23. Low: 20. REPEAT ROLES/DOCS VS CODE: DareTrophy prices every trophy by the CURRENT parameter version's priceOracle, so a future version's oracle re-prices trophies of tasks of every version, against MECHANI

`contracts/src/DareTrophy.sol:103`

```
        IPriceOracle o = IPriceOracle(registry.core(registry.currentVersion()).priceOracle);
```

Roles audited: ParamsRegistry owner (48h timelocked params within bounds, global attester set, pause of new tasks, treasury of future sweeps; no function moves deposits: the vault's bank functions are onlyDesk and the desks have no owner), ProtocolDirectory owner (a timelocked list nothing in the core reads), HiddenList owner (instant visibility only). One docs-vs-code gap: docs/MECHANICS.md:294 says the price oracle 'is per parameter version, so the params owner can swap it for future tasks only', but DareTrophy.price() reads registry.core(registry.currentVersion()).priceOracle, not the task's version. After a new version (48h public) naming an oracle with usdValue(yolo, 1e18) = 1e36, price() = ceilDiv(1.5e18 * 1e18, 1e36) = 1 wei, so every trophy of every version mints for 1 wei of YOLO instead of about 13,000 YOLO ($1.50); a tiny usdValue only makes trophies dear, bounded by the minter's maxYolo. YoloSweetener uses the task's version for its cap (correct). Owner-controlled, visible 48h ahead, trophies (burn revenue) only, so low. Smallest fix: read the oracle of the task's paramsVersion (vault.getTask(s.taskId).paramsVersion) or state the current-version rule in MECHANICS and the DareTrophy NatSpec.

**Reproduction**

State: registry currentVersion v2 whose priceOracle returns hasPrice(yolo) = true and usdValue(yolo, 1e18) = 1e36. Call DareTrophy.price(): expected by the docs about 13,043 YOLO from the task version's oracle; actual 1 wei (lines 103-110). Other promises in README.md, MECHANICS.md, TOKENOMICS.md, LAUNCH-TASK.md and NatSpec were checked against the code without another gap.

### 1. Info: 0. VERDICT: ready for mainnet once the USDC/IMD pool's observation ring is raised and verified before launch 2; no critical or high; one medium (HiddenList cooldown grief), four low

`docs/LAUNCH-TASK.md:130`

```
   - `increaseObservationCardinalityNext(180)` on the USDC/IMD pool `0x894d…c520` and 30 minutes of swaps. Without this
```

1. No critical or high: no path steals, double-pays or strands money; all 11 fixes of audit 117cf05a are closed (entries 1-11) with no regression; HiddenList touches no money and no core contract (entry 12). 2. One operational blocker, not a code blocker: the USDC/IMD v3 pool must have its observation cardinality raised and 30 minutes of swaps BEFORE the first ETH/USDC task of launch 2 can take a submission; the doc lists it after launch 3 as 'outside the launches' (entry 19, low; the specialist's medium and its 'refill DoS' claim did not hold up). 3. Medium: one identity.md NFT holder can keep an item un-hideable by vote for its whole life through the 7-day cooldown (entry 13b). 4. Low: the 3-distinct-wallet rule is met by one holder through delegate.xyz (13a); a last-block hide swing cannot be answered and an owner VISIBLE restores no cooldown (13c); a withdrawal notice renews while live, so a standing notice gives no fresh hour (8/16); DareTrophy prices by the current version's oracle, not the task's (20). 5. Business: below the $300 check threshold an undisputed false proof is paid with no swarm or human in the loop, so the illegal-content prefix never applies to it (22, 24). 6. forge test on forge 1.8.3: 388 passed, 0 failed, 5 fork suites skipped; on forge 1.8.5 37 suites fail in setUp (test harness only, entry 25). 7. Recommendation: launch after fixing 13b (one line), running the cardinality step before launch 2, and recording 13a/13c/8/20 in the docs or a later version.

**Reproduction**

Judge's reproduction of every specialist finding: the four attached proofs and the judge's RollingNotice.t.sol fail on this commit for the stated reasons (forge test --match-path 'test/scratch/*'); the full suite passes on forge 1.8.3; git diff --stat b86a5f8 7714a24 -- contracts/src contracts/script shows HiddenList.sol (new), LaunchPlan.sol (+14/-5) and one line of VaultParams.sol only.

### 2. Info: 1. FIX CHECK YoloPoolSamples two-block sample: closed yes, new issue no

`contracts/src/oracles/YoloPoolSamples.sol:114`

```
        step = p.blockNumber != 0 && p.blockNumber < block.number && p.poolState == state ? 2 : 1;
```

Reproduced: a read in block N stores only through a call in a later block whose keccak(slot0, feeGrowthGlobal0, feeGrowthGlobal1) equals the read; the swap back of a one-block sandwich changes fee growth and voids it. highestYoloTick (highest of the 2h window), the $0.01 cap and the 30,000 YOLO floor of the sweetener cap hold. Residual, acknowledged in AUDIT-INTERNAL.md and the NatSpec: a price held across one block boundary samples. The flow specialist's 'bundle' variant (swap + read last in block N, store + swap back first in block N+1) needs the attacker's EOA to win the top-of-block slot of N+1 against arbitrageurs who profit from the inflated tick; it is bounded (2h of dearer YOLO submissions, sweetener cap at its 30,000 YOLO floor) and does not change the acknowledgement's reason, so it stays an info note, not a finding.

**Reproduction**

forge test --match-path 'test/audit/swarm117/SampleSandwich.t.sol': SampleSandwichTest 2 passed, DoubleSandwichTest 5 passed (read and store in one block store nothing; any swap between voids the read).

### 3. Info: 2. FIX CHECK ProjectVesting.bindStarter: closed yes, new issue no

`contracts/src/token/ProjectVesting.sol:91`

```
        if (msg.sender == starter && startOf[token] == 0 && IERC20(token).balanceOf(address(this)) != 0) {
```

Reproduced: only `starter` starts a clock; bindStarter is accepted once and only while the transient flag set in the constructor is still 1 (the deployment transaction); YoloSplitter's constructor binds all four vestings and split() is its only release path. A 1-wei transfer and release() by anyone pays only the unlocked part and starts nothing. Note (procedure, not a defect): YoloSplitter._bind silently skips a vesting with no code, so if launch 0 were ever split into two transactions with the splitter first, no clock could ever start; launch 0 is one launchContracts call, vestings before splitter, and the fork rehearsal runs it, so this is a one-line check to add to the launch-0 text (assert starter() == YoloSplitter on all four vestings before launch 1).

**Reproduction**

forge test --match-contract 'VestingClockPreStart|VestingStarterBinding': 4 passed (clock cannot start before the share arrives; rebinding reverts BindingClosed).

### 4. Info: 3. FIX CHECK MAX_SHORTFALL_BPS = 20%: closed yes, new issue no

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

```
        uint256 shortCap = penalty * MAX_SHORTFALL_BPS / BPS;
```

Reproduced: after the winning caller's fee, relayers of the case share min(shortfallTotal, 20% of the penalty); the remaining >= 80% goes to winners and correct votes by winnerBondBps whichever side the relayer voted; unpaid shortfall is dropped and claimed pro rata.

**Reproduction**

forge test --match-test test_losingVoterDrainsPenaltyByRelayingOwnBallots: passed. Arithmetic: winners + voters receive P - min(S, P/5) >= 0.8 P.

### 5. Info: 4. FIX CHECK doer cannot appeal or callSwarm against own accepted submission: closed yes, new issue no

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

```
        if (sender == s.doer && s.nftDecision != Verdict.Reject) revert DoerCannotDispute();
```

Reproduced: appeal() reverts DoerCannotDispute for the doer unless the NFT vote rejected him; callSwarm() reverts for the doer unless he sides with himself; dispute() reverts for the doer. Puppet addresses remain the acknowledged C-01b/c Sybil limit.

**Reproduction**

forge test --match-test 'test_doerAppealsAgainstOwnAcceptance|test_doerMayStillAppealARejection|test_doerCannotCallTheSwarmAgainstHimself': 3 passed.

### 6. Info: 5. FIX CHECK VoteLib one wallet, one side: closed yes, new issue no (delegate variant is info, entry 18)

`contracts/src/libraries/VoteLib.sol:89`

```
            if (seen != 0 && seen != sideBit) {
```

Reproduced: walletSides[voter] is set on the first counted ballot; a later ballot by the same voter on the other side is skipped with BallotSkipped reason 4 before any tokenId bit is set, so its NFTs stay unused and unpaid; same voter, same side, more NFTs is added.

**Reproduction**

forge test --match-test test_walletVotesBothSidesAndIsPaid: passed.

### 7. Info: 6. FIX CHECK YoloSweetener NO_PRICE_WAIT: closed yes, new issue no

`contracts/src/YoloSweetener.sol:186`

```
                if (block.timestamp < uint256(since) + NO_PRICE_WAIT) {
```

Reproduced: payout() without a YOLO price and without swarm confirmation records noPriceSince on the first call, emits PayoutDeferred and moves nothing; 7 days later, still without a price, the doer gets the full bonus minus the fee; with a price the largeRewardUsd cap applies; returnBase guards a second payout.

**Reproduction**

forge test --match-test 'test_sweetenerPayoutWithoutPriceReturnsAllToSweetener|test_sweetenerPayoutAfterAWeekWithoutPricePaysInFull|test_sweetenerCapStillAppliesWithAPrice': 3 passed.

### 8. Info: 7. FIX CHECK SWARM_DELIVERY_GRACE: closed yes, new issue no

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

```
        if (r.answered && r.answerTime <= deadline && r.recordedAt <= lastDelivery) {
```

Reproduced: an answer counts iff signed within swarmTimeout (issuedAt clamped to [askedAt, now]) and recorded within the 1h grace; silence is declared only at askedAt + timeout + grace or when the Intake reports the request completed with no recorded answer; stepDeadline includes the grace. The outcome is the same whether the poke or the delivery lands first.

**Reproduction**

forge test --match-test 'test_pokeAtDeadlineTurnsLateAnswerIntoSilence|test_answerAfterTheGraceIgnoredWhoeverPoked|test_answerDeliveredAfterTheGraceIgnoredEvenUnpoked|test_answerSignedAfterTheTimeoutIsSilenceAtTheEndOfTheGrace': 4 passed.

### 9. Info: 8. FIX CHECK TaskVault.announceWithdraw notice: closed yes; new issue yes, low (standing notice renews while live, see entry 16)

`contracts/src/TaskVault.sol:325`

```
            if (at != 0 && block.timestamp >= at && block.timestamp <= at + WITHDRAW_NOTICE_VALID) {
```

Reproduced: an active share of an Open task leaves only with activeSub == 0 and a notice 1h..25h old; pending top-ups leave at any time; Expired/Unwound need no notice; a submission inside the hour locks the share. The fix closes the reported attack (withdraw with no notice in the block the proof appears). What it leaves open is reported under entry 16: announceWithdraw overwrites a live notice, so a notice renewed every 24h is always due.

**Reproduction**

forge test --match-test 'test_authorFrontRunsSubmitWithWithdrawal|test_announcedWithdrawalGivesTheDoerAnHour|test_noticeExpiresAfterADay|test_noNoticeNeededAfterExpiry': 4 passed.

### 10. Info: 9. FIX CHECK AttestationChecker expiresAt: closed yes, new issue no

`contracts/src/helpers/AttestationChecker.sol:53`

```
        if (a.expiresAt != 0 && a.expiresAt < block.timestamp) return 6;
```

Reproduced: code 6 (surfaced as AttestationIgnored reason 16 by TaskVault.onOracleResult) when expiresAt is set and in the past; 0 means no expiry, as in the oracle reference.

**Reproduction**

forge test --match-test test_expiredAttestationIsIgnored: passed.

### 11. Info: 10. FIX CHECK last attester cannot be removed; MECHANICS matches: closed yes, new issue no

`contracts/src/ParamsRegistry.sol:198`

```
                if (attesterCount == 1) revert LastAttester();
```

Reproduced: executeAttester refuses a removal at attesterCount == 1; the count tracks additions and removals; the constructor sets 1. docs/MECHANICS.md 'Trust assumptions' states the global attester set, the effect of a removal on open cases, LastAttester and the add-first rotation, and the owner's lever through a new version's counterparties, matching the code.

**Reproduction**

forge test --match-test 'test_lastAttesterCannotBeRemoved|test_Fixed_B02': passed. grep 'LastAttester' docs/MECHANICS.md contracts/src/ParamsRegistry.sol.

### 12. Info: 11. FIX CHECK LaunchPlan single INTAKE = Intake v2: closed yes, new issue no

`contracts/script/LaunchPlan.sol:55`

```
    address internal constant INTAKE = 0xa43e6F75ee006411F79Ac1C84120606C2330DE82;
```

Reproduced: the only Intake constant in LaunchPlan.sol is version 2 (0xa43e6f75ee006411f79ac1c84120606c2330de82), used for the ParamsRegistry of launch 1; version 1 appears only in the comment above it. docs/LAUNCH-TASK.md names v2 in the launch 1 and launch 2 texts.

**Reproduction**

grep -n '0x1397\|0xa43e' contracts/script/LaunchPlan.sol docs/LAUNCH-TASK.md; forge test --match-test test_L2_docLaunch1Args_matchPlan: passed.

### 13. Info: 12. HIDDENLIST MONEY: no issue. No payable, receive or token call; plain ETH reverts; the HiddenList commit touched no core source

`contracts/src/HiddenList.sol:18`

```
contract HiddenList {
```

HiddenList has no payable function, no receive or fallback, and makes only two view calls inside try/catch (IERC721.ownerOf, IDelegateRegistry.checkDelegateForERC721 with the correct v2 argument order and rights 0). Nothing in contracts/src imports or calls it. git diff b86a5f8..7714a24 over contracts/src and contracts/script: HiddenList.sol (new), LaunchPlan.sol (extras index 3) and one line of VaultParams.sol (the questionPrefix string); TaskVault, CaseDesk, DisputeDesk, ParamsRegistry, ProtocolDirectory and NftGate sources are byte-identical, so their bytecode differs only through the prefix constant compiled into LaunchParams.initial.

**Reproduction**

forge test --match-contract HiddenListTest: 41 passed incl. test_PlainEthTransferReverts and test_EthWithDataReverts; git diff --stat b86a5f8 7714a24 -- contracts/src contracts/script: 3 files.

### 17. Info: 14. HIDDENLIST GAS and the new questionPrefix: no issue. Launch 3 fits; the registry byte bound holds; questionHash is informational only

`contracts/src/VaultParams.sol:242`

```
        _req(p.questionPrefix.length + 2 * uint256(p.maxTextBytes) <= 2_000 - QUESTION_FIXED, "questionPrefix");
```

ParamsRegistry's constructor runs LaunchParams.validate: prefix bytes + 2 x maxTextBytes (256) <= 2,000 - QUESTION_FIXED, and the worst-case body ((maxClaims+3) x (maxTextBytes+40) + prefix + fixed parts) <= 16,384; the new prefix (one line longer in VaultParams.sol:167, the illegal-content sentence) passes both, since the registry deploys in every suite and ReviewQuestionSize passes. Launch 3 (YoloSweetener, DareTrophy, ProtocolDirectory, HiddenList) is 6.90M gas by the docs' fork rehearsal (not re-run here, no FORK_URL); HiddenList's runtime is small (21.5 KB per LAUNCH-TASK.md:60); margin about 9.9M under 16,777,216. questionHash = keccak256(question) changes with the prefix, but it is only recorded (SwarmAsked, SwarmRequest.questionHash 'checks nothing', VaultTypes.sol) and the callback binds by Intake request id and msg.sender, so nothing depends on the old hash.

**Reproduction**

forge test --match-contract 'ReviewLaunch|ReviewQuestionSize|HiddenList': 51 passed, 1 fork test skipped; grep questionHash contracts/src: recorded in TaskVault/VaultTypes only, never compared.

### 18. Info: 15. REPEAT MONEY: no issue. Every outcome closes the books; no steal, double pay or stuck money found

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

```
        penalty -= shortPay;
```

Traced each split: Accepted without dispute (10% fee to treasury, 90% to doer); Accepted after NFT-right (nftFeeBps to correct votes, rest of the fee to treasury, loser bonds split winners/voters by winnerBondBps); tie / NoDecision timeout (50% of disputer bonds to the doer unless contributor, rest treasury); Rejected by check (doer bond to treasury when there is no winner bond); swarm yes/no after appeal (caller fee first, then the 20% relayer cap, then winners); silence (dropCaller credits bond + unspent fee); emergency Unwound (_settleNeutral: every bond and prepayment back); queued Void (bond + checkFunds back). Every bucket pays remaining-over-remaining so the last claimant takes the dust; liabilities move only on entry and exit; failed pushes become credits and withdrawTo reverts rather than losing a credit; pending top-ups stay withdrawable after an acceptance and merge only when the queue is empty.

**Reproduction**

forge test on forge 1.8.3: Invariant.t.sol (100 runs x depth 150), Payments, Disputes, Scenarios, Emergency suites pass; 388/388 non-fork tests pass; no counterexample constructed.

### 20. Info: 17. REPEAT SWARM PATH: no issue. Binding, panel, domain, timeouts, refunds and voided appeals hold

`contracts/src/TaskVault.sol:462`

```
        uint8 code = attestationChecker.check(address(this), via.minPanelSize, via.minQuorum, a, signature);
```

The callback is accepted only from the Intake the task's version names, for a known unanswered request id; the attestation must be bool/32 bytes, panel >= 7, quorum >= 5, quorum <= agreed <= panelSize, not expired, signed in the consumer domain ('IdentityMD Oracle', '2', chainid, vault) by a registry attester; OracleAttestation's 15-field struct and type string match the oracle reference. The 200k callback stipend covers two SSTOREs, one external view and one ecrecover. Prepayments: unspent check funds are credited at activation, at Void and at settlement; appeal funds minus the spent price go back on silence, void and settlement; an Intake price rise voids an appeal or skips a check with a full refund (documented) and maxImdIn protects payers. Residual, documented (C-05 and the oracle reference): a condition text the Intake wording screen refuses makes every question on that task silence (check passes, appeals void); a definitions pin or allowAmbiguous in bodyFields would remove this author-side lever.

**Reproduction**

forge test --match-contract 'Swarm|ReviewAttestation|ReviewSwarmOrders|AuditB': passed on forge 1.8.3.

### 21. Info: 18. REPEAT NFT VOTING: no issue in the mechanics; the one-side-per-wallet rule is per signer address, so a holder can vote both sides through a delegate.xyz delegate or a second wallet (nominal rule,

`contracts/src/libraries/VoteLib.sol:88`

```
            uint8 seen = book.walletSides[b.voter];
```

EIP-712 digest Vote(uint256 subId,uint8 side,uint256[] tokenIds,uint64 voteEnd,address payTo) with the array hashed as keccak(abi.encodePacked), domain ('IMD TaskVault','1',chainid,vault); ECDSA first, then ERC-1271 under a 500k gas cap with 32-byte returndata, so a gas burner is skipped, never fatal; ownership or delegation per tokenId at relay time; bitmap per submission; reimbursement capped at 30k + 60k per counted NFT x basefee x 1.1, paid from the dispute pot then <= 20% of the penalty, so stuffing invalid tokenIds costs more gas than it returns. walletSides keys on the ballot's voter: a holder of NFTs A and B signs side 1 with A from his wallet and side 2 with B from a delegate.xyz hot wallet; both ballots count and he is paid for whichever side wins. Economically identical to splitting NFTs between two people, nobody else loses, so info; the documented rule 'a wallet votes one side' is only nominal. If wanted: key walletSides by the NFT's owner, which also makes HiddenList's hideWallets meaningful (13a).

**Reproduction**

Reading of VoteLib.sol:88-121 and :186-205; forge test --match-contract 'Votes|AuditC': passed; the delegate variant follows from walletSides being keyed by b.voter while _mayVote accepts the delegate.

### 24. Info: 21. BOARD ABUSE: self-dares cost about 10% of the reward plus a $1.10 author fee; 10 identity.md NFTs in one hand decide an unopposed dispute vote or any HiddenList round; spam costs about one author

`contracts/src/TaskVault.sol:237`

```
        if (authorFee < p.minAuthorFee) revert FeeTooLow();
```

At IMD = $7.65. (a) Author-doer collusion: the author may be the doer; a self-dare of reward R with a self-sweetened bonus B returns 90% of each after 24h with no dispute, plus a trophy (burn about $1.50 of YOLO) and a completed dare on the leaderboard. Cost per fake completion at the $5 minimum: 10% of R + 10% of B + 0.0005 ETH author fee (about $1.10) + gas, about $1.60; who gains is whoever the off-chain incentives (250M YOLO) reward per completed dare; who loses is that budget and the board's credibility. Recommendation: pay incentives only for dares with author != doer that a disputer or the swarm touched, or require the 0.5 IMD ($3.83) check for incentive-eligible dares. (b) Sybil swing: an NFT decision needs margin >= 10 NFTs and minWinnerWallets = 1, so 10 NFTs in one hand decide any vote nobody opposes; the remedy is an appeal (bond 1 IMD + fee 0.5 IMD = $11.48, returned with the fee if the swarm agrees), so a swing pays only through the voter share (50% of the loser bond, >= $3.83) and is worth it only on a large task (a $5,000 task: 3% doer bond = 10 IMD = $76 to the swinging side against the $11.48 appeal the loser files). HiddenList: 10 NFTs hide anything until the owner reacts (13a-13c); 1 NFT keeps an item un-hideable by vote (13b). Identity.md NFT floor price is not in the repository, so the NFT cost is stated in NFTs. (c) Spam: a task costs the author fee (about $1.10) + gas (about $2 at 2 gwei), the $5 reward is recoverable after a 1h notice; a thousand junk tasks cost about $3,000 and are the owner's and voters' cleanup on the site (HiddenList batches). Queue stuffing behind an active submission costs doubling bonds (2^n of 1 IMD base: 510 IMD = $3,900 for 8 slots), refunded only if the active submission is accepted, so not cheap.

**Reproduction**

Numbers from VaultParams.sol:133-160 (minRewardUsd 5e18, minAuthorFee 0.0005 ETH, minBond 1e18, bondBase 0.5e18, doerBondBps 300, minWinnerWallets 1, winnerBondBps 5000, feeBps 1000) and HiddenList constants (QUORUM_NFTS 10, MIN_WALLETS 3).

### 25. Info: 22. DEPOSIT/FEE ECONOMICS: below the $300 check threshold a false proof risks max(1 IMD, 0.5 IMD + 3% of R) to win 90% of R, so fraud is +EV whenever fewer than about 3-4% of false proofs get disputed

`contracts/src/VaultParams.sol:142`

```
        p.doerBondBps = 300;
```

Sizes at launch, IMD = $7.65: doer bond max(1 IMD, 0.5 IMD + 3% of R in IMD) = $7.65 on a $5 task (153% of R), $12.90 on a $300 task (4.3%), $153 on a $5,000 task (3%); dispute and appeal bond max(1 IMD, 0.5 + 1% of R) = $7.65 up to R = $382; dispute ETH fee 0.0003-0.01 ETH funds relayers; swarm check 0.5 IMD = $3.83; settlement fee 10% of R; author fee $1.10. Too cheap to deter: for R < $300 a dishonest doer loses 3% of R if caught and gains 90% if not, and nothing but an NFT holder's attention stops it inside the 24h window; a disputer who is right earns 50% of that bond (NFT right) or 100% (swarm) = $3.83-$6.45 for a $0.66 fee plus an appeal risk of $11.48, so disputing a $200 fraud is barely worth a stranger's time, and the author (the natural disputer) must out-argue the doer within 24h. Too costly for a small task: a $5 dare needs a $7.65 doer bond, fine for play, but a dispute on a $20 dare costs $7.65 + $0.66 and returns at best $3.83. Recommendation, parameters only and inside ParamsBounds, no code: doerBondBps 1,000 (10%) so a false proof on a $200 task risks $20, and largeRewardUsd lowered toward $100 so the $3.83 check covers the band where NFT attention is thin; keep minBond 1 IMD for sub-$10 dares.

**Reproduction**

Formulas: Desk.sol:116-131 (bond), VaultParams.sol:133-160 (launch values), ParamsBounds lines 195-227; worked example MECHANICS.md ('300 USDC task').

### 26. Info: 23. TOKENOMICS: the split and clocks match docs/TOKENOMICS.md; about 54% is liquid on day 0, of which 286.6M YOLO sits in three project wallets against a 150M-YOLO pool side; $YOLO demand is discretio

`contracts/src/token/YoloSplitter.sol:32`

```
    uint256 public constant STABILIZER = 150_000_000e18;
```

Verified against code: splitter 250M / 150M / 77,777,777 / 50M / remainder (222,222,223 plus factory rounding); vestings 62.5M at once then 730 days, 74,074,074 at once then 365 days, dev and team 365 days with no cliff, clocks start at the split (entry 2). Day 0 unlocked = pool 150M + Stabilizer 150M + swarm 100M + incentives 62.5M + treasury 74.07M = 536.6M (53.7%, docs say 54%); month 6 and 12 figures reproduce. Stabilizer / MM fund (kept, per the brief; risk and disclosure only): 150M at once plus 136.6M of other at-once project shares = 286.6M liquid in project wallets on day 0, 1.9x the pool's 150M YOLO side at an opening valuation of 15,000 IMD for 1B, so these wallets can move or cap the price at will; the docs say the wallet is public and every move visible but give no mandate, limit or reporting cadence. Recommend publishing the fund's rules (no net selling for N days, position sizes) before launch. Demand for $YOLO: optional reward currency, YoloSweetener bonuses, the trophy burn (about $1.50 per accepted dare, 10,000 YOLO without a price), the NFT-gated first 30 minutes; no fee is converted into YOLO. Do fees fund the project: yes, 10% of every accepted reward in its own currency, author fees in ETH, the treasury part of disputed rewards and penalty remainders, plus the pool's LP fee share outside this code; small at $5-$300 task sizes, so the treasury vesting is the larger asset.

**Reproduction**

Arithmetic from token/Vestings.sol, token/YoloSplitter.sol:31-36 and script/LaunchPlan.sol; docs/TOKENOMICS.md tables reproduced to the stated rounding; forge test --match-contract 'YoloToken|Yolo' passed.

### 27. Info: 24. HARMFUL DARES: the illegal-content prefix reaches the panel only when a swarm question is asked (R >= $300, an appeal, or callSwarm), and HiddenList only hides on the site, so a sub-$300 illegal d

`contracts/src/VaultParams.sol:166`

```
        p.questionPrefix =
```

What exists: (1) the questionPrefix tells the panel to answer no for crime, minors, violence, injury risk, harassment/doxxing or hate, reached by the mandatory check (R >= $300 or >= 40 IMD, or any $YOLO task without a price), by an appeal (1 IMD + 0.5 IMD) or by callSwarm in NoDecision; (2) HiddenList: the owner or 10 NFTs stop the site from rendering the item, with the limits of 13a-13c; (3) TextRules bounds the condition to 256 UTF-8 bytes without quotes or control characters, nothing semantic. Gaps: createTask accepts any valid text; submit for R < $300 in ETH/USDC/IMD sets large = false, _activate opens the window without a question, and with no dispute _finalize(Accepted) pays 90% to the doer; hiding removes the audience, not the payout, and the proof lives off-site anyway; an NFT vote cannot reject a submission without a dispute bond and the appeal path, where the prefix finally applies; 13b lets the dare's author block the hide vote himself. Adequate as a gate against an illegal dare being paid: no. Adequate against being displayed: mostly, owner instant and voters 24h, minus 13b. Cheapest addition, no code: a parameter version with largeRewardUsd = minRewardUsd ($5) and largeRewardImd = minRewardImd makes every submission prepay the 0.5 IMD ($3.83) check so the prefix is applied to every proof before any payout (ParamsBounds allows it, VaultParams.sol:197-198). Cheapest code addition if $3.83 per submission is too much: CaseDesk._activate forces the check when HiddenList.status(vault, KIND_TASK, taskId) != VISIBLE, one view call, so a hide vote becomes a 'send it to the swarm' vote while the owner still has no money power (the swarm answers, not the owner).

**Reproduction**

Trace: TaskVault.createTask validates text only (TextRules); Desk._priceReward line 150 sets large = usdValue >= 300e18; CaseDesk._activate asks a question only when large; no dispute => Accepted at windowEnd => _payReward; grep HiddenList contracts/src: read by nothing in the core.

### 28. Info: 25. COVERAGE: all contracts/src files, LaunchPlan.sol and the five docs read; forge test 388 passed / 0 failed / 5 fork suites skipped on forge 1.8.3; on forge 1.8.5 the harness fails 37 suites in set

`contracts/README.md:13`

```
forge test                                   # 388 pass, 5 fork suites (11 tests) skip without FORK_URL
```

Read in full: HiddenList, TaskVault (withdraw, create, oracle callback paths), VaultParams, DareTrophy.price/mint, desks/Desk, DisputeDesk and CaseDesk settlement and timing paths, libraries/VoteLib, oracles/YoloPoolSamples and ChainlinkUniV3PriceOracle TWAP path, token/YoloSplitter and ProjectVesting binding, script/LaunchPlan.sol; docs/AUDIT-INTERNAL.md in full, README.md, MECHANICS.md (participants, withdraw, trust assumptions, HiddenList), TOKENOMICS.md (tables), LAUNCH-TASK.md (post-launch steps, launch 3 text); test/Base.t.sol, HiddenList.t.sol, audit/swarm117/* in part. Tools: forge 1.8.5 (installed on the machine) then forge 1.8.3 (installed for this review, the version the repository's figures come from), solc 0.8.28 via-IR cancun. Results: forge 1.8.3 full suite 59 suites, 388 passed, 0 failed, 5 skipped (fork suites, no FORK_URL); the five scratch tests (four specialist proofs plus the judge's RollingNotice.t.sol) all fail on this commit for the stated reasons. forge 1.8.5: 37 suites fail in setUp with WiringClosed() or BindingClosed() because Desk.wire and ProjectVesting.bindStarter read a transient flag set in a constructor called earlier in setUp, and that forge version clears transient storage between the test contract's calls; on chain each launch is one factory transaction, so this only affects the test harness (worth pinning the forge version in contracts/README.md or deploying and wiring inside one helper call). Not reached: the fork rehearsal and launch gas figures (taken from the docs), the live pool and Intake reads (taken from the math specialist's report, consistent with LAUNCH-TASK.md:130), the ProjectFactory, delegate.xyz, the identity.md NFT contract and the site. Slither/Mythril not run (not provided). Nothing outside contracts/test/scratch/ and .imd-findings.json was written.

**Reproduction**

Commands: forge build; forge test --no-match-path 'test/scratch/*' (on 1.8.5 then 1.8.3); forge test --match-path 'test/scratch/*'; git diff --stat b86a5f8 7714a24 -- contracts/src contracts/script; sed/grep of the cited lines.

---

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