{"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":"6b607200-d29f-4c7a-8476-0dca8091f48f","kind":"audit","nodes":[{"acceptedSubmissionHash":"1b40377d6f8e557f76e63e313f63d45edd7ea81043ac36ede64d43be9c6e8741","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":"b2ea968a43c6b604fe788c6617868ab82335c9c913a06fb308e4bab4aae90314","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":"de62449b121100d34ceebbfd8b197c6da1dc9db719041aa89b853a220babc8b1","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":"f5a1983a3cad107a9a0c4b7bac98080395198e244edbf28e8dc5cabaf3223e59","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":"a6c9122932b7b948be8de53bfcabf25452605ce434dd6f8df1415d1923cc06d4","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":"Focused review of HookJobRequester.sol at commit 0d9d440063465654f39ef7f3939b1b4d9c90c5f6. Re-run the four actionable findings and proofs from audit ef1f6915-b9df-441d-b2e5-db5e9d9e9b35. Check both callback/expiry transaction orders, owner-confirmed expiry, conflicting or reused UUIDs, receipt discard, cross-request binding, replay, and the 200k-gas one-shot callback with ERC-1271. Report any material blocker and the unresolved live-Intake/owner-trust assumptions. Do not deploy. Unchanged hook, listing and ad contracts are not the focus.","parentJobId":null,"planHash":"4b70019acd87a2e2440a29be884716b3096dba1e6b92358e5a72ee809c9c2cd1","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"6b607200-d29f-4c7a-8476-0dca8091f48f","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":"51450","feedbackHash":"f210c0ea19b740ab38483d2881abf813070af191ca3d5fb104bda0eccdc402ba","nodeKey":"audit_economics","submissionHash":"1b40377d6f8e557f76e63e313f63d45edd7ea81043ac36ede64d43be9c6e8741","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50907","feedbackHash":"d8685693b6664e050024365e64c0f88f16867b83b4c035d295f975ec5d8003df","nodeKey":"audit_flow","submissionHash":"b2ea968a43c6b604fe788c6617868ab82335c9c913a06fb308e4bab4aae90314","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52131","feedbackHash":"09d9eb6c8555bcbf08e3996069e7767ed394b72d9449e70992ade5f60d54430c","nodeKey":"audit_judge","submissionHash":"de62449b121100d34ceebbfd8b197c6da1dc9db719041aa89b853a220babc8b1","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51232","feedbackHash":"1b1fece6d314bf2f529249aa434934c8dae15e25f3c43cec4e609a6aec5ba849","nodeKey":"audit_math","submissionHash":"f5a1983a3cad107a9a0c4b7bac98080395198e244edbf28e8dc5cabaf3223e59","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52157","feedbackHash":"3a86b68a20c243ce3da30ec4b3451f60b44b6b61bd3e11d431313c529cf65a6b","nodeKey":"audit_permissions","submissionHash":"a6c9122932b7b948be8de53bfcabf25452605ce434dd6f8df1415d1923cc06d4","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"b412d57af71106f151d22c6a5c11f3d58a4e68af6b016bd815eed27b81e99010","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f2101c240e0b52a9","findings":[{"citation":"resolved","description":"bindReview may be called before the Intake delivers (the 'ask -> exact owner binding -> matching verified callback' order documented in IMD_INTEGRATION.md). Once bound, onReviewResult requires all five signed context fields to equal the binding and reverts otherwise. The review question template carries a relative window (\"window\":{\"hours\":1}) whose resolved fromBlock/toBlock and canonical questionHash are only known after the plane resolves it, so a pre-binding is necessarily a guess for at least the window. When the genuine, validly signed callback arrives with any field different from the guess, the revert happens inside the Intake's single 200k try: the attempt is settled, no receipt is retained, no UUID is consumed, the ask stays Pending and bound, and the requester's oracle fee is spent with nothing to show. The unbound path (line 567-583) already retains such a callback as evidence; the bound path discards it. Because ask.oracleRequestId is immutable (line 393) the owner cannot correct the binding, and because expireReview is requester-only (line 625) the owner cannot even start closure. The honest recovery costs the requester a second fee: requester expireReview after reviewDeadline, owner confirmReviewExpiry, owner releaseReservation, requester requestReview again. Minimal fix that preserves the documented design: in the bound branch, instead of reverting on a field mismatch, retain the verified callback as a receipt (same StoredReviewReceipt write as the unbound branch, which already fits the stipend at 168k with the 5-of-7 signer) and emit ReviewReceived; the ask then stays Pending with its reservation, and the owner resolves it through the existing discardReviewReceipt rules (different available UUID, or exact context with conflict) or, if the design owner prefers, a new owner-only 'unbind an undelivered ask that holds a mismatching receipt' step. Alternatively, drop pre-binding entirely (require a receipt in bindReview); that is a design change and needs the requester's scope decision.","line":550,"path":"src/imd/HookJobRequester.sol","reproduction":"State: job Delivered; requester calls requestReview -> askId. Owner calls bindReview(askId, keccak256(body), X, Q, 31337, fromBlock=1, toBlock=2). Input: Intake delivers onReviewResult(askId, a, sig) where a = {requestId X, questionHash Q, chainId 31337, fromBlock 1, toBlock 3, bool false, panel 7/5/6, fresh issuedAt/expiresAt} correctly signed by oracleSigner. Expected (per the receipt-first guarantee the contract gives unbound asks): the verified answer is retained for owner inspection. Actual: ReviewAttestationMismatch revert inside MockIntake.complete's 200k try; complete() returns false, intake.reqs(askId).settled == true, getReviewReceipt(askId).oracleRequestId == 0, getJob(jobId).review == Pending, consumed[X] == false, intakeReviewOfOracle[X] == askId. Afterwards owner.expireReview(jobId) reverts NotRequester, owner.confirmReviewExpiry(askId) reverts ReviewExpiryNotRequested, owner.releaseReservation(X) reverts ReservationNotReleasable. Verified with a scratch Foundry test on HookJobRequesterFixture (test/scratch/Probe.t.sol::testPreboundWindowGuessLosesGenuineAnswer, passes on current code showing delivered == false).","severity":"low","snippet":"        ) revert ReviewAttestationMismatch(requestId);","title":"Pre-delivery binding turns any divergence in the genuine callback into permanent loss of the one-shot answer and the paid fee"},{"citation":"resolved","description":"Closure of a pending review ask has exactly three triggers: an Intake failure callback (onReviewFailure), owner discard (requires a receipt, line 458), or owner confirmReviewExpiry which requires reviewExpiryRequested (line 645), a flag only the job's requester can set (expireReview, this line). releaseReservation additionally requires reviewOf[holder] == 0 (line 495). So for an ask the owner pre-bound to UUID X whose Intake one-shot was lost (see the previous finding, or an over-budget signer, or a wrong ABI) and whose requester never returns, nothing the owner or Intake can do releases X or moves the job out of Pending. The contract's own UUID-reuse support (releaseReservation exists precisely because the protocol may legitimately reuse a UUID for a retry) is therefore conditional on a third party's cooperation. Impact is bounded: only that requester's job is stuck, and other asks are affected only if the protocol later issues X again (then their callbacks are retained but DuplicateRequestId blocks binding until the conflict path discards them, costing that other requester a second fee). Minimal fix preserving the design: let the owner also call expireReview after the snapshotted deadline (the flag is only a request; closure still needs confirmReviewExpiry and the no-receipt check), or let releaseReservation accept a holder whose ask is past reviewDeadline, has no receipt and has an expiry request from either party.","line":625,"path":"src/imd/HookJobRequester.sol","reproduction":"State: job Delivered; requester requestReview -> askA; owner bindReview(askA, bodyHash, X, Q, 31337, 1, 2). Intake's single callback for askA reverts (any cause) or never comes; the requester's key is lost. Input: after reviewDeadline+1, owner calls expireReview(jobA) -> NotRequester; owner calls confirmReviewExpiry(askA) -> ReviewExpiryNotRequested(askA); owner calls releaseReservation(X) -> ReservationNotReleasable(X). Expected: an owner path to close a deadline-passed, receipt-less ask. Actual: getJob(jobA).review stays Pending and intakeReviewOfOracle[X] == askA indefinitely. If the protocol later issues X for ask B of another job, B's callback is retained (unbound path ignores reservations) but bindReview(B, ..., X, ...) reverts DuplicateRequestId(X) and B can only be closed as 'receipt UUID conflict' Unanswered, after which B pays PRICE again. Verified with test/scratch/Probe.t.sol::testPreboundWindowGuessLosesGenuineAnswer for the three owner reverts.","severity":"low","snippet":"        if (msg.sender != job.requester) revert NotRequester();","title":"A bound ask that never receives a callback can only be closed by its requester, so an absent requester pins the reserved oracle UUID forever"},{"citation":"resolved","description":"The only bound on what a user's standing allowance can be charged is the contract-wide maxPrice, which the owner is instructed to raise whenever the published price moves up (IMD_INTEGRATION.md 'setPayment' row). Between the user signing openJob/requestReview and its inclusion, the Intake (a third party) can raise priceOf and the user pays the new price with no opt-in, as long as it is at or under the ceiling. The user never states the price they agreed to. The 'no refund' rule makes the overpayment final. This is not an owner-trust issue when the ceiling was raised ahead of time for operational convenience; it is the ordinary missing-slippage-bound pattern on a price read from an external contract. Minimal fix preserving the design: add a `uint256 maxPrice_` argument to openJob and requestReview and revert if price > min(maxPrice_, maxPrice); the static site already shows the current price before the user signs.","line":788,"path":"src/imd/HookJobRequester.sol","reproduction":"State: user approved type(uint256).max IMD to the requester (the pattern the README's flow and tests use); owner has set maxPrice = 5e18 to cover an announced price change; intake.priceOf(jobAction, IMD) currently 5e17. Input: user submits openJob(spec, brief) expecting 0.5 IMD; before inclusion the Intake sets priceOf to 5e18. Expected: the user pays the 0.5 IMD they saw, or the call reverts. Actual: _pull reads 5e18, 5e18 <= maxPrice, transferFrom(user, this, 5e18) succeeds and is forwarded to the Intake; balanceOf(user) drops by 5e18 (test/scratch/Probe.t.sol::testCeilingRaiseFrontRunsUser shows 5 ether pulled). No refund path exists.","severity":"low","snippet":"        if (price > maxPrice) revert PriceAboveCeiling(price, maxPrice);","title":"openJob and requestReview have no caller-supplied price limit: the amount pulled is whatever intake.priceOf returns at execution, up to the owner's ceiling"},{"citation":"resolved","description":"Documented in IMD_INTEGRATION.md and AUDIT_EF1F6915.md, recorded here as the reviewed trust boundary rather than as defects. (1) setIntake/setOracleSigner apply at callback time to asks paid under the previous settings; there is no per-ask snapshot or timelock, so the owner can point intake at an address it controls and oracleSigner at its own key, then call onReviewResult for any pending ask with a self-signed attestation whose fields match its own bindReview (or deliver onJobResult/onJobFailure for any Open job), and restore both. (2) confirmReviewExpiry proves no absence of an in-flight answer; a confirmation before a late valid callback discards it (UnknownOrSettled inside the Intake try). (3) discardReviewReceipt's different-UUID path accepts any well-formed, unconsumed, unreserved UUID the owner names; the owner can therefore close a genuine NotPassed receipt as 'misdelivered receipt' and let the requester re-ask. (4) bindReview's mapping is owner-asserted; an incorrect pairing produces a verdict for the wrong question. (5) setPayment raises the ceiling against users' standing allowances (see the separate low finding). (6) Owner unavailability blocks every finalization, discard, release and confirmed expiry; asks and reservations stay Pending indefinitely. (7) sweep can move any token the contract holds, which is only residue. Mitigations the repository already names: multisig owner, event monitoring of IntakeSet/OracleSignerSet/ReviewReceiptDiscarded/ReviewUnanswered, drain pending work before rotation. None is enforced on-chain.","line":699,"path":"src/imd/HookJobRequester.sol","reproduction":"Concrete state for (1): job J Delivered, ask A pending and unbound. Owner: setOracleSigner(ownerKeyAddr); setIntake(ownerEOA); bindReview(A, bodyHash, X, Q, 31337, 1, 2); as ownerEOA call onReviewResult(A, a, sig) with a = {X, 31337, Q, bool true, panel 7/5/6, fresh times} signed by ownerKey. Expected per users' reading of 'Passed': an IdentityMD oracle panel attested. Actual: getJob(J).review == Passed, consumed[X] == true, with no IdentityMD involvement; the owner then restores intake and signer. For (3): ask B holds a genuine NotPassed receipt with UUID Xb; owner calls discardReviewReceipt(B, bodyHash, Y, Q, 31337, 1, 2) with any unused Y; result ReviewUnanswered(reason 'misdelivered receipt'), and requestReview(jobB) is accepted again.","severity":"info","snippet":"        intake = IIntake(to);","title":"Owner trust assumptions that the consistency checks do not remove: current Intake and signer authorize deliveries for already-paid asks, and discard/expiry/ceiling powers can suppress genuine answers "},{"citation":"resolved","description":"All local evidence runs against test/helpers/MockIntake.sol. The following are assumptions about the live Intake v2 / oracle that remain unverified at this commit and that fail closed into an irrecoverable spent fee (the ask is settled on the Intake side, nothing is retained, and the requester must pay again after owner-confirmed expiry): (a) the job.open completion ABI is (bytes32,bytes) and the review ABI is (bytes32, Attestation, bytes), with selectors taken from this contract; (b) callbacks are asynchronous: _recordIntakeId and reviewOf are written only after requestWithFailure returns (lines 282-283, 357-359), so a synchronous callback during the request reverts UnknownOrSettled; (c) the Intake pulls exactly `amount` (MockIntake enforces amount == price); if it pulls less, the difference stays in the contract and is only recoverable by owner sweep, if more, the request reverts; (d) the attestation arrives within its own expiresAt (template validForSeconds 3600) and issuedAt within 300 s of chain time and not more than 300 s before askedAt; an answer delivered 3601 s after issuance reverts AttestationExpired at line 564 even on the unbound path; (e) the oracle signer at 0x5598aa... behaves within the 60k cold isValidSignature budget and 325-byte signature size assumed by ReviewReceiptGas.t.sol: measured minimum forwarded gas is 123,013 (ECDSA), 168,106 (5-of-7 ERC-1271), 185,260 (22-cold-read registry) against the 200,000 stipend, so a live registry costing about 15k more than the stressor loses every receipt-first delivery; (f) oracle UUIDs are not reused for distinct questions across asks; a reuse after consumption makes the later callback revert AlreadyConsumed (line 566) and a reuse while reserved forces the conflict-discard and a second fee; (g) an authenticated post-delivery Intake-ID to oracle-UUID and canonical-question source exists so the owner can bind at all; without it every ask stays Pending. Items (a), (b), (d), (e) are the material blockers for paid use; the README and docs already say no pilot was run.","line":302,"path":"src/imd/HookJobRequester.sol","reproduction":"Example for (d): ask A unbound, askedAt = T. Oracle issues a = {issuedAt T+10, expiresAt T+3610, ...} validly signed. Intake delivers onReviewResult(A, a, sig) at block.timestamp = T+3611. Expected by the operator: late answers are retained for owner confirmation. Actual: _verifyAttestation reverts AttestationExpired(T+3610) inside the Intake's single try; receipt stays empty, fee spent, only path is requester expireReview after reviewDeadline plus owner confirmReviewExpiry plus a new paid ask. Example for (e): set oracleSigner to a registry whose cold isValidSignature costs 75k instead of the tested 60k; forwarded 200,000 gas is exhausted before the four receipt SSTOREs (ReviewReceiptGas.t.sol::testOverBudgetSignerStillFailsClosedAndLosesSingleAttemptWithoutPartialReceipt reproduces this with the 64-read fixture).","severity":"info","snippet":"    function onJobResult(bytes32 requestId, bytes calldata result) external onlyIntake {","title":"Unresolved live-Intake assumptions that this contract cannot verify locally and that each lose the one-shot callback plus fee if wrong"}],"hash":"1b40377d6f8e557f76e63e313f63d45edd7ea81043ac36ede64d43be9c6e8741","nodeId":"d6aa8461-f2cb-45f7-b196-651b790dcc57","outcome":"completed","summary":"Review complete. The findings file holds five entries: three low, two info. No critical or high defect was found in HookJobRequester.sol at commit 0d9d440, so no proof tests are attached.\n\n**Re-run of the four audit ef1f6915 fixes.** All hold on this commit. The literal proofs in `test/AuditExpiryProof.t.sol` and `test/AuditReceiptConflictProof.t.sol` pass, and the 122 requester-related tests pass. I traced each mechanism by hand as well:\n\n- Expiry front-run: `expireReview` only sets a flag. Both orders keep a late callback, the flag never closes anything, and `confirmReviewExpiry` refuses when a receipt exists or the ask settled.\n- Conflicting UUID: same-UUID discard needs all five fields equal plus an on-chain conflict. Different-UUID discard needs an unconsumed, unreserved replacement. No state about the other ask changes.\n- Reservation burning the retry: the unbound callback path ignores reservations and retains the receipt. Release then exact binding finalizes without a second fee.\n- Typo discard: same-UUID field typos revert even with a real conflict.\n\n**Replay and cross-request binding.** EIP-712 domain pins chain and contract. Consumption happens once, either at bound delivery or at binding over a receipt. Intake IDs are never recycled. Job and review callbacks cannot be confused because each checks its own mapping. I found no unprivileged path to shop verdicts or grief another requester.\n\n**ERC-1271 under the 200k stipend.** Measured cold minimum forwarded gas is 123,013 for ECDSA, 168,106 for the 5-of-7 signer, and 185,260 for the 22-cold-read stressor. The live registry has never been measured, so the headroom is a pilot item, not a local guarantee.\n\n**What I reported.**\n\n1. **Low.** A pre-bound ask drops any genuine callback whose resolved window or hash differs from the owner's guess. The unbound path retains such a callback. The bound path reverts inside the Intake's single try, so the fee is lost. Fix: retain the receipt in the bound branch too, then resolve through the existing discard rules.\n2. **Low.** Only the requester can start closure of a bound, undelivered ask. An absent requester pins the reserved UUID forever and the owner has no path out.\n3. **Low.** No caller-supplied price limit on `openJob` or `requestReview`. A user with a standing allowance pays whatever the Intake charges at inclusion, up to the owner's ceiling.\n4. **Info.** Owner-trust boundary: live Intake and signer settings authorize deliveries for already-paid asks, discard can close a genuine negative with any unused UUID, and expiry confirmation proves no absence of an answer.\n5. **Info.** Live-Intake blockers: callback ABI shape, asynchronous delivery, exact-amount pull, the 3600 second arrival window, real signer gas, and UUID reuse policy. Each one, if wrong, burns the one-shot delivery and the fee.\n\nScratch tests were removed after verification. Nothing was deployed and no repository file outside the findings file was changed.","treeHash":null,"usage":{"cachedInputTokens":1255785,"inputTokens":322,"model":"claude-fable-5-1","outputTokens":37409,"runtime":"claude","turns":37,"wallClockMs":644836}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"37eed9f56188ea8b","findings":[{"citation":"resolved","description":"Asymmetry (access x asymmetry seam): requestReview accepts Delivered or Expired but rejects Failed. expireJob is requester-controlled once block.timestamp > deadline and is checked only against status == Open, and onJobFailure/onJobResult revert UnknownOrSettled unless the job is still Open. A requester who sees the Intake's late failure (status 2, 'ended without a result') or completion in the mempool after the 7-day deadline can send expireJob first. The Intake's single attempt reverts inside its try and is lost: the job shows Expired with no failureStatus/failureReason (or no resultHash), and the requester can now pay for a review that a Failed job could not buy. Impact is bounded because the shipped bool question defines a missing review as false, so the bought review should settle NotPassed; the material effect is that the Intake's authenticated terminal status and reason never reach the contract or the static page, and the Failed/Expired eligibility split is bypassable by timing. Minimal fix preserving the design: let onJobResult/onJobFailure also land on an Expired job (status Expired -> Delivered/Failed, keeping the one-shot evidence), or treat Failed and Expired symmetrically in requestReview; either way the requester's choice of transaction order stops changing what the contract records.","line":352,"path":"src/imd/HookJobRequester.sol","reproduction":"State: job opened by creator at t0 (status Open, deadline t0+7d); warp to deadline+1. Tx1 (creator): expireJob(jobId) -> status Expired. Tx2 (Intake): fail(jobId, 2, 'no result', 0, 0) -> callback reverts UnknownOrSettled, MockIntake.complete/fail returns false, status stays Expired with failureStatus 0 and failureReason 0. Tx3 (creator): requestReview(jobId) -> succeeds (reviewOf[askId] == jobId). Control: same job with Tx2 delivered first -> status Failed, and requestReview reverts NotReviewable(jobId). Expected: the Intake's terminal status is recorded regardless of the requester's ordering, and review eligibility is not a function of who wins the race. Actual: the ordering decides both. Reproduced in test/scratch/ExpireJobFrontRun.t.sol (both tests pass on current code, showing the asymmetry): testRequesterTurnsImminentFailureIntoExpiredAndBuysReview and testJobInFailedStateIsNotReviewable, using the repository's HookJobRequesterFixture and MockIntake.","severity":"low","snippet":"            (job.status != JobStatus.Delivered && job.status != JobStatus.Expired)","title":"Requester can front-run a late Intake failure with expireJob, turning an unreviewable Failed job into a reviewable Expired one and burning the Intake's one-shot failure notice"},{"citation":"resolved","description":"openJob and requestReview call the Intake (interaction) and only afterwards record usedIntakeRequestIds, jobs[requestId] / reviewOf[intakeRequestId] (effects). If the live Intake ever invokes onJobFailure/onJobResult/onReviewFailure/onReviewResult in the same call (for example an immediate refusal, status 1), the callback passes onlyIntake but reverts UnknownOrSettled, the Intake's single attempt is consumed, the fee is spent, and the ask stays Open/Pending: a job can only be closed by the requester after 7 days, a review ask only by requester expiry request plus owner confirmation after 1 day. docs/IMD_INTEGRATION.md line 316 already lists 'callbacks must be asynchronous' as an unsupported-if-false pilot assumption; it is restated here because it is the one ordering in this contract that the local MockIntake cannot exercise and that no onchain guard covers. Not fixable in this contract without knowing the Intake id ahead of the call; it is a pilot blocker to confirm, not a code change.","line":282,"path":"src/imd/HookJobRequester.sol","reproduction":"Deploy HookJobRequester against an Intake whose requestWithFailure pulls the fee, derives id, and calls cb.target with onFailure(id, 1, 'refused', 0, 0, '') under 200k gas before returning id. creator: openJob(specHash, 'hook brief'). Expected (if the live Intake behaves this way): job recorded Failed with status 1. Actual: callback reverts UnknownOrSettled(id) inside the Intake, requestWithFailure returns id, job is recorded Open, 0.5 IMD paid, expireJob reverts DeadlineNotReached for 7 days. Reproduced in test/scratch/SyncCallback.t.sol::testSynchronousRefusalIsDroppedAndJobStaysOpen (passes on current code, demonstrating the drop).","severity":"info","snippet":"        requestId = _ask(jobAction, body, this.onJobResult.selector, this.onJobFailure.selector, price);","title":"Unresolved live-Intake assumption: a callback delivered synchronously inside request/requestWithFailure is dropped because job/ask state is written after the external call"},{"citation":"resolved","description":"Trust gap (access x asymmetry): for a different-UUID discard the contract checks only that the proposed UUID is well-formed, unconsumed and unreserved; nothing ties it to the Intake ask. A dishonest or mistaken owner therefore invents a fresh UUID, marks a correctly delivered, signature-verified NotPassed receipt as 'misdelivered receipt', the job becomes Unanswered, and the requester may buy another ask whose Passed answer the owner then binds. This is the documented boundary (docs/AUDIT_EF1F6915.md 'Expiry and owner trust', docs/IMD_INTEGRATION.md lines 307-309, docs/THREAT_MODEL.md 'Malicious requester owner'); the contract cannot authenticate the Intake-to-oracle mapping onchain. Report, not a bypass: keep the intended owner role, run it from the requester-approved multisig, and have the static page surface ReviewReceiptDiscarded with its receivedOracleRequestId vs authenticatedOracleRequestId so users can see when a receipt was closed against a UUID that never appeared in any signed answer.","line":470,"path":"src/imd/HookJobRequester.sol","reproduction":"1) creator: openJob, Intake completes, creator: requestReview -> askA (unbound). 2) Intake delivers a validly signed NotPassed for askA with UUID U -> receipt retained, review Pending. 3) owner: discardReviewReceipt(askA, bodyHashA, U' = any unused bytes32, qA, chainA, fromA, toA) with U' != U -> succeeds, emits ReviewUnanswered(.., 'misdelivered receipt'), job review = Unanswered. 4) creator: requestReview -> askB; owner binds askB; Intake delivers Passed -> job review = Passed although a genuine NotPassed for the same job was received. Expected under a stricter model: the owner cannot retire a verified receipt without onchain evidence; actual: allowed by design. Reproduced in test/scratch/OwnerTrust.t.sol::testOwnerLaundersRetainedNotPassedWithInventedUuid (passes on current code).","severity":"info","snippet":"            if (consumed[oracleRequestId] || holder != bytes32(0)) revert DuplicateRequestId(oracleRequestId);","title":"Owner-trust assumption: discardReviewReceipt's misdelivery path accepts any unused UUID as the 'true' context, so the owner can close a genuine retained NotPassed and let the requester retry"},{"citation":"resolved","description":"The audit ef1f6915 fix for finding 1 moved final expiry from the requester to the owner. Both callback/expiry orders were re-run and hold: an expiry request alone never blocks delivery or binding, and a receipt always blocks confirmation (ReviewAuditRegression.t.sol, AuditExpiryProof.t.sol, both pass). What remains is the stated residual: confirmReviewExpiry checks only reviewExpiryRequested and absence of a receipt. After it, reviewOf is deleted and the Intake's one-shot onReviewResult reverts UnknownOrSettled; the job is Unanswered and the requester may buy a new ask. The contract cannot prove absence of an in-flight answer; the owner must hold authenticated Intake terminality evidence (docs/IMD_INTEGRATION.md lines 107-114, 324). This is reported as the live/owner assumption the task asked for, not as a bypass. If the operator wants a bound on this power, a minimum delay between ReviewExpiryRequested and confirmation (e.g. the attestation lifetime, 1 hour, after the ask deadline) would at least rule out confirmations that race a fresh signature, at the cost of a slower recovery path; that is a design decision, not a required fix.","line":649,"path":"src/imd/HookJobRequester.sol","reproduction":"1) creator: openJob, delivered, requestReview -> askId (deadline t+1d). 2) warp deadline+1; creator: expireReview(jobId) -> reviewExpiryRequested[askId] = true, still Pending. 3) owner: confirmReviewExpiry(askId) -> review Unanswered, reviewOf[askId] = 0. 4) Intake delivers a validly signed, fresh NotPassed for askId -> callback reverts UnknownOrSettled, MockIntake.complete returns false, attempt consumed. 5) creator: requestReview(jobId) succeeds. Expected under an onchain-evidence model: step 3 refused while a valid answer can still arrive; actual: allowed, by documented design. Reproduced in test/scratch/OwnerTrust.t.sol::testOwnerConfirmedExpiryBurnsLaterGenuineNotPassed and in the repository's ReviewAuditRegression.t.sol::testExpiryNeedsRequesterDeadlineAndCurrentOwnerApprovalForExactAsk (assertFalse(_deliver(askId, ...)) after confirmation).","severity":"info","snippet":"        delete reviewOf[intakeRequestId];","title":"Owner-trust assumption: confirmReviewExpiry needs no onchain evidence that the Intake is terminal, so an early confirmation burns a later genuine answer and opens a paid retry"},{"citation":"resolved","description":"onlyIntake compares against the current intake and _verifyAttestation against the current oracleSigner at delivery time. Rotating either while asks are pending has two asymmetric effects: the old Intake's in-flight one-shot callbacks revert NotTheIntake and are lost (requester paid, no refund, needs owner-assisted expiry), and a new Intake/signer chosen by the owner can deliver and sign any verdict for any pending Intake ID visible in ReviewRequested events, including a Passed for a bound ask whose context the owner also chose. The powers are intentional (documented at the setters, docs/IMD_INTEGRATION.md lines 298-306, THREAT_MODEL 'Malicious requester owner') and the existing test ReviewBinding.t.sol::testOwnerRotationPowersApplyToAlreadyPendingRequestsAsDocumented asserts them. Listed as the trust assumption the task asked to be reported; no code change is proposed beyond the documented multisig/event-monitoring controls, and a per-ask snapshot of intake/signer would be a design change with its own stuck-ask cost.","line":699,"path":"src/imd/HookJobRequester.sol","reproduction":"State: askA pending (paid 0.5 IMD), bound by owner to (U, q, chain, from, to). owner: setOracleSigner(ownerKeyAddress); owner signs OracleAttestation{requestId U, questionHash q, chainId, fromBlock, toBlock, answer true, panelSize 7, quorum 5, agreed 6, fresh issuedAt/expiresAt} in the contract's EIP-712 domain; owner: setIntake(ownerContract); ownerContract calls onReviewResult(askA, a, sig) -> review Passed, consumed[U] = true. Conversely with the genuine Intake still holding askA's callback: after setIntake(other), the genuine Intake's complete(askA, ...) returns false (NotTheIntake) and the attempt is gone. Expected under a snapshot model: pending asks keep the Intake/signer they were paid under; actual: current values govern, by documented design.","severity":"info","snippet":"        intake = IIntake(to);","title":"Owner-trust assumption: setIntake and setOracleSigner apply immediately to already-paid pending asks with no timelock or per-ask snapshot"},{"citation":"resolved","description":"Re-measured on this commit with ReviewReceiptGas.t.sol (binary search, cold consumer and signer, exact 200k MockIntake call): cold EOA receipt 123,013; cold 5-of-7 ERC-1271 threshold signer (five real recoveries, 325-byte signature) 168,106 receipt-first and 132,880 pre-bound; the audit's '+22 cold reads' registry 185,260 receipt-first. Each fits; the 64-cold-read fixture does not, and testOverBudgetSignerStillFailsClosedAndLosesSingleAttemptWithoutPartialReceipt shows the fail-closed result: no receipt, no consumption, attempt settled, fee spent, owner-assisted expiry required. The receipt-first path is the heavy one because it writes four cold context slots plus the preallocated details word before the stipend ends. The production OracleSignerRegistry, proxy layers, signature size and chain gas schedule are not in this tree, so whether the live callback fits is an unresolved pilot item (docs/IMD_INTEGRATION.md line 322, 60k cold signer budget). No code defect; recorded so the operator requalifies the budget on every signer or policy change.","line":564,"path":"src/imd/HookJobRequester.sol","reproduction":"forge test --match-path test/ReviewReceiptGas.t.sol -vv on commit 0d9d440 prints: 'Cold EOA receipt minimum forwarded gas: 123013', 'Cold 5-of-7 receipt minimum forwarded gas: 168106', 'Cold 5-of-7 bound minimum forwarded gas: 132880', 'Cold audit registry +22 reads receipt minimum forwarded gas: 185260'. Failing input: setOracleSigner(new ColdReadReceiptSigner(signer, 64)) then Intake.complete(askId, abi.encode(askId, validAttestation, validSig)) under 200k -> returns false, getReviewReceipt(askId).oracleRequestId == 0, consumed[a.requestId] == false, Intake request settled. Expected for production: the actual registry's isValidSignature measured cold under 60k and the whole receipt-first callback under 200k on the target chain; actual: only local fixtures measured.","severity":"info","snippet":"        bytes32 digest = _verifyAttestation(a, signature);","title":"Unverified live assumption: the 200,000-gas one-shot receipt-first callback has 14.7k to 31.9k headroom with the local ERC-1271 fixtures; the real signer registry is unmeasured and an over-budget sign"}],"hash":"a6c9122932b7b948be8de53bfcabf25452605ce434dd6f8df1415d1923cc06d4","nodeId":"d1ba9998-6153-4e78-b807-b1cce455db0a","outcome":"completed","summary":"The findings file validates: six entries, every snippet matches its line in the tree. The review is complete.\n\n**Outcome: no material blocker in `HookJobRequester.sol` at commit 0d9d440.** The four audit ef1f6915 fixes hold, and the only new code-level issue is a low-severity asymmetry. Findings are in `.imd-findings.json`.\n\n**Re-run of the four prior findings** (all 80 tests in the Review and Audit suites pass):\n- Expiry front-run of a late NotPassed: both transaction orders, both verdicts, pre-bound and receipt-first, all preserve the verdict and block retries.\n- Stuck receipt whose UUID was consumed elsewhere: the exact-context conflict discard works and leaves the other ask's state untouched.\n- Terminal reservation burning the retry callback: an unbound callback is retained before release, then bound after.\n- Same-UUID typo discard: every field typo reverts even with a real conflict.\n- Gas, re-measured myself: EOA receipt 123,013; 5-of-7 ERC-1271 receipt 168,106 and bound 132,880; audit +22-cold-reads registry 185,260. All under 200k; the 64-read fixture fails closed and loses the attempt.\n\n**New finding (low):** `requestReview` accepts Expired but not Failed jobs, while `expireJob` is requester-timed. A requester who sees the Intake's late failure in the mempool after day 7 can expire first, burning the one-shot failure notice and buying a review a Failed job could not. Impact is bounded because the oracle defines a missing review as false. Suggested fix: let a late `onJobResult`/`onJobFailure` still land on an Expired job, or treat Failed and Expired symmetrically.\n\n**Unresolved live-Intake and owner-trust assumptions, each recorded as info with a concrete reproduction:**\n- A synchronous callback inside `request` is dropped, since state is written after the external call.\n- The misdelivery discard path accepts any unused UUID, so an owner can retire a genuine NotPassed.\n- Owner-confirmed expiry needs no onchain terminality evidence and burns a later genuine answer.\n- `setIntake` and `setOracleSigner` govern already-paid pending asks with no snapshot or timelock.\n- The production signer registry's gas is unmeasured; only local fixtures were qualified.\n\nNot covered: the unchanged hook, listing and ad contracts, per the brief. Scratch tests under `test/scratch/` demonstrate each behaviour and pass on the current code; they are not fix-proofs, since nothing rose to high severity.","treeHash":null,"usage":{"cachedInputTokens":1560293,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":35400,"runtime":"claude","turns":42,"wallClockMs":533478}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"d5e40d018a0986f3","findings":[{"citation":"resolved","description":"Verification is final at receipt and there is no receipt revocation. A retained receipt whose UUID, question hash, chain and window all equal the ask's true context, but which the owner must not finalize (signed by a key later found compromised, or an ERC-1271 registry member revoked after delivery), blocks every closure path: discardReviewReceipt on the same UUID reverts ReviewReceiptMatches unless the UUID is consumed or reserved by another ask (line 465-467); confirmReviewExpiry and expireReview revert ReviewAlreadyReceived (lines 629-631, 646-648); onReviewFailure reverts ReviewAlreadyReceived (line 613). The only remaining owner moves are to bind it (accepting the untrusted verdict) or to supply a fabricated different UUID through the 'misdelivered receipt' branch, which the docs forbid. The job's review stays Pending forever and requestReview reverts NotReviewable, so the requester can never buy a retry. docs/IMD_INTEGRATION.md 'Two retained design choices' records this as intentional; it is reported because the resulting state is unrecoverable without a dishonest call. Fixing it needs a scope decision: either an owner-only void operation gated on an explicit signer-rotation epoch (receipts verified under a signer address that is no longer oracleSigner may be closed as Unanswered), or acceptance of the stuck state as policy.","line":465,"path":"src/imd/HookJobRequester.sol","reproduction":"State: HookJobRequesterFixture setUp (owner, creator, ECDSA signer). creator openJob -> intake.complete(job) -> creator requestReview(job) = askA (unbound). Intake delivers attestation a = _attestation(askA,true) signed by signerKey: receipt stored, ReviewReceived. Owner then learns signerKey was compromised and calls setOracleSigner(newKey). Expected: owner can close askA as Unanswered so creator may retry. Actual: discardReviewReceipt(askA, bodyHash, a.requestId, a.questionHash, 31337, 1, 2) reverts ReviewReceiptMatches(askA); confirmReviewExpiry(askA) reverts ReviewExpiryNotRequested then, after warp past deadline and creator expireReview(job), expireReview itself reverts ReviewAlreadyReceived(askA); creator requestReview(job) reverts NotReviewable(job). getJob(job).review == Pending indefinitely.","severity":"low","snippet":"            if (!consumed[oracleRequestId] && (holder == bytes32(0) || holder == intakeRequestId)) {","title":"Receipt with exact true context but untrusted origin has no honest closure: job stuck Pending, no retry"},{"citation":"resolved","description":"requestReview accepts Expired jobs so that a lost completion notice does not block a verdict, while Failed jobs (Intake refused, status 1, or no result, status 2) are not reviewable. Because expireJob only requires status Open and the deadline, a requester who sees the Intake's onJobFailure transaction after the 7-day deadline can send expireJob first. The one-shot failure callback then reverts UnknownOrSettled inside the Intake's try and the job records Expired instead of Failed, so the requester can pay for a review of a job that the Intake refused. The oracle still signs the verdict, so no false Passed can be manufactured; the effect is a purchasable review on a job with no delivered code and a lost failure reason on chain. If Failed and Expired are meant to differ in reviewability, record the failure path before the deadline check or allow expiry only when the Intake request is terminal; otherwise document that Expired may hide a late refusal. Untested edge: no suite exercises expireJob racing onJobFailure.","line":352,"path":"src/imd/HookJobRequester.sol","reproduction":"State: fixture setUp. id = creator openJob(SPEC_HASH, brief). vm.warp(block.timestamp + 7 days + 1). creator expireJob(id) -> status Expired. intake.fail(id, 1, \"body_invalid\", 0, 0) returns false (callback reverted UnknownOrSettled(id)); getJob(id).failureStatus == 0. creator requestReview(id) succeeds and reviewOf(ask) == id. Expected under a Failed outcome: requestReview reverts NotReviewable(id). Actual: a paid review ask is opened for the refused job.","severity":"info","snippet":"            (job.status != JobStatus.Delivered && job.status != JobStatus.Expired)","title":"After the job deadline, expireJob can front-run a pending onJobFailure and make a refused job reviewable"},{"citation":"resolved","description":"Re-run of audit ef1f6915: all four remediations hold locally. Both literal proofs (test/AuditExpiryProof.t.sol, test/AuditReceiptConflictProof.t.sol) and the regression suites pass at this commit (122 tests), and the cold one-shot ERC-1271 measurements stay under the 200k stipend (ECDSA receipt 123,013; 5-of-7 receipt 168,106 / bound 132,880; +22 cold reads 185,260). What cannot be established without a live pilot and therefore remains a material blocker before paid use: (1) the job.open callback shape (bytes32, bytes) at line 302 and the failure shape at line 313 are assumptions; a different live ABI reverts inside the Intake try, leaves the job Open, and the fee is spent with only expiry as exit. (2) The attestation's requestId/questionHash/window are never derivable on chain from the Intake request id or the body; bindReview takes them as owner input (lines 381-389). (3) The Intake forwards exactly one attempt; any real signer or proxy above the 60k cold isValidSignature budget (SignatureChecker forwards all gas, line 85 of the vendored library) loses the answer and the fee, as test/ReviewReceiptGas.t.sol testOverBudgetSigner shows. (4) Intake IDs are assumed unique and nonzero; a reused id reverts the whole paid ask (line 781). None of these is a code defect to fix here; they are the acceptance conditions for a pilot.","line":302,"path":"src/imd/HookJobRequester.sol","reproduction":"State: fixture setUp, id = creator openJob(...). intake.complete(id, hex\"deadbeef\") returns false and getJob(id).status stays Open (test/HookJobRequester.t.sol testCallbackWithUnexpectedAbiFailsSafeAndJobCanExpire). Expected on a live Intake with the assumed ABI: Delivered. Actual if the live shape differs: Open until the requester calls expireJob after 7 days; the PRICE already transferred to the Intake is not refunded.","severity":"info","snippet":"    function onJobResult(bytes32 requestId, bytes calldata result) external onlyIntake {","title":"Live-Intake blockers remain unverified: job.open callback ABI, UUID mapping and one-shot 200k delivery"},{"citation":"resolved","description":"These powers are intentional and documented (docs/THREAT_MODEL.md 'Malicious requester owner', docs/AUDIT_EF1F6915.md 'Expiry and owner trust') but are not enforced by any on-chain check, so they are listed as the trust boundary a requester accepts. (a) setIntake/setOracleSigner apply immediately to every pending ask: a new Intake address can call onReviewResult for any pending ask with any attestation the new signer signs, producing Passed/NotPassed with no requester consent. (b) bindReview pins whatever context the owner supplies; the contract checks only consistency with the ask body hash and an existing receipt, never the pairing itself, so a wrong binding before the callback makes the genuine one-shot answer revert ReviewAttestationMismatch and be lost (scratch probe: bind askB with askA's receipt context, then askB's own callback returns false). (c) confirmReviewExpiry needs only the requester flag and no receipt; the owner cannot prove absence of an in-flight answer, so a premature confirmation discards a later NotPassed (callback reverts UnknownOrSettled) and lets the requester buy a retry. (d) discardReviewReceipt's misdelivery branch accepts any unused UUID as the 'true' one, so an owner can close a genuine NotPassed receipt by inventing a UUID. (e) An absent owner blocks finalization of every receipt-first answer and every retry. Mitigation is operational: a requester-approved multisig owner, event monitoring, and the pilot evidence listed in docs/IMD_INTEGRATION.md.","line":697,"path":"src/imd/HookJobRequester.sol","reproduction":"State: fixture setUp, (job, ask) pending unbound. Owner setOracleSigner(attackerKeyAddr) and setIntake(attacker). attacker calls onReviewResult(ask, a, sig) with a = _attestation(ask,true) signed by attackerKey: receipt stored (or, if bound to a's context, Passed immediately). Alternatively with the honest Intake: after deadline, creator expireReview(job); owner confirmReviewExpiry(ask) with no evidence; the Intake's later genuine NotPassed completion returns false (UnknownOrSettled) and creator requestReview(job) succeeds, which is the verdict-shopping outcome the ef1f6915 fix moved behind owner confirmation.","severity":"info","snippet":"    function setIntake(address to) external onlyOwner {","title":"Owner-trust assumptions: current Intake/signer authorize old asks; owner can suppress or misattribute answers"}],"hash":"b2ea968a43c6b604fe788c6617868ab82335c9c913a06fb308e4bab4aae90314","nodeId":"2ae1d813-1000-4097-9ce7-f3e1cb8a2099","outcome":"completed","summary":"The review is complete. Findings are written to `.imd-findings.json` in the repository root, the scratch tests are removed, and no source or configuration file was changed.\n\n**Outcome.** HookJobRequester.sol at commit 0d9d440 holds up under the control-flow review. The four remediations from audit ef1f6915 re-run cleanly: both literal proofs and the regression, recovery, binding, delivery and gas suites pass, 122 tests in total. I found no critical or high defect, so no proof files were needed.\n\n**What I checked.** Both callback-before-expiry and expiry-before-callback orders, with prebound and receipt-first delivery and both verdict values. Owner-confirmed expiry against a late answer, a receipt, Intake failure, and ownership rotation. Conflicting and reused UUIDs across asks, including the same signed answer delivered to two unbound asks, which settles exactly once. Receipt discard on both branches, cross-request binding by a mistaken owner, replay of consumed attestations, and the one-shot 200k callback with the 5-of-7 ERC-1271 signer and the 22-cold-read stressor. Every sequence I probed matched the documented model.\n\n**Findings recorded, most severe first:**\n\n- **Low, line 465.** A retained receipt that exactly matches the ask's true context but comes from a key later found compromised has no honest closure. Same-UUID discard reverts, expiry and failure are blocked by the receipt, and the job stays Pending with no retry. The docs call this intentional, so fixing it is a scope decision.\n- **Info, line 352.** After the job deadline, a requester can send expireJob ahead of a pending onJobFailure. The refused job records Expired instead of Failed and becomes reviewable. The oracle still signs any verdict, so no false Passed results. This race has no test.\n- **Info, line 302.** Live-Intake blockers that local tests cannot resolve: the job.open callback ABI shape, the Intake-id-to-oracle-UUID mapping, and real signer gas under the one-shot stipend.\n- **Info, line 697.** Owner-trust assumptions: the current Intake and signer authorize callbacks for old asks, a wrong binding loses the one-shot answer, premature expiry confirmation discards a late NotPassed, and the misdelivery branch accepts any unused UUID.\n\n**Limits.** Checks ran locally with Foundry only. No Slither or Mythril, no fork, no live Intake. The hook, listing and ad contracts were out of focus and not reviewed.","treeHash":null,"usage":{"cachedInputTokens":1310482,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":36972,"runtime":"claude","turns":28,"wallClockMs":558010}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"de319b702da6aa2e","findings":[{"citation":"resolved","description":"Merged from audit_permissions and audit_flow. expireJob (line 330-337) checks only status==Open, requester and deadline; onJobFailure/onJobResult revert UnknownOrSettled unless the job is still Open; requestReview (line 351-354) accepts Delivered or Expired but not Failed. After the job deadline a requester who sees the Intake's onJobFailure (status 1 refused / 2 no result) in the mempool can send expireJob first. The Intake's single attempt then reverts inside its try and is lost: the job records Expired with failureStatus/failureReason 0 (or no resultHash for a late completion), and the requester can buy a review the Failed state would forbid. Impact is bounded (the oracle still signs the verdict and the shipped question defines a missing review as false; the requester pays the extra fee), but the Intake's authenticated terminal status never reaches chain or the static page and the Failed/Expired eligibility split is bypassable by timing. Untested before this review. Fix that keeps the design: let onJobResult/onJobFailure also land on an Expired job (Expired -> Delivered/Failed, recording the evidence), or treat Failed and Expired the same in requestReview.","line":352,"path":"src/imd/HookJobRequester.sol","reproduction":"Reproduced in a scratch Foundry test on HookJobRequesterFixture + MockIntake at 0d9d440 (test/scratch/Judge.t.sol::testExpireJobFrontRunsLateFailure / testControlFailedJobNotReviewable, passes on current code demonstrating the behaviour). State: id = creator openJob(SPEC_HASH, brief); warp deadline+1. Tx1 creator expireJob(id) -> Expired. Tx2 intake.fail(id,1,'body_invalid',0,0) returns false (callback reverted UnknownOrSettled), getJob(id).failureStatus == 0. Tx3 creator requestReview(id) succeeds, reviewOf(ask)==id. Control: with Tx2 first the job is Failed and requestReview reverts NotReviewable(id). Expected: recorded outcome and review eligibility independent of who wins the race. Actual: ordering decides both.","severity":"low","snippet":"            (job.status != JobStatus.Delivered && job.status != JobStatus.Expired)","title":"expireJob can front-run the Intake's late one-shot failure/completion, so the requester's tx order decides Failed vs Expired and makes a refused job reviewable"},{"citation":"resolved","description":"From audit_economics. bindReview may run before delivery. The review template uses a relative window ('window':{'hours':1}) resolved by the plane after payment, so a pre-binding of fromBlock/toBlock/questionHash is a guess. In the bound branch any mismatch reverts ReviewAttestationMismatch inside the Intake's single 200k try: no receipt is stored (unlike the unbound branch, which retains a verified answer), the UUID is not consumed, the ask stays Pending and bound, and ask.oracleRequestId is immutable (line 393) so the owner cannot rebind. The verified answer and the fee are lost; recovery needs requester expireReview after the deadline, owner confirmReviewExpiry, owner releaseReservation and a second paid ask. Fix that keeps the design: in the bound branch, retain a verified-but-mismatching callback as a receipt (same write as the unbound path, already measured within the stipend) for owner resolution, or operationally never bind before a receipt exists (require a receipt in bindReview, a scope decision).","line":550,"path":"src/imd/HookJobRequester.sol","reproduction":"Reproduced in a scratch Foundry test on HookJobRequesterFixture + MockIntake at 0d9d440 (test/scratch/Judge.t.sol::testPreboundMismatchLosesOneShotAndOwnerCannotClose, passes on current code demonstrating the behaviour). State: job delivered; askId = creator requestReview(jobId); owner bindReview(askId, bodyHash, X, Q, 31337, 1, 2). Input: Intake complete(askId, ...) with a correctly signed fresh NotPassed attestation {requestId X, questionHash Q, chainId 31337, fromBlock 1, toBlock 3}. Expected: verified answer retained (as an unbound ask would). Actual: complete returns false; getReviewReceipt(askId).oracleRequestId == 0; consumed[X] == false; review Pending; intakeReviewOfOracle[X] == askId.","severity":"low","snippet":"        ) revert ReviewAttestationMismatch(requestId);","title":"A pre-bound ask whose genuine callback differs in any context field (e.g. the plane-resolved window) loses the one-shot answer with no receipt, and the binding cannot be corrected"},{"citation":"resolved","description":"From audit_economics. Closure of a pending review needs onReviewFailure, discardReviewReceipt (requires a receipt) or confirmReviewExpiry, which requires reviewExpiryRequested, a flag only the job's requester can set here. releaseReservation also requires reviewOf[holder]==0. If a bound ask's one-shot callback is lost (previous finding, over-budget signer, wrong ABI) and the requester never returns, neither the owner nor the Intake can move the job out of Pending or release the reserved UUID. If the protocol later reuses that UUID for another ask, that ask's receipt can't be bound (DuplicateRequestId) and must be discarded as 'receipt UUID conflict', costing that requester a second fee. Fix that keeps the design: let the owner also set the expiry request after the snapshotted deadline (closure still needs confirmReviewExpiry and the no-receipt check).","line":625,"path":"src/imd/HookJobRequester.sol","reproduction":"Reproduced in a scratch Foundry test on HookJobRequesterFixture + MockIntake at 0d9d440 (test/scratch/Judge.t.sol::testPreboundMismatchLosesOneShotAndOwnerCannotClose, passes on current code demonstrating the behaviour). State: as above, bound ask with lost callback; warp reviewDeadline+1. Owner calls expireReview(jobId) -> NotRequester; confirmReviewExpiry(askId) -> ReviewExpiryNotRequested(askId); releaseReservation(X) -> ReservationNotReleasable(X). Expected: an owner path to close a deadline-passed, receipt-less ask. Actual: review stays Pending and intakeReviewOfOracle[X] == askId until the requester acts.","severity":"low","snippet":"        if (msg.sender != job.requester) revert NotRequester();","title":"Only the requester can start closure of a receipt-less pending ask, so an absent requester leaves a bound ask Pending and its oracle UUID reserved forever"},{"citation":"resolved","description":"From audit_flow. With a receipt present: discardReviewReceipt on the same UUID reverts ReviewReceiptMatches unless the UUID is consumed or reserved elsewhere; expireReview and confirmReviewExpiry revert ReviewAlreadyReceived; onReviewFailure reverts ReviewAlreadyReceived; requestReview reverts NotReviewable. The owner's only moves are bindReview (finalizing the untrusted verdict) or a different-UUID 'misdelivered receipt' discard with a fabricated UUID, which the docs forbid. Recorded in docs/IMD_INTEGRATION.md as a retained design choice; reported because the resulting state is unrecoverable without a dishonest call. Fix needs a scope decision, e.g. an owner-only void for receipts verified under a signer that has since been rotated out, or explicit acceptance of the stuck state as policy.","line":465,"path":"src/imd/HookJobRequester.sol","reproduction":"Reproduced in a scratch Foundry test on HookJobRequesterFixture + MockIntake at 0d9d440 (test/scratch/Judge.t.sol::testExactContextReceiptHasNoHonestClosure, passes on current code demonstrating the behaviour). State: askId unbound; Intake delivers validly signed Passed for (UUID U, Q, 31337, 1, 2) -> receipt stored. Owner setOracleSigner(rotated). Owner discardReviewReceipt(askId, bodyHash, U, Q, 31337, 1, 2) -> ReviewReceiptMatches(askId). After deadline creator expireReview(jobId) -> ReviewAlreadyReceived(askId); creator requestReview(jobId) -> NotReviewable(jobId). Expected: an honest owner can close the ask as Unanswered. Actual: review stays Pending indefinitely.","severity":"low","snippet":"            if (!consumed[oracleRequestId] && (holder == bytes32(0) || holder == intakeRequestId)) {","title":"A retained receipt that matches the ask's true context but must not be finalized (e.g. signer later found compromised) has no honest closure path; the job is stuck Pending and cannot be retried"},{"citation":"resolved","description":"From audit_math. _pull transfers intake.priceOf(...) from the caller and approves that amount; _ask resets the allowance to zero but never checks how much the Intake actually took. The documented invariant 'pulls exactly that price ... and holds nothing between calls' therefore rests on unverified live-Intake behaviour. If the Intake pulls less, the requester has overpaid and only the owner's sweep can move the difference (to any address). Fix: after _ask, refund asset.balanceOf(this) surplus to msg.sender, or revert if nonzero.","line":804,"path":"src/imd/HookJobRequester.sol","reproduction":"Reproduced in a scratch Foundry test on HookJobRequesterFixture + MockIntake at 0d9d440 (test/scratch/Judge.t.sol::testIntakePullsLessStrandsSurplus, passes on current code demonstrating the behaviour). State: owner setIntake(LessIntake) whose priceOf is 0.5e18 but whose requestWithFailure transferFroms amount-0.1e18. Input: creator openJob(SPEC_HASH, brief). Expected: creator pays what the Intake charged, or revert. Actual: creator -0.5e18, Intake +0.4e18, HookJobRequester holds 0.1e18 with allowance 0.","severity":"low","snippet":"        _approve(address(intake), 0);","title":"Requester is charged the quoted price even if the Intake pulls less; the surplus is stranded and owner-sweepable"},{"citation":"resolved","description":"From audit_economics. The only bound is maxPrice, which the owner raises ahead of announced price changes. Between signing and inclusion the Intake's priceOf can rise and a user with a standing allowance pays the new price, non-refundably. Fix that keeps the design: add a caller maxPrice argument (revert if price > min(callerMax, maxPrice)); the static site already shows the current price. Changing the external signatures is an ABI change for the site and SDK.","line":788,"path":"src/imd/HookJobRequester.sol","reproduction":"Reproduced in a scratch Foundry test on HookJobRequesterFixture + MockIntake at 0d9d440 (test/scratch/Judge.t.sol::testPriceRaiseUpToCeilingChargesUser, passes on current code demonstrating the behaviour). State: creator approved max; owner setPayment(imd, 5e18); intake price raised from 0.5e18 to 5e18 before inclusion. Input: creator openJob(SPEC_HASH, brief) expecting 0.5 IMD. Expected: 0.5 IMD charged or revert. Actual: 5e18 pulled from creator.","severity":"low","snippet":"        if (price > maxPrice) revert PriceAboveCeiling(price, maxPrice);","title":"openJob/requestReview have no caller-supplied price limit: the price read at execution is pulled up to the owner's contract-wide ceiling"},{"citation":"resolved","description":"Merged from audit_math, audit_permissions, audit_flow and audit_economics (item b). openJob/requestReview learn the Intake ID only from the return value and record usedIntakeRequestIds, jobs[id] and reviewOf[id] afterwards, so an in-call onJobFailure/onJobResult/onReviewFailure/onReviewResult reverts UnknownOrSettled, the one-shot is consumed and the fee spent; the job stays Open for jobTimeout (requester expiry) or the review stays Pending (requester expiry + owner confirmation). docs/IMD_INTEGRATION.md lists asynchronous delivery as an unverified pilot assumption; MockIntake cannot exercise this order. Pilot blocker to confirm, not a local code change.","line":282,"path":"src/imd/HookJobRequester.sol","reproduction":"Reproduced in a scratch Foundry test on HookJobRequesterFixture + MockIntake at 0d9d440 (test/scratch/Judge.t.sol::testSynchronousRefusalDropped, passes on current code demonstrating the behaviour). State: owner setIntake(SyncIntake) whose requestWithFailure pulls 0.5e18, derives id and calls onFailure(id,1,'refused',0,0,'') under 200k before returning. Input: creator openJob. Expected: job Failed status 1. Actual: inner call returned false, job status Open, 0.5e18 held by the Intake.","severity":"info","snippet":"        requestId = _ask(jobAction, body, this.onJobResult.selector, this.onJobFailure.selector, price);","title":"Live-Intake assumption: a callback delivered synchronously inside request/requestWithFailure is dropped, because job/ask state is written after the call returns"},{"citation":"resolved","description":"From audit_math. onReviewResult rejects issuedAt more than 5 minutes before askedAt. If, on a retry, the protocol re-delivers the original signed answer (same UUID, issuedAt from the first ask, still before expiresAt) instead of re-signing, the retry's single attempt reverts and the requester has paid twice with no receipt. The anti-pre-signing intent is sound; whether the oracle re-signs on retry is an unverified protocol behaviour (UUID reuse/timestamp windows listed as unverified). Pilot item: confirm re-signing on retry, or anchor a retry's askedAt to the original by a design decision.","line":555,"path":"src/imd/HookJobRequester.sol","reproduction":"Reproduced in a scratch Foundry test on HookJobRequesterFixture + MockIntake at 0d9d440 (test/scratch/Judge.t.sol::testRetryRedeliveryPredatesAsk, passes on current code demonstrating the behaviour). State: ask1 at T; attestation for ask1 issuedAt T+1h, expiresAt T+1h+3d. At T+1d+1 creator expireReview, owner confirmReviewExpiry(ask1), creator requestReview -> ask2 (same body). Input: Intake delivers that signed attestation to onReviewResult(ask2). Expected per retry design: receipt retained. Actual: revert AttestationPredatesAsk(T+1h, T+1d+1); complete returns false; no receipt.","severity":"info","snippet":"        if (uint256(a.issuedAt) + ISSUED_AT_TOLERANCE < ask.askedAt) {","title":"After owner-confirmed expiry and a retry, a re-delivered answer issued during the earlier ask is rejected by AttestationPredatesAsk, burning the retry's one-shot"},{"citation":"resolved","description":"Merged from audit_math, audit_permissions, audit_flow and audit_economics (item e). _verifyAttestation forwards all remaining gas to isValidSignature. Re-measured at 0d9d440 with test/ReviewReceiptGas.t.sol: cold EOA receipt 123,013; 5-of-7 ERC-1271 receipt 168,106 (also after an expiry request and on a reserved retry), bound 132,880; +22 cold-read registry receipt 185,260, bound 150,053. All fit under 200,000. The 64-cold-read fixture does not fit: the callback runs out of gas, the Intake marks the request settled, nothing is retained or consumed, and recovery is owner-assisted expiry plus a new fee. No code defect. Launch blocker until the real signer/proxy is measured cold on the target chain.","line":564,"path":"src/imd/HookJobRequester.sol","reproduction":"forge test --match-path test/ReviewReceiptGas.t.sol -vv prints the figures above; testOverBudgetSignerStillFailsClosedAndLosesSingleAttemptWithoutPartialReceipt passes (setOracleSigner(ColdReadReceiptSigner(signer,64)), complete under 200k returns false, receipt.oracleRequestId == 0, consumed false, review Pending). Expected for launch: the production isValidSignature at or under 60k cold and the whole callback under 200k; actual: only local fixtures measured.","severity":"info","snippet":"        bytes32 digest = _verifyAttestation(a, signature);","title":"200,000-gas one-shot callback: ERC-1271 signer headroom is about 77k cold receipt-first; the production signer is unmeasured and an over-budget signer loses the answer and fee"},{"citation":"resolved","description":"Merged from audit_flow and audit_economics (items a, d, f, g). (1) The job.open callback shapes (bytes32,bytes) and (bytes32,uint8,bytes32,uint16,uint16,bytes) are assumed; a different live ABI reverts in the Intake's try and the job can only be expired after 7 days. (2) An answer delivered after expiresAt (template validForSeconds 3600) reverts AttestationExpired even unbound, so a delivery lag beyond one hour loses the answer. (3) Oracle UUID/questionHash/window are not derivable on chain; bindReview relies on an authenticated off-chain mapping source that doesn't exist yet. (4) Intake IDs are assumed unique and nonzero (_recordIntakeId reverts the whole paid ask otherwise). None is a local code defect; they are pilot acceptance conditions.","line":302,"path":"src/imd/HookJobRequester.sol","reproduction":"Reproduced in a scratch Foundry test on HookJobRequesterFixture + MockIntake at 0d9d440 (test/scratch/Judge.t.sol::testLateDeliveryAfterSignatureExpiryLost, passes on current code demonstrating the behaviour). for (2): unbound ask, attestation issuedAt=now, expiresAt=now+1h; warp now+1h+1; Intake complete returns false; no receipt. For (1): existing test/HookJobRequester.t.sol testCallbackWithUnexpectedAbiFailsSafeAndJobCanExpire (complete(id, hex'deadbeef') returns false, job stays Open).","severity":"info","snippet":"    function onJobResult(bytes32 requestId, bytes calldata result) external onlyIntake {","title":"Unverified live-Intake assumptions that each fail closed into a lost one-shot plus spent fee: job callback ABI, attestation validity window, UUID mapping and uniqueness"},{"citation":"resolved","description":"Merged from audit_permissions (3 findings), audit_flow and audit_economics. These are intentional, documented powers (docs/AUDIT_EF1F6915.md 'Expiry and owner trust', THREAT_MODEL 'Malicious requester owner'), not bypasses: (a) discardReviewReceipt's different-UUID branch (line 470) lets the owner close a genuine NotPassed as 'misdelivered receipt' with an invented UUID, after which a retry can be bound to Passed; (b) confirmReviewExpiry (line 642-652) checks only the requester flag and no receipt, so an early confirmation burns a later genuine answer and opens a paid retry; (c) bindReview's Intake-to-oracle pairing is unauthenticated on chain; (d) setIntake/setOracleSigner govern deliveries for asks paid earlier, so a rotated Intake+signer can settle any pending ask, and the old Intake's in-flight callbacks revert NotTheIntake; (e) owner unavailability blocks every receipt-first finalization and retry. Both ef1f6915 callback/expiry orders hold: an expiry request alone never blocks delivery or binding, and a receipt always blocks confirmation (AuditExpiryProof.t.sol, ReviewAuditRegression.t.sol pass). Mitigation is operational: requester-approved multisig owner, event monitoring, preserved evidence.","line":699,"path":"src/imd/HookJobRequester.sol","reproduction":"Reproduced in a scratch Foundry test on HookJobRequesterFixture + MockIntake at 0d9d440 (test/scratch/Judge.t.sol::testOwnerDiscardsGenuineNotPassedWithInventedUuid / testConfirmedExpiryBurnsLaterAnswer, passes on current code demonstrating the behaviour). (a) askA receives a signed NotPassed; owner discardReviewReceipt(askA, bodyHash, keccak256('invented'), Q, 31337, 1, 2) succeeds; creator re-asks askB, owner binds, Intake delivers Passed -> review Passed. (b) bound ask, deadline+1, creator expireReview, owner confirmReviewExpiry, Intake's genuine NotPassed returns false (UnknownOrSettled), creator requestReview succeeds. (d) existing ReviewBinding.t.sol::testOwnerRotationPowersApplyToAlreadyPendingRequestsAsDocumented passes.","severity":"info","snippet":"        intake = IIntake(to);","title":"Owner-trust assumptions: misdelivery discard accepts any unused UUID, expiry confirmation needs no on-chain evidence, bindReview pairing is owner-asserted, and setIntake/setOracleSigner apply immediat"}],"hash":"de62449b121100d34ceebbfd8b197c6da1dc9db719041aa89b853a220babc8b1","nodeId":"ec5b5196-c56a-4d50-b031-6affc85ea95e","outcome":"completed","summary":"I found no critical or high defect in `HookJobRequester.sol` at 0d9d440, and no material code-level blocker. The blockers to paid use are the live-Intake assumptions below, which nobody has checked against a real Intake. All 19 specialist findings were re-run and reproduced. After merging duplicates, 11 findings are in `.imd-findings.json` (6 low, 5 info). None carries a proof file, because those are only required for critical and high.\n\n**The four ef1f6915 fixes all hold.** The two audit proofs (`AuditExpiryProof.t.sol`, `AuditReceiptConflictProof.t.sol`) and the requester suites pass: 115 tests, 0 failures. On expiry, both transaction orders are safe:\n- A requester's expiry request alone never blocks a late answer or the owner's binding.\n- An answer already received always blocks the owner from confirming expiry.\n\nConflicting and reused UUIDs, receipt discard, binding one ask to another ask's context, and replay of a consumed UUID all behave as the fixes describe. The gas measurements reproduce exactly: 123,013 / 168,106 / 132,880 / 185,260 against the 200,000 stipend.\n\nI re-ran everything not already covered by in-tree tests in an 11-test scratch Foundry file. All 11 pass, showing each behaviour. I deleted the file afterwards and changed no repository files.\n\n**Low**\n1. **Expiring a job can pre-empt the Intake's failure notice** (line 352). After the job deadline, a requester can call `expireJob` ahead of a late refusal. The Intake's single attempt is then lost, the job shows Expired instead of Failed, and the requester can buy a review the Failed state would forbid.\n2. **A binding made before delivery loses the answer on any mismatch** (line 550). If the window the plane resolves differs from the owner's guess, the single callback reverts and nothing is kept. The binding cannot be corrected, so recovery needs a second fee.\n3. **Only the requester can start closing an ask that received nothing** (line 625). If they never come back, the ask stays Pending and its oracle UUID stays reserved forever.\n4. **An answer received with the correct context has no honest way to be closed** (line 465). For example, if the signer is later found compromised, the job is stuck Pending and can't be retried.\n5. **If the Intake pulls less than its quoted price, the difference is stranded** (line 804). It stays in the contract and only the owner's `sweep` can move it.\n6. **Users can't cap the price they pay** (line 788). If the Intake raises its price before the transaction lands, the user pays the new price, up to the owner's ceiling.\n\n**Info: unchecked live-Intake assumptions (pilot blockers)**\n- **Synchronous callbacks** (line 282): if the Intake calls back inside the request itself, the callback is dropped. Duplicates from four specialists, merged.\n- **Signer gas** (line 564): the 200k callback leaves about 77k for an ERC-1271 signer. The production signer has not been measured; a 64-cold-read signer loses the answer and the fee.\n- **Answer timing on retry** (line 555): if the oracle re-sends the first ask's answer to a retry instead of re-signing, the check that rejects answers issued before the ask fails it.\n- **Other assumptions** (line 302): the job callback ABI, answers delivered more than an hour after signing (they expire), and how Intake IDs map to oracle UUIDs and whether each is unique.\n\n**Info: owner trust (line 699).** These are intentional, documented powers, not bypasses:\n- The owner can discard a genuine NotPassed by naming an invented UUID.\n- The owner can confirm expiry with no on-chain evidence, which burns a later genuine answer.\n- The owner's pairing of Intake IDs to oracle UUIDs is not checked on chain.\n- `setIntake` and `setOracleSigner` apply immediately to asks that are already paid.\n- An absent owner blocks every finalization and retry.\n\nWhat I didn't do: no live pilot, no deployment, no Slither or Mythril, and nothing measured against the real signer contract.","treeHash":null,"usage":{"cachedInputTokens":1068041,"inputTokens":26,"model":"claude-opus-5-5","outputTokens":19899,"runtime":"claude","turns":16,"wallClockMs":217852}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"a18a0c6087e1362f","findings":[{"citation":"resolved","description":"_pull transfers exactly intake.priceOf(action, asset) from the requester into HookJobRequester and approves that amount to the Intake; _ask then calls request/requestWithFailure and resets the allowance to zero. Nothing checks how much the Intake actually took. The documented invariant (README and IMD_INTEGRATION.md step 1: 'pulls exactly that price ... and holds nothing') therefore depends on the live Intake pulling exactly `amount`, which is unverified (no live pilot). If the Intake charges less than the quote for any reason (discount, rounding, a different internal price than priceOf, or ignoring `amount`), the surplus stays in HookJobRequester, the requester has overpaid, and the only path out is the owner's sweep(). Seam: boundary (external call return/behaviour) x invariant ('contract holds nothing between calls'). Fix: after _ask, read asset.balanceOf(address(this)) and either refund any surplus to msg.sender or revert if it is nonzero; this preserves the agreed no-refund-after-ask economics because the Intake's charge is unchanged.","line":804,"path":"src/imd/HookJobRequester.sol","reproduction":"State: HookJobRequester configured with maxPrice 0.5e18, Intake whose priceOf returns 0.5e18 but whose request/requestWithFailure pulls amount - 0.1e18 (=0.4e18) via transferFrom. Input: creator with 1e18 IMD and max allowance calls openJob(keccak256('spec'), 'hook brief'). Expected: creator pays what the Intake charged (0.4e18) or the call reverts. Actual: creator balance 0.5e18 (charged 0.5e18), Intake 0.4e18, HookJobRequester holds 0.1e18 with allowance reset to 0; owner.sweep(imd, owner, 0.1e18) moves the requester's overpayment to the owner. Verified with a Foundry test using a mock Intake that pulls amount-0.1e18 (test passes on current code, asserting the 0.1e18 residual and the successful sweep).","severity":"low","snippet":"        _approve(address(intake), 0);","title":"Requester is charged the full quote even when the Intake pulls less; the difference is stranded in the contract and only owner-sweepable"},{"citation":"resolved","description":"openJob and requestReview record the Intake request id (usedIntakeRequestIds, jobs[id].status=Open, reviewOf[id]) only after the Intake call returns, because the id is the Intake's return value. If the live Intake ever calls the callback or failure function inside request/requestWithFailure (for example an immediate refusal with status 1, or a cached answer), onJobFailure/onReviewFailure/onJobResult/onReviewResult revert with UnknownOrSettled because jobs[id].status is None / reviewOf[id] is zero at that moment. With one-shot delivery the fee is spent, the job stays Open (closable by the requester after jobTimeout) or the review stays Pending (closable only via expireReview plus owner confirmReviewExpiry). The docs list 'asynchronous one-shot 200k delivery' as unverified; this finding gives the concrete failing state. Fix options if the pilot shows synchronous delivery: have the Intake return the id before delivering, or accept callbacks for the in-flight request inside the nonReentrant window by pre-reserving the id (e.g. compute it deterministically) - a protocol-level decision, not a silent contract change.","line":800,"path":"src/imd/HookJobRequester.sol","reproduction":"State: configured HookJobRequester; mock Intake v2 that, inside requestWithFailure, pulls the fee, stores the request, marks it settled and immediately calls onFailure(id, 1, 'refused', 0, 0, '') with 200k gas inside a try. Input: creator calls requestReview(deliveredJobId) (or openJob). Expected: the refusal is recorded (review Unanswered / job Failed). Actual: the inner call reverts UnknownOrSettled(id), requestReview returns id with job.review == Pending (openJob leaves status Open), 0.5e18 fee pulled, and a later Intake retry is impossible because the request is settled ('not pending'). Verified with a Foundry test on current code.","severity":"info","snippet":"            requestId = intake.requestWithFailure(action, body, callback, onFailure, asset, price);","title":"Synchronous (in-call) Intake delivery or refusal is lost: state is written only after requestWithFailure returns"},{"citation":"resolved","description":"onReviewResult rejects any attestation whose issuedAt is more than 5 minutes before the ask's askedAt. The retained-receipt and releaseReservation design (audit ef1f6915 finding 3) explicitly supports protocol UUID reuse on a retry, but if the protocol re-delivers the same signed answer (same UUID, issuedAt from the first ask's lifetime, still within expiresAt) to the retry's callback, this check reverts, the single delivery attempt is consumed, and the requester has paid two review fees with no verdict and no receipt; closing again needs expireReview after reviewTimeout plus owner confirmReviewExpiry. Whether the oracle re-signs with a fresh issuedAt or re-delivers the original is an unverified protocol behaviour ('UUID reuse policy, timestamp windows ... remain unverified'). Keep the anti-pre-signing intent but note it as a live-pilot item: either the protocol must re-sign on retry, or askedAt for a retry could be anchored to the original ask (owner-confirmed lineage) by design decision.","line":555,"path":"src/imd/HookJobRequester.sol","reproduction":"State: job delivered; ask1 = requestReview at T; oracle signs NotPassed for ask1 with issuedAt T+1h, expiresAt T+1h+3d; Intake never delivers to ask1. At T+1d+1 creator calls expireReview(job), owner calls confirmReviewExpiry(ask1), creator calls requestReview(job) -> ask2 (askedAt = T+1d+1). Input: Intake delivers the same signed attestation (valid signature, unexpired, panel 7/5/6) to onReviewResult(ask2, a, sig) under 200k gas. Expected per the retry design: receipt retained for owner binding (UUID is unreserved and unconsumed). Actual: revert AttestationPredatesAsk(T+1h, T+1d+1); intake.complete returns false; getReviewReceipt(ask2).oracleRequestId == 0; Intake holds 1.5e18 (job + two review fees) with no verdict. Verified with a Foundry test on current code.","severity":"info","snippet":"        if (uint256(a.issuedAt) + ISSUED_AT_TOLERANCE < ask.askedAt) {","title":"After owner-confirmed expiry and a retry, a re-delivered attestation issued for the earlier ask is rejected by AttestationPredatesAsk, burning the retry's one-shot and second fee"},{"citation":"resolved","description":"The receipt-first path (cold) costs 123,013 gas with an ECDSA key before the ERC-1271 staticcall is reached, leaving about 77k of the Intake's 200,000 stipend for isValidSignature (which is forwarded all remaining gas via staticcall(gas(), ...)). The in-tree 5-of-7 threshold fixture fits at 168,106 (31.9k headroom); the audit stressor with 22 cold reads fits at 185,260 (14.7k headroom); the 64-cold-read fixture does not fit: the callback reverts, the Intake marks the request settled, no receipt or consumption is written, and the review fee is lost until expireReview + owner confirmReviewExpiry. A production Safe-style multisig with 5 signatures (proxy delegatecall, fallback-handler hop, threshold/owner-linked-list reads, 5 ecrecovers) is commonly 60-90k cold and is not measured here; the documented 60k signer envelope must be verified against the actual signer contract on the target chain before any paid use. Material blocker for launch if the chosen signer is not the local-style registry.","line":564,"path":"src/imd/HookJobRequester.sol","reproduction":"State: configured HookJobRequester; oracleSigner set to a contract whose isValidSignature performs 2 config reads + 64 cold mapping reads then ECDSA recovery (test/helpers/ReceiptSigners.sol ColdReadReceiptSigner(signer, 64)); ask pending unbound. Input: Intake delivers a correctly signed, fresh, panel-valid attestation under exactly 200,000 gas. Expected: receipt retained. Actual: out-of-gas revert inside the stipend, MockIntake.complete returns false and marks the request settled, getReviewReceipt(ask).oracleRequestId == 0, consumed[uuid] == false, job.review == Pending; fee remains with the Intake (balance 7*PRICE unchanged after owner-confirmed expiry). Reproduced by the in-tree test ReviewReceiptGas.t.sol::testOverBudgetSignerStillFailsClosedAndLosesSingleAttemptWithoutPartialReceipt; headroom numbers measured by the same file's _minimumGas binary search: 123,013 (EOA), 168,106 (5-of-7), 185,260 (+22 cold reads).","severity":"info","snippet":"        bytes32 digest = _verifyAttestation(a, signature);","title":"200k one-shot callback: ERC-1271 signer budget is ~77k cold; a heavier signer burns the delivery and the fee with no receipt"}],"hash":"f5a1983a3cad107a9a0c4b7bac98080395198e244edbf28e8dc5cabaf3223e59","nodeId":"f0512efc-cdc1-4a0b-bdba-b22b3ed8d569","outcome":"completed","summary":"The working tree is clean apart from the findings file, which `.gitignore` already covers. The review is complete.\n\n**Outcome: no material blocker found in the contract logic.** The four actionable findings from audit ef1f6915 are fixed, and both literal proof tests plus the 98 tests across the requester suites pass at commit 0d9d440. The findings file holds one low and three info items, each with a concrete failing input, and none needs a proof file.\n\n**Re-run of the prior audit's findings**\n\n- **Expiry front-running (finding 1):** closed. Requesting expiry no longer closes the ask, so a late NotPassed still lands in either transaction order, and retries stay blocked until the owner confirms closure without a receipt.\n- **Stuck receipt with a conflicting UUID (finding 2):** closed via the same-UUID discard path, which requires every field to match plus an on-chain conflict.\n- **Terminal reservation burning the retry callback (finding 3):** closed. Unbound callbacks retain a receipt regardless of reservations, and the owner releases the old reservation before binding.\n- **Same-UUID typo discard (finding 4):** closed. Field typos revert atomically, and a different UUID must be unconsumed and unreserved.\n\n**Math, boundary and numerical-gap passes.** The contract has no division, scaling or decimals logic. The receipt bit-packing at bits 0 to 224 plus the allocation marker at bit 255 unpacks correctly, the timestamp additions are checked uint64 arithmetic bounded by the 30-day timeout cap, the UTF-8 validator rejects overlongs, surrogates and out-of-range code points, and operator precedence in the continuation-byte check is correct for Solidity. Deadline and signature-expiry comparisons are consistent at the exact boundary.\n\n**What the findings file reports**\n\n- **Low, residual funds:** the requester is charged the full `priceOf` quote while nothing checks what the Intake actually pulled. An Intake that takes less strands the difference in the contract, where only the owner's sweep reaches it. Repro: mock Intake that pulls 0.4 IMD of a 0.5 IMD quote.\n- **Info, synchronous delivery:** a callback or refusal issued inside `requestWithFailure` reverts with UnknownOrSettled because state is written after the call returns. The fee is spent and the ask sits Pending until owner-assisted expiry.\n- **Info, retry re-delivery:** after owner-confirmed expiry and a retry, the same signed answer re-delivered to the new ask fails AttestationPredatesAsk, burning the one-shot and a second fee. This collides with the UUID-reuse recovery design unless the protocol re-signs.\n- **Info, ERC-1271 budget:** the cold receipt path costs 123k before the signer call, leaving roughly 77k of the 200k stipend. The local 5-of-7 fixture fits with 32k headroom. A Safe-style production multisig is unmeasured and may not fit, and an over-budget signer loses the delivery and the fee with no receipt.\n\n**Unresolved live-Intake and owner-trust assumptions**\n\n- The Intake's callback ABI, one-shot asynchronous delivery, exact charge of `amount`, and UUID reuse and timestamp policy are all unverified without a pilot.\n- The owner alone links each Intake ID to the oracle UUID and question hash. A careless or hostile owner can attach one job's verdict to another job, bind a wrong context before delivery with no unbind path, which guarantees the one-shot is burned, or confirm expiry prematurely. Binding after the receipt arrives avoids the pre-bind risk.\n- Owner unavailability blocks every retry, since only the owner can confirm expiry, discard a misdelivered receipt, or release a reservation.\n- The configured minimum agreement must not exceed the action's actual quorum, or every valid answer reverts with PanelTooWeak.\n- Rotating the Intake or signer applies to already-pending asks.\n\nTools run: forge build and forge test only. No Slither or Mythril, no deployment, no network transaction. The hook, listing and ad contracts were not reviewed.","treeHash":null,"usage":{"cachedInputTokens":1346584,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":41562,"runtime":"claude","turns":32,"wallClockMs":626092}}],"verification":[]}