{"jobId":"eb0f6c65-174a-45e8-9fe5-9312c66d4880","repoUrl":"https://github.com/OoJae/help-wanted.git","baseCommit":"94fb4730e1a8a641358516b3d8550e6a72254a8a","count":13,"submissions":[{"hash":"b01670dc4a04684ece3c10fec4ce6a277dfc41ba3fcb19edb5df720905aee193","nodeKey":"audit_judge","role":"review","attempt":1,"deviceKey":"c0fc4ea4f50e3380927cfa7df7d414d9a3689c513aca5f117e3c35ba351067e8","seat":{"tokenId":"286","agentId":"52252"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":13,"runtime":"claude","inputTokens":386,"wallClockMs":739383,"outputTokens":13335,"cachedInputTokens":844116},"baseCommit":"94fb4730e1a8a641358516b3d8550e6a72254a8a","startCommit":"f45846074c3abdad70f06744f6586905e3b76e37","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":["d2a63b4814f0a662cc31bd080327f5e80954bff826929e769a75e1dddc640be3","355cd9b972f5d82b1128cdd7f6ef7e759b28b53a010ab29da09a905804b038ba","a098bc72544072e0a56516c620ece49c482dd773228b13e6f04965889d7e43c5"],"verificationErrors":0,"summary":"The findings file is written and validated. The review is complete.\n\n**Outcome: no blocking findings.** My one medium finding from the first round is settled as fixed, and the two advisory notes stand unchanged as documented trust assumptions. The findings file holds three info entries and a 30-row coverage record.\n\n**The medium finding a96c4f58 is fixed in words, as I had offered.** The author kept the desk's rule and corrected what CrowdPots told contributors about the bond. I checked each part of the answer:\n\n- The three CrowdPots statements and the THREATS W2, W3, W5 and C2 sections now say the bond comes back whole on a receipt that shows the swarm failed or ran another kind, and that this can end a receipt-only pot past its hire window. I found no remaining sentence claiming a stall always forfeits the bond.\n- The two new tests in PotsRealDesk reproduce both of my cases on the real desk and pass.\n- I built the accepted commit in a separate directory and compared it with the current tree. The creation and runtime bytecode of all four contracts hash identically, so no code changed and nothing could have broken.\n- The full suite passes.\n\n| Check | Result |\n|---|---|\n| forge test, whole suite | 673 passed, 0 failed |\n| Two new regression tests | 2 passed |\n| Bytecode vs accepted commit 94fb473 | identical for all four contracts |\n| launch.json vs docs/LAUNCH.md and the brief | identical, schema keys only |\n\n**The two advisory notes stay as recorded.** The author disputed changing code for the Doorbell claimed-path exposure and the chipInFor beneficiary check, and in both cases I agree. Both are documented behaviour the brief asks for, ADAPTATION section 5 now lists the Doorbell watcher float as an open item, and the three specialists' findings this round were the same two observations, merged into mine.\n\nNo files in the tree were changed. The only new file is the required .imd-findings.json.","createdAt":"2026-10-13T15:45:05.509Z","verdict":null,"findings":[{"severity":"info","title":"Settled: a96c4f58 (SwarmFailed and wrong-kind endings return the whole bond) is fixed in words, as offered; the desk is unchanged and the corrected statements are now pinned by two tests","path":"src/pots/CrowdPots.sol","line":73,"description":"Second-round settlement of my medium finding a96c4f58, kept here as a record and not as an open defect. The finding gave the author two ways to close it: charge the release share and/or bar the runner on the SwarmFailed and wrong-kind endings of reportFailed, or correct the three CrowdPots statements and THREATS C2/W5 that claimed the bond is forfeited by any stall. The author took the second, which the finding said was theirs to choose and which matches the brief (deploy the code as it is) and the requester's own handling of the same class in THREATS section 11. I checked each part of the answer. (1) src/pots/CrowdPots.sol: the contract notes (lines 73-81), Terms.bond (lines 123-128) and Terms.allowClaimed (lines 149-153) now say the bond is the most a runner loses, is lost whole by silence, a job that is not the order's or a contradicted claim, and comes back whole on a receipt that shows the swarm did not complete the job or ran it under another kind, which past the hire window ends a receipt-only pot in a refund. That is exactly what WorkOrders.reportFailed does at lines 441-445 (`} else if (sameObjective) { if (!completed && msg.sender != c.runner) revert NotRunner(); _endClaim(id, o, c, End.SwarmFailed, bond, 0, 0);`) together with _close at line 825 (`if (refundNow || block.timestamp >= o.expiresAt)`). (2) docs/THREATS.md W2, W3, W5 and C2 and the docs/LAUNCH.md 'writes another kind' row now state the two endings, the NotRunner gate, the missing barred flag and the costs to each side; I found no remaining sentence in the three documents or in CrowdPots.sol that says a stall always forfeits the bond. (3) The two new tests in test/pots/PotsRealDesk.t.sol reproduce my two cases on the real desk and pass: test_realDesk_aFailedJobsReceiptPastTheHireWindow_refundsThePotWithTheBondReturned asserts Refunded, runner balance back to WALLET, barred false, returned == TARGET, pot Refundable and contributors refunded exactly their chips; test_realDesk_aWrongKindReceipt_reopensWithTheBondReturned_andTheSameRunnerMayTakeItAgain asserts three cycles with Open, balance unchanged, barred false, returned 0. (4) The bytecode is unchanged: I built the accepted commit 94fb473 in a separate directory and the current tree, and the creation and runtime bytecode of WorkProof, WorkOrders, CrowdPots and Doorbell hash identically (runtime sizes 4580, 19290, 14178 and 5973 bytes, all under 24,576). Nothing in the fix broke or introduced anything, since no code changed. Full suite: 673 passed, 0 failed. What remains true and is now documented rather than contradicted: a receipt-only pot can be held past its hire window and refunded with no bond forfeited, for one swarm job and gas, with no IMD lost.","reproduction":"Run `forge test --match-path test/pots/PotsRealDesk.t.sol --match-test 'aFailedJobsReceiptPastTheHireWindow|aWrongKindReceipt_reopens'`: both pass on this tree (2 passed, 0 failed) and assert the behaviour my finding described (Refunded, bond whole, barred false, returned == escrow, pot Refundable; and three wrong-kind cycles reopening the order with the bond whole). Bytecode check: `git archive 94fb473 | tar -x -C /tmp/prevtree; cp -r lib /tmp/prevtree; forge build` there and `forge build` here, then sha256 of out/<C>.sol/<C>.json bytecode.object and deployedBytecode.object for the four contracts: identical pairwise. Expected after the fix: the statements contributors read match the desk's rule and are tested. Actual: they do. Settled.","snippet":"/// - The size of the bond next to the target. The bond is the most a runner loses by taking the\n///   order and not delivering, and it loses all of it by letting the order time out. It loses\n///   none of it when the registered job's receipt shows that the swarm did not complete the job\n///   or ran it under another kind: the desk then returns the bond whole, bars nobody, and the","citation":"resolved","blocking":false},{"severity":"info","title":"Trust assumption the manifest turns on (bff72b72, unchanged): with ALLOW_CLAIMED = 1 and no arbiter, each Doorbell escrow is paid to an unchallenged bonded claim, and a correct challenger's best outco","path":"src/pots/Doorbell.sol","line":249,"description":"Not a code defect, carried forward unchanged from the first round; the author disputed a change and agreed with every figure, which is the right outcome. Merged this round again with audit_economics (df1c3820) and audit_flow (5ac1a4bb), which report the same root cause with the same numbers. The brief fixes the eighth constructor argument at 1; the Doorbell's notes (lines 30-38) and docs/THREATS.md D2, L2 and L6 state the rule; ADAPTATION.md section 5 now lists the watcher float it implies as an open item for whoever funds the Doorbell. At the manifest's values (MIN_PAY = MAX_PAY = 1e18, BOND = 0.25e18, DELIVERY_SECS = 604800, ALLOW_CLAIMED = 1): each undefended false claim costs the Doorbell 1 IMD; a defended attempt costs the attacker 0.025 IMD and locks the defender's 0.25 IMD for seven days with no reward, because timeout on an undecided dispute (WorkOrders.sol line 472, End.DisputeExpired) pays the challenger exactly its bond. Ringing is free apart from gas, so with balance B anyone opens floor(B / 1e18) orders in one transaction and can repeat a wave every 7 days. Recorded so the requester sizes the float before funding; no action in code is asked for.","reproduction":"Code trace at the manifest's values. (1) Transfer 1e18 IMD to the Doorbell. (2) Call ring() 25 times; the 25th opens order N with escrow 1e18, flags FLAG_CALLBACK | FLAG_ALLOW_CLAIMED, arbiter 0, expiresAt now + 2 days. (3) Attacker approves 0.25e18 and calls claim(N), register(N, 0x00000000000000000000000000000001), claimDelivered(N, bytes32(uint256(1))) in one block. (4) 24 hours later finalize(N) pays the attacker 0.9e18 plus its 0.25e18 bond, Payroll 0.1e18, and Doorbell.onWorkDelivered records the job as delivered. If a defender challenges with 0.25e18 inside the window, timeout(N) at registeredAt + 7 days pays the defender 0.25e18, the attacker 0.225e18 and the Doorbell 1.025e18. Expected by the documents: exactly this. The project's test test_realDesk_theDoorbellHires_andAnUnchallengedClaimIsReportedBack shows step (4).","snippet":"        if (ALLOW_CLAIMED) flags |= WORK_ORDERS.FLAG_ALLOW_CLAIMED();","citation":"resolved","blocking":false},{"severity":"info","title":"Note (2094daa9, unchanged): chipInFor accepts the desk, IMD, Payroll and WorkProof as beneficiary; a refund claimed for such a chip is stranded in a contract that keeps no books of it","path":"src/pots/CrowdPots.sol","line":737,"description":"Not a defect, carried forward unchanged; the author disputed a change, which I accept. Merged with audit_flow (a5b4373d), the same observation. Only the payer's own IMD is at stake, by the payer's own choice of beneficiary, and docs/DEVIATIONS.md D7 and docs/THREATS.md C11 say so in words. The brief asks that the claimRefund rule stay as it is. The check moved from line 726 to line 737 because comments above it grew; the code is byte-identical to the accepted commit.","reproduction":"openPot(terms: target 1e18, fundSecs 1 hours, hireSecs 1 days, deliverySecs 3 days, bond 0.25e18, minPay 1e18, rampSecs 0, kind 'audit', allowClaimed false), spec 'x', firstChip 0 -> potId P. Approve 1e18 and call chipInFor(P, 1e18, address(WORK_ORDERS)): accepted, raised = 1e18. Nobody launches; warp past deadline + LAUNCH_GRACE. claimRefund(P, address(WORK_ORDERS)) returns 1e18 and transfers 1e18 IMD to WorkOrders, which only ever sends amounts it booked for an order. Expected if refused: BadBeneficiary. Actual: accepted, as documented in D7.","snippet":"        if (beneficiary == address(0) || beneficiary == address(this)) revert BadBeneficiary();","citation":"resolved","blocking":false}],"coverage":{"rows":[{"target":"CrowdPots.chipIn(uint256,uint256)","verdict":"holds","why":"Delegates to _chip for msg.sender; stage, remaining and amount checks, raised accounted before transferFrom under nonReentrant; code byte-identical to the round-one tree I traced."},{"target":"CrowdPots.chipInFor(uint256,uint256,address)","verdict":"holds","why":"Same path as chipIn with a beneficiary; refuses only zero and self (note 3, documented in D7), no defect."},{"target":"CrowdPots.claimRefund(uint256,address)","verdict":"holds","why":"Anyone may claim for a contributor by design (D7); pays only when the pot is Refundable, zeroes the chip before transfer, slither reentrancy lines are the IMD transfer under nonReentrant."},{"target":"CrowdPots.claimRefunds(uint256,address[])","verdict":"holds","why":"Loops claimRefund logic over caller-chosen addresses; a zero chip reverts the batch (caller's own gas), no shared state harmed."},{"target":"CrowdPots.launch(uint256,bytes)","verdict":"holds","why":"Requires Filled stage and matching spec hash, approves exactly raised, createOrder on the fixed desk, reads employer back; slither unused-return is a tuple destructure, reentrancy is the desk call under the lock."},{"target":"CrowdPots.onWorkDelivered(uint256,bytes16,bytes32)","verdict":"holds","why":"Only WORK_ORDERS may call; records delivery for the pot that owns the order, no funds move."},{"target":"CrowdPots.openPot((string,uint96,uint96,uint32,uint96,uint32,uint32,uint32,bool),bytes,uint256)","verdict":"holds","why":"Validates bond against MIN_BOND and target, ramp, times and kind; optional first chip through the same _chip path."},{"target":"CrowdPots.openSeries((string,uint96,uint96,uint32,uint96,uint32,uint32,uint32,bool),bytes)","verdict":"holds","why":"Stores terms; rounds are created lazily by _round with the same validation as openPot."},{"target":"CrowdPots.reconcile(uint256)","verdict":"holds","why":"Pulls the desk's accountOf and stateOf, books returned escrow and forfeited bonds, moves the pot to Refundable or Delivered; exercised by the two new tests."},{"target":"Doorbell.onWorkDelivered(uint256,bytes16,bytes32)","verdict":"holds","why":"Only WORK_ORDERS may call; records the delivery; the claimed-path exposure it records is note 2."},{"target":"Doorbell.ring()","verdict":"holds","why":"Counts rings, hires every 25th when balance >= MAX_PAY with fixed constructor terms; free and unlimited by design, cost sized in note 2."},{"target":"WorkOrders.cancel(uint256)","verdict":"holds","why":"Employer only, Open state only, escrow returned and booked via _credit."},{"target":"WorkOrders.challenge(uint256)","verdict":"holds","why":"Asserted state inside the window, bond equal to the runner's, moves to Disputed; challenger's outcome at timeout is its bond back (note 2)."},{"target":"WorkOrders.claim(uint256)","verdict":"holds","why":"Open and unexpired, not barred, runnerOnly honoured, bond pulled, price fixed by _price at claim time."},{"target":"WorkOrders.claimDelivered(uint256,bytes32)","verdict":"holds","why":"Runner only, FLAG_ALLOW_CLAIMED required, 24h+24h must fit before registeredAt + deliverySecs; the trust assumption is note 2."},{"target":"WorkOrders.createOrder((bytes32,uint96,uint96,uint32,uint32,uint40,uint96,uint8,address,address,address,bytes4,bytes32,uint8,uint256),bytes)","verdict":"holds","why":"Range checks on pay, bond, times and flags, escrow pulled from msg.sender, spec hashed; cast at line 845 is a checked SafeCast."},{"target":"WorkOrders.evict(uint256)","verdict":"holds","why":"Claimed with no job after the claim window; whole bond to employer, order reopened or refunded via _close."},{"target":"WorkOrders.finalize(uint256)","verdict":"holds","why":"Asserted and past until; pays runnerPay plus bond, fee and tip to Payroll with fallback transfer, callback to the employer last."},{"target":"WorkOrders.redeliver(uint256)","verdict":"holds","why":"Runner only, replaces the registered job inside the delivery time without touching the bond."},{"target":"WorkOrders.refund(uint256)","verdict":"holds","why":"Open and expired only; escrow returned and booked; anyone may call."},{"target":"WorkOrders.register(uint256,bytes16)","verdict":"holds","why":"Runner only, nonzero job id, within claim window, sets registeredAt."},{"target":"WorkOrders.release(uint256)","verdict":"holds","why":"Runner only; release share to employer, rest back, barred set, order reopened or refunded."},{"target":"WorkOrders.reportFailed(uint256,bytes,address[])","verdict":"holds","why":"SwarmFailed and WrongJob endings behave as the desk documents; the overstated CrowdPots wording was my finding a96c4f58, now settled as finding 1 (fixed in words, tests pin it)."},{"target":"WorkOrders.resolve(uint256,bool)","verdict":"holds","why":"Arbiter only, Disputed only; pays the winner per L3; the Doorbell and pots never name an arbiter."},{"target":"WorkOrders.settle(uint256,bytes,address[])","verdict":"holds","why":"Proves the receipt through WorkProof against headerOf plus spec and kind, pays runner, fee and tip, callback last."},{"target":"WorkOrders.stamp(uint256,bytes,address[])","verdict":"holds","why":"Records a proven receipt on an already paid claimed order without moving funds."},{"target":"WorkOrders.timeout(uint256)","verdict":"holds","why":"Registered past deliverySecs: bond to employer; undecided dispute: challenger its bond, runner minus release share; order reopened or refunded."},{"target":"WorkOrders.trigger(uint256)","verdict":"holds","why":"Condition-gated orders only; reads the configured contract and selector, opens the order when the condition holds."},{"target":"invariant: bytecode unchanged since accepted commit 94fb473","verdict":"holds","why":"Creation and runtime bytecode of all four contracts hash identically before and after the revision; runtime sizes 4580, 19290, 14178, 5973 bytes."},{"target":"invariant: launch.json matches docs/LAUNCH.md and the brief","verdict":"holds","why":"Same four contracts in dependency order, same arguments word for word, only kind, contracts and notes keys, all arguments strings."}],"summary":{"reported":true,"total":28,"answered":28,"ratio":1,"missing":[],"unreached":[]}}},{"hash":"8c0c8d1e5836506e7faf3c3814e1f3d914963fff58cedeecff3079268e91abd5","nodeKey":"write_foundry_tests","role":"tests","attempt":1,"deviceKey":"776c025db24644e83396ec3b43a4f8b9aa10b354c5b76483681c847e3c8e29bd","seat":{"tokenId":"507","agentId":"52685"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":41,"runtime":"claude","inputTokens":642,"wallClockMs":842735,"outputTokens":23949,"cachedInputTokens":1727228},"baseCommit":"94fb4730e1a8a641358516b3d8550e6a72254a8a","startCommit":"2896f29d8278942325e3f6ad1cb451960834de7f","resultTreeHash":"c88f8072983f801b453a1f8baa8c973843f114a6","bundleHash":"355cd9b972f5d82b1128cdd7f6ef7e759b28b53a010ab29da09a905804b038ba","bundleBytes":28287,"artifacts":[],"changedPaths":["test/launch/LaunchSystem.invariant.t.sol"],"allowedPaths":["test","test/**"],"dependencyBundleHashes":["d2a63b4814f0a662cc31bd080327f5e80954bff826929e769a75e1dddc640be3"],"verificationErrors":0,"summary":"The revision is done. The full suite passes with 673 tests and 0 failures, and the only file changed is my launch-wide invariant test.\n\n**What this round changed.** The other contributor's review-driven commit altered no bytecode; it corrected the comments and documents on the `reportFailed` endings that return the bond whole and added two unit tests for them. My work from the previous round was intact and passing on the merged tree. I extended the launch-wide handler in test/launch/LaunchSystem.invariant.t.sol so the random sequences also exercise those endings, and left LaunchFloor.t.sol untouched.\n\n- **Three more desk endings in the handler.** On a registered order the handler now also lands a completed receipt of another kind, reportable by anyone, and a receipt for a job that is not the order's. The failed-job report by the runner now sometimes waits out the order's expiry first.\n- **Ghost checks on every bond-back report.** The runner's balance rises by exactly the bond, the runner is not barred, the employer is credited nothing unless the order expired, in which case exactly the escrow, and the order ends open or refunded. A stranger's attempt to report the runner's failed job must be refused. Any breach is recorded and fails the existing no-unexpected-revert invariant.\n- **Wrong-job report checked the other way.** The runner gets nothing back and the employer is credited at least the bond.\n- **A directed test for the C2 ending.** The runbook's first pot, opened receipt-only on the manifest-made contracts, is held an hour before its hire window closes, cannot be freed by anyone else, and is refunded by the runner's own report with the whole bond back. The desk still holds exactly what it owes and both contributors get back exactly their chips.\n- **The soak now asserts the new paths are reached** and that every bond-back ending counted was a report that went through.\n\n**One wrong property on my side, not a defect.** My first version asserted that a wrong-job report bars the runner. The desk only bars on `release`; the wrong-job ending's deterrent is the forfeited bond. I removed that check after confirming it in the source.\n\nNo defects were found, so no findings file was written.","createdAt":"2026-10-13T15:27:34.049Z","verdict":{"status":"accepted","profile":"foundry","evaluation":"checks","rejectionCode":null,"detail":"all checks passed","verifierVersion":"0.1.0+327e1e9f","verifiedTreeHash":"c88f8072983f801b453a1f8baa8c973843f114a6","at":"2026-10-13T15:32:36.557Z","failedChecks":[]},"findings":[],"coverage":null},{"hash":"0187e38f8b3dac77e9d534d8ca6c46df9077a2941ed569a8579ec64ba1060c90","nodeKey":"manifest","role":"integrate","attempt":1,"deviceKey":"4dd67dae195771b6441fdb6a5194f0cb584055f2db71093414434f19e593aa16","seat":{"tokenId":"225","agentId":"52158"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":10,"runtime":"claude","inputTokens":162,"wallClockMs":599897,"outputTokens":5421,"cachedInputTokens":305583},"baseCommit":"94fb4730e1a8a641358516b3d8550e6a72254a8a","startCommit":"edc00b23d69e0f7e05344fdd2dff716172ed9e16","resultTreeHash":"d58a439346443a547c28fc73550269505dec42c6","bundleHash":"a098bc72544072e0a56516c620ece49c482dd773228b13e6f04965889d7e43c5","bundleBytes":13088,"artifacts":[],"changedPaths":[],"allowedPaths":["launch.json"],"dependencyBundleHashes":["d2a63b4814f0a662cc31bd080327f5e80954bff826929e769a75e1dddc640be3"],"verificationErrors":0,"summary":"The manifest in the tree is already correct and nothing needed to change. This revision brief listed no findings, and every check I could run confirms the accepted launch.json.\n\n**What I verified**\n\n- launch.json has exactly the keys kind, contracts and notes. It names WorkProof, WorkOrders, CrowdPots and Doorbell in that order with 1, 3, 2 and 8 arguments, matching the four constructor signatures in src.\n- Every argument matches docs/LAUNCH.md section 2 byte for byte: the WorkRegistry, IMD and Payroll mainnet addresses, the three backward $contract references, the kind hash, the two 1 IMD prices, the 0.25 IMD bond, 604800 seconds and allowClaimed written as 1. No argument is $owner, as the brief requires.\n- The notes string is 160 characters, under the 4000 limit.\n- The build is clean and the full suite passes with 694 tests, including the 12 manifest tests and 8 runbook tests in test/launch that parse LAUNCH.md and compare it with the code.\n- I ran the pinned protected test from test/scratch against the real initcode with the manifest's arguments resolved in order through CREATE2 predictions. All four constructors succeed on an empty chain, each runtime is under the EIP-170 limit, and no runtime holds DELEGATECALL, CALLCODE or SELFDESTRUCT.\n- The tree has no .gitmodules and no submodule entries; lib is ordinary committed files. The working tree is clean, so no file other than the already committed launch.json carries this deliverable.\n\nNothing is left to do: the manifest is submitted as it stands.","createdAt":"2026-10-13T15:23:40.420Z","verdict":{"status":"accepted","profile":"foundry","evaluation":"checks","rejectionCode":null,"detail":"all checks passed","verifierVersion":"0.1.0+327e1e9f","verifiedTreeHash":"d58a439346443a547c28fc73550269505dec42c6","at":"2026-10-13T15:29:07.860Z","failedChecks":[]},"findings":[],"coverage":null},{"hash":"a34ea4ad8bc2f91f9c1ca20f0136d9792a0f695129f37f9635101004951d9075","nodeKey":"adapt_contract_project","role":"implement","attempt":1,"deviceKey":"e67cc6ec4009b22617765aa23b1e65dd021d8871446db2c0c3c8aa0777aec862","seat":{"tokenId":"1637","agentId":"51104"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":38,"runtime":"claude","inputTokens":482,"wallClockMs":898270,"outputTokens":26526,"cachedInputTokens":1704182},"baseCommit":"94fb4730e1a8a641358516b3d8550e6a72254a8a","startCommit":"0ca93eac1499330d10e756dd98b055a3507e6170","resultTreeHash":"a91750af149a253a99d77f761acf974e60f25ce2","bundleHash":"d2a63b4814f0a662cc31bd080327f5e80954bff826929e769a75e1dddc640be3","bundleBytes":11913,"artifacts":[],"changedPaths":["ADAPTATION.md","docs/LAUNCH.md","docs/THREATS.md","src/pots/CrowdPots.sol","test/pots/PotsRealDesk.t.sol"],"allowedPaths":[],"dependencyBundleHashes":[],"verificationErrors":0,"summary":"The revision is complete. All three findings are answered in `.imd-responses.json`, and the build and full suite pass.\n\n**The reopened finding (a96c4f58) reproduced** exactly as reported, both cases, on the real desk with real receipts. A runner holding a receipt-only pot past its hire window can end it in a refund with its whole bond returned, and a wrong-kind receipt reopens the order with nothing forfeited and nobody barred.\n\n**I fixed it by correcting the words, not the desk.** The reviewer offered that as one of two acceptable resolutions and left the choice to the authors. The desk rule is deliberate and documented three times over, the brief asks to deploy the code as it is, and THREATS.md section 11 records the requester handling the same class of finding in their first launch the same way. What was wrong was what CrowdPots told contributors the bond sizes.\n\nChanges made:\n- `src/pots/CrowdPots.sol`: the three overstated comments now say the bond is the most a runner loses, is lost whole by silence, a wrong job or a contradicted claim, and comes back whole on a receipt showing the swarm failed the job or ran it under another kind. Compiled bytecode of all four contracts is byte-for-byte unchanged, which I verified by hashing the artifacts before and after.\n- `docs/THREATS.md` W2, W3, W5 and C2 state the mechanics, the cost to each side, and why the desk does not charge the release share there. One row in `docs/LAUNCH.md` carried the same overstatement and now distinguishes the two cases.\n- `test/pots/PotsRealDesk.t.sol`: two permanent tests pin the reviewer's exact scenarios against the real desk, so the corrected sentences are tied to code.\n- `ADAPTATION.md` section 5 records each change, the reasoning, and the two advisory findings.\n\n**The two advisory findings** were reproduced and answered as disputed with reasoning: the Doorbell claimed-path exposure is the requested behaviour with the eighth constructor argument fixed by the brief, and the `chipInFor` beneficiary note concerns only the payer's own IMD under the D7 rule the brief asks to keep. Neither changed code. The Doorbell float sizing is recorded as an open item.\n\nResults: `forge build` passes, `forge test` reports 656 passed and 0 failed. Nothing is committed, since no instruction asked for a commit.","createdAt":"2026-10-13T15:07:46.887Z","verdict":{"status":"accepted","profile":"foundry","evaluation":"checks","rejectionCode":null,"detail":"all checks passed","verifierVersion":"0.1.0+327e1e9f","verifiedTreeHash":"a91750af149a253a99d77f761acf974e60f25ce2","at":"2026-10-13T15:12:15.591Z","failedChecks":[]},"findings":[],"coverage":null},{"hash":"af15545e1483ac741f90961df99684fa108f2fe10fcb6e0824610aea62b20716","nodeKey":"audit_judge","role":"review","attempt":1,"deviceKey":"4576f670afa0131d2ebb5587aa540d766ca443e424eacfd686cc685348341489","seat":{"tokenId":"798","agentId":"51305"},"outcome":"completed","accepted":false,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":29,"runtime":"claude","inputTokens":420,"wallClockMs":810297,"outputTokens":32421,"cachedInputTokens":2084251},"baseCommit":"94fb4730e1a8a641358516b3d8550e6a72254a8a","startCommit":"bb146e1c9584e519ec8395be962e1add404b7dfe","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":["d2a63b4814f0a662cc31bd080327f5e80954bff826929e769a75e1dddc640be3","355cd9b972f5d82b1128cdd7f6ef7e759b28b53a010ab29da09a905804b038ba","a098bc72544072e0a56516c620ece49c482dd773228b13e6f04965889d7e43c5"],"verificationErrors":0,"summary":"Review complete. The findings file is written and validated, and I changed nothing outside `.imd-findings.json` and my own `test/scratch/` file.\n\n**What I kept, after reproducing each against the code**\n\n- **Medium (finding 1, from audit_permissions).** The SwarmFailed and wrong-kind endings of `reportFailed` return the runner's whole bond, never set `barred`, and refund the order for good when it has expired. I reproduced both variants in a scratch test on the real desk, WorkProof and CrowdPots: a runner holds a receipt-only pot's order past its hire window with a \"blocked\" receipt of the right objective, ends it itself before the deadline, and the pot is refunded with nothing forfeited. The wrong-kind loop lets the same runner re-claim at once, three rounds with its balance unchanged. This contradicts the three bond statements CrowdPots makes to contributors and C2/W5 in the threat model. No IMD is lost, so medium as a broken guarantee, not a loss. The scope choice (keep the release share and/or bar on those endings, or correct the documents) is the author's.\n- **Info (finding 2, merged from audit_economics and audit_flow).** The Doorbell's mainnet values with `ALLOW_CLAIMED = 1` and no arbiter: each escrow is paid to an unchallenged claim, and a correct challenger only ever gets its own bond back after 7 days. Documented in the contract and threat model, listed as the one trust assumption the manifest turns on. Both specialists' numbers check out against the code.\n- **Info (finding 3, from audit_flow).** `chipInFor` accepts the desk and the token as beneficiary, so a refund claimed for such a chip is stranded. Self-harm only, covered by D7, kept as a note with the cheap extra check.\n\n**What I verified myself**\n\n- All 28 entry points traced; 27 hold, one carries finding 1, and `chipInFor` carries info 3.\n- The manifest matches the constructors, the kind hash is keccak256 of \"skill:create-image\", every hex string is even length, and all four contracts are far inside the size limits. The 12 launch manifest tests pass.\n- On mainnet through the contributor relay: IMD at the given address reports symbol \"IMD\", the WorkRegistry returns the exact fixture receipt hash for job e9efec16, and Payroll has code.\n\nNothing blocks deployment. Finding 1 is the only one that reopens the author's work.","createdAt":"2026-10-13T14:52:04.767Z","verdict":null,"findings":[{"severity":"medium","title":"The SwarmFailed and wrong-kind endings of reportFailed return the whole bond and never bar the runner, so a receipt-only crowd pot can be held past its hire window and refunded with nothing forfeited,","path":"src/desk/WorkOrders.sol","line":441,"description":"Reproduced (merged from audit_permissions; the other three areas reported nothing on this path). Every other way a runner leaves an order without delivering costs it bond: release() keeps the release share for the employer and sets barred[id][runner]; evict() and timeout() forfeit the whole bond. The SwarmFailed branch of reportFailed returns the whole bond, sets no barred flag, and is reachable until registeredAt + deliverySecs. Two things make the moment of that exit the runner's: (a) when the receipt's state is anything but completed, only the runner may submit it (line 442, NotRunner for everyone else), so nobody can free the order sooner; (b) _endClaim calls _close, which refunds the order for good once block.timestamp >= expiresAt (line 825). The wrong-kind variant (sameObjective, completed, another kind) needs no failed job at all: anyone may report it, the bond comes back whole, barred stays false, and the same address may claim(id) again immediately, so a runner can cycle claim -> register -> cheap wrong-template job -> reportFailed on one order for as long as it likes, paying only the swarm's fee per cycle. For an ordinary employer this is the documented limit W3 in docs/THREATS.md (the order reopens or the employer opens another). For CrowdPots it breaks what the contract tells contributors: src/pots/CrowdPots.sol lines 73-74 ('The bond is all a runner loses by taking the order and not delivering. A larger bond makes that dearer'), 118-120 (the bond 'is forfeited to the pot ... if the runner lets the order time out') and 139-142 ('On a receipt-only pot the same stall costs the whole bond and the order is open again afterwards'), repeated in docs/THREATS.md C2 and W5. A pot hires once, and the bond is the one lever contributors are told sizes a stall. With this path a stranger ends any receipt-only pot in a refund with the bond returned in full, whatever bond the opener chose, at a cost of gas plus the swarm's job fee, and contributors' IMD has been locked for the funding time, the hire window and part of the delivery time with nothing hired. No IMD is lost, so this is a broken guarantee rather than a loss: medium. The desk-level choice is documented in the reportFailed comment (charging a runner for the swarm's failure is the trade-off the authors weighed), so the scope decision is theirs: either keep the release share for the employer and/or set barred on the SwarmFailed and wrong-kind endings (so neither is ever cheaper than release, the rule L3 already states for challenged claims), or correct the three CrowdPots statements and C2/W5 to say that a receipt-only pot can be held past its hire window and refunded with no bond forfeited.","reproduction":"Ran as test/scratch/SwarmFailed.scratch.t.sol deriving the project's PotsRealDeskTest (real WorkOrders, WorkProof, CrowdPots, Doorbell; stand-in IMD, registry, Payroll), both tests pass on the tree as it is. Case 1 (blocked receipt): pot terms target 10e18+3, minPay 4e18, rampSecs 1 days, bond 1e18, deliverySecs 3 days, hireSecs 30 days, allowClaimed false; alice chips 6e18, bob the rest, launch -> orderId. warp(orderExpiresAt - 2 days); runner: claim(orderId) (posts 1e18), register(orderId, JOB). Registry receives a receipt for JOB whose objective is headerOf(orderId) ++ SPEC and whose state is 'blocked' (the project's BLOCKED_FIXTURE with job id, kind and objective swapped). alice calls reportFailed(orderId, record, []) -> reverts NotRunner. warp(orderExpiresAt + 1), still before registeredAt + 3 days. runner calls reportFailed(orderId, record, []). Expected per CrowdPots' stated rules: the stall costs the runner its whole bond (1e18 to the pot) and the order is open again. Actual: stateOf(orderId) == Refunded, imd.balanceOf(runner) == its starting balance (ClaimEnded reason SwarmFailed, toRunner 1e18, toEmployer 0), accountOf(orderId).returned == 10e18+3 exactly (the escrow, no bond), pots.reconcile(potId) == Refundable: the pot is over with nothing hired and nothing forfeited. Case 2 (wrong kind, no failed job): same pot; three rounds of {runner claim + register(job_i); registry receives a completed receipt with the right objective and kind 'skill:create-image' (the pot's kind is 'skill:research-report'); alice calls reportFailed}. After each round: state Open, runner balance unchanged, barred(orderId, runner) == false, and the runner claims again at once; returned stays 0 and the pot stays Hiring.","snippet":"        } else if (sameObjective) {\n            if (!completed && msg.sender != c.runner) revert NotRunner();\n            _endClaim(id, o, c, End.SwarmFailed, bond, 0, 0);","citation":"resolved","blocking":true},{"severity":"info","title":"Trust assumption the manifest turns on: with ALLOW_CLAIMED = 1 and no arbiter, each Doorbell escrow is paid to an unchallenged bonded claim, and a correct challenger's best outcome is its own bond bac","path":"src/pots/Doorbell.sol","line":249,"description":"Not a code defect: merged from audit_economics and audit_flow (same root cause, same numbers). The Doorbell's notes (lines 30-38), docs/THREATS.md D2, L2 and L6 state the rule, and the brief fixes the eighth constructor argument at 1. Recorded as the one trust assumption the mainnet manifest turns on, with its cost at the manifest's values (MIN_PAY = MAX_PAY = 1e18, BOND = 0.25e18 = the desk minimum, DELIVERY_SECS = 604800, ALLOW_CLAIMED = 1). The Doorbell's orders carry no arbiter and no runnerOnly, the Doorbell never challenges, and ringing is free apart from gas with no rate limit, so with balance B anyone can open floor(B / 1e18) orders in one transaction (25 rings each). A runner that takes one, registers any nonzero job id and calls claimDelivered is paid 0.9e18 plus its bond 24 hours later unless somebody posts an equal bond. A correct challenge of such a claim can never be decided early (no receipt will ever exist, no arbiter): the challenger's 0.25e18 is locked until registeredAt + 7 days, when timeout returns exactly its bond (DisputeExpired at WorkOrders.sol:472 pays the challenger `bond`), the runner gets 0.225e18 back and the Doorbell 1.025e18. So each undefended order costs the Doorbell 1e18, each defended attempt costs the attacker 0.025e18 and the defender 0.25e18 of float for a week with no reward, and a wave can be repeated every 7 days while the Doorbell holds at least 1e18. This is what the documents say; it is listed so the requester sizes the keeper float the docs call for.","reproduction":"Code trace against the manifest's values. (1) Anyone transfers 1e18 IMD to the Doorbell. (2) Attacker calls ring() 25 times; the 25th opens order N on WorkOrders with escrow 1e18, flags FLAG_CALLBACK | FLAG_ALLOW_CLAIMED (= 3), arbiter 0, expiresAt now + 2 days. (3) Attacker approves 0.25e18 and calls claim(N) (price fixed at 1e18 since MIN_PAY == MAX_PAY), register(N, 0x00000000000000000000000000000001), claimDelivered(N, bytes32(uint256(1))) in the same block: until = now + 24h, until + 24h <= registeredAt + 7d, accepted. (4) 24 hours later anyone calls finalize(N): _deliver pays the attacker runnerPay 0.9e18 plus its 0.25e18 bond, 0.05e18 fee and 0.05e18 tip go to Payroll, unused escrow is 0, and Doorbell.onWorkDelivered records jobId 0x..01 and recordHash 0x..01 as a delivery. Expected by the documents: exactly this. If instead a defender calls challenge(N) with 0.25e18 inside the window, then after registeredAt + 7 days timeout(N) sends 0.25e18 to the defender, 0.225e18 to the attacker and 1.025e18 to the Doorbell: the defender nets zero, the attacker loses 0.025e18, and the next 25th ring can be attacked again. The project's own test test_realDesk_theDoorbellHires_andAnUnchallengedClaimIsReportedBack shows step (4) with a 2e18 Doorbell.","snippet":"        if (ALLOW_CLAIMED) flags |= WORK_ORDERS.FLAG_ALLOW_CLAIMED();","citation":"resolved","blocking":false},{"severity":"info","title":"chipInFor accepts the desk, IMD, Payroll and WorkProof as beneficiary; a refund claimed for such a chip is then stranded in a contract that keeps no books of it","path":"src/pots/CrowdPots.sol","line":726,"description":"Reproduced from audit_flow. BadBeneficiary excludes only the zero address and CrowdPots itself. A chip for address(WORK_ORDERS) is accepted, and claimRefund(potId, address(WORK_ORDERS)) later pushes IMD into the desk, which only ever sends amounts it booked for an order (_send is called with escrow, bond or a share of one, never with a balance), so the IMD can never leave. Only the payer's own money is at stake and docs/DEVIATIONS.md D7 says that naming a beneficiary that cannot use IMD gives the IMD away, so this is a note, not a defect. The check could cheaply also refuse address(WORK_ORDERS) and address(IMD), the two addresses the contract already holds.","reproduction":"openPot(terms with target 1e18, fundSecs 1 hours, hireSecs 1 days, deliverySecs 3 days, bond 0.25e18, minPay 1e18, rampSecs 0, kind 'audit', allowClaimed false), spec 'x', firstChip 0 -> potId P. Caller approves 1e18 and calls chipInFor(P, 1e18, address(WORK_ORDERS)): accepted (the only refusals at line 726 are address(0) and address(this)), raised = 1e18, PotFilled. Nobody launches; warp past deadline + LAUNCH_GRACE. claimRefund(P, address(WORK_ORDERS)) returns 1e18 and transfers 1e18 IMD to the WorkOrders contract. Expected: the chip is refused as BadBeneficiary like a chip for CrowdPots itself. Actual: 1e18 IMD sits in WorkOrders with no order, no escrow entry and no function that can move it.","snippet":"        if (beneficiary == address(0) || beneficiary == address(this)) revert BadBeneficiary();","citation":"resolved","blocking":false}],"coverage":{"rows":[{"target":"CrowdPots.chipIn(uint256,uint256)","verdict":"holds","why":"takes min(amount, remaining), books raised/owed before the pull, series id routes to the current round, no revert on a full or ended pot"},{"target":"CrowdPots.chipInFor(uint256,uint256,address)","verdict":"finding","findingRef":3,"why":"same as chipIn; beneficiary check refuses only zero and self (info 3)"},{"target":"CrowdPots.claimRefund(uint256,address)","verdict":"holds","why":"reconciles first (desk refund/evict/timeout inside try), WrongStage unless Delivered/Refundable, pro rata rounds down, last claimer drains the pool, owed decremented; open to anyone by design (D7)"},{"target":"CrowdPots.claimRefunds(uint256,address[])","verdict":"holds","why":"loop over the caller's list, duplicates and strangers skipped by claimed/amount checks"},{"target":"CrowdPots.launch(uint256,bytes)","verdict":"holds","why":"Funding, full, within grace, spec hash matches; owed reduced then exact allowance consumed, employer and fresh order id checked; no arbiter, runnerOnly or condition"},{"target":"CrowdPots.onWorkDelivered(uint256,bytes16,bytes32)","verdict":"holds","why":"desk only, known order only, records ids and moves no money"},{"target":"CrowdPots.openPot((string,uint96,uint96,uint32,uint96,uint32,uint32,uint32,bool),bytes,uint256)","verdict":"holds","why":"bounds on every term, desk token/minimums read, spec checked with the desk's own rule, opener gets no rights"},{"target":"CrowdPots.openSeries((string,uint96,uint96,uint32,uint96,uint32,uint32,uint32,bool),bytes)","verdict":"holds","why":"same pin, stage Series holds no money; launch/claimRefund on the series id revert WrongStage"},{"target":"CrowdPots.reconcile(uint256)","verdict":"holds","why":"ends what ran out on the desk, then books pool = returned from accountOf with DeskShortfall guard; every desk credit is pushed in the same call so the guard cannot fire by itself"},{"target":"Doorbell.onWorkDelivered(uint256,bytes16,bytes32)","verdict":"holds","why":"desk only, own order only, once per order, moves no money"},{"target":"Doorbell.ring()","verdict":"holds","why":"counts every ring, hires on each 25th with balance >= MAX_PAY, exact allowance consumed and employer checked; the claimed-path exposure it opens is info 2, a documented trust assumption"},{"target":"WorkOrders.cancel(uint256)","verdict":"holds","why":"employer only, Open only, escrow zeroed and credited before the transfer"},{"target":"WorkOrders.challenge(uint256)","verdict":"holds","why":"Asserted only, inside the window, first challenger wins, bond pulled from msg.sender after state"},{"target":"WorkOrders.claim(uint256)","verdict":"holds","why":"Open, not expired, runnerOnly/barred/condition checks, price fixed, BooksFull bound, bond pulled last; barred is not set by the SwarmFailed ending (see finding 1 on reportFailed)"},{"target":"WorkOrders.claimDelivered(uint256,bytes32)","verdict":"holds","why":"runner only, Registered only, flag required, 48h must remain before the deadline"},{"target":"WorkOrders.createOrder((bytes32,uint96,uint96,uint32,uint32,uint40,uint96,uint8,address,address,address,bytes4,bytes32,uint8,uint256),bytes)","verdict":"holds","why":"spec, pay range, delivery 3-90 days, bond floor, expiry, flags and condition validated; maxPay pulled from msg.sender"},{"target":"WorkOrders.evict(uint256)","verdict":"holds","why":"Claimed only, after CLAIM_WINDOW, whole bond to the employer, order reopens or refunds"},{"target":"WorkOrders.finalize(uint256)","verdict":"holds","why":"Asserted only, after the window, pays as settle with proven=false, callback outside the lock"},{"target":"WorkOrders.redeliver(uint256)","verdict":"holds","why":"Delivered, callback flag, not yet notified, refuses reentrant entry, no money"},{"target":"WorkOrders.refund(uint256)","verdict":"holds","why":"Open only, after expiresAt, escrow credited then sent to the employer"},{"target":"WorkOrders.register(uint256,bytes16)","verdict":"holds","why":"runner only, Claimed/Registered, nonzero and unused job id, registeredAt set once"},{"target":"WorkOrders.release(uint256)","verdict":"holds","why":"runner only, inside the claim window, bars the runner, release share to the employer"},{"target":"WorkOrders.reportFailed(uint256,bytes,address[])","verdict":"finding","findingRef":1,"why":"SwarmFailed/wrong-kind endings return the whole bond, never bar, and refund the order for good when expired"},{"target":"WorkOrders.resolve(uint256,bool)","verdict":"holds","why":"arbiter only (nobody on pot and Doorbell orders), Disputed only, delivered pays with both bonds, else refute"},{"target":"WorkOrders.settle(uint256,bytes,address[])","verdict":"holds","why":"Registered..Disputed, WorkProof receipt must be completed, right kind, this claim's header and spec, job unused; fee/tip rounded up, Payroll offered fixed gas, callback last"},{"target":"WorkOrders.stamp(uint256,bytes,address[])","verdict":"holds","why":"Delivered and unproven only, same receipt checks as settle, no money"},{"target":"WorkOrders.timeout(uint256)","verdict":"holds","why":"Registered/Disputed after the deadline; whole bond to employer, or DisputeExpired with release share and refund"},{"target":"WorkOrders.trigger(uint256)","verdict":"holds","why":"Open and untriggered only, full CONDITION_GAS required, one true reading latches"},{"target":"launch.json manifest","verdict":"holds","why":"kind evm_contracts, four contracts in dependency order, argument counts match constructors, even-length hex, kindHash == keccak256('skill:create-image'), Doorbell values pass its constructor checks; IMD (symbol IMD), WorkRegistry (latest(e9efec16..) returns the fixture hash) and Payroll have code on"}],"summary":{"reported":true,"total":28,"answered":28,"ratio":1,"missing":[],"unreached":[]}}},{"hash":"5622411fe008f23688abd735acb5e79ec5027cb49d98fa42ea931ab676357291","nodeKey":"write_foundry_tests","role":"tests","attempt":1,"deviceKey":"a7ac9f7a7687c25ead29841d9c4e1344e6c1ac0523fc26ad7d1e3c15f948f2c2","seat":{"tokenId":"563","agentId":"53924"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":55,"runtime":"claude","inputTokens":1186,"wallClockMs":1271789,"outputTokens":48281,"cachedInputTokens":6738975},"baseCommit":"94fb4730e1a8a641358516b3d8550e6a72254a8a","startCommit":"0ca93eac1499330d10e756dd98b055a3507e6170","resultTreeHash":"70ddb8ad6d001c7c48de514c95c36bc803f3162e","bundleHash":"fa73aa8665748ba4d9e1418209446c4a16859220f6d04ad1aa2e642ee4f9ede4","bundleBytes":17914,"artifacts":[],"changedPaths":["test/launch/LaunchFloor.t.sol","test/launch/LaunchSystem.invariant.t.sol"],"allowedPaths":["test","test/**"],"dependencyBundleHashes":["d2a63b4814f0a662cc31bd080327f5e80954bff826929e769a75e1dddc640be3"],"verificationErrors":0,"summary":"Done. The two new test files are formatted and committed, and the full suite passes.\n\n**What I found.** The repository already carried a deep suite (654 tests, per-contract invariants for the desk, CrowdPots and the Doorbell, constructor failure paths for all four contracts). No defect surfaced in any test I wrote, so there is no findings file. What was missing was coverage of the launch itself as the pipeline will run it, and an invariant run over the four contracts working together on the manifest's exact bytes.\n\n**What I added**, both under `test/launch/`:\n\n- **LaunchFloor.t.sol** (14 tests). Rehearses the protected harness without environment variables: a probe etched at the real ProjectFactory address, chain id 1, CREATE2 with each `$contract:` reference resolved to the address predicted *before* deployment, then the harness's own opcode walk, EIP-170 and EIP-3860 limits, and a check that the factory holds no privilege afterwards. The failure paths it pins: one wei sent to any of the four nonpayable constructors, a bool written as 2, a zero address in any slot of the desk, the pots or the Doorbell, the Doorbell's arguments in the wrong order, a forward reference, a reused salt, and a stranger using the probe. It also writes down the one slot the floor cannot catch: WorkProof accepts a zero registry address, which is documented as failing safe.\n- **LaunchSystem.invariant.t.sol**. Deploys the four contracts from the manifest in `docs/LAUNCH.md` with stand-ins etched at the three mainnet addresses, then drives everything at once: Doorbell funding and rings, pots opening, filling, launching, reconciling and refunding, wallet orders, and every desk transition on every order. Seven invariants hold after each of 5,760 random calls with zero reverts: the desk holds exactly its live escrows and bonds, IMD is conserved across all parties, the Doorbell's money leaves only as its own escrow, CrowdPots covers what it owes and pays only the desk or contributors, no allowance is left behind, WorkProof holds nothing, and every order's employer matches the model. A fixed 2,500-step soak proves the random runs are not vacuous: it asserts hires, pot launches, wallet orders, and all twelve desk paths including challenge and dispute endings were reached.\n\n**Verification.** `forge test` passes: 670 tests across 32 suites. The commit on the working tree is `d916013`, touching only the two files above.","createdAt":"2026-10-13T14:33:01.362Z","verdict":{"status":"accepted","profile":"foundry","evaluation":"checks","rejectionCode":null,"detail":"all checks passed","verifierVersion":"0.1.0+327e1e9f","verifiedTreeHash":"70ddb8ad6d001c7c48de514c95c36bc803f3162e","at":"2026-10-13T14:38:28.895Z","failedChecks":[]},"findings":[],"coverage":null},{"hash":"4163f2fee917a76226b9f284789e44dacba19c1903f868fa2722ee1b547e9a91","nodeKey":"audit_permissions","role":"review","attempt":1,"deviceKey":"739094f0e08668ee6fa7e7cc35af2027375a89a94c2040e9f5433d2de88c4b0d","seat":{"tokenId":"963","agentId":"51534"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-opus-5-5","turns":22,"runtime":"claude","inputTokens":38,"wallClockMs":603760,"outputTokens":39105,"cachedInputTokens":2403223},"baseCommit":"94fb4730e1a8a641358516b3d8550e6a72254a8a","startCommit":"0ca93eac1499330d10e756dd98b055a3507e6170","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":["d2a63b4814f0a662cc31bd080327f5e80954bff826929e769a75e1dddc640be3"],"verificationErrors":0,"summary":"The review is written to `.imd-findings.json`: one medium finding and a coverage row for each of the 28 entry points. No repository file was changed, and the scratch test I used is removed.\n\n## Finding (medium)\n\n**A runner can hold a receipt-only order past its expiry and leave with its whole bond** (`src/desk/WorkOrders.sol:441`, the `SwarmFailed` branch of `reportFailed`).\n\n- **What happens:** every other exit without delivery costs the runner bond (`release` 10% and a bar, `evict` and `timeout` the whole bond). This branch returns the whole bond and does not bar the runner. The only time limit on it is the delivery deadline.\n- **Why the runner controls it:** the branch checks the objective but not the kind, and the runner picks the job's template. When the receipt says anything but `completed`, only the runner may submit it, so the runner alone decides when the claim ends.\n- **Why it matters for pots:** a pot hires once. `CrowdPots.sol` and THREATS C2/W5 tell contributors that a stall on a receipt-only pot costs the whole bond and the order reopens. Instead, any such pot can be ended in a refund with nothing forfeited, for gas and the swarm's 0.5 IMD job fee.\n- **Precondition:** the registry must write a receipt for a job that did not complete, or one of another kind.\n- **Reproduction:** I ran it on the real desk, WorkProof and CrowdPots with the repository's own harness.\n  1. The runner claims and registers two days before the order expires.\n  2. A `blocked` receipt with the right objective lands; anyone else's `reportFailed` reverts `NotRunner`.\n  3. The runner reports after expiry. The order is `Refunded`, the runner's balance is whole, `returned` equals the target exactly, and the pot is `Refundable`.\n- **Variant:** with a completed receipt of another kind, the same address took the order three times in a row and got its bond back each time.\n\nNo funds are lost, only locked and returned; I rated it medium because it breaks a stated guarantee. THREATS W3 already records the desk-level behaviour, but not its consequence for pots. The fix I suggest is to keep the release share and/or bar the runner on this ending. That charges a runner whose job the swarm really failed, which is your decision; otherwise the statements in `CrowdPots.sol` and C2/W5 need correcting.\n\n## Coverage\n\n- **Holds:** 26 of the 28 entry points. I traced each for who may call it, where the money goes, and whether its time boundary matches its paired function.\n- **Finding:** `WorkOrders.reportFailed` and `CrowdPots.reconcile` both point to the finding above.\n- **Left as asked:** `claimRefund` and `claimRefunds` stay open to anyone; the money goes only to the chip's owner.\n- **Manifest:** argument order and count match each constructor, and the Doorbell kind hash is `keccak256(\"skill:create-image\")`. On mainnet today, IMD, the WorkRegistry and Payroll all have code, and `Payroll.IMD()` returns the manifest's IMD.\n- **Not reached:** WorkProof and JsonScan parsing — whether a job participant can plant `kind`, `objective` or `state`. It is outside the permissions area; I traced only its caller surface.\n\nI did not run the full test suite or the fork tests; only the real-desk pot tests ran, alongside my scratch test, and passed.","createdAt":"2026-10-13T14:15:41.595Z","verdict":null,"findings":[{"severity":"medium","title":"A runner can hold a receipt-only order past its expiry and leave with its whole bond, so a crowd pot's single hire can be ended for the swarm's fee alone","path":"src/desk/WorkOrders.sol","line":441,"description":"Seam: access x economics x asymmetry. Every other way a runner leaves an order without delivering costs it bond: release() costs the release share (10%) and bars the address, evict() and timeout() cost the whole bond. The SwarmFailed branch of reportFailed returns the whole bond, does not bar the runner, and has no time limit other than the delivery deadline. Two things make that exit the runner's to choose rather than the swarm's: (1) the branch looks only at the objective, not at the kind, and the runner alone picks the template the job is opened with; (2) when the receipt says anything but completed, only the runner may submit it (NotRunner for everyone else), so the runner alone decides when the claim ends, up to registeredAt + deliverySecs. _endClaim then calls _close, which refunds the order for good when block.timestamp >= expiresAt.\n\nFor an ordinary employer this is the limit written down as W3 in docs/THREATS.md (the order reopens or is refunded and the employer can open another). For CrowdPots it breaks what the contract tells contributors. A pot hires once; src/pots/CrowdPots.sol says 'The bond is all a runner loses by taking the order and not delivering', that the bond 'is forfeited to the pot ... if the runner lets the order time out', and that 'On a receipt-only pot the same stall costs the whole bond and the order is open again afterwards' (docs/THREATS.md C2 repeats it, W5 says a silent hold 'costs the runner its bond'). With this branch a stranger can end any receipt-only pot in a refund with the bond returned in full, whatever size the opener chose for it: contributors' IMD has then been locked for the funding time, the hire window and part of the delivery time, and nothing was hired. The cost to the stranger is gas and the swarm's 0.5 IMD job fee. It needs the registry to write a receipt for a job that did not end completed (the live registry holds 'blocked' receipts, per WorkProof's notes). The wrong-kind variant (objective matches, completed, another kind) needs no failed job at all: the bond comes back each time and the same address may claim again at once, since only release() sets barred.\n\nPossible fixes that keep the design: have the SwarmFailed ending keep the release share for the employer and/or set barred[id][runner], so that it is never cheaper than release (the rule L3 already states for challenged claims). Charging the runner of a job the swarm really failed is the tradeoff the authors weighed in the reportFailed comment, so that is a scope decision for them. If the code stays as it is, the three statements in CrowdPots.sol and C2/W5 in docs/THREATS.md should say that a receipt-only pot can be held past its hire window and refunded with no bond forfeited.","reproduction":"Setup as test/pots/PotsRealDesk.t.sol (real WorkOrders, WorkProof, CrowdPots; stand-in IMD, registry, Payroll). Pot terms: target 10e18+3, minPay 4e18, rampSecs 1 days, bond 1e18, deliverySecs 3 days, hireSecs 30 days, allowClaimed false. alice chips 6e18, bob the rest, launch.\n1. warp to orderExpiresAt - 2 days. runner: desk.claim(orderId) (posts 1e18 bond), desk.register(orderId, JOB).\n2. Registry receives a receipt for JOB whose objective is headerOf(orderId) ++ SPEC and whose state is \"blocked\" (the runner opened the job with the right text under a template that does not complete).\n3. Anyone else calling desk.reportFailed(orderId, record, []) reverts NotRunner, so nobody can free the order.\n4. warp to orderExpiresAt + 1 (still a day before the delivery deadline). runner: desk.reportFailed(orderId, record, []).\nExpected from the pot's stated rules: a stall on a receipt-only pot costs the runner its whole bond (1e18 to the pot) and the order is open again. Actual: stateOf(orderId) == Refunded, imd.balanceOf(runner) is back to its starting balance (ClaimEnded reason SwarmFailed, toRunner = 1e18, toEmployer = 0), accountOf(orderId).returned == target exactly, and pots.reconcile(potId) == Refundable: the pot is over with nothing hired and nothing forfeited. I ran this as a scratch test deriving PotsRealDeskTest and it passed with exactly these assertions.\nVariant (no failed job needed): land a receipt with the right objective, state \"completed\" and kind \"skill:create-image\" on a pot whose kind is another; anyone's reportFailed returns the whole bond and the same runner address can claim(orderId) again immediately. Three rounds in a row, 2 days apart, left the runner's balance unchanged each time (also run).","snippet":"        } else if (sameObjective) {\n            if (!completed && msg.sender != c.runner) revert NotRunner();\n            _endClaim(id, o, c, End.SwarmFailed, bond, 0, 0);","citation":"resolved","blocking":true}],"coverage":{"rows":[{"target":"CrowdPots.chipIn(uint256,uint256)","verdict":"holds","why":"Open by design; takes min(amount, remaining) from msg.sender only, chip booked to msg.sender, no path credits more than was pulled; deadline boundary matches _peek and launch."},{"target":"CrowdPots.chipInFor(uint256,uint256,address)","verdict":"holds","why":"Payer is always msg.sender; beneficiary only gains a claim; zero address and the contract itself refused; no approval of a third party can be spent."},{"target":"CrowdPots.claimRefund(uint256,address)","verdict":"holds","why":"Open to anyone on purpose (D7, left as asked); money goes only to the chip's owner, claimed flag set before transfer, share bounded by pool; lock held."},{"target":"CrowdPots.claimRefunds(uint256,address[])","verdict":"holds","why":"Same path as claimRefund per entry; duplicates and non-contributors pay zero; loop is over the caller's own list."},{"target":"CrowdPots.launch(uint256,bytes)","verdict":"holds","why":"Open to anyone; order built from stored terms only (no runnerOnly, arbiter or condition), spec must hash to the pinned one, allowance is exactly the target and checked spent, employer re-read from the desk."},{"target":"CrowdPots.onWorkDelivered(uint256,bytes16,bytes32)","verdict":"holds","why":"msg.sender must be the immutable desk and the order must be a pot's; writes two informational fields, moves no money; the desk calls it once per order on success."},{"target":"CrowdPots.openPot((string,uint96,uint96,uint32,uint96,uint32,uint32,uint32,bool),bytes,uint256)","verdict":"holds","why":"Opener gets no rights; every term bounded and published; first chip is pulled from the opener itself."},{"target":"CrowdPots.openSeries((string,uint96,uint96,uint32,uint96,uint32,uint32,uint32,bool),bytes)","verdict":"holds","why":"No lock but no money and no untrusted call; a series id cannot be launched, reconciled into a payout or refunded (WrongStage)."},{"target":"CrowdPots.reconcile(uint256)","verdict":"finding","findingRef":1,"why":"Access and bookkeeping hold (books read once from accountOf, DeskShortfall stop); but the pot's one hire can be ended in a refund with no bond forfeited, see finding 1."},{"target":"Doorbell.onWorkDelivered(uint256,bytes16,bytes32)","verdict":"holds","why":"Only the immutable desk, only for an order the Doorbell recorded, once per order; moves no money and fits the 150,000 gas allowance."},{"target":"Doorbell.ring()","verdict":"holds","why":"Open by design; order parameters are immutables and constants, allowance exactly MAX_PAY and checked spent; a refused order reverts the ring; nothing a ringer supplies reaches the order."},{"target":"WorkOrders.cancel(uint256)","verdict":"holds","why":"Employer only and only while Open; a held order cannot be cancelled, so a runner's work cannot be pulled from under it."},{"target":"WorkOrders.challenge(uint256)","verdict":"holds","why":"Open once per claim inside the window (boundary mirrors finalize); challenger is msg.sender and posts its own bond; payouts to it are fixed by _refute / timeout."},{"target":"WorkOrders.claim(uint256)","verdict":"holds","why":"runnerOnly, barred, expiry and condition checked; claim record fully overwritten so no stale challenger or job id carries over; ticket binds chain, desk, order, seq and runner."},{"target":"WorkOrders.claimDelivered(uint256,bytes32)","verdict":"holds","why":"Runner only, flag required, Registered only, 48h must remain; cannot be repeated or followed by register."},{"target":"WorkOrders.createOrder((bytes32,uint96,uint96,uint32,uint32,uint40,uint96,uint8,address,address,address,bytes4,bytes32,uint8,uint256),bytes)","verdict":"holds","why":"Employer is msg.sender and funds its own escrow; flags and condition fields validated; arbiter and runnerOnly are the employer's published choices (trust assumptions W10, L6)."},{"target":"WorkOrders.evict(uint256)","verdict":"holds","why":"Open after the claim window only (boundary mirrors release); whole bond to the employer, recipient not caller-chosen."},{"target":"WorkOrders.finalize(uint256)","verdict":"holds","why":"Open after the window; pays the claim's runner fixed amounts; tip goes agentless (documented W7); callback after the lock is released with full gas check."},{"target":"WorkOrders.redeliver(uint256)","verdict":"holds","why":"Refused inside any locked call, only for a Delivered order with a failed callback; notified set before the call; moves no money."},{"target":"WorkOrders.refund(uint256)","verdict":"holds","why":"Open only for an Open order at or after expiry (mirrors claim's Expired check); escrow goes to the stored employer."},{"target":"WorkOrders.register(uint256,bytes16)","verdict":"holds","why":"Runner only; reserves nothing so a copied job id blocks nobody; delivery clock not restarted by a retry."},{"target":"WorkOrders.release(uint256)","verdict":"holds","why":"Runner only inside the claim window, costs the release share and bars the address."},{"target":"WorkOrders.reportFailed(uint256,bytes,address[])","verdict":"finding","findingRef":1,"why":"Proof and payee logic hold, but the SwarmFailed branch is a bond-free, unbarred exit whose timing the runner alone controls: finding 1."},{"target":"WorkOrders.resolve(uint256,bool)","verdict":"holds","why":"Only the order's arbiter and only when Disputed; an order with no arbiter can never be resolved (msg.sender is never zero); pots and the Doorbell name none."},{"target":"WorkOrders.settle(uint256,bytes,address[])","verdict":"holds","why":"Open to anyone, pays only the claim's runner; header binds the receipt to this claim; JobUsed stops reuse; Payroll offered its full gas and limited to fee+tip allowance, leftover zeroed."},{"target":"WorkOrders.stamp(uint256,bytes,address[])","verdict":"holds","why":"Open, moves no money, same receipt checks as settle; cannot be used to squat another order's job (header must match this order's claim)."},{"target":"WorkOrders.timeout(uint256)","verdict":"holds","why":"Open only after the delivery deadline, only Registered or Disputed; recipients and amounts fixed; an unchallenged claim is not touched."},{"target":"WorkOrders.trigger(uint256)","verdict":"holds","why":"Open; latches only on a true reading taken with the whole condition gas; target is static-called with capped gas and a 32-byte return copy."},{"target":"invariant: no owner, admin, setter, initializer or delegatecall in any of the four contracts","verdict":"holds","why":"Constructors store immutables only and never read msg.sender or tx.origin; no storage variable has a privileged writer."},{"target":"invariant: no standing third-party allowance","verdict":"holds","why":"Desk approves Payroll exactly fee+tip and zeroes what is left; CrowdPots and Doorbell approve the desk exactly one escrow and check it was spent."},{"target":"invariant: manifest constructor arguments","verdict":"holds","why":"Order and count match each constructor; Doorbell kind hash equals keccak256('skill:create-image'); IMD, WorkRegistry and Payroll have code on mainnet and Payroll.IMD() is the manifest's IMD (checked by RPC on 2026-10-13)."},{"target":"WorkProof / JsonScan parsing (can a job participant plant kind, objective or state)","verdict":"unreached","why":"Outside the permissions area; only the caller surface (view, stateless, registry call gas-capped and hash-checked) was traced."}],"summary":{"reported":true,"total":28,"answered":28,"ratio":1,"missing":[],"unreached":[]}}},{"hash":"f176e96da233112a430790d2694c0ef6a94d2251f12887ee3cb81a749fbea817","nodeKey":"audit_math","role":"review","attempt":1,"deviceKey":"03f15d1296244279ebdd0e54df271264fe51f911902957fe042ff85c368f0173","seat":{"tokenId":"1850","agentId":"51432"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":30,"runtime":"claude","inputTokens":356,"wallClockMs":1228226,"outputTokens":37931,"cachedInputTokens":1423684},"baseCommit":"94fb4730e1a8a641358516b3d8550e6a72254a8a","startCommit":"0ca93eac1499330d10e756dd98b055a3507e6170","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":["d2a63b4814f0a662cc31bd080327f5e80954bff826929e769a75e1dddc640be3"],"verificationErrors":0,"summary":"The review is complete and the findings file is written.\n\n**Outcome:** no substantiated defect in the assigned area. The findings list in `.imd-findings.json` is empty, with 34 coverage rows: one for each of the 28 listed entry points plus the invariants I checked. No source file was changed.\n\n**What I covered, against the Math Precision, Boundary and Numerical Gap guides:**\n\n- **Fee split in the desk.** Fee and tip each round up to the next wei, the tip is capped so the runner's pay never underflows, and runner plus Payroll plus returned escrow equals the escrow exactly. The project's own tests hit prices 0, 1, 2, 19, 20, 21, 39 and 41, and a fuzz run confirms the sum.\n- **Price ramp.** The linear formula cannot overflow, rounds down by under one wei, handles a zero ramp and an untriggered condition correctly, and the ramp pause on reopen can never push the start past the present.\n- **Bond splits.** Release, evict, timeout, refute and arbitration all conserve escrow plus the one or two bonds held. Every transfer to an employer is paired with one credit, so the returned figure CrowdPots relies on is exact, and the BooksFull guard keeps the uint96 casts safe.\n- **Pot refunds.** The pro rata share rounds down, the rate is non-decreasing across claims, the last claimer empties the pool, and chipsLeft is always at least the chip so there is no division by zero. The chip and launch deadlines use complementary comparisons, so no second is both chippable and refundable.\n- **Time boundaries.** Claim and refund, release and evict, challenge and finalize, and the delivery deadline versus claimDelivered are all complementary at the exact second.\n- **Doorbell constants and manifest values.** The constructor bounds mirror the desk's minimums and the manifest arguments pass them. The kind hash matches keccak256 of the create-image skill name.\n\n**Checks run:** the full non-fork suite, 790 tests including fuzz and invariant runs, all passing.\n\n**Not reached:** I read the JsonScan lane arithmetic and beyond-buffer read paths without finding a counterexample but did not fuzz them independently, and I did not run the fork tests against live mainnet contracts. Both are marked `unreached` in the coverage record.","createdAt":"2026-10-13T14:03:11.433Z","verdict":null,"findings":[],"coverage":{"rows":[{"target":"CrowdPots.chipIn(uint256,uint256)","verdict":"holds","why":"taken = min(amount, target - raised) so the uint96 cast and raised += chip never exceed target; owed += taken matches the pull; chips accepted at ts == deadline and refused at deadline+1, the same boundary _peek uses; fuzz test chipsAreCutAndAddUp passes."},{"target":"CrowdPots.chipInFor(uint256,uint256,address)","verdict":"holds","why":"Same _chip path as chipIn; beneficiary 0 / self rejected; series id routes to _round which opens a new round only when _remaining == 0."},{"target":"CrowdPots.claimRefund(uint256,address)","verdict":"holds","why":"share = pool*chip/chipsLeft rounds down; chipsLeft >= chip always (chipsLeft = raised at settle, minus each paid chip) so no div-by-zero and share <= pool; pool/chipsLeft is non-decreasing across claims so each claimer gets >= floor(fair share) and the last claim empties the pool to the wei; owed -="},{"target":"CrowdPots.claimRefunds(uint256,address[])","verdict":"holds","why":"Loop over the caller's list calling the same _pay; repeated or unknown addresses pay 0; sum equals the individual payments."},{"target":"CrowdPots.launch(uint256,bytes)","verdict":"holds","why":"Requires raised == target exactly; allowed through ts == deadline+LAUNCH_GRACE, complementary to _peek's > test; owed -= target before the desk pulls exactly target (allowance left must be 0); expiresAt = uint40(now)+hireSecs is a checked uint40 add far from overflow; rampSecs <= hireSecs enforced a"},{"target":"CrowdPots.onWorkDelivered(uint256,bytes16,bytes32)","verdict":"holds","why":"No arithmetic; records jobId/recordHash for a known order from the desk only."},{"target":"CrowdPots.openPot((string,uint96,uint96,uint32,uint96,uint32,uint32,uint32,bool),bytes,uint256)","verdict":"holds","why":"Bounds checked at every edge: target >= 1e18, 1 <= minPay <= target, rampSecs == 0 iff minPay == target else 1..hireSecs, MIN_BOND <= bond <= target, MIN_DELIVERY <= deliverySecs <= 30d, hireSecs 1..30d, fundSecs 1h..30d, kind 1..64 bytes under SpecText; deadline = uint40(now)+fundSecs; firstChip go"},{"target":"CrowdPots.openSeries((string,uint96,uint96,uint32,uint96,uint32,uint32,uint32,bool),bytes)","verdict":"holds","why":"Same _pin bounds; holds no money; rounds re-use the validated terms via _termsOf."},{"target":"CrowdPots.reconcile(uint256)","verdict":"holds","why":"Funding -> Refundable at ts > deadline (unfilled) or > deadline+grace (full) with pool = raised already counted in owed; Hiring -> Delivered/Refundable with pool = desk.returned (uint96) and owed += pool checked against balance (DeskShortfall); every desk _credit has a matching _send so returned is "},{"target":"Doorbell.onWorkDelivered(uint256,bytes16,bytes32)","verdict":"holds","why":"Counters only; desk sets notified before the call so AlreadyDelivered cannot be hit by redeliver."},{"target":"Doorbell.ring()","verdict":"holds","why":"count % 25 on a uint64; hires only when balance >= MAX_PAY; expiresAt = uint40 + 2 days checked add; allowance must be fully consumed; constructor bounds (minPay 1..maxPay, bond >= 0.25e18, delivery 3..30 days) match the desk's constants and the manifest values 1e18/1e18/0.25e18/604800 pass them; KI"},{"target":"WorkOrders.cancel(uint256)","verdict":"holds","why":"escrow -> 0, returned += escrow fits uint96 (returned + escrow invariant while Open), whole escrow sent to the employer."},{"target":"WorkOrders.challenge(uint256)","verdict":"holds","why":"Refused at ts >= assertedAt + 24h, exactly complementary to finalize's < test; pulls bond equal to the runner's."},{"target":"WorkOrders.claim(uint256)","verdict":"holds","why":"price = minPay + (maxPay-minPay)*elapsed/rampSecs with elapsed < rampSecs guard (rampSecs == 0 -> maxPay, untriggered conditional -> minPay); product <= 2^96 * 2^40 cannot overflow; rounds down <= 1 wei; BooksFull guard keeps returned+escrow+bond <= uint96.max so later _credit casts cannot truncate;"},{"target":"WorkOrders.claimDelivered(uint256,bytes32)","verdict":"holds","why":"TooLate unless now + 24h + 24h <= registeredAt + deliverySecs, so a challenge at the last second still leaves > 24h before timeout; with MIN_DELIVERY 3 days the runner has 1 day to assert, as documented."},{"target":"WorkOrders.createOrder((bytes32,uint96,uint96,uint32,uint32,uint40,uint96,uint8,address,address,address,bytes4,bytes32,uint8,uint256),bytes)","verdict":"holds","why":"maxPay > 0, minPay <= maxPay (minPay 0 allowed: price 0 then gives fee 0, tip 0, runner 0, unused = escrow, no revert), delivery 3..90 days, bond >= MIN_BOND, expiresAt > now, flags <= 7, condCmp 0..6 consistent with condTarget; escrow = maxPay pulled."},{"target":"WorkOrders.evict(uint256)","verdict":"holds","why":"TooEarly at ts < claimedAt + 1h, complementary to release's TooLate at >=; whole bond credited and sent to the employer, order reopens or refunds if expired."},{"target":"WorkOrders.finalize(uint256)","verdict":"holds","why":"Same _deliver arithmetic as settle with the runner's hash and an empty agent list; TooEarly at ts < assertedAt + 24h."},{"target":"WorkOrders.redeliver(uint256)","verdict":"holds","why":"No arithmetic; guarded by the transient lock state and the 64/63 gas floor."},{"target":"WorkOrders.refund(uint256)","verdict":"holds","why":"Allowed at ts >= expiresAt; _close zeroes escrow, credits and sends it in full."},{"target":"WorkOrders.register(uint256,bytes16)","verdict":"holds","why":"No arithmetic; registeredAt fixed on the first registration so a retry does not extend the delivery deadline."},{"target":"WorkOrders.release(uint256)","verdict":"holds","why":"cut = bond*1000/10000 floors (<= 9 wei stays with the runner); runner gets bond - cut, employer cut: sums to bond."},{"target":"WorkOrders.reportFailed(uint256,bytes,address[])","verdict":"holds","why":"Every branch conserves the bonds: SwarmFailed bond->runner, WrongJob/Refuted(asserted) bond->employer, Disputed cut->employer and 2*bond-cut->challenger; HeadTooLong retry hashes an empty header which no claim matches."},{"target":"WorkOrders.resolve(uint256,bool)","verdict":"holds","why":"delivered -> _deliver pays runner runnerPay + 2*bond (challenger's bond included); false -> _refute split; both conserve escrow + 2 bonds."},{"target":"WorkOrders.settle(uint256,bytes,address[])","verdict":"holds","why":"fee = tip = ceil(price/20), tip capped at price - fee, runnerPay = price - fee - tip, unused = escrow - price (price <= maxPay = escrow); verified at prices 0,1,2,19,20,21,39,41 and by fuzz thePartsAlwaysSumToThePrice; exactly fee+tip leaves to Payroll with the allowance remainder sent by transfer."},{"target":"WorkOrders.stamp(uint256,bytes,address[])","verdict":"holds","why":"No money; marks proven and records the receipt hash."},{"target":"WorkOrders.timeout(uint256)","verdict":"holds","why":"TooEarly at ts < registeredAt + deliverySecs; Registered: bond -> employer; Disputed: runner bond - cut, employer cut, challenger its own bond, escrow refunded: sums to escrow + 2 bonds."},{"target":"WorkOrders.trigger(uint256)","verdict":"holds","why":"Requires the full CONDITION_GAS*64/63 + slack; a return shorter than 32 bytes is false; comparison table 1..6 maps exactly; rampFrom set at the trigger."},{"target":"invariant: desk IMD balance == sum(escrow) + held bonds; every transfer to the employer is matched by one _credit","verdict":"holds","why":"cancel, refund, _deliver and _endClaim each pair _credit with _send of the same amount; DeskInvariants and CrowdPots invariant suites pass (64 runs x 48 depth)."},{"target":"invariant: CrowdPots IMD balance >= owed; a pot's pool is paid out to the wei with no dust","verdict":"holds","why":"owed moves only by chips in, target out at launch, returned in at settle and shares out; fuzz anExpiredPotRefundsEveryChipExactly and _assertPotIsEmpty confirm pool, chipsLeft, owed and balance all reach 0."},{"target":"price ramp stands still while held: rampFrom += now - claimedAt on reopen","verdict":"holds","why":"rampFrom <= claimedAt at every claim so the new rampFrom <= now and elapsed never underflows; tests theRampStandsStillWhileARunnerHoldsTheOrder and fuzz holdingAnOrderNeverRaisesItsPrice pass."},{"target":"SpecText.firstBad / OrderHeader.build byte boundaries","verdict":"holds","why":"Partial last word padded with 0x61 so bytes past the spec never match a bad class; word-wide tests may over-report but the byte loop is exact; header writes 19 + 64 + 2 = 85 bytes inside the 85-byte allocation."},{"target":"JsonScan / WorkProof bytes decoding (Boundary step 5)","verdict":"unreached","why":"Read the quote/backslash lane arithmetic, _uuid, agentIds digit accumulation (77 digits < 2^256) and the beyond-buffer read paths and found no counterexample, but did not independently fuzz them within budget; the project's adversarial and fuzz suites (61 tests) pass."},{"target":"fork tests (test/fork) against live IMD, WorkRegistry and Payroll","verdict":"unreached","why":"Not run: no mainnet RPC used in this review; IMD transfer semantics (no fee, allowance decrements) are assumed from the project's fork suite."}],"summary":{"reported":true,"total":28,"answered":28,"ratio":1,"missing":[],"unreached":[]}}},{"hash":"69dd741f4489caa269efa598dc600d4fb3fcfaf9a8919db49881f5f07987e94a","nodeKey":"audit_flow","role":"review","attempt":1,"deviceKey":"357c46e3781993d449f398d7eae2be8718b1cfa8deff2cc3661e944506942b5e","seat":{"tokenId":"205","agentId":"52222"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":27,"runtime":"claude","inputTokens":450,"wallClockMs":1126538,"outputTokens":44368,"cachedInputTokens":1753374},"baseCommit":"94fb4730e1a8a641358516b3d8550e6a72254a8a","startCommit":"0ca93eac1499330d10e756dd98b055a3507e6170","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":["d2a63b4814f0a662cc31bd080327f5e80954bff826929e769a75e1dddc640be3"],"verificationErrors":0,"summary":"Review complete. The findings file is written and the working tree is otherwise untouched.\n\n**Outcome.** No defect found in the control flow of WorkOrders, CrowdPots or Doorbell. Every one of the 28 listed entry points traced as intended, and the file carries 33 coverage rows with none unreached.\n\n**What I checked, against the Execution Trace, Periphery and First Principles guides:**\n- **WorkOrders:** every state transition, the order of checks, state writes and transfers in each path, the bond and escrow sums at each claim ending, the exactness of `returned`, the gas floors before the Payroll, callback and condition calls, and the callback running only after the lock is released.\n- **CrowdPots:** the one desk call that ends a timed-out order before the books are read, the `owed` and balance coupling on chip, launch, reconcile and pay, pro rata rounding with the last claimer emptying the pool, and series rounds.\n- **Doorbell:** the ring, skip and hire path with its post-call allowance and employer checks, and the once-only delivery record.\n- **Periphery:** SpecText, OrderHeader, WorkProof's registry read and chunk join, the condition staticcall, and JsonScan's structure and capture rules. JsonScan's word-lane arithmetic I did not re-derive; it rests on the passing adversarial, fuzz and reference-scanner suites.\n- **Manifest:** four contracts in dependency order, argument counts and order per constructor, addresses matching the sources and docs, and the kind hash equal to the keccak of `skill:create-image`.\n\n**Test runs:** all 649 non-invariant tests pass, and the three invariant suites pass at reduced depth. The via_ir build alone took about eight minutes.\n\n**Two informational notes recorded, both already stated in the author's own documentation:**\n1. The mainnet Doorbell deploys with the claimed path on. A runner can take each 1 IMD hire with a 0.25 IMD bond, register any job id, claim delivery and collect 0.9 IMD after 24 hours unless a stranger posts an equal bond, and that stranger earns nothing for doing so. The reproduction gives the exact call sequence and amounts.\n2. `chipInFor` refuses only the zero address and CrowdPots itself as beneficiary, so a chip for the desk address is accepted and its refund is later paid into WorkOrders, where nothing can move it.\n\nNeither carries a proof test, since proofs are only required for critical or high findings.","createdAt":"2026-10-13T14:01:31.642Z","verdict":null,"findings":[{"severity":"info","title":"Doorbell is launched with ALLOW_CLAIMED = 1: each hire's escrow can be taken by an unchallenged bonded claim, and a defender earns nothing for challenging","path":"src/pots/Doorbell.sol","line":249,"description":"Not a code defect: the contract's own notes (Doorbell.sol lines 30-38) state this exposure and the brief fixes the eighth constructor argument at 1. It is recorded here as the one trust assumption in the control flow that the mainnet manifest turns on, so the judge sees its concrete cost. The Doorbell's orders carry no arbiter and no runnerOnly, and the Doorbell never challenges, so the only thing between its balance and a runner who registers a job id that has no receipt is a third party willing to post an equal bond. That third party's best outcome, when no receipt ever lands, is to get its bond back at timeout (WorkOrders.timeout, End.DisputeExpired: challenger receives exactly its own bond), so there is no economic reason for anyone to defend the Doorbell, while the attacker's cost of a defended attempt is one tenth of the bond (0.025 IMD) against a prize of 0.9 IMD per undefended attempt. The deliverySecs of 7 days (604800) and the 24h+24h rule in claimDelivered leave the claim allowed from the moment of registration.","reproduction":"Mainnet values from the manifest: MIN_PAY = MAX_PAY = 1e18, BOND = 0.25e18, DELIVERY_SECS = 604800, ALLOW_CLAIMED = 1. (1) Anyone transfers 1e18 IMD to the Doorbell. (2) Attacker calls ring() 25 times; the 25th opens order N on WorkOrders with escrow 1e18, flags 3, expiresAt now+2d. (3) Attacker approves 0.25e18 IMD and calls claim(N) (price fixed at 1e18), then register(N, 0x00000000000000000000000000000001) with any job id, then claimDelivered(N, bytes32(uint256(1))) in the same block: until = now+24h, until+24h <= registeredAt+7d so it is accepted. (4) 24 hours later anyone calls finalize(N): WorkOrders._deliver pays the attacker runnerPay 0.9e18 plus its 0.25e18 bond back, sends 0.1e18 to Payroll, and the Doorbell's onWorkDelivered records jobId 0x..01 and recordHash 0x..01 as a delivery. Expected by the author: documented behaviour. Actual: identical; the Doorbell's 1 IMD is gone for a job that never existed. If instead a defender calls challenge(N) with 0.25e18 inside the window, then after registeredAt+7d timeout(N) returns exactly 0.25e18 to the defender, 0.225e18 to the attacker, and 1.025e18 to the Doorbell: the defender's net is zero, the attacker's loss 0.025e18, and the next 25th ring can be attacked again.","snippet":"        if (ALLOW_CLAIMED) flags |= WORK_ORDERS.FLAG_ALLOW_CLAIMED();","citation":"resolved","blocking":false},{"severity":"info","title":"chipInFor accepts the desk (WORK_ORDERS) and any other contract that cannot use IMD as beneficiary; the refund is then stranded","path":"src/pots/CrowdPots.sol","line":726,"description":"BadBeneficiary excludes only the zero address and CrowdPots itself. A chip for the WorkOrders address (or for IMD, Payroll, WorkProof) is accepted, and claimRefund(potId, WORK_ORDERS) later pushes IMD into the desk, which keeps no books of unsolicited balances (nothing in WorkOrders reads its own IMD balance), so that IMD is lost for good. Only the payer's own money is at stake and docs/DEVIATIONS.md D7 states that naming a beneficiary that cannot use IMD gives the IMD away, so this is recorded as a note, not a defect. The check could cheaply also refuse address(WORK_ORDERS) and address(IMD), the two addresses the contract already knows.","reproduction":"openPot(terms with target 1e18, fundSecs 1 hours, hireSecs 1 days, deliverySecs 3 days, bond 0.25e18, minPay 1e18, rampSecs 0, kind \"audit\", allowClaimed false), spec \"x\", firstChip 0 -> potId P. Caller approves 1e18 and calls chipInFor(P, 1e18, address(WORK_ORDERS)): accepted, raised = 1e18, PotFilled. Nobody launches; warp past deadline + 1 day. claimRefund(P, address(WORK_ORDERS)) returns 1e18 and transfers 1e18 IMD to the WorkOrders contract. Expected: the chip is refused as BadBeneficiary like a chip for CrowdPots itself. Actual: 1e18 IMD now sits in WorkOrders with no order, no escrow entry and no function that can move it.","snippet":"        if (beneficiary == address(0) || beneficiary == address(this)) revert BadBeneficiary();","citation":"resolved","blocking":false}],"coverage":{"rows":[{"target":"CrowdPots.chipIn(uint256,uint256)","verdict":"holds","why":"Traced _chip: series redirect, _remaining clamp, raised/owed/chips written before the pull, zero-taken returns without transfer; nothing stale."},{"target":"CrowdPots.chipInFor(uint256,uint256,address)","verdict":"finding","findingRef":2,"why":"Same flow as chipIn; only the beneficiary filter is narrower than the stranding it is meant to stop (info note)."},{"target":"CrowdPots.claimRefund(uint256,address)","verdict":"holds","why":"_settled runs _reconcile (desk call first, then accountOf read) before _pay; pool/chipsLeft/owed move together; last claimer empties the pool; chip of zero or claimed returns 0 before the division."},{"target":"CrowdPots.claimRefunds(uint256,address[])","verdict":"holds","why":"Loop over caller's list only; duplicates and strangers pay 0; same _pay as claimRefund."},{"target":"CrowdPots.launch(uint256,bytes)","verdict":"holds","why":"Stage/full/grace/spec checks, owed reduced before createOrder, allowance exactly target checked to zero, employer and fresh orderId checked after the desk answers, under the transient lock."},{"target":"CrowdPots.onWorkDelivered(uint256,bytes16,bytes32)","verdict":"holds","why":"Desk-only, pot must exist, writes jobId/recordHash only; amounts come from accountOf later, so a failed or repeated callback moves nothing."},{"target":"CrowdPots.openPot((string,uint96,uint96,uint32,uint96,uint32,uint32,uint32,bool),bytes,uint256)","verdict":"holds","why":"_pin checks every bound against the desk's constants and SpecText; firstChip goes through _chip; opener keeps no rights."},{"target":"CrowdPots.openSeries((string,uint96,uint96,uint32,uint96,uint32,uint32,uint32,bool),bytes)","verdict":"holds","why":"No money moves; rounds are created by _round from stored terms; a full or expired round is replaced, a full round still launches within its grace."},{"target":"CrowdPots.reconcile(uint256)","verdict":"holds","why":"_endWhatRanOut makes one desk call that always suffices (refund/evict/timeout each also refund when expired), then _peek reads terminal accountOf once; owed += returned with the balance floor check; Series/None ids are no-ops."},{"target":"Doorbell.onWorkDelivered(uint256,bytes16,bytes32)","verdict":"holds","why":"Desk-only, known order, once; the desk sets notified before the call so AlreadyDelivered is unreachable from a successful callback."},{"target":"Doorbell.ring()","verdict":"finding","findingRef":1,"why":"Flow is correct (count, skip or hire, allowance and employer checked after the desk answers); the info note is about the mainnet ALLOW_CLAIMED=1 exposure of each hire, which the contract documents."},{"target":"WorkOrders.cancel(uint256)","verdict":"holds","why":"Open and employer only; escrow zeroed, credited and sent to msg.sender == employer; returned stays exact."},{"target":"WorkOrders.challenge(uint256)","verdict":"holds","why":"Asserted and inside the window; state and challenger written before the bond pull; a second challenger reverts BadState."},{"target":"WorkOrders.claim(uint256)","verdict":"holds","why":"Open, unexpired, runnerOnly, barred, latch-before-price, BooksFull bound, seq incremented before the ticket, whole Claim struct overwritten so no stale challenger survives."},{"target":"WorkOrders.claimDelivered(uint256,bytes32)","verdict":"holds","why":"Registered, runner, flag, non-zero hash, and window+dispute must end by the delivery deadline measured from the first registration."},{"target":"WorkOrders.createOrder((bytes32,uint96,uint96,uint32,uint32,uint40,uint96,uint8,address,address,address,bytes4,bytes32,uint8,uint256),bytes)","verdict":"holds","why":"SpecText, pay range, delivery bounds, bond, expiry, flags<=7 with COND_ARG only when conditional, condCmp coupled to condTarget; rampFrom 0 for conditional orders; escrow pulled last."},{"target":"WorkOrders.evict(uint256)","verdict":"holds","why":"Claimed and past the window; whole bond to employer via _endClaim; order reopens with the ramp moved on, or refunds when expired."},{"target":"WorkOrders.finalize(uint256)","verdict":"holds","why":"Asserted and past the window under the lock, _deliver with the named hash and no agents, callback after the lock; expiry does not matter once held."},{"target":"WorkOrders.redeliver(uint256)","verdict":"holds","why":"Refused while the guard is entered, only Delivered+flag+not notified; notified set before the call so a re-entering callback finds nothing."},{"target":"WorkOrders.refund(uint256)","verdict":"holds","why":"Open and expired; _close credits and sends the escrow to the employer."},{"target":"WorkOrders.register(uint256,bytes16)","verdict":"holds","why":"Claimed or Registered, runner only, non-zero, not a job that settled another order; registeredAt fixed by the first registration only."},{"target":"WorkOrders.release(uint256)","verdict":"holds","why":"Claimed, runner, inside the window; barred set; 10% to employer, rest to runner; expired order refunds in the same call."},{"target":"WorkOrders.reportFailed(uint256,bytes,address[])","verdict":"holds","why":"Registered..Disputed; HeadTooLong re-proven with headLen 0 so a short objective is WrongJob; a settling receipt is NotFailed; each branch names fixed recipients summing to the bonds held."},{"target":"WorkOrders.resolve(uint256,bool)","verdict":"holds","why":"Disputed and arbiter only (no arbiter means nobody); true pays as Disputed delivery (2 bonds to runner) and notifies after the lock; false refutes with the release share to the employer."},{"target":"WorkOrders.settle(uint256,bytes,address[])","verdict":"holds","why":"Registered..Disputed, proof via staticcall with reverts bubbled, _requireSettles incl. JobUsed, orderOfJob written before payout, state then transfers then Payroll with gas floors, callback last outside the lock."},{"target":"WorkOrders.stamp(uint256,bytes,address[])","verdict":"holds","why":"Delivered and unproven, same checks as settle, no money moves, orderOfJob and recordHash written."},{"target":"WorkOrders.timeout(uint256)","verdict":"holds","why":"Registered or Disputed past registeredAt+deliverySecs; Registered forfeits the bond and reopens/refunds, Disputed refunds for good with challenger made whole and runner losing the release share."},{"target":"WorkOrders.trigger(uint256)","verdict":"holds","why":"Open and not yet met; gas floor before the capped staticcall; one true reading latches and starts the ramp."},{"target":"invariant: accountOf.returned equals every IMD transfer the desk makes to the employer","verdict":"holds","why":"Every _send to o.employer (cancel, refund, _deliver unused, _endClaim back) is preceded by _credit of the same amount; CrowdPots books pots from it once at a terminal state."},{"target":"invariant: CrowdPots IMD balance >= owed","verdict":"holds","why":"chip, launch and _pay move balance and owed together; reconcile adds only what the desk already pushed and reverts DeskShortfall otherwise."},{"target":"periphery: SpecText.firstBad, OrderHeader.build/ticket/hash, WorkProof._receipt/_join, WorkOrders._condition","verdict":"holds","why":"Word tests only pre-filter and the byte scan finds the exact index; header is exactly 85 bytes; registry call is gas-floored, one-word, non-zero; condition reads one word of a capped staticcall."},{"target":"periphery: JsonScan.scan/agentIds","verdict":"holds","why":"Traced key order, the five required captures before state, string-only values for them, the depth walk and the escaped-quote rule, and agentIds' all-or-nothing list; the word-lane arithmetic itself was not re-derived and rests on the passing adversarial, fuzz and reference-scanner suites."},{"target":"launch manifest: constructor arguments against the code","verdict":"holds","why":"Four contracts in dependency order, argument counts and order match each constructor, addresses match Mainnet.sol and docs, kindHash is keccak256(\"skill:create-image\"), Doorbell values pass its constructor checks."}],"summary":{"reported":true,"total":28,"answered":28,"ratio":1,"missing":[],"unreached":[]}}},{"hash":"e9e009a65e39f1681bf55086f254f5c67d79488666999cfcb9c064f3a2d00ea8","nodeKey":"manifest","role":"integrate","attempt":1,"deviceKey":"1c29c7c6e4cf57eb4c72b96b17a6fb292f15fbb1012bc7c728803edb294547d7","seat":{"tokenId":"92","agentId":"52130"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-opus-5-5","turns":7,"runtime":"claude","inputTokens":12,"wallClockMs":303392,"outputTokens":2818,"cachedInputTokens":200288},"baseCommit":"94fb4730e1a8a641358516b3d8550e6a72254a8a","startCommit":"0ca93eac1499330d10e756dd98b055a3507e6170","resultTreeHash":"c916d5426def7c5ccbf48efebae1cd09c42b5b0b","bundleHash":"4b5f31106ca2415965865920740fb67602214beb671aad569314e1eb8a070ab8","bundleBytes":5652,"artifacts":[],"changedPaths":["launch.json"],"allowedPaths":["launch.json"],"dependencyBundleHashes":["d2a63b4814f0a662cc31bd080327f5e80954bff826929e769a75e1dddc640be3"],"verificationErrors":0,"summary":"I wrote `launch.json` in the repository root. It matches the manifest in your brief and in `docs/LAUNCH.md`, and no other file was changed.\n\n**Checks against the code:**\n- **Constructors:** the argument counts and order match each constructor: `WorkProof(workRegistry)`, `WorkOrders(imd, workProof, payroll)`, `CrowdPots(imd, workOrders)` and `Doorbell(imd, workOrders, kindHash, minPay, maxPay, bond, deliverySecs, allowClaimed)`.\n- **Mainnet addresses:** the three fixed addresses (IMD, WorkRegistry, and Payroll from launch 1531) are the same as the constants in `src/interfaces/external/Mainnet.sol`.\n- **Doorbell kind hash:** `0x7679ff00…6869185` is `keccak256(\"skill:create-image\")`, as section 2 of `docs/LAUNCH.md` says.\n- **Contract references:** the `$contract:` references only point to contracts listed earlier, and only fill address arguments.\n- **No owner:** there is no `$owner`, no token field and no extra key.\n\n**Build and tests:** `forge build` succeeded. `forge test` ran 654 tests with the fork tests left out, and all passed. That includes `test/launch/LaunchManifest.t.sol`, which checks the manifest in `LAUNCH.md` against the code. I did not run the fork tests in `test/fork/`, because they need a mainnet RPC connection. The pinned protected test under `.imd/reads/` is outside `test/`, so it wasn't part of this run.\n\n`git status` also lists untracked files I did not create, mostly settings and dotfiles (`.bashrc`, `.claude/`, `.gitmodules`, `.mcp.json` and others). At the start the tree was reported clean, so they probably come from this machine's setup. I left them alone.","createdAt":"2026-10-13T14:00:30.434Z","verdict":{"status":"accepted","profile":"foundry","evaluation":"checks","rejectionCode":null,"detail":"all checks passed","verifierVersion":"0.1.0+327e1e9f","verifiedTreeHash":"c916d5426def7c5ccbf48efebae1cd09c42b5b0b","at":"2026-10-13T14:05:04.020Z","failedChecks":[]},"findings":[],"coverage":null},{"hash":"464b6da7137f52073c2698cb9b9e54c60f82d93328337ea70793dcc250f8ee25","nodeKey":"audit_economics","role":"review","attempt":1,"deviceKey":"c722c2e9ac9aa0844d0c645fdb70fe9e6e139c9e0eb6d845666d11f4c86a049e","seat":{"tokenId":"826","agentId":"52180"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":22,"runtime":"claude","inputTokens":290,"wallClockMs":985227,"outputTokens":32389,"cachedInputTokens":952218},"baseCommit":"94fb4730e1a8a641358516b3d8550e6a72254a8a","startCommit":"0ca93eac1499330d10e756dd98b055a3507e6170","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":["d2a63b4814f0a662cc31bd080327f5e80954bff826929e769a75e1dddc640be3"],"verificationErrors":0,"summary":"The review is complete and the findings file is written. The working tree is otherwise untouched.\n\n**Outcome.** I found no economic defect in WorkOrders, CrowdPots or Doorbell. Every value flow conserves funds, every pro rata share and fee rounds in a stated direction, and every incentive gap I could construct is already named in docs/THREATS.md with a test beside it. The findings file holds one info item and a coverage record for all 28 entry points plus three invariants.\n\n**What I checked, from the Economic Security, Invariant and Flow Gap guides:**\n\n- **Conservation on the desk.** Each exit sends exactly what it zeroes. Delivery pays price plus unused escrow plus bonds, which equals escrow plus bonds held. Fee and tip are capped so the runner share never underflows at a one-wei price.\n- **The employer's `returned` figure.** It is credited beside every transfer to the employer, and the claim guard keeps it under 96 bits. CrowdPots books from it once at a terminal state, so stray transfers and order reopenings never shift a pot's total.\n- **CrowdPots solvency.** The margin between balance and `owed` can only grow. Shares round down and the last claimer takes the remainder. Launch verifies the desk pulled exactly the target and names the pot as employer.\n- **Price ramp.** It pauses while held, so claim-and-release cannot raise the price, and a reopened order resumes where the claim found it.\n- **Deadlines.** Release/evict, challenge/finalize, claim/refund and claimDelivered/timeout pairs use complementary comparisons with no gap or overlap.\n- **Flow gap seams.** The desk callback moves no money and runs after the lock. Payroll is offered full gas and exactly fee plus tip leaves whether it pulls or not. Pot and Doorbell orders have no condition, arbiter or reserved runner, so no external target can touch their money.\n\n**The one item reported** is informational. With the manifest's Doorbell values and the claimed path on, a false-claim wave costs the attacker 2.5% of the bond per order while a defender must lock 25% of the Doorbell's balance for up to 7 days to cover it, and undefended orders drain 0.9 IMD each. This is the documented D2 and L6 limit; I quantified it so the requester can size the keeper float the docs ask for.\n\n**What I could not complete.** The full forge suite exceeded my time box because of the invariant runs, so I did not re-confirm the author's tests pass on this tree. I inspected the invariant handlers instead and they exercise every entry point, so I have no untested edge to name.","createdAt":"2026-10-13T13:59:04.471Z","verdict":null,"findings":[{"severity":"info","title":"Doorbell manifest values make an unchallenged false claim cost the attacker 2.5% of the bond and the defender 25% of the Doorbell's balance in locked float","path":"src/pots/Doorbell.sol","line":204,"description":"Not a code defect: THREATS.md D2, L2 and L6 state the rule, and the contracts behave as documented. This records the concrete economics for the manifest's values (MAX_PAY = 1 IMD, BOND = 0.25 IMD = the desk minimum, DELIVERY_SECS = 7 days, ALLOW_CLAIMED = 1) so the requester can size the keeper float the docs call for. Ringing is free apart from gas and has no rate limit, so with balance B the Doorbell can be driven into floor(B) simultaneous open orders in one transaction batch (25 rings each). A runner that takes each one, registers a job id no job has and calls claimDelivered is paid 0.9 IMD plus its bond per order 24 hours later unless somebody challenges. A correct challenge of such a claim can never be decided early (no receipt will ever exist and the order has no arbiter): the challenger posts 0.25 IMD per order, waits until registeredAt + 7 days, and timeout returns exactly its bond (WorkOrders.sol:472, DisputeExpired pays the challenger `bond`). The runner loses 0.025 IMD per order (the release share) and the Doorbell keeps 1.025 IMD. So each attack wave costs the attacker 0.025 IMD per order and gas, costs a defending watcher 0.25 IMD per order locked for up to 7 days plus gas, and succeeds fully on every order the watcher does not cover. The attacker can repeat a wave every 7 days while the Doorbell holds at least 1 IMD, and the undefended share of each wave is drained at 0.9 IMD per order.","reproduction":"State: Doorbell deployed with the manifest's constructor values and funded with 10 IMD; a watcher with a float of 1 IMD. Attacker: call Doorbell.ring() 250 times (10 orders open, each escrow 1 IMD, Doorbell balance 0); for each order call WorkOrders.claim(id) with 0.25 IMD bond, WorkOrders.register(id, <random nonzero bytes16>), WorkOrders.claimDelivered(id, <any nonzero bytes32>). Watcher: can challenge at most 4 orders (4 x 0.25 IMD). After 24 hours anyone calls WorkOrders.finalize(id) on the 6 unchallenged orders: runner receives 6 x (0.9 + 0.25) IMD, Payroll 6 x 0.1 IMD, Doorbell gets nothing back for them. After 7 days WorkOrders.timeout(id) on the 4 disputed orders: watcher gets 4 x 0.25 IMD back (no gain), runner gets 4 x 0.225 IMD back, Doorbell gets 4 x 1.025 IMD. Net: Doorbell loses 5.9 IMD of 10, attacker nets 5.4 - 0.1 = 5.3 IMD for 0.1 IMD at risk plus gas. Expected per the documents: the same; this is the documented D2 risk, quantified for the manifest.","snippet":"        uint256 balance = IMD.balanceOf(address(this));\n        if (balance < MAX_PAY) {","citation":"resolved","blocking":false}],"coverage":{"rows":[{"target":"CrowdPots.chipIn(uint256,uint256)","verdict":"holds","why":"Takes min(amount, remaining), books owed += taken before the pull, SafeCast bounds the chip to the uint96 target; a full, launched, expired or unknown pot takes zero without reverting."},{"target":"CrowdPots.chipInFor(uint256,uint256,address)","verdict":"holds","why":"Same path as chipIn with beneficiary != 0 and != this; the chip and its refund belong to the beneficiary only, so a stranger's refund call changes timing, never recipient (D7 accepted)."},{"target":"CrowdPots.claimRefund(uint256,address)","verdict":"holds","why":"Reconciles first (desk calls then one accountOf read), pays floor(pool*chip/chipsLeft), marks claimed before transfer, owed -= share; the last claimer empties the pool, sum of shares == pool."},{"target":"CrowdPots.claimRefunds(uint256,address[])","verdict":"holds","why":"Loops over the caller's list only; duplicates and strangers pay zero; one settlement per call, same arithmetic as claimRefund."},{"target":"CrowdPots.launch(uint256,bytes)","verdict":"holds","why":"Only a full Funding pot within deadline+1 day; owed -= target before the desk pulls exactly target (allowance check), id uniqueness and employer identity verified; every term was bounded at _pin so the desk cannot refuse it."},{"target":"CrowdPots.onWorkDelivered(uint256,bytes16,bytes32)","verdict":"holds","why":"Desk-only, records jobId/recordHash for a known order and moves no money; amounts are read later from accountOf so a missing or failed callback costs nothing."},{"target":"CrowdPots.openPot((string,uint96,uint96,uint32,uint96,uint32,uint32,uint32,bool),bytes,uint256)","verdict":"holds","why":"_pin enforces every bound (target >= 1 IMD, bond in [MIN_BOND, target], delivery in [desk min, 30d], hire 1-30d, fund 1h-30d, ramp consistent) and the desk's spec rules, so money cannot be parked beyond the documented 91-day worst case."},{"target":"CrowdPots.openSeries((string,uint96,uint96,uint32,uint96,uint32,uint32,uint32,bool),bytes)","verdict":"holds","why":"Holds no money; rounds are ordinary pots with the pinned terms; a one-wei stranger chip starting a round's clock costs standing funders time only (C10)."},{"target":"CrowdPots.reconcile(uint256)","verdict":"holds","why":"Has the desk refund/evict/timeout first, then books pool = accountOf.returned once at a terminal state; owed += pool is covered because returned is the exact sum of transfers the desk made to the pot, and DeskShortfall cannot fire with the real desk."},{"target":"Doorbell.onWorkDelivered(uint256,bytes16,bytes32)","verdict":"holds","why":"Desk-only, once per known order, records history and moves no money; a revert here only leaves the desk's notified flag false."},{"target":"Doorbell.ring()","verdict":"finding","findingRef":1,"why":"Behaves as written (every 25th ring escrows MAX_PAY if held); the info item quantifies the documented drain exposure of ALLOW_CLAIMED = 1 with the manifest's amounts."},{"target":"WorkOrders.cancel(uint256)","verdict":"holds","why":"Employer-only while Open; escrow zeroed and credited to returned before the transfer; CrowdPots and Doorbell never call it."},{"target":"WorkOrders.challenge(uint256)","verdict":"holds","why":"Once per claim inside the 24h window, bond equal to the runner's pulled after state change; its three exits (settle, resolve, reportFailed/timeout) each pay 2*bond or bond in full, never less."},{"target":"WorkOrders.claim(uint256)","verdict":"holds","why":"Fixes price = _price at claim, BooksFull keeps returned exact, fresh Claim struct clears any old challenger, bond pulled last; the ramp is paused while held so a claim-and-release cannot raise the price."},{"target":"WorkOrders.claimDelivered(uint256,bytes32)","verdict":"holds","why":"Runner-only on flagged orders, must leave 48h before the delivery deadline; no money moves; the economics of an unchallenged false claim are the documented L1/D2 limit (finding 1)."},{"target":"WorkOrders.createOrder((bytes32,uint96,uint96,uint32,uint32,uint40,uint96,uint8,address,address,address,bytes4,bytes32,uint8,uint256),bytes)","verdict":"holds","why":"Pulls exactly maxPay after the struct is written; escrow = maxPay; bounds on delivery (3-90d), bond (>= 0.25 IMD), expiry, flags and condition fields; minPay 0 only hurts the employer that chose it."},{"target":"WorkOrders.evict(uint256)","verdict":"holds","why":"Anyone after the 1h window, whole bond to the employer via _endClaim; windows are complementary with release (>= vs <) so no gap or overlap."},{"target":"WorkOrders.finalize(uint256)","verdict":"holds","why":"Anyone after the 24h window; pays price split (fee and tip rounded up, runner gets the rest) plus bond, unused escrow back to the employer; conservation price+unused+bond == escrow+bond."},{"target":"WorkOrders.redeliver(uint256)","verdict":"holds","why":"Only retries a failed callback on a Delivered order outside the lock; moves no money."},{"target":"WorkOrders.refund(uint256)","verdict":"holds","why":"Anyone once expiresAt has passed on an Open order; escrow credited and sent to the employer; a held order is refunded instead when its claim ends."},{"target":"WorkOrders.register(uint256,bytes16)","verdict":"holds","why":"Runner-only, reserves nothing, refuses a job that already settled another order; delivery clock starts at first registration and retries do not restart it."},{"target":"WorkOrders.release(uint256)","verdict":"holds","why":"Runner-only inside the 1h window, 10% of the bond to the employer and the rest back, runner barred; rampFrom advanced by the held time so the price resumes where the claim found it."},{"target":"WorkOrders.reportFailed(uint256,bytes,address[])","verdict":"holds","why":"Four outcomes each pay fixed parties fixed amounts summing to the bonds held (SwarmFailed bond back, WrongJob/Refuted-unchallenged bond to employer, Refuted-disputed 10% employer and 90%+own bond to challenger); a settling receipt reverts NotFailed."},{"target":"WorkOrders.resolve(uint256,bool)","verdict":"holds","why":"Arbiter-only on Disputed; delivered pays as _deliver with 2*bond to the runner, otherwise _refute; CrowdPots and Doorbell orders have no arbiter so this path cannot touch their money."},{"target":"WorkOrders.settle(uint256,bytes,address[])","verdict":"holds","why":"Receipt must carry this claim's header and the order's spec and kind; pays price-fee-tip plus bond(s) to the claim's runner whoever submits, unused escrow credited and returned to the employer; Payroll offered full gas and exactly fee+tip leaves either way."},{"target":"WorkOrders.stamp(uint256,bytes,address[])","verdict":"holds","why":"Marks a claim-paid order proven on the same receipt checks as settle; moves no money."},{"target":"WorkOrders.timeout(uint256)","verdict":"holds","why":"Registered past the deadline: whole bond to the employer, order reopens or refunds; Disputed: employer refunded plus 10% of the runner's bond, runner 90%, challenger exactly its bond (the challenger-incentive gap is the documented L6, quantified in finding 1)."},{"target":"WorkOrders.trigger(uint256)","verdict":"holds","why":"Latches a condition once with full CONDITION_GAS and starts the ramp; pot and Doorbell orders are never conditional, so no money of this launch's contracts depends on an external target."},{"target":"invariant: CrowdPots IMD.balanceOf(this) >= owed","verdict":"holds","why":"owed rises with chips and with returned at settlement, falls with launch (desk pulls the same amount) and refunds (same amount sent); balance only falls on those two paths, so the margin never shrinks."},{"target":"invariant: WorkOrders balance == sum of live escrows and bonds held","verdict":"holds","why":"Every exit (_deliver, _endClaim, cancel, refund) sends exactly what it zeroes; _payPayroll sends exactly fee+tip by allowance plus fallback transfer."},{"target":"invariant: accountOf.returned equals the sum of transfers to the employer","verdict":"holds","why":"_credit is called once beside every _send to the employer with the same amount, and claim refuses an order whose returned+escrow+bond would exceed uint96."}],"summary":{"reported":true,"total":28,"answered":28,"ratio":1,"missing":[],"unreached":[]}}},{"hash":"c5c4fb6afe3e7d590b4f86d7f05500e3a2168f1211c692d50c191943b47192ec","nodeKey":"adapt_contract_project","role":"implement","attempt":1,"deviceKey":"1507f63d3f1b973a93ee467f9c3eeb74d74589571fa5072d45112deb2949dddc","seat":{"tokenId":"1540","agentId":"51231"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":42,"runtime":"claude","inputTokens":450,"wallClockMs":1164694,"outputTokens":24878,"cachedInputTokens":2071106},"baseCommit":"94fb4730e1a8a641358516b3d8550e6a72254a8a","startCommit":"94fb4730e1a8a641358516b3d8550e6a72254a8a","resultTreeHash":"bdfb66f102094efd645d0943fd263bc365d001f6","bundleHash":"384d2eac327982c24e8f41ff9bfe9813f51458084fce660eb00e8b3c5f9cefbc","bundleBytes":4930,"artifacts":[],"changedPaths":["ADAPTATION.md"],"allowedPaths":[],"dependencyBundleHashes":[],"verificationErrors":0,"summary":"The work is complete. The only new file in the tree is ADAPTATION.md; the scratch directory is excluded by the harness, and no launch.json or blocked file exists.\n\n**Outcome: the repository launches as it is. No source, test, document or configuration file was changed.** The single deliverable added is ADAPTATION.md at the repository root.\n\n**What I verified on the unchanged code**\n\n- `forge build` with the project's own foundry.toml succeeds. `forge test` passes with no network and an empty environment.\n- The protected factory harness from the task's read list was run in test/scratch with chain id 1, the real ProjectFactory address etched, and the four initcodes built from the manifest's exact arguments with the `$contract:` references resolved to predicted CREATE2 addresses. All four deployed at their predicted addresses and the runtime scan found no forbidden opcode.\n- Every launch rule the brief lists is already met and already pinned by the repository's own tests in test/launch/LaunchManifest.t.sol: static-word nonpayable constructors with no external call or code check, no owner or `$owner`, no proxy or initializer or minting or pausing, nothing to call after deployment, and internal-only libraries with empty link references.\n\n| Check | Result |\n|---|---|\n| Default test run | 654 passed, 0 failed, 30 suites |\n| Runtime bytes (WorkProof, WorkOrders, CrowdPots, Doorbell) | 4,579 / 19,289 / 14,177 / 5,972 |\n| Initcode with arguments | 4,744 / 19,726 / 14,646 / 7,032 |\n\n**The imported audit**\n\nAll four entries are informational. I reproduced the three behavioural ones step by step against the real desk, pots and Doorbell, including the Doorbell scenario with the manifest's exact values, where one address with 0.25 IMD walks away with 1.15 IMD on an unchallenged false claim. Each reproduces exactly as written. None was changed, because each is the requester's documented and tested design choice rather than a defect: the claimed path is a stated launch-day decision with a required watcher, the pot's reliance on someone calling finalize is written into the contract header and pinned by an existing test, and the bond-to-target ratio is the opener's published choice within the contract's bounds. ADAPTATION.md names the reproduction, the reasoning, and the permanent test that pins each behaviour.\n\n**Open items for the requester, none needing code**\n\n- The Doorbell's `allowClaimed` argument is permanent once deployed. The document's launch-day command decides it.\n- The fork suite was not run because it needs a mainnet RPC. The three mainnet addresses were taken from the requester's document and Mainnet.sol, not re-read from the chain.\n- The claimRefund rule the requester asked to keep is kept.","createdAt":"2026-10-13T13:38:18.284Z","verdict":{"status":"accepted","profile":"foundry","evaluation":"checks","rejectionCode":null,"detail":"all checks passed","verifierVersion":"0.1.0+327e1e9f","verifiedTreeHash":"bdfb66f102094efd645d0943fd263bc365d001f6","at":"2026-10-13T13:42:28.628Z","failedChecks":[]},"findings":[],"coverage":null},{"hash":"b6425780491ad8c81a4c4a82463c0d7087be97c7703215effe7aae74ce34df83","nodeKey":"audit_imported_code","role":"review","attempt":1,"deviceKey":"a406deaac63a93b0cabe27b72ad5e03f107fdd08e4651a9233cdf1923e9aac93","seat":{"tokenId":"1465","agentId":"52011"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":24,"runtime":"claude","inputTokens":356,"wallClockMs":1300903,"outputTokens":40527,"cachedInputTokens":1558042},"baseCommit":"94fb4730e1a8a641358516b3d8550e6a72254a8a","startCommit":"94fb4730e1a8a641358516b3d8550e6a72254a8a","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":[],"verificationErrors":0,"summary":"The review is complete. The findings file is in place and nothing else in the tree was touched.\n\n**Outcome: no defect substantiated.** All four contracts were read in full and checked against the Pashov checklist and the entry-point inventory, and nothing reproduced as a loss, misdirected payment, broken accounting, reentrancy, weak guard, or bad rounding. The findings file `.imd-findings.json` holds four info entries: a coverage statement and three documented trust assumptions with concrete reproductions, so the adapter sees them without a revision being opened.\n\n**What was read.** WorkProof, JsonScan, WorkOrders, OrderHeader, SpecText, CrowdPots, Doorbell, and the IWorkOrders, IPayroll and Mainnet interfaces, line by line. Not read line by line: the 490-line IWorkOrdersRunner interface, which holds only events, errors and structs, and the pinned OpenZeppelin libraries.\n\n**What was checked and held.**\n- The full forge suite passes with zero failures, invariant suites included.\n- An independent scan of the four compiled runtimes finds no DELEGATECALL, CALLCODE, SELFDESTRUCT or CREATE, and all sizes are under the EIP-170 limit.\n- Constructor ABIs match the manifest in count and type. Every constructor is nonpayable, makes no external call and checks no code size.\n- The three mainnet addresses hold code. IMD reports symbol IMD with 18 decimals and exposes no pause, mint or blacklist selector. The registry runtime contains the `latest(bytes16)` selector and Payroll contains `pay`, `tip` and `book`.\n- The Doorbell's kind hash equals keccak256 of `skill:create-image`, and its manifest values pass its own constructor checks.\n- Accounting traces cleanly: every credit to an order's `returned` figure is paired with a transfer to the employer, so the pots' pool arithmetic is exact and refunds pay to the wei.\n- Nothing the launch forbids is present: no owner, setter, proxy, initializer, pause, freeze, mint or post-deployment configuration.\n\n**Trust assumptions recorded as info.**\n- With the manifest's `allowClaimed = 1`, any bonded address can ring the Doorbell 25 times, take its order, claim delivery with a made-up job id, and be paid after 24 hours unless someone challenges. The contract header states this and asks that it hold small amounts.\n- CrowdPots never calls `finalize`, so a pot whose runner claimed delivery and walked away stays in the hiring stage until a third party calls the desk. The authors' own test confirms this behaviour.\n- A runner posting the opener-chosen bond, as low as 0.25 IMD, can hold a pot's whole target for up to 30 days by registering a job id with no receipt. The Terms documentation discloses the trade-off.\n\nNo proof tests were written because no critical or high finding exists. Time remained on the clock, with roughly 30 minutes unused.","createdAt":"2026-10-13T13:18:12.916Z","verdict":null,"findings":[{"severity":"info","title":"Coverage: all four contracts and their libraries read in full; no defect substantiated","path":"src/desk/WorkOrders.sol","line":78,"description":"Read in full: src/proof/WorkProof.sol, src/proof/JsonScan.sol, src/desk/WorkOrders.sol, src/desk/OrderHeader.sol, src/desk/SpecText.sol, src/pots/CrowdPots.sol, src/pots/Doorbell.sol, and the interfaces IWorkOrders, IPayroll, Mainnet. Not read line by line: src/interfaces/IWorkOrdersRunner.sol (490 lines of events/errors/structs) and the OpenZeppelin dependencies (SafeERC20, ReentrancyGuardTransient, SafeCast), which are the pinned library versions. Every external and public state-changing function of the four contracts was walked for caller, guard, money moved and what it trusts (Pashov SOP plus the ToB entry-point inventory). Checks run: forge build with the pinned settings; the full forge test suite (every suite passed, 0 failures, invariant suites included); an independent scan of the four compiled runtimes for DELEGATECALL (0xf4), CALLCODE (0xf2), SELFDESTRUCT (0xff), CREATE/CREATE2, which found none, with sizes 4,579 / 19,289 / 14,177 / 5,972 bytes, all under EIP-170; constructor ABIs match the manifest argument counts and types (1, 3, 2, 8), every constructor is nonpayable, makes no external call and checks no code size; the manifest's three mainnet addresses hold code on chain 1 (IMD: symbol IMD, 18 decimals, no pause/mint/blacklist selectors in its runtime; WorkRegistry runtime contains the latest(bytes16) selector 0xebe9712c; Payroll runtime contains pay(uint256,bytes32), tip(uint256[],uint256) and book()); keccak256(\"skill:create-image\") equals the manifest's kindHash; the Doorbell's manifest values pass its own constructor checks (minPay == maxPay == 1e18, bond 0.25e18 >= desk minimum, 604800 s within 3..30 days). Accounting was traced: every _credit in WorkOrders is paired with a _send to the employer, so CrowdPots' pool = accountOf.returned is exact; CrowdPots' owed never exceeds its balance; refunds pay to the wei; reentrancy locks hold on every money-moving path and the employer callback runs after the lock is released with state final. Launch-forbidden features (minting, pausing, freezing, blacklisting, seizing, proxies, initializers, post-deploy configuration, owner or setter) are absent. The three info entries below are documented trust assumptions with reproductions, recorded for the adapter; none is a code defect against the stated design.","reproduction":"forge build && forge test (all suites pass). Opcode scan over out/*/ *.json deployedBytecode with PUSH-data skipping finds no 0xf4/0xf2/0xff. cast keccak \"skill:create-image\" == 0x7679ff00d2b015b122f4c00147c1004ea6657aada5fff65e72986b90c6869185. Expected: a defect reproducing against the code; actual: none found within budget.","snippet":"contract WorkOrders is IWorkOrdersRunner, ReentrancyGuardTransient {","citation":"resolved","blocking":false},{"severity":"info","title":"Doorbell manifest sets allowClaimed = 1: every order it opens can be paid to a runner without any work, if nobody challenges within 24 hours","path":"src/pots/Doorbell.sol","line":249,"description":"The manifest's eighth Doorbell argument is \"1\", so ALLOW_CLAIMED is true and each order carries FLAG_ALLOW_CLAIMED. On such an order WorkOrders.claimDelivered accepts any nonzero jobId/recordHash from the runner, and finalize pays the runner 90% of the fixed price plus its bond after CHALLENGE_WINDOW with no receipt. Nothing in the Doorbell can be changed later (no owner), so whatever balance the Doorbell ever holds is available to any bonded address that rings it 25 times and makes a false claim, unless a watcher posts the 1x bond challenge each time; and a challenge that nothing decides refunds the order, so a true claim can also be blocked at no cost. The contract header (lines 30-38) states exactly this and asks that it hold small amounts; the choice is made on launch day. Recorded as a trust assumption for the adapter and the manifest reviewer, not as a defect: it is the requested behaviour.","reproduction":"State: Doorbell deployed with the manifest values (minPay = maxPay = 1e18, bond = 0.25e18, deliverySecs = 604800, allowClaimed = 1); anyone transfers 1 IMD to it. Attacker A holds 0.25 IMD and approves the desk. Calls: A calls ring() 25 times (the 25th opens order N with escrow 1 IMD); A calls WorkOrders.claim(N) (bond 0.25 pulled, price fixed at 1e18); A calls register(N, 0x11111111111111111111111111111111) (any nonzero id); A calls claimDelivered(N, bytes32(uint256(1))); warp 24 hours; anyone calls finalize(N). Actual: A receives runnerPay 0.9e18 + bond 0.25e18 = 1.15e18 IMD; Payroll receives 0.1e18; the Doorbell receives 0 back and its hire is marked delivered (unproven) via onWorkDelivered. Expected under a receipt-only configuration (allowClaimed = 0): claimDelivered reverts ClaimedPathOff and the escrow returns to the Doorbell by timeout/refund.","snippet":"        if (ALLOW_CLAIMED) flags |= WORK_ORDERS.FLAG_ALLOW_CLAIMED();","citation":"resolved","blocking":false},{"severity":"info","title":"CrowdPots never calls WorkOrders.finalize: a pot whose runner claimed delivery and walked away stays Hiring until some third party calls the desk","path":"src/pots/CrowdPots.sol","line":794,"description":"_endWhatRanOut makes the desk call an order may be waiting for (refund, evict, timeout) but has no branch for State.Asserted. An unchallenged delivery claim is ended only by finalize, settle or reportFailed on the desk, none of which the pot makes. The contract header promises that a contributor needs neither a keeper nor any knowledge of the desk, and line 516 and test_* in test/pots/CrowdPots.t.sol (line 1464 onward) document the exception. Contributors are not at risk of loss beyond what the claimed path already implies (the runner is paid either way); the effect is that claimRefund reverts WrongStage(Hiring) indefinitely until someone calls WorkOrders.finalize(orderId) themselves. Recorded as a documented limitation so the adapter can decide whether reconcile should also call finalize when block.timestamp >= assertedAt + CHALLENGE_WINDOW; doing so would spend about CALLBACK_GAS + 2 x PAYROLL_GAS more gas in reconcile.","reproduction":"State: a pot with allowClaimed = true, target 10e18, filled and launched (order N). Runner R: claim(N), register(N, anyNonZeroId), claimDelivered(N, anyHash). Warp 25 hours (challenge window over, no challenger). Contributor C calls CrowdPots.claimRefund(potId, C). Actual: revert WrongStage(Stage.Hiring); stageOf(potId) reads Hiring; CrowdPots.reconcile(potId) returns Hiring and makes no desk call (desk state is Asserted). Repeat after any delay: same result until an external caller runs WorkOrders.finalize(N), after which reconcile moves the pot to Delivered and claimRefund pays C's share of accountOf(N).returned (zero here, since minPay == target). Expected by the header's wording: a contributor's own claimRefund call would be enough.","snippet":"        } else if (state == IWorkOrders.State.Registered || state == IWorkOrders.State.Disputed) {","citation":"resolved","blocking":false},{"severity":"info","title":"A pot's escrow can be held for its whole deliverySecs (up to 30 days) by any runner posting the opener-chosen bond, down to 0.25 IMD, by registering a job id that has no receipt","path":"src/pots/CrowdPots.sol","line":649,"description":"The opener chooses Terms.bond anywhere between the desk minimum (0.25 IMD) and the target. Once a runner has registered a job id, nothing can end the claim before registeredAt + deliverySecs unless a receipt exists for that id (reportFailed needs a record WorkProof can prove); a fabricated id has none. So on a receipt-only pot the runner loses only the bond to hold the whole target for deliverySecs, and when the order reopens (hire window not yet over) a fresh address can repeat it. The Terms documentation (lines 73-76) states this trade-off and the forfeited bonds are booked to the contributors through accountOf.returned, so it is a disclosed economic limit rather than a defect. Recorded so the adapter and the manifest reviewer see the ratio: with the minimum bond a 1,000 IMD pot is delayed 30 days for 0.25 IMD.","reproduction":"State: pot P opened with target 1000e18, bond 0.25e18, deliverySecs 30 days, hireSecs 30 days, allowClaimed false; filled and launched as order N. Attacker A (0.25 IMD): claim(N); register(N, 0x22222222222222222222222222222222). Calls by contributors during the next 30 days: CrowdPots.reconcile(P) -> desk.timeout(N) reverts TooEarly inside the try/catch, pot stays Hiring; claimRefund reverts WrongStage(Hiring); WorkOrders.reportFailed(N, anyRecord, []) reverts NoReceipt (no registry entry for that id). Actual: 1000 IMD unavailable for 30 days at a cost of 0.25 IMD to A, which is then paid to the pot by timeout. Expected by contributors reading only the target: the pot's money is either spent on the job or refundable when the hire window closes.","snippet":"        if (t.bond < WORK_ORDERS.MIN_BOND() || t.bond > t.target) revert BondOutOfRange();","citation":"resolved","blocking":false}],"coverage":null}]}