{"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":"a5960b3e-4ac1-4e65-8fc2-087178af730a","kind":"audit","nodes":[{"acceptedSubmissionHash":"b2de66cb6526e3ff473d8c37e721e999cee2338e59fc06795b3dc092c41c7efa","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":"f0ad80db074c88d232cc89e603a6fba3f62099d31e8f35d41296e96dc9d4d13e","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":"cafc72172ed3c2ec642e2c2590d730cecedf42967b9c32a325df3474ddf928fb","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":"3c00d97f87068b4427c8d2e4bd1e7c020eb9771027db23f5bff1548c542248bd","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":"5641ac94f797fd49f5a50032846f06339431c008e8093489d40e2aa25e3ecd8f","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 contracts in vadiszzz/briefs-contracts as they are.","parentJobId":null,"planHash":"a30e462385dfdaf2eaf1255bd9e5e8fde9c46e0e29bab274a24d4893899b1837","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"a5960b3e-4ac1-4e65-8fc2-087178af730a","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":"51163","feedbackHash":"29d2b18c2661ed94e702f55c087dcab9d60eb25a7dada4d29e178aa8a38b7f2e","nodeKey":"audit_economics","submissionHash":"b2de66cb6526e3ff473d8c37e721e999cee2338e59fc06795b3dc092c41c7efa","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51367","feedbackHash":"f9f0d0a0a8b2c44b0534435972bd971e8fbfba7526abbac052b23e219982f03c","nodeKey":"audit_flow","submissionHash":"f0ad80db074c88d232cc89e603a6fba3f62099d31e8f35d41296e96dc9d4d13e","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52606","feedbackHash":"b58202139361a4b1a36b8b0cec39bdb825d0d2dcffebb8bddc88c62bd5e9539b","nodeKey":"audit_judge","submissionHash":"cafc72172ed3c2ec642e2c2590d730cecedf42967b9c32a325df3474ddf928fb","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51140","feedbackHash":"bb5605c3fa3b94f60e200f0daf404ff31f710c2b5adb359425501b3f2f7574b3","nodeKey":"audit_math","submissionHash":"3c00d97f87068b4427c8d2e4bd1e7c020eb9771027db23f5bff1548c542248bd","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52145","feedbackHash":"af8412f67d8894f537c3d85e67092d71db42e1f2f1f59ce7917b2b4f88d8b103","nodeKey":"audit_permissions","submissionHash":"5641ac94f797fd49f5a50032846f06339431c008e8093489d40e2aa25e3ecd8f","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"d968781de727955ecfbaf194ccc6685488498698ee8cb748f64f461b16615150","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ddc5542bddcad225","findings":[{"citation":"resolved","description":"The tenth review (finding 2) made the first refund of a signed message cost at least the case's entry fee, so that nobody holding a signature can cancel a paid filing for free. The floor is read by _feeOf(caseId) from word 15 of Briefs.getCase(caseId). For a caseId above caseCount the struct is empty, so the call succeeds (32 words of zeros, so the `r.length < 16 * 32` guard does not fire) and the floor is 0. Any amount >= 1 wei then passes, relayed[h] becomes 2, and the message can never be filed. Case ids are sequential (`id = ++caseCount`), so the next id is known in advance: a message signed for caseId = caseCount + 1 (pre-signed for a case about to open, e.g. by the creator's launch flow or a relayer queuing before the opening transaction lands) is cancellable by anyone who holds the signature for 1 wei, and the floor that was meant to make cancelling cost what filing would does not apply. Boundary x invariant: the invariant 'the first refund pays at least the fee' is enforced by a value that is 0 at the edge (unopened case). Impact is limited to messages signed before their case exists; the author still receives the 1 wei refund and the on-chain `refunded` record then says 1 wei, so the relayer pays the rest, but the signed brief is lost and whoever voided it paid nothing meaningful. Fix: in _feeOf, revert NothingToRefund when the case does not exist (fee == 0, or caseId > caseCount read from the court), or require the case's status to be Open/Settled before accepting a refund.","line":318,"path":"src/BriefsJury.sol","reproduction":"Deploy as in test/Briefs.t.sol (FEE = 5 ether). Let next = briefs.caseCount() + 1 (no case with that id yet). Author dan signs jury.relayMessage(next, words, dan, deadline) with personal_sign. Anyone calls jury.refundRelay(next, words, dan, deadline, sig, 1). Expected (per the documented rule): revert NothingToRefund because 1 wei < the entry fee. Actual: succeeds, relayed[h] == 2, refunded[h] == 1. Then the creator opens a case, which gets id == next, and fileBriefFor(next, words, dan, deadline, sig) reverts BadSignature: the brief can never be filed. Verified with a scratch Foundry test (test_RefundFloorIsZeroForACaseNotYetOpened) against the current tree: both assertions hold.","severity":"low","snippet":"        if (amount == 0 || (relayed[h] == 0 && amount < _feeOf(caseId))) revert NothingToRefund();","title":"refundRelay's entry-fee floor is zero for a case id that has not opened yet, so a signed brief for the next case can be voided for 1 wei"}],"hash":"3c00d97f87068b4427c8d2e4bd1e7c020eb9771027db23f5bff1548c542248bd","nodeId":"f2e9a64c-8581-4c75-b1d4-9c6ce2b7c64d","outcome":"completed","summary":"Review complete. The findings file holds one substantiated defect, and the working tree is clean apart from it.\n\n**Result: one low finding, no critical, high or medium.** The math in `Briefs` is sound under the three assigned lenses. I traced every division, cast, packing and time subtraction by hand and confirmed the delicate ones with scratch tests against the current tree.\n\n**The finding** (low, `src/BriefsJury.sol:318`): the relay refund floor reads the entry fee from `getCase(caseId)`. For a case id that has not opened yet the struct is empty, so the floor is zero and a 1 wei refund marks the signed message refunded. Case ids are sequential, so a brief pre-signed for the next case can be voided by anyone holding the signature for nothing, and once that case opens the brief can never be filed. The rule from the tenth review's finding 2 (cancelling costs what filing would) does not hold at this edge. A scratch Foundry test reproduced both steps. Fix: have `_feeOf` revert when the case does not exist, or require an existing case before accepting a refund.\n\n**Verified as sound, with the concrete edges I checked:**\n- Fee split: creator and platform shares round down, dust stays in the pot. The remainder after the jury's price is always positive because the reserve is strictly below the fee at creation and after every raise.\n- Time in the lead: each leader's seconds are credited from its filing and the per-leader seconds sum exactly to `reignTotal` in every path, including same-second leads, overtime leads and a leader heard after the deadline. No zero divisor: `reignTotal` is at least 1 whenever any leader has seconds, and the pool denominator is at least the minimum duration.\n- Pool formula: bounded at 90% of the pot net of overtime entries, so the final leader always keeps at least 10%. The pot minus the overtime pot is at least the seed. No intermediate overflow at realistic or uint128 scale.\n- Claim rounding: 2 wei of dust across three leaders in my probe, stays in the contract, already documented as accepted.\n- Packing: the rules, terms, request and question-hash words have no overlapping fields, and every unpack mask matches the layout.\n- Casts: every narrowing cast is bounded by a prior check or by a param cap.\n- Boundaries at external calls: all low-level calls check return length before decoding, a code-less token or requester degrades to a held payment or a stall, and the balance-delta check in `openHearing` rejects a requester that returns funds.\n\n**Test suite:** all 164 project tests pass. The scratch tests were deleted after use and `git status --porcelain` is empty; `.imd-findings.json` is in the git exclude list and present.\n\n**Not reported, for the record:** the gas reserve floor in `_hearNext` can leave too little for `skipStalled` after a failed hearing, but the caller sets the gas and the site and keeper send 7M, so this is a liveness quirk at the floor only.","treeHash":null,"usage":{"cachedInputTokens":1690798,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":37843,"runtime":"claude","turns":27,"wallClockMs":565684}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"e02bec63aa9dcaa5","findings":[{"citation":"resolved","description":"Guard asymmetry on the same storage slot. relayed[h] is written by two functions with different guards: useRelay (line 302, msg.sender must be the court, so only Briefs.fileBriefFor sets it to 1) and refundRelay (lines 312-323, no caller check at all: anyone who can present the author's signature sets it to 2 by paying at least the case's entry fee to the author). Once relayed[h] == 2, _relayHash (line 344) rejects the message for filing forever. The signature is not secret in practice: it travels in the relayer's fileBriefFor calldata, so it is on chain the moment a relayer's attempt reverts (OracleTooExpensive, NotHolder, an allowance mistake) or is visible to anyone who sees the pending transaction. The actor with an incentive is the standing leader: near the deadline, burning a challenger's signed brief for the fee (5 IMD in the example) protects a pot that can be hundreds of IMD, and the author cannot re-sign in time once entries close. The author does receive the fee, but loses the entry they already paid the relayer for off-chain and the chance at the pot; the relayer loses the fee it would have earned from. The authors' own test (test_Relay_ARefundedSignatureCanNeverBeFiled, 'anyone holding the signature, paying at least the fee') shows the openness is deliberate, so this is reported as a trust gap to decide on rather than a permission bypass: the fee floor prices the cancellation but does not bind it to a party the author chose. Minimal fix that keeps the design: bind the refund to the author or to a relayer the author named, e.g. add the relayer's address to relayMessage and require msg.sender == author || msg.sender == relayer in refundRelay (the message then says who may file and who may refund); or, keeping the message as is, require msg.sender == author for a refund of an unfiled message and let a relayer refund only through a second signed message. Either way useRelay and refundRelay would then agree on who may spend relayed[h].","line":318,"path":"src/BriefsJury.sol","reproduction":"Scratch test test/scratch/RelayCancel.t.sol (passes on the current code, demonstrating the behaviour). State: a case c with fee 5 IMD, the creator's opening brief leading. Author dan signs relayMessage(c, words, dan, deadline) with personal_sign. Input: a third party `leader` (not dan, not the relayer) calls jury.refundRelay(c, words, dan, deadline, sig, 5 ether) after approving the jury. Actual: the call succeeds, 5 IMD moves from leader to dan, relayed[h] becomes 2, and the relayer's subsequent b.fileBriefFor(c, words, dan, deadline, sig) reverts with BadSignature; waiting(c) stays 0 so dan's brief is never heard. Expected under the filing guard's model: only the party the author authorised (the court via fileBriefFor, or the author/relayer for a refund) can decide the message's fate; a stranger should not be able to spend it.","severity":"low","snippet":"        if (amount == 0 || (relayed[h] == 0 && amount < _feeOf(caseId))) revert NothingToRefund();","title":"refundRelay lets any holder of a relayed signature cancel the brief, while filing it is court-only"}],"hash":"5641ac94f797fd49f5a50032846f06339431c008e8093489d40e2aa25e3ecd8f","nodeId":"ec2deade-f0bb-4738-99b8-f875d491fb7a","outcome":"completed","summary":"The review is complete and the findings file is written and validated.\n\n**Outcome.** One Low finding, no Critical or High, across the assigned permission surface of all five contracts in the repository at the pinned commit. Nothing in the tree was changed beyond the findings file and one scratch test under `test/scratch/`.\n\n**Finding (Low).** In `BriefsJury.refundRelay`, anyone presenting a relayed author's signature can mark the message refunded by paying the entry fee to the author, after which the relayer's `fileBriefFor` reverts forever. The filing side of the same slot is court-only. A leader who sees the signature, for example in a relayer's reverted transaction, can burn a late challenger's brief for the price of one fee to protect a much larger pot. A scratch test reproduces the sequence. The openness is intentional per the authors' own test, so it is reported as a trust gap with a design decision attached, not a bypass.\n\n**Checked and found sound.** Every state-changing entry point was inventoried with its caller and guard. Owner powers are bounded and apply to new cases only, and ownership cannot be renounced. `openHearing` is self-only and the requester is the one bound at the hearing. `onImdAnswer` is open but keyed by the caller, so only the setup's Intake can make an answer land. Oracle changes sit behind a seven-day delay with a lapse window. Payouts go only to the recorded payee, with blocked transfers parked for the payee alone. Paired paths such as `fulfill` versus `mistrial`, `useRelay` versus `refundRelay`, `_landable` versus `verdict`, and `canSettle` versus `_maybeSettle` agree on what they check and write. The settings-agreement check in `openCase` covers every value a case fixes for life.\n\n**Already documented, not re-reported.** The leader-driven `skipStalled` and late-answer mistrial races, the surplus-lock, the fee-on-transfer assumption, and that `raiseReserve` lets anyone lift a running case's jury reserve are all recorded in the repository's own audit notes with passing tests.\n\n**Not covered.** Arithmetic in the time-in-the-lead pool, the UTF-8 text checker, and the EIP-712 encoding were read only as far as the access and asymmetry questions required. No static analysis tool ran. A clean pass on this area is not proof that no defect exists elsewhere.","treeHash":null,"usage":{"cachedInputTokens":1337858,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":38398,"runtime":"claude","turns":21,"wallClockMs":609648}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"8af9903f4ad1eed0","findings":[{"citation":"resolved","description":"The tenth review's fix for 'refundRelay of 1 wei cancelled a signed brief' requires the first refund of a message to pay at least the case's entry fee, read by _feeOf(caseId) from word 15 of Briefs.getCase. For a case id that does not exist yet (caseId > caseCount, or 0) getCase returns an all-zero struct, so _feeOf returns 0 and the check `amount < 0` can never be true: any amount >= 1 wei marks the message refunded (relayed[h] = 2) and it can never be filed. The signature names the case id, so a brief signed for the next case id (a case announced but not yet opened, or opened in the same block after the refund) is cancelled for 1 wei instead of the 5 IMD fee the rule intends. Only a holder of the signature (the author or the relayer) can do it, so the loss is bounded to the relayer's off-chain accounting: the rule 'cancelling costs what filing would' no longer holds for that message. Fix: in _feeOf, `if (fee == 0) revert NothingToRefund();` (every real case has fee >= minFee > 0), or check caseId against the court's caseCount.","line":318,"path":"src/BriefsJury.sol","reproduction":"State: Briefs deployed with the Deploy.s.sol params (minFee 1 IMD), caseCount = 0. Input: author dan signs relayMessage(1, words, dan, deadline) for case id 1 (not yet opened), then calls jury.refundRelay(1, words, dan, deadline, sig, 1) paying 1 wei to themselves. Expected (per the documented rule): revert NothingToRefund because 1 wei < the case's 5 IMD fee. Actual: the call succeeds, relayed[h] = 2, refunded[h] = 1. Then creator opens case 1 (fee 5 IMD) and the relayer's fileBriefFor(1, words, dan, deadline, sig) reverts BadSignature forever. Verified with test/scratch/Refund.t.sol (test_RefundOfASignatureForACaseNotYetOpenedCostsOneWei passes on the current code, showing the 1-wei refund is accepted).","severity":"low","snippet":"        if (amount == 0 || (relayed[h] == 0 && amount < _feeOf(caseId))) revert NothingToRefund();","title":"refundRelay: a signed brief for a case id that is not open yet can be cancelled for 1 wei, bypassing the 'first refund pays at least the entry fee' rule"},{"citation":"resolved","description":"testFuzz_Reign_TheBooksAlwaysBalance files one brief, judges it (or mistrials it), then files the next; the loop stops as soon as a filed brief is not the one being heard. So every brief in the fuzz is heard at once, and the paths that make v2's accounting subtle are never fuzzed: a challenger queued behind a hearing whose filedAt is earlier than the previous leader's verdict (reignOf credited from filing, _newLeader lines 756-767), a brief filed before reignEnd and heard after an overtime push, several queued briefs crossing the first deadline, and an overtime-filed brief heard after an extended endsAt. Those are the inputs where 'seconds add up to reignTotal', 'claims never exceed the pool' and 'balance equals liabilities' could diverge. I checked those scenarios by hand (test/scratch/Probe.t.sol: test_QueueAcrossDeadline and test_OvertimeChain) and the books balance to the wei, so this is a coverage gap, not a defect found. Fix: let the fuzz file 1-4 briefs per step before judging them in docket order, and keep the existing assertions.","line":2099,"path":"test/Briefs.t.sol","reproduction":"Input: run the fuzz with any seed and log docketOf(c).length - head when a brief is filed: it is always 1 (the brief just filed) because `if (b.getCase(c).hearing != id) break;` ends the run the first time a brief is queued. Expected: random seeds exercise queued briefs (waiting(c) > 1) crossing the deadline and overtime. Actual: no seed does; the end-of-run assertions on reignTotal, reignPool and solvency are only ever evaluated on one-hearing-at-a-time histories.","severity":"info","snippet":"                if (b.getCase(c).hearing != id) break;","title":"The time-in-the-lead fuzz never queues a brief behind a running hearing, so the from-filing crediting with queued briefs is covered only by hand-written cases"}],"hash":"b2de66cb6526e3ff473d8c37e721e999cee2338e59fc06795b3dc092c41c7efa","nodeId":"ab384ee6-a7ab-4de4-adf6-ebbe0cb755c0","outcome":"completed","summary":"Review complete. The findings file holds two entries, both verified against the tree.\n\n**Scope and method.** I read all five production contracts, the interfaces, the deploy script, the internal audit history, and the money and reign test suites. Passes run: Economic Security, Invariant and Flow Gap from the Pashov reference, plus an entry-point inventory and the x-ray temporal checks. I wrote two scratch suites under test/scratch to probe queued briefs crossing the first deadline, chained overtime leads, and the relay refund rule. Scratch files are not part of the submission.\n\n**Findings written to .imd-findings.json:**\n\n- **Low, BriefsJury.sol:318.** A signed brief naming a case id that is not open yet can be cancelled with a 1 wei refund. The fee floor reads word 15 of getCase, which is zero for a nonexistent case, so the \"first refund pays at least the entry fee\" rule from the tenth review does not apply. Once that case opens, the signature can never be filed. Only the signature holder can do it, so the loss is bounded to relayer accounting. Fix: revert when the fee read is zero.\n- **Info, test/Briefs.t.sol:2099.** The reign fuzz stops the run as soon as a brief is queued behind a running hearing, so the from-filing crediting, overtime pushes with queued briefs and the overtime pot are only covered by hand-written cases. My own probes of those paths balance to the wei, so this is a coverage gap rather than a defect.\n\n**Checked and found sound, with concrete traces:** reign seconds always sum to reignTotal across queued, mistrialed and overtime briefs. The pool formula never exceeds the pot. Settlement, mistrial, skip and claim paths keep balance equal to liabilities. Overtime guarantees every filed brief is visible for at least the overtime window before entries close, including after an earlier push. Fee split rounding stays in the pot. The reserve can never reach the whole fee. No path lets a payee block a docket or a settlement.\n\n**Not covered.** No full-suite run and no long fuzz within the time budget. The late-answer mistrial race, unlimited default overtime, and the creator's second-wallet play remain documented accepted risks, not new defects.","treeHash":null,"usage":{"cachedInputTokens":1772845,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":51251,"runtime":"claude","turns":24,"wallClockMs":760131}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"829d84438fb3fdc8","findings":[{"citation":"resolved","description":"Merged from three specialists (audit_math, audit_economics finding 1, audit_flow finding 2), all the same mechanism. The tenth review's fix makes the first refund of a signed message pay at least the case's entry fee, which _feeOf reads from word 15 of Briefs.getCase(caseId). For a caseId that has no case yet (caseId > caseCount, or 0) getCase returns an all-zero Case struct: the call succeeds and returns 32 words, so the `r.length < 16 * 32` guard does not fire, and the fee read is 0. `amount < 0` is never true, so any amount >= 1 wei marks the message refunded (relayed[h] = 2) and it can never be filed. Case ids are sequential (`id = ++caseCount`), so the id the next openCase will take is known in advance; a brief signed for it (a relayer queuing a filing before the opening transaction lands, or a creator's launch flow) can be cancelled for 1 wei by anyone holding the signature, instead of the entry fee the rule intends. Impact is bounded to messages signed before their case exists; the author receives 1 wei and the on-chain `refunded` record then says 1 wei. Fix (minimal, keeps the design): in _feeOf revert NothingToRefund when the fee read is 0 (every real case has fee >= minFee > 0), or when word 5 (status) is 0, so a refund needs an existing case; alternatively compare caseId with the court's caseCount.","line":318,"path":"src/BriefsJury.sol","reproduction":"Verified with test/scratch/Judge.t.sol::test_RefundFloorIsZeroForACaseNotYetOpened on the current tree (FEE = 5 ether, minFee 1 ether). State: caseCount = 0, next = 1. Input: author dan signs jury.relayMessage(1, B1, dan, deadline) with personal_sign; carol (anyone) calls jury.refundRelay(1, B1, dan, deadline, sig, 1). Expected per the documented rule ('the first refund of a message must pay at least the case's entry fee'): revert NothingToRefund. Actual: succeeds, jury.relayed(h) == 2, jury.refunded(h) == 1. Then creator opens a case, which gets id 1 with fee 5 ether, and alice's b.fileBriefFor(1, B1, dan, deadline, sig) reverts BadSignature forever. Control in the same test: for the open case, refundRelay(..., 1) reverts NothingToRefund as intended.","severity":"low","snippet":"        if (amount == 0 || (relayed[h] == 0 && amount < _feeOf(caseId))) revert NothingToRefund();","title":"refundRelay's entry-fee floor is zero for a case id that is not open yet, so a signed brief for the next case can be voided for 1 wei"},{"citation":"resolved","description":"From audit_permissions; reproduced. relayed[h] is written by two functions with different guards: useRelay requires msg.sender == court (only Briefs.fileBriefFor sets it to 1), while refundRelay has no caller check: any address that can present the author's signature sets it to 2 by paying at least the case's fee to the author, after which _relayHash rejects the message for filing forever. The signature is not secret in practice: it is in the relayer's fileBriefFor calldata, so it is public the moment a relayer's attempt reverts (OracleTooExpensive, NotHolder, an allowance error) or is seen pending. The party with an incentive is the standing leader near the deadline: for the fee (5 IMD in the example) it removes a challenger from the docket to protect a pot that can be far larger; the author is made whole for the fee but loses the entry and the chance at the pot, and cannot re-sign in time once entries close. The NatSpec ('Anyone holding the signature may call it') and test_Relay_ARefundedSignatureCanNeverBeFiled show this openness is deliberate, so this is a trust-model gap to decide on, not a permission bypass: the fee floor prices the cancellation but does not bind it to a party the author chose. Minimal fix that keeps the design: require msg.sender == author or a relayer named in the signed message (add the relayer's address to relayMessage), so useRelay and refundRelay agree on who may spend relayed[h]; or let a relayer refund only with a second author signature.","line":312,"path":"src/BriefsJury.sol","reproduction":"Verified with test/scratch/Judge.t.sol::test_AStrangerHoldingTheSignatureCancelsTheBrief on the current tree. State: open case c (fee 5 IMD). Author dan signs relayMessage(c, B1, dan, deadline). Input: `leader` (neither dan nor the relayer), holding the signature and 5 IMD approved to the jury, calls jury.refundRelay(c, B1, dan, deadline, sig, 5 ether). Actual: succeeds, 5 IMD moves from leader to dan, relayed[h] == 2, and the relayer alice's b.fileBriefFor(c, B1, dan, deadline, sig) reverts BadSignature; waiting(c) stays 0, dan's brief is never heard. Expected under the filing guard's model: only the court (filing) or the author/their relayer (refund) can decide the message's fate.","severity":"low","snippet":"    function refundRelay(uint256 caseId, string calldata words, address author, uint256 deadline, bytes calldata signature, uint256 amount)\n        external\n    {","title":"refundRelay lets anyone holding a relayed signature cancel the brief for the entry fee, while filing it is court-only"},{"citation":"resolved","description":"From audit_flow finding 1; reproduced. _newLeader decides whether a late win opens overtime by comparing filedAt + overtime with `end` = c.endsAt as it stands at verdict time, which an earlier leader in the FIFO docket may already have pushed forward. The README and the NatSpec promise that a brief 'filed in the last `overtime` seconds before the deadline, however long the jury took' pushes the deadline (the tenth review's finding 1 fix). When a brief ahead of it in the docket wins first and moves endsAt by `overtime`, the later brief's filing time is no longer within `overtime` of the new endsAt even though it was within `overtime` of the deadline it was filed against. If its verdict then lands after the pushed deadline (a late delivery, or two mistrial briefs between them, each costing answerTimeout + MISTRIAL_GRACE), it takes the lead without opening overtime, and with nothing queued behind it the case settles in the same call, so nobody can answer it: exactly the outcome the tenth review's fix was meant to prevent, now reachable whenever more than one lead changes in a window. No direct profit path was found (the jury answers junk briefs quickly, so an attacker cannot force the needed delay without mistrials), hence low. Fix: measure the window against the deadline the brief was filed under (record endsAt at filing, e.g. in the Brief's unused bytes of its second slot, or keep a per-case 'window opened at' marker), or state on the site that the window is measured against the current deadline.","line":770,"path":"src/Briefs.sol","reproduction":"Verified with test/scratch/Judge.t.sol::test_OvertimeLostWhenAnEarlierLeaderMovedTheDeadline and ::test_OvertimeLostBehindMistrials on the current tree. Params: overtime 10 min, answerTimeout 4 min. Case ends at E. Bob files at E-9min (heard at once), Alice files at E-9min+30s (queued). Bob wins at E-8min: endsAt becomes end1 = E+2min and Alice's hearing opens. Variant A: Alice's attestation (issuedAt = heardAt+30, inside her window) is delivered at end1+1 with nobody having called mistrial. Variant B: two junk briefs between Bob and Alice are mistrialed, so Alice's verdict lands at about E+5min. Expected per README/NatSpec: Alice was filed 8.5 min before the deadline she filed against, inside the 10-min window, so her win pushes endsAt to now + 10 min and the case stays Open. Actual in both variants: Alice becomes precedent, getCase(c).endsAt stays end1, no Overtime event, status == Settled and winner == alice in the same call; getBrief(aliceId).filedAt + 10 min <= end1.","severity":"low","snippet":"        if (t < end || filedAt + c.overtime > end) {","title":"Overtime is not opened for a brief filed inside the window when an earlier queued leader already pushed the deadline"},{"citation":"resolved","description":"From audit_flow finding 3; reproduced. openCase pins the setup the creator saw (x.oracleId must equal jury.latest()). BriefsJury.applyOracle is permissionless once readyAt has passed and stays callable for ORACLE_WINDOW (7 days). During that window any address can call applyOracle ahead of a creator's openCase, which then reverts WrongOracle (the caseFee transferFrom is rolled back with it). The grief is one-shot per proposal and the creator can resubmit with the new id, so this is a usability note, not a loss; the pinning itself is the intended protection. Suggested handling: the site refreshes oracleId on a WrongOracle revert, and the owner applies a ready proposal promptly to keep the window short.","line":624,"path":"src/Briefs.sol","reproduction":"Verified with test/scratch/Judge.t.sol::test_ApplyOracleMakesAPendingOpenCaseRevert. Owner calls jury.proposeOracle(o); warp 7 days. Creator prepares CaseInput with oracleId = jury.latest() = 0. carol (anyone) calls jury.applyOracle(): latest() becomes 1. Creator's openCase reverts WrongOracle. Expected: the case opens with the setup the creator agreed to, or the site tells them it changed. Actual: revert; a retry with oracleId = 1 opens the case.","severity":"info","snippet":"        if (jury.latest() != x.oracleId) revert WrongOracle();","title":"A ready oracle proposal lets anyone make a creator's pending openCase revert with WrongOracle"},{"citation":"resolved","description":"From audit_economics finding 2; confirmed by reading the fuzz. testFuzz_Reign_TheBooksAlwaysBalance files one brief, then judges it (or mistrials it) before the next iteration, so at every filing no hearing is running, the filed brief is heard at once, and the `hearing != id` break can only fire on a stall (which the mock requester never produces). No seed therefore exercises the paths that make v2's accounting subtle: a challenger queued behind a hearing whose filedAt precedes the previous leader's verdict (reignOf credited from filing in _newLeader), several queued briefs crossing the first deadline, a brief filed before reignEnd and heard after an overtime push, or an overtime-filed brief heard after an extended endsAt. The end-of-run assertions ('seconds add up', pool formula, 'solvent to the wei') are only ever evaluated on one-hearing-at-a-time histories. The hand-written tests cover some of these, and the specialist reports that the books balance in their own probes, so this is a coverage gap, not a defect found. Fix: let the fuzz file 1-4 briefs per step before judging them in docket order, keeping the existing assertions.","line":2099,"path":"test/Briefs.t.sol","reproduction":"Read test/Briefs.t.sol lines 2090-2112: each loop iteration calls _file once and, in the same iteration, either b.mistrial(c) or _verdictAt(id, ...) on that brief, so c.hearing == 0 at the next _file and the new brief opens its own hearing (hearing == id). Expected: random seeds reach waiting(c) > 1. Actual: waiting(c) is always 1 immediately after a filing, for every seed; the break at line 2099 never triggers in a non-stalled run.","severity":"info","snippet":"                if (b.getCase(c).hearing != id) break;","title":"The time-in-the-lead fuzz never queues a brief behind a running hearing, so from-filing crediting with queued briefs is covered only by hand-written cases"}],"hash":"cafc72172ed3c2ec642e2c2590d730cecedf42967b9c32a325df3474ddf928fb","nodeId":"ace4b545-3233-4fc8-aa27-3dff42a7d7d3","outcome":"completed","summary":"Review complete. The findings file holds five substantiated entries, every one reproduced against the current tree with a scratch Foundry test.\n\n**What the specialists reported, and what survived**\n\n- **Seven specialist reports merged into five findings.** The refund-floor defect was reported three times (math, economics, flow) with the same mechanism and the same fix, so it is one finding now.\n- **Nothing was dropped.** Every claim reproduced on the code as it is.\n\n**Findings kept, in severity order**\n\n- **Low, `src/BriefsJury.sol` line 318.** The first-refund floor reads the entry fee from `getCase`, which returns an all-zero struct for a case id that does not exist yet. A brief signed for the next sequential id can be voided for 1 wei by anyone holding the signature, and it can never be filed once the case opens. Reproduced: refund of 1 wei accepted, case then opens with that id, filing reverts with BadSignature.\n- **Low, `src/BriefsJury.sol` line 312.** Any holder of a relayed signature may cancel the brief by paying the fee to the author, while filing is court-only. The signature is public once a relayer's attempt reverts. This is documented design, so it is reported as a trust-model gap, not a bypass. Reproduced with a third party cancelling and the relayer's filing then reverting.\n- **Low, `src/Briefs.sol` line 770.** The overtime window is measured against the current deadline, so a brief filed inside the window loses its push once an earlier queued leader has already moved the deadline. In both reproduced variants the later leader wins after the pushed deadline and the case settles in the same call. No direct profit path was found, hence low.\n- **Info, `src/Briefs.sol` line 624.** A ready oracle proposal lets anyone call applyOracle ahead of a creator's openCase, which reverts WrongOracle. One-shot and recoverable by resubmitting.\n- **Info, `test/Briefs.t.sol` line 2099.** The reign fuzz judges each brief before filing the next, so no seed ever queues a brief behind a running hearing. Coverage gap only.\n\n**Checks run**\n\n- Scratch reproductions are in `test/scratch/Judge.t.sol`, all five passing on the current code, meaning each defect is present.\n- The project's own suite passes with no failures.\n\n| Suite | Result |\n|---|---|\n| Project tests (excluding scratch) | 164 passed, 0 failed |\n| Scratch reproductions | 5 passed |\n\nNo finding is critical or high, so none carries a proof file. No repository file outside `test/scratch/` and `.imd-findings.json` was changed.","treeHash":null,"usage":{"cachedInputTokens":974954,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":21195,"runtime":"claude","turns":25,"wallClockMs":300634}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ddfb1efa72fe9a94","findings":[{"citation":"resolved","description":"_newLeader decides whether a late win opens overtime by comparing filedAt + overtime with `end`, which is the deadline in force at verdict time (c.endsAt, possibly already pushed by an earlier leader), not the deadline the brief was filed against. The README and the NatSpec promise that a brief 'filed in the last `overtime` seconds before the deadline, however long the jury took' pushes the deadline. When a brief ahead of it in the FIFO docket wins first and pushes endsAt forward by `overtime`, the later brief's filing time is no longer within `overtime` of the new endsAt even though it was within `overtime` of the deadline it was filed against. If its verdict then lands after the pushed deadline (two mistrial briefs between them are enough: each takes answerTimeout + MISTRIAL_GRACE), it takes the lead without opening overtime, and if nothing is queued behind it the case settles in the same call. The protection the tenth review added (filedAt + overtime > endsAt) therefore only holds for the first lead change in a window. Fix: compare against the deadline the brief was filed under (store endsAt at filing in the Brief's free 12 bytes of slot 2, or keep a per-case 'window opened at' marker), or make the rule explicit that the window is measured against the current deadline and document it on the site.","line":770,"path":"src/Briefs.sol","reproduction":"Params: overtime = 10 min, answerTimeout = 4 min, minDuration 10 min. Open a case ending at E. At E-9min Bob files; at E-9min+30s Alice files; Carol files three more. Bob is heard and wins at E-8min: endsAt becomes end1 = E+2min (Overtime event). Alice's hearing opens in that same call. Deliver Alice's valid COOKED attestation (issuedAt = heardAt+30, within her window) at end1+1 (nobody called mistrial; or equivalently put two mistrial briefs between Bob and Alice so her verdict lands at about E+5min). Expected per README/NatSpec: Alice was filed 8.5 min before the deadline that stood when she filed, inside the 10-min window, so her win pushes endsAt to now + 10 min. Actual: getCase(c).endsAt stays end1 (= E+2min) because filedAt + overtime = E+1.5min <= end1; no Overtime event; with an empty queue behind her the case settles immediately. Verified with test/scratch/Probe.t.sol::test_Probe_OvertimeLostWhenAnEarlierLeaderMovedTheDeadline (logs endsAt == end1 and alice filedAt + overtime < end1).","severity":"low","snippet":"        if (t < end || filedAt + c.overtime > end) {","title":"Overtime for a brief filed inside the window is lost when an earlier queued leader has already moved the deadline"},{"citation":"resolved","description":"The tenth review's fix makes the first refund of a signed message pay at least the case's entry fee, read by _feeOf from word 15 of Briefs.getCase. For a caseId that has not been opened (status None) getCase returns an all-zero struct, so _feeOf returns 0 and any amount >= 1 wei passes the floor. A signature for a case id that is about to be opened (caseCount + 1, which the next openCase will take) can therefore be marked refunded for 1 wei by anyone who holds it, and it can never be filed once the case exists. The guard exists precisely so that cancelling costs what filing would; for a not-yet-open case it costs nothing. Fix: in _feeOf, also read word 5 (status) and revert NothingToRefund when it is 0 (case not open), or require fee != 0.","line":318,"path":"src/BriefsJury.sol","reproduction":"caseId = briefs.caseCount() + 1 (no case yet). Author signs relayMessage(caseId, words, author, deadline) with personal_sign. Anyone calls jury.refundRelay(caseId, words, author, deadline, sig, 1) from an address holding 1 wei IMD. Expected: revert NothingToRefund (1 wei is below the entry fee the case will have, 5 IMD here; the floor was added so that cancelling costs what filing would). Actual: succeeds, relayed[h] == 2, refunded[h] == 1, RelayRefunded emitted. Then creator opens a case (it gets id caseId, fee 5 IMD) and fileBriefFor(caseId, words, author, deadline, sig) reverts BadSignature forever. Verified with test/scratch/Probe.t.sol::test_Probe_RefundFloorIsZeroForAnUnknownCase.","severity":"low","snippet":"        if (amount == 0 || (relayed[h] == 0 && amount < _feeOf(caseId))) revert NothingToRefund();","title":"refundRelay's entry-fee floor is zero for a case id that does not exist yet, so 1 wei cancels a signed brief for it"},{"citation":"resolved","description":"openCase pins the setup the creator saw (x.oracleId must equal jury.latest()). BriefsJury.applyOracle is permissionless once readyAt has passed and stays callable for ORACLE_WINDOW (7 days). During that window any address can call applyOracle in front of a creator's openCase transaction, which then reverts WrongOracle (after the creator's caseFee transferFrom has been attempted and rolled back). The grief is one-shot per proposal and the creator can resubmit with the new id, so this is a usability note rather than a loss, but the site should refresh oracleId on a WrongOracle revert, and the owner may prefer to call applyOracle promptly when a proposal is ready so the window in which this is possible is short.","line":624,"path":"src/Briefs.sol","reproduction":"Owner calls jury.proposeOracle(o); warp 7 days. Creator prepares openCase with oracleId = jury.latest() (0). Griefer calls jury.applyOracle() (latest becomes 1). Creator's openCase reverts WrongOracle. Expected: the creator's transaction opens the case with the setup they agreed to, or the site tells them the setup changed. Actual: revert; a retry with oracleId = 1 succeeds.","severity":"info","snippet":"        if (jury.latest() != x.oracleId) revert WrongOracle();","title":"A ready oracle proposal lets anyone make a creator's pending openCase revert with WrongOracle"}],"hash":"f0ad80db074c88d232cc89e603a6fba3f62099d31e8f35d41296e96dc9d4d13e","nodeId":"a7cbb76c-bf74-4694-bc79-c45a7dbc8bda","outcome":"completed","summary":"The review is complete and the findings file is written. Nothing else depends on a further tool call.\n\n**Outcome.** The audit of vadiszzz/briefs-contracts at commit 8d014e4 found no critical, high or medium defect. Three substantiated findings are in `.imd-findings.json`: two low, one info. Each names a concrete failing input and was confirmed by a scratch Foundry test that I ran.\n\n**Findings**\n\n- **Low, overtime window measured against a moved deadline.** In `_newLeader` the test `filedAt + overtime > end` uses the current deadline, which an earlier queued winner may already have pushed forward. A brief filed inside the window it was filed against, heard after that push and after the new deadline, wins without opening overtime and the case can settle in the same call. Reproduced with Bob winning at E minus 8 min, pushing the deadline to E plus 2 min, then Alice, filed at E minus 8.5 min, winning after that with no Overtime event.\n- **Low, relay refund floor is zero for an unopened case id.** `_feeOf` reads the fee from `getCase`, which returns zeros for a case that does not exist yet. A signature for the next case id can be marked refunded for 1 wei by anyone holding it, and can never be filed once the case opens. The guard was added so that cancelling costs what filing would.\n- **Info, permissionless `applyOracle` can make a pending `openCase` revert.** Anyone may apply a ready proposal in front of a creator's transaction, which then fails `WrongOracle`. One shot per proposal, no loss.\n\n**Coverage.** I traced every state-changing entry point in Briefs, BriefsJury, BriefsText, ImdGatewayRequester and ImdOracle through the Execution Trace, Periphery and First Principles passes: the hearing state machine and its stall and skip paths, the time-in-the-lead and overtime accounting, the fee split and solvency, the relay signature lifecycle, the Intake callback and verdict checks, the JSON and questionHash packing, and the text validator. The existing suite of 164 tests passes. Passes I did not run in depth: the twelve-specialty Asymmetry, Numerical Gap and Trust Gap guides beyond what the assigned three covered, and any live-chain verification of the IMD domain and Intake behaviour, which the project's own docs already list as open. A clean result on the core accounting is not proof that no defects exist.\n\n**Files.** Findings are in `.imd-findings.json` at the repository root. The verification test is `test/scratch/Probe.t.sol`, which imports the project's mocks and is for verification only, not attached as proof since no finding is high or critical. No source or configuration file was changed.","treeHash":null,"usage":{"cachedInputTokens":1666427,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":44746,"runtime":"claude","turns":28,"wallClockMs":643946}}],"verification":[]}