{"assessments":[],"deployments":[],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"b9c4399e-2616-411c-86e3-415b1e889f29","kind":"audit","nodes":[{"acceptedSubmissionHash":"39ee6f6d4342bc83ff4e1c75518cdc3302b30b1d9463ea9473e8da2014dad6b2","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"af7c807120fa052e3de285f46861e3e8c004fd18af56192ced58844c1b873d4d","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"067b0944bb874efb4eb46a782a246247699709c7ce0c9f44fe0817b8a56ec461","dependsOn":["audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"d03ecd763bae25e541c96ba8bcae5dec4848baa56d219168d400b4b0e47b1b18","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"ded9fb52f6ceee79d97d959f17187f69928bdbfac93bb89b71f0660edda586cc","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"}],"objective":"Audit the exact repository commit named in this request. This is the corrected fixed-term ETH agreement escrow plus its actual public-evidence/IMD-oracle/native-Claus arbitration boundary. Review all specified paths, not older readonly contracts or sample Counter modules. The previous original escrow review a272377e-2868-43fc-a142-37351c9d8b7f covered source9db5611861aed8099aba4e5578f8fb8f1bb560ff; its 7low/3info findings are corrected or explicitly disclosed in this candidate. Do not assume that previous report approves this changed code.\n\nContract subject: contracts/src/ClausDealEscrow.sol and its focused tests. Immutable native ETH agreements, named distinct-address arbiter, max deal/accounted custody limits, 24h buyer acceptance window, 3day dispute timeout to buyer, pull credits, buyer-only finite recurring runs, cancellation and explicit run index. No proxy, arbitrary admin withdrawal, fee or customer key. Check conservation, reentrancy, role separation, replay/time boundaries, plan accounting, forced ETH and live exit availability.\n\nOffchain subject: src/deals/*, runtime/deal-ports.mjs, runtime/arbitration-ports.mjs, adapters/imd-oracle.mjs, adapters/deal-oracle-proof.mjs, unchanged adapters/imd-payment.mjs and imd-cost.mjs, api/deals.mjs, shared identity/billing/storage boundaries, shared interfaces and docs/paid-deals.md. One hash-bound public case per deal, no private evidence, fixed 5-member/4-quorum zero-tolerance signed IMD oracle, original quote/order/payment retained, separate finite subsidy, exact independent result proposed to genuine native Claus, fresh current escrow and observation block checks, dedicated unfunded signer only signs Resolution for this contract/deal/amount/evidence/expiry. API never signs user transactions. Once issued signatures remain valid until expiry; docs disclose that and default timeout behavior. Check state races, identity/revocation, missing proof, unknown effects, duplicate or selective oracle results, changed parties/amount/input, malleable native decisions and ledger accounting.\n\nNative decision integration: exact source snapshots in reference/deal-review-native/ with mapping README/source hashes, shared core copied byte-identically at native build. Separate networkDealReview capability and pins, maximum8 lifetime decisions and unchanged2USDC/request payer ceiling, existing queue/unknown payment preservation, no direct model signing key. Existing readonly/managed contract profiles still reject financial products. Check whether genuine financial decisions and current authorization are actually required, not fabricated or inferred from a code audit.\n\nReport actual findings with path and impact. Explicitly distinguish source review from deployment/source verification/activation, provider evidence from truth/guaranteed fairness, immutable rules from infallibility, and required live acceptance from local fixtures. No deployment or funding of this escrow is requested from IMD.\n","parentJobId":null,"planHash":"78217f35ca3143e9aaca418c09579370c652827dd9887663e614430dc7626d04","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"b9c4399e-2616-411c-86e3-415b1e889f29","publication":{"commit":null,"deliveredAt":null,"repoUrl":null},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52257","feedbackHash":"f470ff3545874c1666fc1bab5fbec79b6c334ccf610ec389692404705fba7fb1","nodeKey":"audit_economics","submissionHash":"39ee6f6d4342bc83ff4e1c75518cdc3302b30b1d9463ea9473e8da2014dad6b2","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51485","feedbackHash":"efd5e691875d4c26baae1fb459ebc94b40e7a7e0965ef70d3239581566973ecd","nodeKey":"audit_flow","submissionHash":"af7c807120fa052e3de285f46861e3e8c004fd18af56192ced58844c1b873d4d","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52590","feedbackHash":"f5a27a7d9041f1d080b47669d813e9f138a6687d3aa1e1f3a13cb71ba0c61141","nodeKey":"audit_judge","submissionHash":"067b0944bb874efb4eb46a782a246247699709c7ce0c9f44fe0817b8a56ec461","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50966","feedbackHash":"39e977994a10aabf81642a8837dc6e8c530a7ff463a25591f9d4a1c817c544af","nodeKey":"audit_math","submissionHash":"d03ecd763bae25e541c96ba8bcae5dec4848baa56d219168d400b4b0e47b1b18","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52701","feedbackHash":"d2b8bedb473c561b1d23f9ab2d78f64bd5b0091a0a1362214d7d429a56e25c93","nodeKey":"audit_permissions","submissionHash":"ded9fb52f6ceee79d97d959f17187f69928bdbfac93bb89b71f0660edda586cc","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"c2b22106b60d686e9078eb431fd3dc9dddc82ed3d3cb40b0db345976668f711c","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"99e4819277ea59fc","findings":[{"citation":"resolved","description":"request() stores the requesting credential id as v.actor (line 28) and every tick() calls permitted(v) (line 36), which requires identity.allowed(v.actor,'deal.review',...). identity.credential() rejects any credential whose expiresAt has passed (src/identity/index.mjs:46), and owner credentials are issued with OWNER_TTL = 60*60*1000 (src/identity/index.mjs:20,146). The case pipeline (quote, x402 payment, 5-member panel, native Claus inference with 60 s re-queues, 3-confirmation chain checks) runs across many cron ticks, so an owner-requested case routinely passes the one-hour mark before signing. permitted() then returns false and tick() writes state 'blocked', error 'authority_closed'. caseId is deterministic per chain/contract/deal and request() returns any existing row before any other check (line 25), so neither party can open a replacement, even with a fresh session. The same binding gives the requester a unilateral veto: revoking the credential or mandate, archiving the project or starting a transfer (identity allowed() lines 66-68) after the oracle is paid kills the case; because the onchain default (settleExpired after resolutionDue) refunds the buyer, a buyer who requests first can block the provider's only arbitration path. identity.allowed() returning false for an expired or revoked requester credential adds nothing over the admission-generation check already performed in permitted(); the party standing check (owner address is buyer or worker) is what should persist. Impact: loss of the subsidised arbitration path (one of 8 lifetime cases plus the paid oracle order) and a buyer-controlled veto over resolution; escrow funds remain safe under the contract's timeout. Fix: after admission, gate tick() on admission generation and current party standing (project not archived, owner address still buyer or worker) rather than liveness of the single requesting credential, or let either party re-attach an authorised actor to the existing case without a new oracle purchase. Disclose in docs/paid-deals.md until fixed.","line":17,"path":"src/deals/arbitration.mjs","reproduction":"node --test test/scratch/repro.test.mjs test 'A' (uses test/apps-fixture.mjs with arbitrationEnabled, a Disputed deal whose buyer is the fixture owner, resolutionDue far in the future, and a stub advance that returns {state:'oracle-pending'}). Steps: core.request(ownerCredentialId, projectId, 1, dealId) -> 'queued'; core.tick() -> state 'oracle-pending', advance called once. Advance the fixture clock by 3600001 ms with no revocation and unchanged admission; core.tick() -> state 'blocked', error 'authority_closed', advance not called again. A fresh owner login (new credential id) calling core.request for the same deal returns the same case id in state 'blocked'; a further tick makes no progress. Expected: an admitted case keeps progressing while admission generation and the onchain deal are unchanged. Actual: the case is permanently dead one hour after the request. Output: 'A: state after TTL blocked authority_closed advances 1', 'A: re-request same case true blocked'.","severity":"medium","snippet":" async function permitted(v,reader=store){try{const p=await admission(reader);return p.generation===v.admissionGeneration&&await identity.allowed(v.actor,'deal.review',{projectId:v.projectId},reader);}catch{return false;}}","title":"Arbitration case is bound to the requester's credential; the 1-hour owner session expiry (or self-revocation) permanently blocks the only case for the deal"},{"citation":"resolved","description":"ClausDealEscrow.dispute() (contracts/src/ClausDealEscrow.sol:148) accepts any non-zero reasonHash with no bond or publication requirement, and settleExpired() (line 171) refunds the full amount to the buyer once resolutionDue passes. The only alternative outcome is a signed Resolution, whose sole issuer is the review case. request() (src/deals/arbitration.mjs:22) calls evidence.bundle(d), which requires a deal-evidence row for termsHash, resultHash and disputeHash of the matching kind and throws not_found / deal_evidence_mismatch otherwise; validateDealDecisionShape (src/deals/decision.mjs:27) requires bundle.dispute too. Only the buyer knows the dispute preimage. A buyer who receives a conforming delivery can call dispute(id, anyNonZeroHash) inside the 24 h window without publishing; then no party can open a case, the arbiter never signs, and the buyer collects the default refund while keeping the delivered work. docs/paid-deals.md:23-25 asks buyers to publish before disputing and says the three documents are required, but does not say that a missing buyer document removes the provider's only recourse. The contract behaves as written; the gap is the offchain admission rule combined with the buyer-favouring default. Fix (product decision): allow request() to open a case when only the dispute document is missing or of the wrong kind, passing an explicit 'dispute text unavailable' marker to the panel question and the native decision input (and adjust validateDealDecisionShape accordingly); or at minimum disclose in DEAL_RULES and docs/paid-deals.md that the provider has no arbitration recourse when the buyer's dispute document is unpublished.","line":20,"path":"src/deals/evidence.mjs","reproduction":"node --test test/scratch/repro.test.mjs test 'B': deal in state 4 with published terms and result documents, buyer's disputeHash set to 0x0101...01 (never published). core.request(partyCredential, projectId, 1, dealId) rejects with code 'not_found' and no deal-case-capacity row is written, so no case exists for either party. Onchain (existing contracts/test/ClausDealEscrow.t.sol lines 62-64 show the same path): funded(1 ether) -> claim -> deliver -> dispute(id, keccak256('second dispute')) -> warp 3 days + 1 -> settleExpired(id) credits the buyer 1 ether, workerPaid 0. Expected: the provider can still have the committed terms and result reviewed when the buyer's dispute text is missing, or the asymmetry is disclosed. Actual: a buyer-controlled missing preimage silently disables arbitration and guarantees a full refund. Output: 'B: request rejected not_found'.","severity":"medium","snippet":"  async bundle(deal,reader=store){const out={};for(const [kind,field]of [['terms','termsHash'],['result','resultHash'],['dispute','disputeHash']]){const evidence=await this.read(deal[field].toLowerCase(),reader);requireThat(evidence.document.kind===kind,'deal_evidence_mismatch');out[kind]=evidence;}return out;}","title":"Buyer alone controls whether arbitration can exist: an unpublished or wrong-kind disputeHash makes request() impossible and guarantees the 3-day full refund"},{"citation":"resolved","description":"request() admits any Disputed deal whose requesting project's owner address is the deal's buyer or worker (line 24), subject only to the global lifetime counter (8), projectDaily 1 and globalDaily 4 (lines 25-27). Nothing requires a minimum amount, distinct principals, or a requester-paid fee. The escrow accepts msg.value >= 1 wei (contracts/src/ClausDealEscrow.sol:221,228) and the decision validator only requires amount >= 1 (src/deals/decision.mjs:26). Each admitted case reserves up to IMD_ORACLE.maximum (0.5e18 of the review asset, adapters/imd-oracle.mjs:6) from the 'deal-arbitration-v1' ledger, and if it reaches the native host uses one of the 8 lifetime native decisions (reference/deal-review-native/network-review.ts:68) at up to 2 USDC. The daily project limit is keyed by projectId and an owner may hold up to 10 projects, so one actor needs only the 4/day global cap: two days of 1-wei deals between two addresses it controls set deal-case-capacity.total to 8, after which discovery() reports enabled:false and genuine disputes fall to the buyer-refund default. Fix: require BigInt(d.amount) >= a configured minimum (for example a minimumArbitrableWei in the pin), bind daily/lifetime counters to the owner principal rather than the project, and/or require a requester-paid deposit before consuming capacity.","line":25,"path":"src/deals/arbitration.mjs","reproduction":"node --test test/scratch/repro.test.mjs test 'C': Disputed deal with amount '1' (1 wei), buyer = fixture owner address, all three documents published. core.request(ownerCredential, projectId, 1, dealId) returns state 'queued' and deal-case-capacity.global.total becomes 1. Repeating with 4 projects per day over two days (projectDaily 1, globalDaily 4) reaches total 8, after which any request fails 'deal_arbitration_capacity'. Expected: the subsidy is reserved for genuine disputes between distinct parties above a meaningful value. Actual: about 8 wei plus gas consumes it. Output: 'C: total 1'.","severity":"low","snippet":"    const old=await tx.read('deal-cases',id);if(old)return publicCase(old.value);const p=await admission(tx),counter=await tx.read('deal-case-capacity','global'),total=counter?.value.total??0;requireThat(total<ARBITRATION_LIMITS.cases,'deal_arbitration_capacity');","title":"Finite 8-case arbitration subsidy can be exhausted by one principal with 1-wei self-dealt disputes; no minimum amount, per-principal limit or requester cost"},{"citation":"resolved","description":"observed() admits a case when resolutionDue > now + minRemainingSeconds (3600) and the identical check is repeated on every tick(); its failure code 'deal_not_reviewable' is in the permanent list (line 41) and blocks the case. Downstream, validateDealDecisionShape (src/deals/decision.mjs:26) and the native host (reference/deal-review-native/network-review.ts:43,59,98) require proposal.expiresAt > now + 3600 at their own later times, with expiresAt <= resolutionDue. The admission minimum therefore leaves zero time for the quote, x402 payment, five-member panel, native inference (up to 650 s plus 60 s re-queues) and confirmations. Any case requested with resolutionDue - now in (3600, 3600 + pipeline latency] is admitted, permanently counts against the 8 lifetime cases (deal-case-capacity.total is never decremented) and the daily caps, usually pays the IMD oracle order in the first tick, and then cannot produce a resolution. docs/paid-deals.md:25 tells users 'at least an hour remaining' is enough. Escrow funds are not at risk; the finite subsidy is. Fix: admission should require minRemainingSeconds = 3600 + worst-case oracle + native + signing latency (for example 3 h), or the downstream margins should be relaxed; document the real minimum.","line":16,"path":"src/deals/arbitration.mjs","reproduction":"node --test test/scratch/repro.test.mjs test 'D': Disputed deal with resolutionDue = now + 3700 s. core.request(...) returns 'queued' and total = 1. First core.tick(): advance runs (the stub stands in for quote+payment) and the case is 'oracle-pending'. Advance the clock 120 s and tick again: observed() sees 3580 s remaining <= 3600, throws deal_not_reviewable, and the case becomes 'blocked' with advance called only once. Expected: admission rejects a case that cannot finish. Actual: admitted, charged, and certain to fail. Output: 'D: blocked deal_not_reviewable advances 1'.","severity":"low","snippet":" async function observed(chainId,dealId){const c=await dealPorts.context(chainId,{fresh:true,confirmations:3}),d=await dealPorts.read(c,'deal',dealId);requireThat(d.state===4&&Number(d.resolutionDue)>seconds(now)+ARBITRATION_LIMITS.minRemainingSeconds,'deal_not_reviewable');return {c,d};}","title":"Admission requires only 3600 s remaining, the same margin every later stage needs, so cases admitted near the deadline pay the oracle and consume a lifetime slot with no possible resolution"},{"citation":"resolved","description":"advance() reserves offer.maximum (up to 0.5e18) from the 'deal-arbitration-v1' ledger (line 26) before any imd-payments record exists. The case can then stop before payment.prepare commits its 'signing' row: stillAllowed() fails at line 28 (revocation, admission generation bump, capability disable), authorize() returns false inside prepare, or provider.challenge fails transiently (provider_outcome_unknown) and the fixed quote (quoteId 'deal-oracle:'+caseId, at most 600 s validity, adapters/imd-oracle.mjs:58; provider.quote returns the stored quote forever, lines 76-82, without checking expiresAt) expires, after which every later prepare throws payment_window_expired (adapters/imd-payment.mjs:49). Nothing requests a replacement quote for the same inputHash and abandonUnsent is never called. The only release path, reconcile() (line 55), skips any allocation whose payment.status() is null ('if(!old)continue'), and settleCost requires a chain receipt. So a hold with no signed payment, where no uncertain effect exists to preserve, is kept forever, the allocation row stays billing:'reserved', and later reserve() calls fail funding_insufficient. The stalled case keeps retrying payment_window_expired each tick until observed() blocks it at resolutionDue-3600, consuming a lifetime slot. Fix: in advance(), when payment.status(offer.id) is null (or 'cancelled' via abandonUnsent) and offer.expiresAt <= now()+60000, settleReservation(tx, ACCOUNT, ASSET, offer.maximum, '0') inside a transaction, mark the allocation 'abandoned', and obtain a fresh quote with a suffixed quoteId still bound to the same inputHash; keep the existing hold semantics for anything that reached signing. Let reconcile() release reservations of terminal cases that have no payment record.","line":55,"path":"runtime/arbitration-ports.mjs","reproduction":"node --test test/scratch/repro.test.mjs test 'F/G' (in-memory store, real arbitrationPorts + createImdOracleProvider + createImdPayment, stubbed fetch for /requests/quote and /requests/{id}/submit, ledger funded with 1e18, capability enabled at generation 1). Tick 1: quote 5e17 returned and checkpointed, reserve() succeeds, challenge throws a network error -> advance rejects provider_outcome_unknown; balance available 5e17 / reserved 5e17. Clock +660 s, challenge now answers 402 with the same (expired) quote -> advance rejects payment_window_expired, again +60 s -> payment_window_expired; no imd-payments row exists. ports.reconcile() leaves available 5e17 / reserved 5e17 and the allocation billing 'reserved'. Second case: stillAllowed returns true on the first call and false on the second -> advance rejects authority_closed with the ledger now available 0 / reserved 1e18, and reconcile() leaves it unchanged. Expected: a hold with no payment intent is released once the case is terminal or the quote is dead, and a fresh quote is obtained for the same input. Actual: the subsidy is held forever and the case stalls. Output: 'F: after tick1 ... reserved: 500000000000000000', 'G: tick2 error payment_window_expired', 'F: after authority_closed ... available: 0, reserved: 1000000000000000000'.","severity":"low","snippet":"  async reconcile(){for(const row of await store.list('deal-oracle-allocations',{limit:8})){const a=row.value;if(a.billing!=='reserved')continue;const old=await payment.status(a.offer.id);if(!old)continue;try{const state=await payment.reconcile(a.offer.id);await settleCost(a.offer,state,a.inputHash);}catch{}}}","title":"Oracle subsidy reservation is never released when a case stops before a payment record exists, and an expired unpaid quote is never re-quoted so the case stalls until the deadline"},{"citation":"resolved","description":"validateDealDecisionShape computes BigInt(d.amount)<=BigInt(pin.maximumDealWei). The pins come from the same CLAUS_DEAL_PINS that runtime/deal-ports.mjs:11 validates (api/deals.mjs:20 passes JSON.parse(CLAUS_DEAL_PINS) to arbitrationPorts; reference/deal-review-native/network-deal-review.ts:8 reads the same variable). deal-ports requires only chainId, contract, codeHash, arbiter, admissionController and reviewed; it never checks maximumDealWei, and test/deals.test.mjs:28 builds an accepted pin without it. For such a pin BigInt(undefined) throws a TypeError with no code for every valid case. In advance() this is first reached at runtime/arbitration-ports.mjs:36, after the oracle order has been paid and attested; tick() treats a code-less error as transient and retries every tick until the deadline blocks the case, so every case under that configuration spends the oracle fee and a lifetime slot. If the field is present but differs from the onchain immutable maximumDeal, the bound is wrong in one direction or the other. The chain value is already available: verified() in deal-ports reads maximumDeal() into observation.maximumDealWei, which the case stores as o. Fix: have deal-ports require a decimal maximumDealWei equal to the onchain maximumDeal(), or compare against BigInt(o.maximumDealWei) (and require it to equal the pin); run the shape check in request() before any oracle spend.","line":26,"path":"src/deals/decision.mjs","reproduction":"node --test test/scratch/repro.test.mjs test 'E': build a valid decision input exactly as test/deal-arbitration.test.mjs does (attested oracle proof, workerAmount 600 of amount 1000). With pins=[{chainId:1,contract,codeHash,arbiter,admissionController,reviewed:true,maximumDealWei:'1000'}] validateDealDecisionShape passes. With the same pin minus maximumDealWei (which runtime/deal-ports.mjs accepts) it throws 'TypeError: Cannot convert undefined to a BigInt' with e.code undefined. Expected: an invalid pin is rejected at configuration time with a coded error, or the chain-read maximum is used. Actual: an uncoded TypeError after payment, retried forever. Output: 'E: TypeError Cannot convert undefined to a BigInt code= undefined'.","severity":"low","snippet":" requireThat(d.state===4&&/^0x[0-9a-fA-F]{40}$/.test(d.buyer)&&/^0x[0-9a-fA-F]{40}$/.test(d.worker)&&d.buyer.toLowerCase()!==d.worker.toLowerCase()&&![d.buyer.toLowerCase(),d.worker.toLowerCase()].includes(pin.arbiter.toLowerCase())&&/^[1-9][0-9]*$/.test(d.amount)&&BigInt(d.amount)<=BigInt(pin.maximumDealWei)&&/^(0|[1-9][0-9]*)$/.test(p.workerAmount)&&BigInt(p.workerAmount)<=BigInt(d.amount)&&Number.isSafeInteger(p.expiresAt)&&p.expiresAt>Math.floor(now/1000)+3600&&p.expiresAt<=Number(d.resolutionDue),'network_review_invalid_input');","title":"Decision validator bounds the amount with the unvalidated pin field maximumDealWei instead of the chain-read maximumDeal; a pin that deal-ports accepts throws TypeError after the oracle is paid"},{"citation":"resolved","description":"api/deals.mjs is the only caller of createArbitration().tick() and arbitrationPorts().reconcile(), and it runs only on a GET carrying the CRON_SECRET bearer. vercel.json schedules /api/compile and /api/workflows only; api/workflows.mjs does not dispatch the deal tick (no reference to deals), .github/workflows is empty and no file in the tree references /api/deals as a scheduled path. Because request() admits and counts cases synchronously (src/deals/arbitration.mjs:25-28) while all progress happens in tick(), the public API would accept and permanently count up to 8 lifetime cases that never leave state 'queued'. This is a source-level observation about the committed deployment configuration; it says nothing about what is scheduled in the operator's live Vercel project, and no deployment or activation is requested here. Fix: add {\"path\":\"/api/deals\",\"schedule\":\"* * * * *\"} and a functions entry for api/deals.mjs with a maxDuration covering the 25 s native and 30 s provider timeouts, or document the external scheduler and have discovery() refuse new cases when worker-health/deals.observedAt is stale.","line":35,"path":"vercel.json","reproduction":"Inspect vercel.json: \"crons\" (line 35) lists only /api/compile and /api/workflows; \"functions\" has no api/deals.mjs entry. grep -rn \"api/deals\" over the tree (excluding node_modules) returns no scheduler, workflow or documentation reference. Deploying this tree as committed with CLAUS_DEAL_ARBITRATION_ENABLED=1 and the deal-arbitration capability enabled, then calling deal_review_request on a Disputed deal, writes deal-case-capacity.global.total += 1 and a 'queued' case; nothing in the repository ever invokes /api/deals, so public_deal_case stays {state:'queued'} until the onchain timeout. Expected: the only deal worker is scheduled or its external scheduler is documented. Actual: neither is in the tree.","severity":"low","snippet":"  \"crons\": [","title":"Committed deployment configuration schedules no worker for /api/deals, so admitted cases are counted but never processed by anything in the tree"}],"hash":"067b0944bb874efb4eb46a782a246247699709c7ce0c9f44fe0817b8a56ec461","nodeId":"bf3971f1-3222-46ef-9fac-cad30d188e47","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds the judged result.\n\n**Outcome.** All ten specialist findings reproduced against commit 935c323. I merged them into 7 findings (2 medium, 5 low), each with a local reproduction. No critical or high finding exists, so no Foundry proof was attached. The escrow contract itself passed every check I ran, and the existing 5 Foundry tests and the deal, arbitration and payment node suites pass.\n\n**Reproduction.** I wrote one node test file under `test/scratch/` using the repo's in-memory fixture, the real arbitration ports, oracle provider and payment adapter with stubbed HTTP. All six scenarios behaved exactly as the specialists claimed.\n\n**Kept findings**\n- **Medium:** the case is bound to the requester's credential, so the 1-hour owner session expiry or a self-revocation permanently blocks the deal's only case. The requester also gains a veto after the oracle is paid.\n- **Medium:** a buyer who disputes with an unpublished or wrong-kind hash makes any case impossible, and the contract default then refunds the buyer in full. Three specialist reports of this were merged.\n- **Low:** 1-wei self-dealt disputes can exhaust the 8-case subsidy (two reports merged).\n- **Low:** admission requires only the same 3600 s margin as every later stage, so near-deadline cases pay the oracle and die.\n- **Low:** subsidy reservations with no payment record are never released, and an expired unpaid quote is never re-quoted (two reports merged).\n- **Low:** the decision validator uses an unvalidated pin field, so an accepted pin without it throws an uncoded TypeError after payment.\n- **Low:** the committed vercel.json schedules no worker for the deals endpoint.\n\n**Boundaries to keep in mind.** This is a source review of the pinned tree, not verification of any deployment, pinned code hash, or activated capability. The native snapshot hashes in source.json match the files, but byte-identical copying at native build time was not observable here. IMD panel attestations and Claus decisions are provider evidence, not truth or guaranteed fairness. The contract's immutable rules worked as specified in every path I traced, which is not proof of infallibility. All reproductions ran against local fixtures. Live acceptance by the operator remains required and was not performed.","treeHash":null,"usage":{"cachedInputTokens":1645289,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":33089,"runtime":"claude","turns":39,"wallClockMs":593286}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"22c9b760d0a15149","findings":[{"citation":"resolved","description":"Economic flow gap across the contract and the review path. ClausDealEscrow.dispute (contracts/src/ClausDealEscrow.sol:148) accepts any non-zero reasonHash with no bond and no publication requirement, and after DISPUTE_WINDOW settleExpired refunds the whole amount to the buyer (line 171). The only way for the provider to obtain a different outcome is a signed Resolution, and the only issuer is the review case. request() builds the case with evidence.bundle(d) (src/deals/evidence.mjs:20), which throws not_found unless the buyer's dispute document was published under exactly d.disputeHash. So a buyer who commits an unpublished or garbage hash makes arbitration structurally impossible for the provider while keeping the zero-cost default refund. docs/paid-deals.md:11 discloses the timeout refund as a provider risk but does not disclose that the buyer unilaterally controls whether the subsidised review can be opened at all; line 25 only says the three matching documents are required. This is a design/economics gap, not a code bug in the contract, and the contract behaves exactly as written.","line":22,"path":"src/deals/arbitration.mjs","reproduction":"State: deal funded (1 ether), worker claim + deliver(resultHash = hash of a published result doc). Buyer calls dispute(id, keccak256('anything')) without ever calling deal_evidence_publish for a dispute document. Provider's project owner (worker address) calls deal_review_request(projectId, chainId, dealId). Expected: a case can be opened and the panel can judge terms vs result (the dispute being unsubstantiated). Actual: evidence.bundle throws not_found (HTTP 404) because deal-evidence has no row for disputeHash; no case exists; after resolutionDue anyone calls settleExpired and credits[buyer] == 1 ether, credits[worker] == 0 although the result was delivered and committed. Fix options (requires a product decision): (a) at minimum document in docs/paid-deals.md that the buyer must publish the dispute document before disputing or the provider has no review path, and surface this in the dispute prepare response; (b) allow request() to open a case when only the dispute document is missing, passing an explicit 'dispute text unavailable' marker to the panel and native decision; (c) an on-chain dispute bond or a split default would change the agreed economics and is noted only as a tradeoff.","severity":"low","snippet":"   requireThat(/^0x[0-9a-fA-F]{64}$/.test(dealId??''),'invalid_deal_id');dealId=dealId.toLowerCase();await identity.authorize(actor,'deal.review',{projectId});const {c,d}=await observed(chainId,dealId),id=caseId(chainId,c.public.contract,dealId),bundle=await evidence.bundle(d);","title":"Buyer alone decides whether a dispute can ever be arbitrated, and the default outcome favours the buyer"},{"citation":"resolved","description":"Economic security. request() admits any Disputed deal whose owner is a party; there is no floor on d.amount and no relation between the deal value and the external cost a case triggers (an IMD oracle order of up to IMD_ORACLE.maximum = 0.5e18 units of the review asset, adapters/imd-oracle.mjs:6, plus a native inference request admitted at up to 2 USDC, reference/deal-review-native/network-review.ts:106). The contract allows amount >= 1 wei (contracts/src/ClausDealEscrow.sol:228) and the same operator may control both party addresses (docs/paid-deals.md:13 admits different addresses do not prove independence). ARBITRATION_LIMITS is cases:8 lifetime, projectDaily:1, globalDaily:4, with no refill, and the lifetime counter is incremented at admission regardless of the later outcome (lines 25-28). The native host independently caps deal reviews at 8 lifetime (network-review.ts:68).","line":25,"path":"src/deals/arbitration.mjs","reproduction":"Inputs: one account with two owned projects whose verified owner address is the buyer, plus any second address as worker. Day 1: createDeal with msg.value = 1 wei twice (different salts), worker claim+deliver, buyer dispute with published terms/result/dispute docs, deal_review_request from project A and project B. Day 2: repeat with two more deals. Day 3-4: repeat. Expected: subsidy spent proportionally to real disputed value, or a minimum stake. Actual: after 8 requests deal-case-capacity.global.total == 8 and discovery() returns enabled:false, remainingCases:0 for every other user permanently; each admitted case also reserves offer.maximum (up to 0.5e18) from the deal-arbitration-v1 ledger account and may spend up to 2 USDC of native inference, for 1 wei of disputed value. Fix: require BigInt(d.amount) >= a configured minimum (for example at least the oracle quote maximum expressed in the pinned chain's native unit, or an explicit minimumArbitrableWei in the pin) before consuming capacity, and/or bind the daily/lifetime counters to the owner principal rather than the project.","severity":"low","snippet":"    const old=await tx.read('deal-cases',id);if(old)return publicCase(old.value);const p=await admission(tx),counter=await tx.read('deal-case-capacity','global'),total=counter?.value.total??0;requireThat(total<ARBITRATION_LIMITS.cases,'deal_arbitration_capacity');","title":"Finite arbitration subsidy can be exhausted with 1-wei self-dealt disputes; no minimum amount or cost ratio"},{"citation":"resolved","description":"Flow gap between the idempotent quote, the payment adapter and the ledger. The quote is keyed by the fixed quoteId 'deal-oracle:'+caseId and checkpointed on the case; provider.quote() returns the stored quote forever (adapters/imd-oracle.mjs:76-82) without checking quote.expiresAt. IMD quotes are valid at most 600 s (imd-oracle.mjs:58). createImdPayment.prepare rejects a quote with q.expiresAt <= now+60 (adapters/imd-payment.mjs:49) and never writes an imd-payments row in that case. Nothing in advance() ever requests a replacement quote for the same inputHash, and abandonUnsent (imd-payment.mjs:37) is never called. The 'no retry buys a new result' rule (arbitration.mjs:39) is about oracle results, not about an unpaid, unsigned quote; re-quoting the identical inputHash cannot change the result. Separately, reserve() moves offer.maximum from available to reserved in the deal-arbitration-v1 account (line 26) and reconcile() (line 55) only ever settles a reservation when payment.status returns a row and a chain receipt exists, so a never-signed reservation is held forever even after the case is terminal.","line":25,"path":"runtime/arbitration-ports.mjs","reproduction":"State: case queued, tick 1 runs advance(): provider.quote succeeds and is checkpointed, reserve() succeeds, payment.status is null, payment.prepare -> provider.challenge throws provider_outcome_unknown (any transient 5xx/timeout). advance throws, tick stores lease:null, case stays oracle-pending. Tick 2 at least ~10 minutes later (or any later tick after the quote's <=600 s validity): v.offer exists, payment.status is still null, payment.prepare -> challenge returns the same quote with expiresAt in the past -> requireThat(q.expiresAt > seconds()+60) throws payment_window_expired. Expected: a fresh quote for the same inputHash is obtained (no payment was ever signed), or the case fails fast and the reservation is released. Actual: every tick repeats payment_window_expired until observed() throws deal_not_reviewable at resolutionDue-3600 and the case is blocked; one of the 8 lifetime cases is consumed; balances[deal-arbitration-v1].reserved stays increased by offer.maximum with no settlement path; the parties fall to the default timeout outcome. Fix: in advance(), when payment.status(offer.id) is null (or state 'cancelled') and offer.expiresAt <= now()+60000, call payment.abandonUnsent inside a transaction, settleReservation(tx, ACCOUNT, ASSET, offer.maximum, '0'), mark the allocation 'abandoned', and request a new quote with a suffixed quoteId (for example 'deal-oracle:'+v.id+':'+attempt) still bound to the same inputHash; keep the existing hold semantics for anything that reached signing.","severity":"low","snippet":"   if(!offer){requireThat(await stillAllowed(),'authority_closed');offer=await provider.quote({projectId:v.id,inputHash,quoteId:'deal-oracle:'+v.id});await checkpoint({offer,state:'oracle-pending'});}","title":"Expired unpaid oracle quote permanently stalls a case and keeps the subsidy reservation held"},{"citation":"resolved","description":"Activation/flow gap. api/deals.mjs is the only caller of createArbitration().tick() and arbitrationPorts().reconcile(), and it only runs when a GET with the CRON_SECRET bearer arrives. vercel.json schedules /api/compile and /api/workflows only; api/workflows.mjs does not dispatch the deal tick; .github/workflows is empty; no document in docs/ names an external scheduler for /api/deals (api/domains.mjs and api/workers.mjs share the same omission, so an undocumented external scheduler may exist, but the repository does not show one). Because request() admits and counts cases synchronously (src/deals/arbitration.mjs:25-28) while all progress happens only in tick(), the public API can accept and permanently count up to 8 lifetime cases that never move from state 'queued'. This is a source-level observation about the committed deployment configuration; it says nothing about what is actually scheduled in the operator's Vercel project.","line":35,"path":"vercel.json","reproduction":"State: this tree deployed as committed with CLAUS_DEAL_ARBITRATION_ENABLED=1, CLAUS_DEAL_PINS set, and network-capabilities/deal-arbitration {enabled:true, profile:'public-deliverable-v1', generation:N}. A party calls deal_review_request on a Disputed deal with published documents. Expected: within a few minutes public_deal_case shows oracle-pending/claus-pending and eventually resolution-ready, blocked or failed. Actual: public_deal_case stays {state:'queued'} indefinitely; deal-case-capacity.global.total is incremented; no oracle order, reservation or native review is created; the deal settles by the on-chain timeout. Fix: add {\"path\":\"/api/deals\",\"schedule\":\"* * * * *\"} (and a functions entry with a maxDuration that covers the 25 s native + 30 s provider timeouts) to vercel.json, or document the external scheduler and have discovery() report worker-health/deals.observedAt so admission can refuse new cases when the worker has not run recently.","severity":"low","snippet":"  \"crons\": [","title":"Deal arbitration worker endpoint has no scheduler in vercel.json; admitted cases are never processed by anything in the tree"}],"hash":"39ee6f6d4342bc83ff4e1c75518cdc3302b30b1d9463ea9473e8da2014dad6b2","nodeId":"77006b44-7372-48ec-8752-2f6e8fcdc03e","outcome":"completed","summary":"The review is complete. The findings file holds four low-severity items. Nothing in the tree was modified apart from `.imd-findings.json`.\n\n**Contract result.** I found no critical, high or medium defect in `contracts/src/ClausDealEscrow.sol`. Conservation holds on every path: `totalLocked + totalCredits` tracks deposits minus withdrawals, plan runs move value from plan budget to deal without double counting, and cancellation returns only the unspent remainder. The only external call is in `withdraw`, which is guarded and updates credits first. Signature verification against the arbiter is a staticcall and cannot reenter. The EIP-712 domain, typehash and the off-chain typed data match field for field. Time boundaries are consistent at every edge between the party action and the permissionless timeout. Role separation holds. Replay is blocked by the deal moving to Settled. The existing five Foundry tests pass. The privileged powers are limited to pausing new deposits and signing a split within one deal's amount, and both are disclosed.\n\n**Findings written** (all low, each with a concrete state):\n\n- **Buyer controls arbitrability.** A dispute needs only a non-zero hash, costs nothing, and defaults to a full buyer refund. The review request requires the dispute document to be published, so an unpublished hash denies the provider any review path. The docs disclose the timeout risk but not this lever.\n- **Subsidy exhaustible by 1-wei disputes.** No minimum deal amount gates a case, so eight trivially small self-dealt disputes consume the lifetime allocation plus oracle and inference spend.\n- **Expired unpaid quote stalls a case for good.** The quote is idempotent per case and never re-requested. One transient provider failure between quote and payment leaves the case retrying until the deadline, and the ledger reservation is never released.\n- **No scheduler for the deal worker.** The committed `vercel.json` schedules only compile and workflows. Cases are admitted and counted synchronously but only progress inside the tick, which nothing in the tree invokes.\n\n**Boundaries I want to state plainly.** This is a source review at commit 935c323 only. The deal escrow appears in no deployment or source-verification manifest, so nothing here speaks to a live deployment, its constructor arguments, or who holds the controller and arbiter keys. The native snapshot hashes in `source.json` match the five files in the tree, and the shared core is referenced rather than duplicated, but I could not verify what the native host actually builds or that its configuration enables the deal capability. The oracle attestation and the native decision are provider evidence, checked for structure, binding and signature, not for truth or fairness. The contract rules are immutable but not infallible: the economics findings above are exactly the kind of outcome the rules permit. Acceptance of a resolution on chain requires a live signed message at a real deployment, which the local fixtures do not exercise. No deployment or funding was performed or is implied.","treeHash":null,"usage":{"cachedInputTokens":1874843,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":44598,"runtime":"claude","turns":44,"wallClockMs":684394}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"234a4a00ca903cb9","findings":[],"hash":"af7c807120fa052e3de285f46861e3e8c004fd18af56192ced58844c1b873d4d","nodeId":"8f99a8a3-0919-4840-a6f1-b0008c298802","outcome":"completed","summary":"I found no defects I could back with a concrete reproduction, so `.imd-findings.json` at the repo root holds `{\"findings\":[]}`. The existing escrow tests pass (`ClausDealEscrow.t.sol`, 5/5). That doesn't prove the code is defect-free. It only means nothing I traced led to a failing input I could demonstrate.\n\n**What I traced in the contract (`contracts/src/ClausDealEscrow.sol`):**\n- **Money in = money out:** every way out (deal settlement, plan cancellation, plan expiry) moves the exact locked amount out of `totalLocked` and into `totalCredits` and the parties' credits. `withdraw` reduces credits before sending ETH and has a reentrancy guard. Accounting never reads the contract's ETH balance, so ETH forced in from outside can't affect it.\n- **Time boundaries:** each deadline check is the exact opposite of its timeout path, with no gap or overlap. `claim`/`deliver` work up to `deliveryDue` and `settleExpired` only after it; the same holds for `dispute` vs `reviewDue` and `resolve` vs `resolutionDue`. `resolve` also rejects signatures past their `expiresAt`.\n- **Roles and replay:** buyer, worker and arbiter must all be different addresses. A resolution signature is tied to this contract and chain (EIP712) and to one deal ID. Deal IDs can't be reused, and IDs from `createDeal` and from plan runs come from separate namespaces, so they can't collide.\n- **Plans:** the budget must equal price × runs. Runs can only be triggered by the buyer and must give the expected run index, and each run requires the previous one to have paid the provider in full. Cancelling returns only the unspent budget. If a run is refunded or partly resolved, the plan stalls until the buyer cancels it; `docs/paid-deals.md` discloses this.\n\n**Off-chain arbitration (`src/deals/*`, `runtime/arbitration-ports.mjs`):**\n- There is one case per deal, with a lifetime cap of 8 and daily caps per project and overall.\n- The oracle input is tied to the case. The oracle quote and payment are reused rather than bought again.\n- Claus's decision is bound to hashes of the input, the proposal and the oracle artifact, and Claus can only approve or reject the proposed amount. Authorization is checked again around signing, along with a fresh read of the deal and a check that the observed block wasn't reorged.\n- The docs say that a signature, once issued, stays valid until its expiry.\n\n**What I didn't cover:**\n- I read `adapters/imd-oracle.mjs` and `api/deals.mjs` only in part.\n- I did not review `src/deals/abi.mjs` (1,248 lines), the snapshots in `reference/deal-review-native/`, or the shared identity, billing and storage code in depth.\n- I wrote no tests of my own.\n\nThis is a source review only. It says nothing about deployment, source verification on an explorer, or activation. The tests are local fixtures, not live acceptance. The oracle and Claus checks verify how answers are bound and authorized, not that they are true or fair.","treeHash":null,"usage":{"cachedInputTokens":298207,"inputTokens":16,"model":"claude-opus-5-5","outputTokens":3907,"runtime":"claude","turns":8,"wallClockMs":72168}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"759c614fdc84ff66","findings":[{"citation":"resolved","description":"request() admits a case when resolutionDue > now + minRemainingSeconds (3600), via observed() at line 16. The same observed() check is repeated on every tick(), and a failure is classed as permanent ('deal_not_reviewable' -> state 'blocked'). Downstream, validateDealDecisionShape (src/deals/decision.mjs:26) and the native host (submitNetworkReview / expireQueued / runNetworkReview) also require proposal.expiresAt > now + 3600, with proposal.expiresAt <= resolutionDue. Every stage therefore needs resolutionDue - now > 3600 at its own later time, but admission only requires that at request time. The admission minimum leaves no time for the work: the quote, x402 payment, five-member panel, native inference (up to 650s plus 60s credit re-queues) and the 3-confirmation checks. Any case requested with resolutionDue - now in (3600, 3600 + pipeline latency] is accepted, permanently uses one of the 8 lifetime subsidy cases plus the project and global daily slots (deal-case-capacity.total is never decremented), and usually pays the IMD oracle order (up to 0.5 IMD reserved and spent in the first tick). It then cannot produce a resolution. docs/paid-deals.md tells users that 'at least an hour remaining' is enough, so a user who follows the documentation spends the network's finite subsidy for nothing. Escrow funds are not at risk because the contract's timeout still refunds the buyer.","line":4,"path":"src/deals/arbitration.mjs","reproduction":"State: a deal in Disputed (4) with resolutionDue = now + 3700. Call request(actor, projectId, 1, dealId): it is accepted ('queued', capacity total = 1). In the first tick(), ports.advance quotes and pays the oracle and returns 'oracle-pending'. 120 s later, while the panel is still running, the next tick() calls observed(): 3700 - 120 = 3580 <= 3600, so it throws deal_not_reviewable, which is permanent, and the case becomes 'blocked'. The oracle is paid, the lifetime slot is used, and no signature is possible. Expected: admission rejects cases that cannot finish, by requiring minRemainingSeconds = 3600 + the worst-case oracle + native + signing latency (for example 3 h), or the downstream margins are relaxed. Actual: the case is admitted and is certain to fail. I ran this locally as a node:test against test/apps-fixture.mjs with createArbitration({now}) and a stub advance that counts the oracle payment. Output: 'blocked deal_not_reviewable 1', with advance called once.","severity":"low","snippet":"export const ARBITRATION_LIMITS=Object.freeze({cases:8,projectDaily:1,globalDaily:4,leaseMs:120000,minRemainingSeconds:3600,panelSize:5,quorum:4});","title":"Case admission minimum (3600s) equals every downstream minimum, so cases admitted near the deadline consume a lifetime slot and the paid oracle with no possible resolution"},{"citation":"resolved","description":"advance() reserves offer.maximum (up to 0.5 IMD) from the finite 'deal-arbitration-v1' ledger (line 26) before any imd-payments record exists. Several things can then stop the case before payment.prepare commits its 'signing' record: stillAllowed() fails at line 28 (identity revocation, an admission generation bump, or capability disable); authorize() returns false inside prepare and throws payment_not_authorized; or provider.challenge fails transiently and the fixed quote (quoteId 'deal-oracle:'+caseId, at most 600 s lifetime, never re-quoted because provider.quote returns the stored intent.quote) expires, after which every retry throws payment_window_expired. The case then ends 'blocked' via tick(). The only release path, reconcile(), skips any allocation whose payment.status() is null ('if(!old)continue'), and nothing else calls settleReservation for it. Here nothing was ever signed, so there is no uncertain effect to preserve. The reserved amount is still never returned to 'available', and the 'billing: reserved' allocation row is permanent. With a subsidy sized for the 8 lifetime cases, each such stop shrinks the remaining budget by up to 0.5 IMD, and later reserve() calls fail with funding_insufficient. The case-level 'holds are not erased' comment is meant for uncertain payments. It does not cover holds with no payment intent.","line":55,"path":"runtime/arbitration-ports.mjs","reproduction":"Ledger 'deal-arbitration-v1' is funded with 1e18 IMD-wei and the deal-arbitration capability is enabled at generation 1. Run advance(v, {checkpoint, stillAllowed}) for a queued case, where the provider quote returns amount 5e17 and stillAllowed returns true on its first call and false on its second (authority revoked right after the hold). advance throws authority_closed. The balance is then available=5e17, reserved=5e17, spent=0. ports.reconcile() leaves it unchanged, and the allocation stays billing:'reserved'. Expected: once the case is terminal and no imd-payments record exists (or the record is 'cancelled' via abandonUnsent), settleReservation(tx, ACCOUNT, ASSET, maximum, '0') releases the hold. Actual: the 5e17 is held forever. I confirmed this locally with a node:test that uses the in-memory store, the real arbitrationPorts/createImdOracleProvider and a stubbed /requests/quote fetch.","severity":"low","snippet":"  async reconcile(){for(const row of await store.list('deal-oracle-allocations',{limit:8})){const a=row.value;if(a.billing!=='reserved')continue;const old=await payment.status(a.offer.id);if(!old)continue;try{const state=await payment.reconcile(a.offer.id);await settleCost(a.offer,state,a.inputHash);}catch{}}}","title":"Oracle subsidy reservation is stranded forever when a case stops before any payment intent exists"},{"citation":"resolved","description":"validateDealDecisionShape computes BigInt(d.amount)<=BigInt(pin.maximumDealWei). The pins come from the same CLAUS_DEAL_PINS that runtime/deal-ports.mjs:11 validates (api/deals.mjs:20 passes JSON.parse(CLAUS_DEAL_PINS) to arbitrationPorts, and the native network-deal-review.ts reads the same variable). deal-ports requires only chainId, contract, codeHash, arbiter, admissionController and reviewed. It never requires or checks maximumDealWei, and test/deals.test.mjs:28 builds a valid pin without it. For such a pin, BigInt(undefined) throws a TypeError for every valid case. The error has no code, so tick() treats it as transient and retries every tick. It is first reached in advance() at runtime/arbitration-ports.mjs:36, which runs after the oracle order has been paid and attested. Every case under that configuration therefore spends the oracle fee and a lifetime slot, then ends 'blocked' at the deadline. If the field is present but differs from the onchain immutable maximumDeal, the bound is also wrong: a value below the onchain limit rejects valid deals after payment, and a value above it is a weaker check than claimed. The chain value is already available: verified() reads maximumDeal() into observation.maximumDealWei, and that is the value the case stores and the shape check holds as `o`.","line":26,"path":"src/deals/decision.mjs","reproduction":"Use the pin {chainId:1, contract, codeHash, arbiter, admissionController, reviewed:true}, which deal-ports accepts. Call validateDealDecisionShape(decisionInput(case, oracle, proof), pins, now) on an otherwise valid input. Actual: 'TypeError: Cannot convert undefined to a BigInt at validateDealDecisionShape (src/deals/decision.mjs:26)'. I reproduced this by removing maximumDealWei from the pins in test/deal-arbitration.test.mjs:18, which makes the 'oracle proof pins question...' test fail with exactly that error. Expected: either deal-ports rejects a pin without a decimal maximumDealWei that equals the onchain maximumDeal(), or the check uses BigInt(o.maximumDealWei) from the pinned chain observation, preferably also requiring it to equal the pin. The check should also run before any oracle spend, for example in request().","severity":"low","snippet":" requireThat(d.state===4&&/^0x[0-9a-fA-F]{40}$/.test(d.buyer)&&/^0x[0-9a-fA-F]{40}$/.test(d.worker)&&d.buyer.toLowerCase()!==d.worker.toLowerCase()&&![d.buyer.toLowerCase(),d.worker.toLowerCase()].includes(pin.arbiter.toLowerCase())&&/^[1-9][0-9]*$/.test(d.amount)&&BigInt(d.amount)<=BigInt(pin.maximumDealWei)&&/^(0|[1-9][0-9]*)$/.test(p.workerAmount)&&BigInt(p.workerAmount)<=BigInt(d.amount)&&Number.isSafeInteger(p.expiresAt)&&p.expiresAt>Math.floor(now/1000)+3600&&p.expiresAt<=Number(d.resolutionDue),'network_review_invalid_input');","title":"Decision validator bounds the amount with an unvalidated pin field (maximumDealWei) instead of the chain-read maximumDeal; a pin set that deal-ports accepts throws TypeError only after the oracle is p"}],"hash":"d03ecd763bae25e541c96ba8bcae5dec4848baa56d219168d400b4b0e47b1b18","nodeId":"52cec157-eab3-4500-b35c-87c7e429d5ff","outcome":"completed","summary":"I found three low-severity defects, all in the off-chain arbitration code. None puts escrowed ETH at risk. They are in `.imd-findings.json`, and I reproduced each one locally. I didn't find a defect I could reproduce in `ClausDealEscrow.sol`.\n\n**Findings:**\n1. **Cases admitted near the deadline cannot finish** (`src/deals/arbitration.mjs:4`). A case is accepted if more than one hour is left before the dispute deadline. But every later step (each processing pass, the decision check, the native Claus queue) needs more than an hour left *at its own later time*. So a case requested with, say, 3,700 seconds left is accepted and usually pays the IMD oracle on its first pass. Two minutes later it is permanently blocked with `deal_not_reviewable`. It uses up one of the 8 lifetime subsidy cases and produces no resolution. `docs/paid-deals.md` tells users an hour is enough. A test using the existing fixture confirmed it.\n2. **The oracle subsidy can stay reserved forever** (`runtime/arbitration-ports.mjs:55`). Up to 0.5 IMD is set aside before any payment record exists. Three things can then stop the case before a payment record is written:\n   - access is revoked;\n   - payment authorization fails;\n   - the 10-minute quote expires after a temporary provider error, and it is never re-quoted.\n\n   `reconcile()` skips any reservation without a payment record, so the money is never released. A test using the in-memory store and the real ports confirmed `reserved=5e17` remains after `reconcile()`.\n3. **The amount check uses a pin field nothing requires** (`src/deals/decision.mjs:26`). The check reads `pin.maximumDealWei`, but `deal-ports` accepts pins without that field. With such a pin, every case throws `TypeError: Cannot convert undefined to a BigInt`, and only after the oracle has been paid. The contract's real limit is already read from the chain and stored in the case's observation; the check should use that. Removing the field from the pins in the existing test reproduces the error.\n\n**Contract coverage:**\n- ETH conservation (locked plus credits equals funded), with no divisions or unsafe narrowing.\n- Every deadline comparison, checked pairwise for gaps or overlaps:\n  - claim/deliver against `settleExpired`;\n  - dispute against the review deadline;\n  - resolve against the resolution deadline;\n  - `runPlan` against `expirePlan`.\n- Plan budget accounting, the per-deal and total custody limits, deal-ID uniqueness, the arbiter's signature scope and replay protection, and reentrancy around `withdraw`.\n\nThe existing Foundry suite passes (5/5). I wrote no Foundry proof because nothing was critical or high. Forced ETH and idle credits blocking new deposits are already disclosed, so I didn't report them.\n\n**Limits of this review:**\n- I didn't run fuzz or invariant testing, and I applied the twelve specialty checklists as passes of my own; no separate reviewers ran.\n- I didn't check the native host modules that the snapshots import but don't include (store, config, the inference transport, `network-review-schema.ts`).\n- This is a source review only. It says nothing about the deployment, source verification, activation, or the real behaviour of the IMD oracle or Claus. A panel answer is provider evidence, not proof of the truth.\n\nNothing in the repository was changed. I ran the reproductions in temporary copies under `/tmp`, and the one scratch file I briefly created in the repo was moved out.","treeHash":null,"usage":{"cachedInputTokens":2688420,"inputTokens":58,"model":"claude-opus-5-5","outputTokens":32055,"runtime":"claude","turns":38,"wallClockMs":424797}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"aac006f7fc669006","findings":[{"citation":"resolved","description":"request() stores the requesting credential id as v.actor, and every tick plus each pre-signing check calls permitted(v), which requires identity.allowed(v.actor,'deal.review',...). identity.credential() rejects any credential past expiresAt, and owner credentials are issued with OWNER_TTL = 60*60*1000 (src/identity/index.mjs:20). The 5-member IMD panel, x402 payment, 3-confirmation chain checks and Claus native review happen over several cron ticks, so an owner-requested case passes the 1-hour mark before it reaches signing. permitted() then returns false, and tick() writes state 'blocked', error 'authority_closed'. caseId is deterministic per chain/contract/deal and request() returns any existing row (line 25: `if(old)return publicCase(old.value)`), so neither party can open a replacement case, even with a fresh session. The same mechanism gives the requester a unilateral veto: they can revoke the credential/mandate, archive the project or start a transfer once public_deal_case shows the oracle request as 'attested' (with its IMD request URL) but before the signing step. The onchain default (settleExpired after resolutionDue) refunds the buyer, so a buyer who files first can kill the worker's only arbitration path. The admission generation check already covers operator-side revocation, so tying the case to one short-lived party credential adds no safety. It is a trust asymmetry, and it fails the brief's goal of preventing selective oracle results.","line":17,"path":"src/deals/arbitration.mjs","reproduction":"Using the test/apps-fixture.mjs harness with arbitration enabled and a Disputed deal whose buyer is the fixture owner (resolutionDue far in the future): core.request(ownerCredentialId, projectId, 1, dealId) -> case queued; core.tick() -> advance runs, state 'oracle-pending'. Then f.advance(3600001) (no revocation, admission unchanged) and core.tick() -> state 'blocked', error 'authority_closed', advance not called again. A fresh owner credential calling core.request(...) for the same deal returns the same blocked case id, and a later tick still makes no progress. Expected: an admitted case keeps progressing while admission/generation and the onchain deal are unchanged (or a party can re-authorize it). Actual: the case is permanently dead 1 hour after the request. Verified with node --test on a scratch copy (output: 'state after TTL blocked authority_closed advances 1', 're-request true blocked'). Fix: after admission, check project/party standing (current owner address is still buyer or worker) and the admission generation, not liveness of the single requesting credential. Or let either party re-attach an authorized actor to the existing case without buying a new oracle result.","severity":"medium","snippet":" async function permitted(v,reader=store){try{const p=await admission(reader);return p.generation===v.admissionGeneration&&await identity.allowed(v.actor,'deal.review',{projectId:v.projectId},reader);}catch{return false;}}","title":"Arbitration case is pinned to the requester's 1-hour owner session; expiry or self-revocation permanently blocks the only case for the deal"},{"citation":"resolved","description":"ClausDealEscrow.dispute() accepts any non-zero reasonHash. Off-chain, request() calls evidence.bundle(d), which requires deal-evidence rows for termsHash, resultHash and disputeHash, each of the matching kind; it throws not_found or deal_evidence_mismatch otherwise. validateDealDecisionShape also requires bundle.dispute. Only the buyer can supply the dispute preimage. A buyer who receives a delivery can call dispute(id, any nonzero hash that was never published, or the hash of a published 'terms' document) inside the 24h window. No case can be opened by either party, the arbiter never signs, and settleExpired() after resolutionDue refunds the full amount to the buyer. The party who benefits from the default decides whether the neutral path exists at all. docs/paid-deals.md asks buyers to publish before disputing, but nothing enforces it, and the docs do not say that a missing buyer document removes the provider's only recourse.","line":20,"path":"src/deals/evidence.mjs","reproduction":"Deal is Submitted after the worker's deliver() with a published result document. Buyer calls dispute(id, 0x0101...01) without calling deal_evidence_publish. Worker's project (owner address == deal.worker, deal.review granted) calls deal_review_request -> createArbitration.request rejects with 'not_found' (reproduced with node --test on a scratch copy using test/apps-fixture.mjs: 'worker request rejected: not_found'). After resolutionDue, anyone calls settleExpired(id) -> credits[buyer] += amount, workerPaid = 0. Expected: the provider can still get the committed terms and result reviewed when the buyer's dispute text is missing (treat it as an absent or empty objection), or the asymmetry is disclosed explicitly. Actual: a buyer-controlled missing preimage silently disables arbitration and guarantees a full refund. Fix: allow a case with a missing or invalid dispute document and present that fact to the panel and Claus. Or disclose in DEAL_RULES/docs that the provider has no arbitration recourse when the buyer's dispute document is unpublished.","severity":"medium","snippet":"  async bundle(deal,reader=store){const out={};for(const [kind,field]of [['terms','termsHash'],['result','resultHash'],['dispute','disputeHash']]){const evidence=await this.read(deal[field].toLowerCase(),reader);requireThat(evidence.document.kind===kind,'deal_evidence_mismatch');out[kind]=evidence;}return out;}","title":"Buyer can make arbitration impossible by disputing with an unpublished or wrong-kind reasonHash, forcing the 3-day refund"},{"citation":"resolved","description":"request() admits a case if the requesting project's owner address is the deal's buyer or worker and the global lifetime counter is below 8. Nothing requires the two parties to be distinct principals, sets a minimum amount, or prices the request. The escrow accepts any msg.value >= 1 wei, and decision validation only requires amount >= 1 (/^[1-9][0-9]*$/). Each case buys an IMD panel order (up to IMD_ORACLE.maximum = 0.5 IMD) from the separate subsidy and takes one of the 8 lifetime native Claus deal decisions (network-review.ts: count(*) < 8). projectDaily is keyed by projectId, and one owner can create up to 10 projects (identity capacity 'projects' = 10), so the per-project limit does not bound a single actor. Exhausting the subsidy needs only the 4/day global cap. Once total reaches 8, discovery reports enabled:false and genuine buyers/providers lose the arbitration path, so their disputes default to buyer refund.","line":25,"path":"src/deals/arbitration.mjs","reproduction":"Principal P with wallet A creates projects prj_1..prj_4 and controls a second address B. Day 1: from A, for i in 1..4: createDeal(salt_i, projectHash, termsHash_i, worker=B, deliveryDue=now+1h) with value 1 wei. From B: claim and deliver(resultHash_i). From A: dispute(disputeHash_i), with all three documents published through deal_evidence_publish (8/day per project limit). Then deal_review_request(prj_i, chainId, dealId_i). The 4 cases are admitted (owner address A == buyer, each project 1/day, global 4/day). Repeat on day 2 -> deal-case-capacity.total = 8, which pays for 8 oracle orders and uses all 8 lifetime native decisions. Any later genuine request fails 'deal_arbitration_capacity'. Expected: the finite subsidy goes to genuine disputes between distinct parties above a meaningful value. Actual: about 8 wei plus gas consumes it. Fix: require a minimum deal amount for subsidized review, a per-owner-principal (not per-project) limit, and/or a requester-paid fee or deposit.","severity":"low","snippet":"    const old=await tx.read('deal-cases',id);if(old)return publicCase(old.value);const p=await admission(tx),counter=await tx.read('deal-case-capacity','global'),total=counter?.value.total??0;requireThat(total<ARBITRATION_LIMITS.cases,'deal_arbitration_capacity');","title":"One principal can use up the 8-case lifetime arbitration subsidy with self-dealt 1-wei disputed deals"}],"hash":"ded9fb52f6ceee79d97d959f17187f69928bdbfac93bb89b71f0660edda586cc","nodeId":"b0531748-8759-4531-89ac-032ba9688098","outcome":"completed","summary":"I reviewed commit `935c323` and recorded three findings in `.imd-findings.json`: two medium, one low. All three are in the off-chain arbitration path; I found nothing to report in the escrow contract. No repository files were changed. I reproduced findings 1 and 2 with `node --test` against a copy of the repo in `/tmp`; finding 3 is traced through the code but not run.\n\n**Findings**\n1. **Medium: a case dies when the requester's 1-hour session expires** (`src/deals/arbitration.mjs:17`). Each case is tied to the exact login that requested it, and owner logins expire after 1 hour (`OWNER_TTL`). Arbitration (panel, payment, confirmations, Claus review) takes longer, so the case flips to `blocked` / `authority_closed`. There is only one case per deal, so neither party can open a replacement. In the test, advancing the clock 1h+1ms without revoking anything blocked the case, and a new login got the same blocked case back. The requester can also do this on purpose: revoke access or archive the project once the oracle result is attested. Since an unresolved dispute refunds the buyer, a buyer who files first can kill the provider's only recourse.\n2. **Medium: a buyer can make arbitration impossible** (`src/deals/evidence.mjs:20`). Opening a case needs a published dispute document, and only the buyer has it. If the buyer disputes with a hash they never published, the provider's request fails with `not_found`. After 3 days `settleExpired` refunds the buyer in full. The docs ask buyers to publish first but nothing enforces it, and the docs don't warn that the provider then has no recourse.\n3. **Low: one person can use up all 8 subsidized cases** (`src/deals/arbitration.mjs:25`). There is no minimum deal amount and no check that buyer and provider are different people. The daily limit counts per project, and one owner can have 10 projects. One person with two wallets can open 1-wei disputed deals with themselves and use up all 8 lifetime cases in two days (the global limit is 4 per day). That spends the oracle subsidy and blocks genuine disputes.\n\n**Escrow contract: checked, nothing to report.** `forge test` on `ClausDealEscrow.t.sol` passes 5/5. I worked through these by hand:\n- **Conservation:** funds are tracked across every path.\n- **Reentrancy:** `withdraw` updates balances before paying and blocks reentry.\n- **Roles:** who may call each function holds, and the arbiter can't be the buyer or provider.\n- **Signatures:** they are bound to this contract, chain and deal and can't be replayed.\n- **Time limits:** the 24h, 3-day and delivery-deadline boundaries agree with each other.\n- **Plans:** the run index and budget tracking are consistent.\n- **Forced ETH:** it is ignored rather than counted.\n- **Exits:** settlement and withdrawal stay available while funding is paused.\n\n**Native Claus decision: checked, nothing to report.** The native code uses the same shared validator, checks the model's approve/reject JSON strictly, caps deal decisions at 8 for its lifetime, and re-checks authorization right before signing.\n\n**Limits of this review**\n- This was a source review only. It does not verify any deployment, source match or activation, and nothing was deployed or funded.\n- Local fixtures stood in for the live IMD oracle and native Claus service, so live acceptance is still unconfirmed.\n- An oracle attestation is evidence from a provider, not proof that its answer is correct or fair.\n- Rules that can't change are not the same as correct: everything still relies on the named arbiter key and the implementation.\n- I didn't run Slither or fuzzing, and didn't look in depth at the unchanged `imd-payment` / `imd-cost` adapters.","treeHash":null,"usage":{"cachedInputTokens":2011986,"inputTokens":50,"model":"claude-opus-5-5","outputTokens":25975,"runtime":"claude","turns":26,"wallClockMs":445795}}],"verification":[]}