{"jobId":"d80d33c1-afdd-48fc-96bc-88570c9d7a48","repoUrl":"https://github.com/contractclaus/claus-network","baseCommit":"1f6f6d7ebb30dab9e14edda5a34da20f74e565e8","count":5,"submissions":[{"hash":"91bcbfd06002021ed842a315dd44c5d59055ad78d6c87b948422e8d045aa9f1b","nodeKey":"audit_judge","role":"review","attempt":1,"deviceKey":"28e346843ec1553064c9e698cd0998a51bb9bb28850f04326398b9e08b2fc00a","seat":{"tokenId":"822","agentId":"52262"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":47,"runtime":"claude","inputTokens":674,"wallClockMs":581532,"outputTokens":41155,"cachedInputTokens":2485942},"baseCommit":"1f6f6d7ebb30dab9e14edda5a34da20f74e565e8","startCommit":"1f6f6d7ebb30dab9e14edda5a34da20f74e565e8","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":[],"verificationErrors":0,"summary":"The review is complete. The findings file at the repository root holds eight entries, every one reproduced against commit 1f6f6d7 with a scratch test, and no tracked file was changed.\n\n**What I verified**\n\n- The full node suite on this tree gives 411 pass and 10 skips, matching the stated source gate.\n- The two medium findings from the prior job are fixed by this delta. A relayed registration without a hash now serializes, worker commits are awaited, and the relayed path reaches a terminal state.\n- All 13 specialist reports reduce to seven distinct mechanisms. I reproduced each with my own node or Foundry test and merged the duplicates: the cancelled-transaction edge had three reports, the lapsed-subsidy callback had four, and the empty-logs cursor had two.\n\n**Findings kept, ranked**\n\n1. **Medium, padded-calldata front-run blocks a project forever.** The byte-exact relay check refuses any mined copy of the exact signed intent whose calldata has trailing bytes. A Foundry test shows padded calldata registers the project and makes Claus's own transaction revert. The job ends blocked and every later intent for that project also blocks, since the project key is fixed. This is a regression against the baseline's event-only match.\n2. **Medium, one stale latest read after Claus's own confirmed registration is terminal.** The post-confirmation state checks throw the same error code that the delta now maps to blocked. A single lagging RPC answer turns a 60-second retry on the baseline into an unrecoverable block with the registration already mined and paid.\n3. **Low, signed-but-unsent delivery cancelled by subsidy expiry or an admission pause answers closed.** This is the cancelled-transaction edge the brief asked about. The request stays open onchain but the worker never revisits it and no later allocation can deliver it.\n4. **Low, callback under a lapsed subsidy stays pending forever** with no retryAt, re-stepped every tick, and a new allocation is never consulted because the request is rebound to the resolve's budget.\n5. **Low, one empty finalized getLogs page advances the cursor past the relay** and the finalized state read then blocks the job permanently with no rescan.\n6. **Low, local wall clock decides expiry on the unregistered-at-latest branch** while the relayed branch uses finalized chain time. The window is seconds wide.\n7. **Low, an expired intent can never be queued** even when its exact relay is already mined, so an owner who relays directly and submits late locks the project.\n8. **Info, retained limits 4, 7, 8 and 9** are now documented and remain non-blocking. Arithmetic re-checked.\n\n**Scope conclusions**\n\n- **B, public sponsored reviews and bounded Sepolia registration: not clean.** Findings 1 and 2 sit on the advertised public registration path, are attacker-triggerable or RPC-triggerable with a permanent outcome, and are regressions of this delta. They should be fixed before public admission. Findings 3 to 7 are liveness and state-accuracy defects with no spend, custody or authorization effect, and could ship with notes. Cancellation, reservations, nonce and hash binding, signatures, and the no-replacement rule all held in every reproduction.\n- **C, Mainnet preparation only: the same two findings apply unchanged to chain id 1**, and the padded-relay case there also costs the public pool one reverted transaction. The fee ceilings in the plan are readiness numbers for the human approval, not defects. Nothing here authorizes Mainnet activation.\n\nScratch tests live under `test/scratch/` and `contracts/test/scratch/`, both excluded from the tree. The installed `node_modules` directory is gitignored and was needed only to run the suite.","createdAt":"2026-10-12T16:45:47.795Z","verdict":null,"findings":[{"severity":"medium","title":"Byte-exact relay calldata check lets any mempool observer permanently block a project's registration by mining the same signed intent with padded calldata","path":"adapters/chain-executor.mjs","line":92,"description":"Merged from audit_permissions (reproduced). 0.12.2 adopts a relayed registration only when the mined transaction's input is byte-identical to the executor's own encodeFunctionData output (lines 92-93). ClausApprovalExecutor.registerProject is permissionless and Solidity's ABI decoder accepts trailing calldata, so the exact signed Registration/configuration/signature can be mined by anyone with extra bytes appended (verified with Foundry: address(executor).call(bytes.concat(exact, new bytes(32))) succeeds, projects(id).target is set, registrationNonces[owner][nonce] is consumed, and the exact call then reverts). The signed calldata is public the moment Claus broadcasts register:<id>, so a front-runner needs no secret, only a higher tip. Afterwards: Claus's own transaction reverts, deployment.step falls into relayedRegistration, finds the single ProjectRegistered log with matching owner/mandateHash, compares getTransaction().input with data, and throws registered_project_mismatch; src/chain-deployments/index.mjs:181 now maps that code to a terminal 'blocked' job. Because projectKey = keccak(owner, keccak(projectId)) is fixed per owner and project (adapters/chain-read.mjs:94-95) and projects[projectKey].target is now non-zero, every later intent for that project also ends in registered_project_mismatch (complete empty recovery with a registered project), each one consuming one of the 16 lifetime intents. The registration onchain is exactly what the owner signed, so no authority was bypassed; the defect is that a valid relay is treated as a conflicting one and the outcome is irreversible without operator database intervention (docs/operations.md: journals are never cleared). Under baseline 2b87522 the event-only match (owner + mandateHash) would have adopted this relay, so this is a regression of the delta. The documented 'nested contract relays unsupported' limit does not cover it: the attacking transaction is a direct call to the executor (transaction.to matches). Applies identically to chain ids 11155111 (B) and 1 (C); on C the public pool also pays Claus's reverted transaction. Fix: decode the relayed transaction's input with decodeFunctionData and compare the decoded (message, configuration, signature) with the signed values instead of comparing bytes, or identify the intent by its onchain effect (registrationNonces[owner][nonce] consumed plus the finalized projects() tuple and the target's executor()/projectId()), which only the owner's exact signature can produce. Keep 'nested relay' as the only unsupported case.","reproduction":"Onchain: contracts/test/scratch/PaddedRelay.t.sol (forge test --match-path test/scratch/PaddedRelay.t.sol, passes): exact = abi.encodeCall(registerProject,(r,'cfg',sig)); padded = exact || 32 zero bytes; executor.call(padded) returns true and registers owner/target/mandate; executor.call(exact) then returns false (target already set). Offchain: test/scratch/registration-edges.test.mjs test A (fixture copied from test/registration-recovery.test.mjs: module receipt block 80, intent observedBlock 100, ProjectRegistered log at block 101, finalized head 110) with client.getTransaction returning { to: executor, input: data + '00'.repeat(32) }. decodeFunctionData on both inputs yields identical arguments. Expected: deployment.step returns 'verified' with a binding to the registered target. Actual: step rejects with code registered_project_mismatch, which runOne finishes as state 'blocked' (see the existing test 'a different registered project permanently blocks its original queued registration'). Any new intent for the same project then fails the same way because projects(projectKey).target is non-zero.","snippet":"    requireThat(transaction.to?.toLowerCase() === value.executor.toLowerCase() &&\n      transaction.input.toLowerCase() === data.toLowerCase(), 'registered_project_mismatch');","citation":"resolved","blocking":false},{"severity":"medium","title":"registered_project_mismatch is terminal for every throw site, so one stale latest-state read after the executor's own confirmed registration permanently blocks the project","path":"src/chain-deployments/index.mjs","line":181,"description":"From audit_flow (reproduced). The 0.12.2 terminal mapping is keyed on the error code, not on whether the evidence was finalized. The same code is thrown by adapters/chain-executor.mjs:140-146 after the executor's OWN register transaction is already confirmed (3 confirmations on Sepolia, finalized on chain 1): read(value.executor,'projects',...) at the default 'latest' tag must already show the registered owner/mandateHash/policyVersion/minDelaySeconds and the target must answer executor()/projectId(). Those reads are not pinned to the receipt's block. The Sepolia executor worker uses a single load-balanced public endpoint with retryCount 0 (runtime/chain-worker.mjs:28), where eth_call can be served by a backend lagging the one that answered eth_getTransactionReceipt; a Mainnet endpoint configured by URL has the same property. One such read returns the pre-registration struct (zero owner) and requireThat throws registered_project_mismatch. On baseline 2b87522 that was a 60 s 'waiting' retry that self-healed on the next tick; on 1f6f6d7 runOne finishes the job 'blocked', writes chain-registrations state 'blocked', and the job is never claimed again. The project is then registered onchain by this exact transaction (gas paid from the public pool) with no chain-bindings row; every later prepareRegistration/submitRegistration creates a new intent whose relayedRegistration finds the first ProjectRegistered event and fails the exact-calldata check at chain-executor.mjs:92-93 (different nonce/expiresAt), blocking again. No authorization is bypassed and no extra funds are spent beyond the one registration; the defect is an irreversible wrong terminal state on a transient read. Fix: throw a distinct, non-terminal code from the post-confirmation state checks at chain-executor.mjs:140-146 (or pin those reads to the confirmed receipt's blockNumber so they cannot lag), and keep the terminal mapping only for the finalized-evidence throw sites (lines 81, 87-88, 92-93).","reproduction":"test/scratch/registration-edges.test.mjs test B (node --test test/scratch/registration-edges.test.mjs). State: chain-transactions 'register:registration' state 'confirmed', sender = executor signer, receipt.blockNumber '105'; network-capabilities 'chain' enabled for the executor; chain-modules 'module' receipt block '80'; chain-registrations 'registration' queued with a project-register job enqueued; review pilot enabled. Mock client: getChainId 11155111, getBlockNumber 110n, readContract('projects') returns [0x0..,0x0..,0x0..,0x0..,0n] on the first tick and the registered struct afterwards. Expected (baseline behaviour): runOne returns state 'waiting' with retryAt now+60000 and the next tick returns 'verified'. Actual on 1f6f6d7: runOne returns state 'blocked', result.error 'registered_project_mismatch', chain-registrations 'registration' state 'blocked'; a direct deployment.step after the read settles returns 'verified' (proving the block was premature); the next runOne is idle and chain-bindings stays empty.","snippet":"        if (['registered_project_mismatch', 'registration_receipt_mismatch', 'recovery_block_changed', 'recovery_event_mismatch'].includes(error.code) ||","citation":"resolved","blocking":false},{"severity":"low","title":"A signed-but-unsent delivery transaction cancelled because its subsidy expired or dispatch was paused is reported as terminal 'closed' while the Inbox request is still open","path":"adapters/contract-delivery.mjs","line":200,"description":"Merged from audit_math, audit_economics and audit_permissions (three reports of the same mechanism, reproduced). This is the cancelled-transaction edge case the task asked to verify. 0.12.2 distinguishes subsidy expiry from request expiry only on the path with no existing transaction (lines 187-188). When a resolve row already exists in state 'prepared' (signed, nonce reserved, never broadcast) its intent.expiresAt = min(resolution.expiresAt, budget.expiresAt) (line 154). On the next step transactions.submit() in adapters/sepolia-transactions.mjs:99-107 cancels the row whenever intent.expiresAt <= now() (so the budget deadline alone triggers it) or whenever authorize() returns false, which includes allowSubmit = false (the worker passes active, false during any admission pause) and active() false for an expired/revoked budget. The cancelled result reaches line 200 and the step answers 'closed'. runtime/contract-intake-worker.mjs:29 skips every request whose delivery.state is 'closed' forever, and createContractDelivery (lines 53-62) rebinds the request to the original budget through the cancelled resolve row, so no later allocation can deliver it either. The Inbox request is still Pending and resolvable until its own deadline, so docs/contract-intake.md ('an expired subsidy alone does not close an otherwise valid onchain request') and the reversible-pause description in docs/mainnet.md are both violated for this state. The same mapping answers 'closed' for a prepared callback cancelled by budget expiry although its resolve is finalized onchain. The window is real but narrow: a 'prepared' row outlives a tick only when submit() fails after prepare() committed (its first call is client.getChainId(); an RPC failure there propagates out of the worker tick with no retryAt) or when the process dies between the two commits; the next successful tick must then fall after budget.expiresAt or inside a pause. Not cancelling the signed transaction is correct (it must stay bound to its allocation); the defect is the terminal answer. No funds are spent and no duplicate transaction is created. Fix: in the cancelled branch for action 'resolve', answer 'closed' only if chain.deliveryState(request).closed or resolution.expiresAt <= now(), otherwise answer 'pending' (or a distinct non-terminal 'unfunded'/'paused' state with a retryAt); for a cancelled callback whose resolve is finalized, answer 'pending'. Note that the no-replacement rule means the cancelled row can never be re-prepared under the same id, so a later allocation would need an explicit decision on whether it may prepare a new transaction under a new id.","reproduction":"test/scratch/delivery-edges.test.mjs tests D and D2 (node --test test/scratch/delivery-edges.test.mjs; fixture copied from test/contract-delivery.test.mjs with the request deadline two days after budget.expiresAt). D: client.getChainId throws once on the call that follows a successful prepare(); step() rejects 'rpc down'; chain-transactions holds one row in state 'prepared', 0 broadcasts, reserved 135000000000. Advance now to budget.expiresAt + 1 (deadline still open, deliveryState.pending true), step(): row becomes 'cancelled', reservation released, outcome.state is 'closed'. Expected: 'pending'. D2: same prepared row, step({allowSubmit:false}) with a valid budget: outcome 'closed'. Expected: 'pending'. The existing test 'an expired subsidy leaves an open request pending' covers only the no-transaction path.","snippet":"        return answer(transaction.state === 'cancelled' ? 'closed' : 'failed');","citation":"resolved","blocking":false},{"severity":"low","title":"A callback whose resolve ran under a now-lapsed subsidy stays 'pending' forever: no terminal state, no retryAt, re-stepped on every worker tick, and a later allocation is never consulted","path":"adapters/contract-delivery.mjs","line":188,"description":"Merged from audit_math, audit_economics, audit_permissions and audit_flow (four reports of one mechanism, reproduced). For action 'callback' expiresAt is budget.expiresAt alone (line 154); ClausRequestInbox.deliverCallback has no deadline and the resolve-only closure checks (lines 177, 187) do not apply. Once the resolve is finalized and the original budget expires before a callback transaction exists (callback preparation deferred by transaction_cost_limit, resolve finality outlasting the budget, or simply a short remaining window), line 188 answers 'pending' on every step. runtime/contract-intake-worker.mjs:60-67 persists {state:'pending', resultHash, observedAt} without retryAt for a returned outcome (retryAt is set only in the catch path), so the request is re-stepped every tick with chain.profile, chain.resolution and two deliveryState reads, for the life of the store. A newly admitted allocation never changes this: createContractDelivery (lines 53-62) rebinds the request to the budget recorded on its resolve row, and revoking the old budget does not help because line 188 precedes the active() check. 0.12.1 answered 'closed' here (the prior accepted finding 3); 0.12.2 replaced a wrong terminal state with an unbounded non-terminal one. The documented rule (existing signed transactions keep their allocation; nothing is replaced) is preserved, the canonical result stays readable onchain, and nothing is spent, so this is a liveness, state-accuracy and polling-cost defect bounded by the number of resolved requests with callbackGas > 0. Fix: either let a callback that has no transaction yet use the currently selected active allocation (only the resolve row is an existing signed transaction), or answer a distinct terminal state (e.g. 'failed' with reason 'callback_subsidy_expired') when the bound allocation is expired or revoked and no callback transaction exists, and in either case persist a retryAt for pending answers that cannot progress.","reproduction":"test/scratch/delivery-edges.test.mjs test E (node --test test/scratch/delivery-edges.test.mjs; fixture from test/contract-delivery.test.mjs with callback = true, observation.chainId set so the worker processes the row). step() broadcasts the resolve under 'fixture-budget'; confirm() finalizes it with the callback not delivered; advance now to budget.expiresAt + 1; three step() calls answer 'pending', broadcasts stay 1, one chain-transactions row. Five createContractIntakeWorker.tick() calls persist contract-requests.delivery = {state:'pending', resultHash, observedAt} with retryAt undefined and perform at least 10 deliveryState reads. Admit 'next-budget' (same inbox/dispatcher/codeHash, expiring 5 days later), revoke 'fixture-budget', step with the new budget: still 'pending', broadcasts 1, next-budget reserved '0'; 365 days later still 'pending'. Expected: a callback signed under the new allocation, or a terminal state, or at least a persisted retryAt.","snippet":"        if (expiresAt <= now()) return answer('pending');","citation":"resolved","blocking":false},{"severity":"low","title":"One empty eth_getLogs answer for the finalized page that holds the exact relay advances the recovery cursor past it, and the finalized state read then blocks the registration permanently","path":"adapters/chain-executor.mjs","line":81,"description":"Merged from audit_economics and audit_flow (same mechanism, reproduced). relayedRegistration treats found.complete with zero logs plus a finalized projects() read showing a non-zero target as proof of a conflicting registration (lines 78-81). recoverChainEvent reaches 'complete' on the strength of one eth_getLogs response for [start, finalizedHead] and persists nextBlock = end+1 before the finalized read runs (adapters/chain-event-recovery.mjs:16-21); nothing cross-checks an empty page against contract state before the cursor moves. If that single page comes back empty while the event sits inside it (a load-balanced backend that has not imported the newest finalized blocks answering eth_getLogs with [] while eth_call at head.number is served by a synced backend; the Sepolia worker uses one public endpoint with retryCount 0, runtime/chain-worker.mjs:28), the finalized read correctly shows the project registered and line 81 throws. On every later tick start = nextBlock > head (or a later empty range), so the event block is never queried again, and with src/chain-deployments/index.mjs:181 the first throw already finishes the job 'blocked' with the error documented as 'a different registration for that project'. The exact signed registration IS the one onchain, so the project ends in the same permanent no-binding state as the padded-relay finding and every new intent blocks on the calldata check. Baseline 2b87522 left this input in an endless 'registering' loop (the prior medium finding on missing terminal outcomes); 0.12.2 converts it into a terminal conflict with no rescan, which is no better for the project and is mislabelled. Fix: before concluding registered_project_mismatch from a finalized state read with zero logs, discard the cursor and rescan [fromBlock, head] once (or query getLogs by the page-end block hash) and block only when the rescan also finds no event; or verify an empty page against projects(projectId).target at block end before persisting it and throw a retryable error on inconsistency.","reproduction":"test/scratch/registration-edges.test.mjs test C (node --test test/scratch/registration-edges.test.mjs; fixture from test/registration-recovery.test.mjs: module receipt block 80, intent observedBlock 100, exact-calldata relay in block 101, finalized head 110). getLogs returns [] once for the page [80,110], then answers correctly. Tick 1: step() rejects with code registered_project_mismatch and chain-event-recovery stores nextBlock '111'. Ticks 2 and 3 with a healthy RPC: step() rejects with registered_project_mismatch again; getLogs was called exactly once in total, never again for block 101. Expected: the relay at block 101 (owner, mandateHash and calldata all match) is adopted and the step returns 'verified'.","snippet":"          requireThat(/^0x0{40}$/i.test(finalized[1]), 'registered_project_mismatch');","citation":"resolved","blocking":false},{"severity":"low","title":"Registration expiry is decided by the local wall clock when the project is still unregistered at latest, so a direct relay mined in the final seconds before the deadline is orphaned","path":"adapters/chain-executor.mjs","line":23,"description":"From audit_math (reproduced). 0.12.2 states that a missing registration expires only after finalized chain time passes its deadline and implements that at relayedRegistration line 83 (head.timestamp >= expiresAt on the finalized block). That branch is only reached when projects(projectId).target is already non-zero at latest (line 64). When the latest read is still zero, relayedRegistration returns null and deployment.step falls through to transact(), whose unsent-action cut-off at line 23 compares value.message.expiresAt * 1000 with the local clock. The contract accepts registerProject while block.timestamp < expiresAt (contracts/src/ClausApprovalExecutor.sol:133), so a relay broadcast by the owner in the last slot before expiry can be mined at chain time < expiresAt while the executor tick already sees now() >= expiresAt and the block is not yet visible at latest (one slot of propagation plus any clock skew). The tick returns cancelled, terminal() maps it to {state:'blocked', error:'authority_or_window_closed'} and runOne persists state blocked with retryAt null. The relay then lands: the project is registered onchain, chain-bindings stays empty, and because prepareRegistration only checks chain-bindings (src/chain-deployments/index.mjs:98) the owner can prepare and submit a new intent, which relayedRegistration finds but rejects on calldata mismatch (different nonce/expiresAt) with registered_project_mismatch, consuming lifetime intents. The window is seconds wide, so the likelihood is low; the inconsistency is that the two branches of one decision use two clocks, the local one being the earlier. No funds are spent and the onchain registration is valid. Fix: in deployment.step, when transact() answers cancelled for an unsent registration with no chain-transactions row, require the finalized head timestamp to be >= value.message.expiresAt before returning terminal (the rule relayedRegistration already applies), otherwise return {state:'registering'}.","reproduction":"test/scratch/registration-edges.test.mjs test F (node --test test/scratch/registration-edges.test.mjs). Intent observedBlock '100', message.expiresAt 2000, module receipt block 80, no chain-transactions row; readContract(projects) at latest returns a zero target; getBlock(finalized) = {number: 90, timestamp: 1900}; executor created with now: () => 2000500 (local clock 0.5 s past the deadline). Expected under the stated 0.12.2 rule: {state:'registering'} because finalized chain time 1900 < 2000. Actual: deployment.step returns {state:'blocked', error:'authority_or_window_closed'} with no chain-transactions row. Then make the relay visible (projects non-zero at latest, ProjectRegistered log at block 101 with the exact calldata, finalized head 160 at timestamp 1990): a fresh step() on the same inputs returns 'verified', proving the registration was recoverable, while the persisted job would be terminal blocked.","snippet":"    if (!status && expiresAt <= now()) return { state: 'cancelled' };","citation":"resolved","blocking":false},{"severity":"low","title":"An exact direct relay of a prepared intent that the owner mines before the local one-hour window ends can never be queued for recovery: submitRegistration rejects the expired intent and every later in","path":"src/chain-deployments/index.mjs","line":129,"description":"From audit_economics (reproduced). docs/testnet.md (0.12.2) advertises that registration recovery accepts a direct transaction to the pinned executor with the exact signed calldata. Recovery is only reached by the worker for a queued intent, and queuing requires submitRegistration(), which refuses any intent whose message.expiresAt has passed (line 129) regardless of whether that exact registration is already mined; the signature is stored only at submit (line 134), so the worker has nothing to match. The onchain executor accepts registerProject while block.timestamp < expiresAt, so an owner who signs the prepared intent and relays it directly, then submits the signature to the API after the hour (or whose API submission is delayed past the hour by an outage), leaves the project registered onchain under the first intent with no chain-binding. prepareRegistration then creates a new intent (new nonce and expiresAt, so different calldata) which the 0.12.2 recovery rules block with registered_project_mismatch, consuming one of the 16 lifetime intents each time; projectKey is fixed per project (adapters/chain-read.mjs:94-95), so no operation adopts the mined registration. This needs owner misordering, so it is a low-likelihood liveness defect, but its outcome is the same permanent lock as the other registration findings. Fix: let submitRegistration accept an expired intent when no other intent is active (the worker already terminates an expired unsent intent as blocked without spending, and 0.12.2 shows a queued intent remains recoverable after signature expiry), so an existing exact relay is recovered and a missing one is blocked explicitly.","reproduction":"test/scratch/submit-expired.test.mjs (node --test test/scratch/submit-expired.test.mjs; fixture from test/registration-delay.test.mjs). prepareRegistration at T returns intent I with message.expiresAt = T + 3600 s. The owner relays registerProject(I.message, configuration, signature) directly and it is mined at chain time < expiresAt (the chain state is irrelevant to the rejection). At T + 3601 s submitRegistration(I.id, signature) rejects with code registration_expired, no job is enqueued and no chain-registration-signatures row exists; prepareRegistration then returns a new intent with a different id and expiresAt (different calldata). Expected: a way to queue I so the worker's relayedRegistration can adopt the exact mined relay. Actual: no call on this tree can queue I afterwards.","snippet":"        requireThat(prepared.message.expiresAt * 1000 > now(), 'registration_expired');","citation":"resolved","blocking":false},{"severity":"info","title":"Retained limits from the prior review (page starvation, 16 lifetime intents, C bootstrap fee ceiling, B callback gas ceiling) are now documented and remain non-blocking for the bounded launch","path":"docs/contract-intake.md","line":106,"description":"Reused assessment from completed job 2bb2335f (findings 4, 7, 8, 9); the underlying code is unchanged in the delta and the four limits are now stated in docs/contract-intake.md, docs/testnet.md and docs/mainnet.md. (4) An admitted requester can delay peers behind the shared 32-event scan page; no fair scheduling, but no spend and no authority effect. (7) 16 lifetime registration intents include expired and blocked records; finite by design, and the registration findings above make it more likely that intents are consumed by blocked recoveries, so fixing those matters more than raising the cap. (8) C bootstrap: 0.0046 ETH over a 24,385,000 padded gas limit tolerates about 0.189 gwei (0.153 gwei at the 30M cap); fails closed above that, so it is a readiness number for the human approval, not a defect. (9) B dispatch: 0.00005 test ETH per transaction and per project, shared by resolve and callback, means a 1,000,000-gas callback is deliverable only below about 0.04 gwei; callbacks are best effort and the canonical result stays readable. None of these is blocking for scope B or the preparation-only scope C. Nothing in this delta changes a Solidity contract or a financial ceiling.","reproduction":"Arithmetic re-checked against adapters/sepolia-transactions.mjs:63-66: gas = estimate*125/100 + 10000; 19,500,000 -> 24,385,000; 4.6e15 / 24,385,000 = 188,640,557 wei (0.189 gwei); 4.6e15 / 30,000,000 = 153,333,333 wei. 5e13 / 1,260,000 = 39,682,539 wei (0.040 gwei). Page size and intent cap: adapters/contract-intake.mjs:71-72 (logs.slice(0, 32)) and src/chain-deployments/index.mjs:101-111 (list limit 17, registrations.length < 16). The repository's own docs now carry the same figures (docs/contract-intake.md:104-106, docs/testnet.md:43-45, docs/mainnet.md:29).","snippet":"The proposed public dispatcher allocation caps each transaction and each project's cumulative dispatch gas at 0.00005 Sepolia ETH. Resolution and callback share that project cap. A 1,000,000-gas callback needs a fee below roughly 0.04 gwei even before resolution costs and other overhead; a 200,000-gas estimate fits a fresh transaction cap only below roughly 0.19 gwei. These are capacity examples, not current fee quotes or delivery guarantees. Prefer a small receiver or `callbackGas: 0` and read the canonical result. Insufficient gas allowance leaves delivery pending; an expired subsidy alone does not close an otherwise valid onchain request. Existing signed transactions retain their original allocation and deadline. No allocation is automatically renewed.","citation":"resolved","blocking":false}],"coverage":null},{"hash":"2dc8fad1ed82a39fd990222b3905e66ca2b1d0eafd935d6be2b00af8dcc4821a","nodeKey":"audit_math","role":"review","attempt":1,"deviceKey":"1559912e747bbcd47e08bea06ad34a8cab66936ffc40556c79cfd0ecc60f74df","seat":{"tokenId":"939","agentId":"52420"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":43,"runtime":"claude","inputTokens":514,"wallClockMs":898142,"outputTokens":46047,"cachedInputTokens":1779651},"baseCommit":"1f6f6d7ebb30dab9e14edda5a34da20f74e565e8","startCommit":"1f6f6d7ebb30dab9e14edda5a34da20f74e565e8","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":[],"verificationErrors":0,"summary":"Review complete. The findings file holds three substantiated findings, all low or info. Nothing in the 0.12.2 delta reopens the two medium findings that blocked the prior review.\n\n**What I verified**\n\n- The two prior mediums are fixed as described. A relayed-but-unfinalized registration now returns a serializable state without a transaction hash, every job commit is awaited so persistence errors become bounded waits, finalized absence past the deadline terminates the intent, a complete empty scan with a registered project blocks explicitly, and adoption requires the exact signed calldata at the pinned executor.\n- Prior low finding 3 (budget lapse reported as closed) is fixed on the no-transaction path. Prior finding 5 (cursor wedge) is now an explicit block with the cursor retained. Findings 4, 7, 8, 9 are retained and documented as stated.\n- Node suite: 411 pass, 10 skips, matching the claim. Both real local anvil runs pass with relay enabled for chain ids 11155111 and 1, after a forge build of the contracts.\n- Fee arithmetic in the docs is correct. Padded gas is estimate × 1.25 + 10000. The 0.0046 ETH bootstrap cap gives about 0.189 gwei at a 19.5M estimate and 0.153 gwei at the 30M limit, which bounds the estimate to 23,992,000 gas. A 1M-gas callback needs under about 0.040 gwei and a 200k estimate under about 0.192 gwei at 0.00005 ETH.\n\n**Findings (all reproduced under test/scratch, which is not kept)**\n\n- **Low, local-clock expiry orphans a last-second relay.** The finalized-chain-time expiry rule is applied only when the project is already registered at latest. When latest is still zero, `transact` cancels on the local clock, the job becomes terminal blocked, and a direct relay mined moments later leaves the project registered onchain with no binding. Every later intent then blocks on calldata mismatch and consumes one of 16 lifetime intents. Narrow window, about one slot plus clock skew.\n- **Low, cancelled-transaction edge of the subsidy fix.** A resolve signed before a crash and resumed after the budget lapsed is cancelled at submit and answered closed while the Inbox deadline is still open. The worker then skips the request permanently, so no later allocation can deliver it.\n- **Info, callback with lapsed subsidy never terminates.** The pending answer carries no retry time, so the worker re-steps the row with four or five RPC reads every tick indefinitely. Rotation rebinds to the lapsed budget by design, so the state can never change. Bounded by the small number of public decisions.\n\n**Scope B conclusion.** Not blocked by this delta. The two mediums are resolved and the new blocking and finality behaviors hold in mock and real-chain runs. The retained limits are capacity and fairness constraints that fail closed, not authorization or spend defects. The two low findings are state-accuracy and liveness edges in rare races and should ship with a note or a small fix.\n\n**Scope C conclusion.** No code defect in the delta blocks preparation. Finding 1 applies identically to chain id 1 since the same local-clock branch runs there. The bootstrap fee ceiling is a real capacity constraint. Note that viem's fee estimate multiplies the base fee by 1.2, so the executable base fee is roughly 20 percent below the 0.189 gwei figure. This review does not authorize Mainnet activation.\n\n**Limits.** The assigned math, boundary and numerical-gap passes were applied to the delta in depth. Solidity was not changed and was not re-audited beyond the registration expiry comparator. The live proposed allocation's gas and fee-per-gas ceilings are not in the tree, so whether a 1M-gas callback fits the gas ceiling could not be confirmed.","createdAt":"2026-10-12T16:35:24.719Z","verdict":null,"findings":[{"severity":"low","title":"Registration expiry is still decided by the local wall clock when the project is unregistered at latest, so a direct relay mined in the final seconds before the deadline is permanently orphaned (block","path":"adapters/chain-executor.mjs","line":23,"description":"0.12.2 states that a missing registration expires only after finalized chain time passes its deadline, and implements that rule in relayedRegistration (line 83, head.timestamp >= expiresAt on the finalized block). That branch is only entered when projects(projectId).target is already non-zero at latest (line 64). When the latest read is still zero, relayedRegistration returns null and deployment.step falls through to transact(), whose unsent-action cut-off at line 23 compares value.message.expiresAt*1000 with the local clock now(). The contract accepts registerProject while block.timestamp < expiresAt (ClausApprovalExecutor.sol line 133), so a relay broadcast by the owner in the last slot before expiry can be mined at chain time < expiresAt while the executor tick already sees now() >= expiresAt*1000 and the block is not yet visible at latest (one slot of propagation plus any clock skew). The tick returns {state:'cancelled'} -> terminal(tx) -> {state:'blocked', error:'authority_or_window_closed'}; runOne writes state blocked with retryAt null and the job is never claimed again. The relay then lands: the project is registered onchain, chain-bindings stays empty, and because prepareRegistration only checks chain-bindings (line 98) the owner can prepare and submit a new intent, which relayedRegistration finds but rejects on calldata mismatch (different nonce/expiresAt) with registered_project_mismatch -> blocked, consuming one of the 16 lifetime intents each time. The boundary is the inconsistency between the two expiry clocks on the two branches of the same decision: the relayed branch uses finalized chain time, the unregistered-at-latest branch uses local time, and the local one is the earlier of the two by roughly the finality lag (about 13 minutes on Sepolia and Mainnet) plus skew. No funds are spent and the onchain registration is valid; the defect is state accuracy and liveness for the project, and it applies to chain ids 11155111 and 1 identically. Fix: in deployment.step, when transact() answers cancelled for an unsent registration (no chain-transactions row), require the finalized head timestamp to be >= value.message.expiresAt before returning terminal, otherwise return {state:'registering'} (the same rule relayedRegistration already applies); equivalently move the line 83 finalized-time check in front of the null return at line 64.","reproduction":"Mock (test/scratch/registration-edges.test.mjs, EDGE 3): chain-registrations value with observedBlock '100', message.expiresAt 2000, chain-modules receipt block 80; no chain-transactions row; client.readContract(projects) at latest returns a zero target; getBlock(finalized) = {number: 90, timestamp: 1900}; executor created with now: () => 2000500 (local clock 0.5 s past the deadline). Expected under the stated 0.12.2 rule: {state:'registering'} because finalized chain time 1900 < 2000 and a relay can still be mined until chain time 2000. Actual: deployment.step returns {state:'blocked', error:'authority_or_window_closed'} and no chain-transactions row exists. Then make the relay visible (projects non-zero at latest, ProjectRegistered log in block 150 with the exact signed calldata, finalized head 160 at timestamp 1990): a fresh step() on the same inputs returns {state:'verified'}, proving the registration was recoverable, but the persisted job is terminal blocked (src/chain-deployments/index.mjs:168-169 writes retryAt null) so it is never re-run. Control (EDGE 3b): identical inputs with the relay already visible at latest return 'registering' then 'verified'. Run: node --test test/scratch/registration-edges.test.mjs","snippet":"    if (!status && expiresAt <= now()) return { state: 'cancelled' };","citation":"resolved","blocking":false},{"severity":"low","title":"A resolve transaction signed before a crash and resumed after the dispatch subsidy lapsed is cancelled and reported as a terminal closed delivery while the Inbox request deadline is still open","path":"adapters/contract-delivery.mjs","line":200,"description":"0.12.2 fixed the no-transaction path so that only resolution.expiresAt (the Inbox deadline) answers closed and a lapsed budget answers pending (lines 187-188). The same distinction is not applied on the cancelled-transaction path. A resolve intent is prepared with expiresAt = min(resolution.expiresAt, budget.expiresAt) (line 154). If the process stops between the prepare commit (sepolia-transactions.mjs line 87, state 'prepared', reservation held) and the submit call on the next line of step (line 192), the row persists as 'prepared'. On restart, transactions.submit() finds intent.expiresAt <= now() when the budget has since expired, cancels the row (sepolia-transactions.mjs line 99-107, releasing the reservation), and step answers 'closed' at line 200 because state === 'cancelled'. runtime/contract-intake-worker.mjs line 29 skips every request whose delivery.state is closed for ever, so neither the original budget nor a later admitted allocation (which the createContractDelivery wrapper would otherwise bind only for a pending transaction) can deliver a request whose onchain deadline is still days away. The same cancelled->closed mapping is reached when the restart happens with allowSubmit=false (admission paused during the outage) even if the budget is still valid, since authorize() returns false and submit cancels. This is the cancelled-transaction edge of the limit the task asks to verify: the subsidy-expiry vs request-expiry distinction holds only when no transaction row exists. Nothing is broadcast and no funds are lost; the defect is a misreported terminal state. Fix: at line 194-200, when transaction.state === 'cancelled' and action === 'resolve', answer 'closed' only if resolution.expiresAt <= now() or the Inbox reports closed, and otherwise answer 'pending' (the cancelled row keeps its id, so a later prepare with the same id would hit idempotency_conflict; a fresh allocation would also need a distinct transaction id or the cancelled row to be re-preparable).","reproduction":"Mock (test/scratch/delivery-edges.test.mjs, EDGE 1): budget expiresAt = T0+1 day, request observation.deadline = T0+3 days, binding valid 10 days, chain.deliveryState pending:true. Wrap store.transaction so the second transaction of the first step (submit's state change) throws, simulating a kill after the prepare commit: step rejects, chain-transactions holds one row in state 'prepared', broadcasts = 0. Restore the store, set now = budget.expiresAt+1 (deadline still 2 days away) and call step again. Expected by the 0.12.2 rule (an expired subsidy alone does not close an otherwise valid onchain request): 'pending'. Actual: 'closed'; the row is now 'cancelled'. Admit a second budget with the same inbox/dispatcher/codeHash expiring in 5 days and step with it: still 'closed', broadcasts remain 0. Run: node --test test/scratch/delivery-edges.test.mjs","snippet":"        return answer(transaction.state === 'cancelled' ? 'closed' : 'failed');","citation":"resolved","blocking":false},{"severity":"info","title":"A resolved request whose callback subsidy lapsed is re-stepped on every worker tick for ever with no backoff and no terminal state; a rotated allocation is always rebound to the lapsed budget","path":"adapters/contract-delivery.mjs","line":188,"description":"For the callback action expiresAt is budget.expiresAt (line 154). When the resolve confirmed under budget A and A expires before the callback is prepared, line 188 answers 'pending' without a retryAt. The intake worker only defers rows with delivery.retryAt in the future (runtime/contract-intake-worker.mjs line 28) and persists only {state, resultHash, observedAt} for a pending answer (line 63), so the request is stepped again on every tick with the full read set (profile, resolution, two or three deliveryState RPC reads) for the lifetime of the deployment. By contrast the cost-limit deferral path sets a 15-minute retryAt. Because the createContractDelivery wrapper rebinds any request whose resolve row carries budgetId A to budget A (lines 53-62), selecting a new allocation B never changes the answer: the callback stays pending under A for ever, and revoking A does not make it terminal either (line 188 precedes the active() check at line 189). The documented policy that a new budget never silently authorizes an old callback is preserved, and the canonical result is readable, so this is a state-accuracy and polling-cost note rather than a functional defect: the row is never closed, failed or delivered. Bounded by the number of resolved requests with callbackGas > 0 (at most 32 public decisions over the proposed allocation). Fix: when the subsidy that owns the resolution has lapsed (or been revoked) and no callback transaction exists, either answer a terminal 'failed' with a reason, or persist a retryAt comparable to the cost-limit backoff.","reproduction":"Mock (test/scratch/delivery-edges.test.mjs, EDGE 2): callback fixture, step broadcasts and confirms the resolve under budget A; set now = A.expiresAt+1; run createContractIntakeWorker.tick() five times. Actual: contract-requests.delivery = {state:'pending', resultHash, observedAt} with no retryAt after every tick, 22 deliveryState reads for 5 ticks, broadcasts stay 1. Then admit budget B (5-day expiry), revoke A, and step with B: still 'pending', broadcasts 1. Expected: a terminal state or a persisted retryAt once the owning subsidy can no longer fund the callback. Run: node --test test/scratch/delivery-edges.test.mjs","snippet":"        if (expiresAt <= now()) return answer('pending');","citation":"resolved","blocking":false}],"coverage":null},{"hash":"794dbb78770266875e3e9883e994c0e2b0a6b1afa1ccde0a334584723bb6b64e","nodeKey":"audit_economics","role":"review","attempt":1,"deviceKey":"dff6c0d3de4aa9136bb50e10fe63d467a75d1b379a902c7dc21e0dca0f4367d9","seat":{"tokenId":"1614","agentId":"52210"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":44,"runtime":"claude","inputTokens":610,"wallClockMs":844574,"outputTokens":44902,"cachedInputTokens":2099479},"baseCommit":"1f6f6d7ebb30dab9e14edda5a34da20f74e565e8","startCommit":"1f6f6d7ebb30dab9e14edda5a34da20f74e565e8","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":[],"verificationErrors":0,"summary":"Review complete. The findings file at the repository root holds four substantiated findings (three low, one info). No tracked file was changed; only the findings file and scratch tests under test/scratch exist.\n\n**What I verified on this tree**\n\n- Operator suite: 411 pass, 10 environment skips, 0 fail, matching the stated gate.\n- Real local anvil runs with CLAUS_TEST_RELAY=1 on chain IDs 11155111 and 1: both pass, including the Sepolia persisted wait before finality.\n- Each 0.12.2 claim traced in code: hash-less pending state is serializable, worker commits are awaited inside the try, recovery starts at the module receipt block, exact calldata match, finalized-absence expiry, finalized conflict and hash conflict are terminal, cancellation and reservation paths unchanged.\n\n**Prior findings status**\n\n- Findings 1 and 2 (the two mediums that blocked B): fixed for the states they described. Finding 5 (external callback after cancel) fixed. Finding 3 fixed only for the no-transaction path; see below. Finding 4 resolved by making the hash conflict terminal, which is documented.\n- Findings 6, 7, 8, 9 are retained limits as stated; my assessment is that none blocks the bounded B launch. The 32-event page is bounded shared capacity among admitted parties. The 16-intent cap only harms the owner's own project. The Sepolia dispatch cap only degrades callbacks, which are best effort with the canonical result readable. The Mainnet bootstrap fee ceiling is a go/no-go number for C and is now recorded in docs/mainnet.md.\n\n**New findings in this delta**\n\n- Low, adapters/contract-delivery.mjs:200: a prepared but never broadcast resolve or callback that is cancelled at submit because the subsidy expired or dispatch was paused is reported closed while the Inbox request is still open. Reproduced in a scratch test. Narrow window (submit must fail after prepare commits), but it contradicts the new documented rule and the request becomes undeliverable by any later budget.\n- Low, adapters/chain-executor.mjs:81: one empty getLogs page from the single public RPC advances the recovery cursor past an exact relay, and the finalized read then blocks the registration permanently with the \"different registration\" error. Reproduced. Before 0.12.2 this looped; now it is irreversible.\n- Low, src/chain-deployments/index.mjs:129: an exact direct relay whose intent was not submitted within the local hour can never be adopted, because submission rejects expired intents and the signature is never stored. The project stays registered onchain with no binding. Reproduced.\n- Info, adapters/contract-delivery.mjs:188: a callback with no transaction stays pending forever after subsidy expiry, re-stepped every tick with no retryAt, even under a new budget. Reproduced. This is the honest-state mirror of the fixed finding 3.\n\n**Conclusions**\n\n- Scope B: no authorization bypass, spending overrun, or silent replacement found. The remaining defects are state-reporting and recoverability edges that need unusual RPC or timing failures. They do not block the bounded public B window, but the first two deserve a fix before a wider window because each leaves a real onchain state with no operator recovery path.\n- Scope C: preparation-only is consistent with the code. Nothing here authorizes Mainnet activation; the recorded fee ceiling (about 0.189 gwei at the padded 19.5M estimate) must be checked against live fees at approval time, and the same three low findings apply to Mainnet registration recovery since the executor path is shared.\n\n**Limits of this review**\n\nReviewed in depth: the delta files, the recovery, executor, dispatcher and job runner paths, and the two Solidity contracts they depend on. Not re-reviewed: unchanged intake scanning, IMD payment, vault and wallet code, which the prior job covered. No fork or live-chain checks were run; all reproductions are mock-client or local anvil.","createdAt":"2026-10-12T16:34:33.267Z","verdict":null,"findings":[{"severity":"low","title":"A prepared but never broadcast delivery transaction that is cancelled at submit because the subsidy expired or dispatch was paused is reported as terminal 'closed' while the Inbox request is still ope","path":"adapters/contract-delivery.mjs","line":200,"description":"The 0.12.2 change at lines 187-188 distinguishes subsidy expiry from request expiry only on the path with no existing transaction. When a transaction row exists in state 'prepared' (signed, nonce reserved, never broadcast), transactions.submit() in adapters/sepolia-transactions.mjs:99-107 cancels it whenever intent.expiresAt <= now() (for resolve, intent.expiresAt = min(resolution.expiresAt, budget.expiresAt), so the budget deadline alone triggers it) or whenever authorize() returns false, which includes !allowSubmit (the worker passes allowSubmit = active, false during any admission pause) and active() returning false for an expired/revoked budget. The cancelled result then reaches line 200 and the step answers 'closed'. runtime/contract-intake-worker.mjs:29 skips every request whose delivery.state is 'closed' forever, and createContractDelivery.step (lines 53-62) rebinds the request to its original budget via the cancelled resolve row, so no later budget can deliver it either. The Inbox request is still Pending and acceptable to resolve until its own deadline. docs/contract-intake.md states that 'an expired subsidy alone does not close an otherwise valid onchain request', and docs/mainnet.md describes the admission pause as reversible; both are violated for this state. The window is real but narrow: a 'prepared' row survives a tick only if submit() fails after prepare() committed (its first call is client.getChainId(), an RPC failure there throws out of the worker tick with no code and no retryAt) or the process dies between the two commits. Same consequence for a prepared callback cancelled by budget expiry: the request is answered 'closed' although its resolve is finalized onchain. Impact: a deliverable result is misreported as closed and becomes undeliverable by the network; no funds are spent and no duplicate transaction is created. Fix: in the cancelled branch, when the cancellation was caused by expiry of the budget/intent rather than onchain closure (for resolve: resolution.expiresAt > now() and latest state still pending; for callback: resolved and not delivered), answer 'pending' as the no-transaction path does, or record a distinct terminal state ('unfunded'/'paused') instead of 'closed', and give the worker a retryAt for it. Note the design rule that no transaction is replaced means a cancelled row can never be re-prepared, so a distinct terminal state is the honest minimum.","reproduction":"test/scratch/delivery-cancelled-edge.test.mjs (fixture copied from test/contract-delivery.test.mjs; request deadline = budget.expiresAt + 1 day; client.getChainId throws once on the submit() call that follows a successful prepare()). Case 1: step() rejects with 'rpc down'; chain-transactions row is 'prepared', 0 broadcasts; advance now to budget.expiresAt + 1; step() -> row becomes 'cancelled', outcome.state is 'closed'. Expected 'pending' (request still open onchain; deliveryState.pending true). Case 2: same prepared row, step({allowSubmit:false}) during a pause -> 'closed'. Expected 'pending'. Both assertions fail on this tree; the existing test 'an expired subsidy leaves an open request pending' only covers the no-transaction path.","snippet":"        return answer(transaction.state === 'cancelled' ? 'closed' : 'failed');","citation":"resolved","blocking":false},{"severity":"low","title":"One empty eth_getLogs answer for the finalized page that holds the exact relay advances the recovery cursor past it and the next finalized read blocks the registration permanently with registered_proj","path":"adapters/chain-executor.mjs","line":81,"description":"relayedRegistration() scans finalized pages through recoverChainEvent(), which persists {nextBlock: end+1} after any page that returns no logs (adapters/chain-event-recovery.mjs:19) and never rescans below nextBlock. Nothing cross-checks a 'no logs' page against contract state before the cursor is advanced. If the RPC answers one page empty although it contains the ProjectRegistered event (the executor worker uses a single public load-balanced endpoint, runtime/chain-worker.mjs:28, with retryCount 0; a node behind the finalized head answers eth_getLogs with [] for blocks it does not have yet, while eth_getBlockByNumber('finalized') and eth_call at that height come from a synced node), the cursor moves past the event. On the same or the next tick found.complete is true, the finalized projects() read at line 79-81 shows the project registered, and the step throws registered_project_mismatch. 0.12.2 made that code terminal in src/chain-deployments/index.mjs:181 ('blocked'), the job is never retried, and the cursor is retained by design, so an exact signed relay that the executor should adopt is permanently refused with the same error that is documented as 'a different registration for that project'. The project has a real onchain registration and no chain-binding; any new intent is also blocked (different calldata). Before 0.12.2 the same input left the job in an endless 'registering' retry; the delta turned a transient RPC fault into an irreversible block without an operator reset. Fix: before persisting an empty page, confirm the queried state at block `end` is still consistent with 'no event' (for registrations, projects(projectId).target == 0 at blockNumber end; cheap, same RPC); if it is not, throw a retryable error instead of advancing. Alternatively, on a finalized-read mismatch with logs.length == 0, rescan once from fromBlock before blocking.","reproduction":"test/scratch/registration-edges.test.mjs, first test (fixture copied from test/registration-recovery.test.mjs: module receipt block 80, intent observedBlock 100, exact-calldata relay in block 101, finalized head 110, latest 174). getLogs returns [] once for the page [80,110], then answers correctly. Tick 1: step() throws registered_project_mismatch and chain-event-recovery stores nextBlock '111'. Tick 2 with a healthy RPC: step() throws registered_project_mismatch again, getLogs was called exactly once in total, no rescan ever happens. Expected: the relay at block 101 (owner, mandateHash and calldata all match) is adopted and the step returns 'verified'. With runOne this is persisted as state 'blocked', error registered_project_mismatch (see the 'a different registered project permanently blocks' test in test/registration-recovery.test.mjs for the terminal mapping).","snippet":"          requireThat(/^0x0{40}$/i.test(finalized[1]), 'registered_project_mismatch');","citation":"resolved","blocking":false},{"severity":"low","title":"An exact direct relay of a prepared intent that the owner submits after the local one-hour window can never be adopted: submitRegistration rejects it as registration_expired and every later intent is ","path":"src/chain-deployments/index.mjs","line":129,"description":"docs/testnet.md (0.12.2) advertises direct relay recovery: 'Registration recovery accepts a direct transaction to the pinned executor with the exact signed registration calldata.' Recovery is only reached by the worker for a queued intent, and queuing requires submitRegistration(), which refuses any intent whose message.expiresAt has passed (line 129) regardless of whether that exact registration is already mined. The onchain executor accepts registerProject while block.timestamp < expiresAt (contracts/src/ClausApprovalExecutor.sol:133), so an owner can sign the prepared intent, relay it in the last minutes of the hour (or simply submit the signature to the API later than the relay), and the mined registration is then unreachable: submitRegistration -> registration_expired; the signature is not stored for unsubmitted intents (chain-registration-signatures is written only at submit, line 134), so the worker has no calldata to match. prepareRegistration() happily creates a new intent (new nonce and expiresAt, hence different calldata) and that one ends in registered_project_mismatch -> blocked by the 0.12.2 recovery rules, consuming one of the 16 lifetime intents each time. projectKey is fixed per project (adapters/chain-read.mjs:94-95), so the project stays registered onchain under the first intent forever with no chain-binding, no proposals, and no operator or owner operation to adopt it. Fix: let submitRegistration accept an expired intent when no other intent is active (the worker already terminates an expired unsent intent as blocked without spending, test 'a missing finalized registration past its deadline terminates'), so an existing exact relay is recovered and a missing one is blocked; or allow the owner to submit the signature of a cancelled/expired intent explicitly for recovery only.","reproduction":"test/scratch/registration-edges.test.mjs, second test (fixture from test/registration-delay.test.mjs). prepareRegistration at T returns intent I with message.expiresAt = T+3600s. Owner relays registerProject(I.message, configuration, signature) directly and it is mined at T+3500s (not simulated; the chain state is irrelevant to the rejection). At T+3601s submitRegistration(I.id, signature) rejects with registration_expired; prepareRegistration then returns a new intent with a different id (different nonce/expiry/calldata). On this tree there is no call that can queue I afterwards; the exact relay is therefore never scanned, while the 0.12.2 recovery of a queued intent ('a mined relay without finalized logs ... remains recoverable after signature expiry') shows the worker itself has no objection to the expired window once the relay exists.","snippet":"        requireThat(prepared.message.expiresAt * 1000 > now(), 'registration_expired');","citation":"resolved","blocking":false},{"severity":"info","title":"A callback that was never prepared before its subsidy expired is answered 'pending' indefinitely with no retryAt: the worker re-runs its RPC reads every tick forever although no allocation can ever de","path":"adapters/contract-delivery.mjs","line":188,"description":"For action 'callback', expiresAt is budget.expiresAt alone (line 154); deliverCallback has no onchain deadline (contracts/src/ClausRequestInbox.sol:86-90), and observed.closed is only consulted for 'resolve'. So once the resolve is finalized and the subsidy expires before a callback transaction exists, line 188 answers 'pending' on every step, forever. runtime/contract-intake-worker.mjs:60-67 stores delivery.state 'pending' without retryAt for a returned outcome (retryAt is only set in the catch path), so the request is re-stepped on every tick: chain.resolution, two deliveryState() reads (each 3-4 RPC calls: finalized/latest block, dispatcher() verify, observation block, requests()), and transactions.status. A later budget cannot change the outcome: createContractDelivery.step() (lines 53-62) binds the request to the original resolve row's budgetId and re-runs the bound step under that expired budget, by design ('No allocation is automatically renewed'). The only exit is an external party calling deliverCallback, which the request's own requester may never do. This is the mirror image of the accepted 0.12.1 finding (closed while deliverable): now it is pending while provably undeliverable by this network, at a steady per-tick RPC cost that grows with the number of such requests across the public window. Not a spending or authorization defect. Fix: when the bound budget is expired or revoked and no callback transaction exists, answer a distinct terminal state (e.g. 'failed' with reason 'subsidy_expired', keeping the resolve receipt) or have the worker set a long retryAt for pending outcomes whose reason is subsidy expiry.","reproduction":"test/contract-delivery.test.mjs fixture with callback = true: step() prepares and broadcasts the resolve (lost response), confirm() mines it; set now to budget.expiresAt + 1 (binding and request deadline still valid); step() -> 'pending' (resolve confirmed/finalized, callback not prepared, line 188). Repeat step() any number of times: always 'pending', broadcasts stay 1, no chain-transactions row for ':callback', and admitting a new public budget and calling createContractDelivery({budget: next}).step() still returns 'pending' because the request is rebound to 'fixture-budget'. The worker persists {state:'pending'} without retryAt, so the row is re-processed on every tick indefinitely.","snippet":"        if (expiresAt <= now()) return answer('pending');","citation":"resolved","blocking":false}],"coverage":null},{"hash":"418fc0e8679c39daa5f1f3a71ff6c2a59da62f57b272bb9cc46f665713e203b8","nodeKey":"audit_flow","role":"review","attempt":1,"deviceKey":"357c46e3781993d449f398d7eae2be8718b1cfa8deff2cc3661e944506942b5e","seat":{"tokenId":"205","agentId":"52222"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":46,"runtime":"claude","inputTokens":674,"wallClockMs":792610,"outputTokens":48383,"cachedInputTokens":2391463},"baseCommit":"1f6f6d7ebb30dab9e14edda5a34da20f74e565e8","startCommit":"1f6f6d7ebb30dab9e14edda5a34da20f74e565e8","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":[],"verificationErrors":0,"summary":"Review complete. The findings file at the repository root holds three reproduced findings; no tracked file was changed, and my scratch tests live under test/scratch, which is excluded.\n\n**What I verified on 1f6f6d7 against baseline 2b87522**\n\n- Full suite: 411 pass, 10 environment skips, matching the stated source gate.\n- Both real local chain fixtures with CLAUS_TEST_RELAY=1 pass against anvil for chain IDs 11155111 and 1, including the new Sepolia persisted-wait-before-finality step.\n- All five material items from the prior judge are addressed in the delta: the hash-less relayed result is now serializable and the worker commit is awaited; the relayed path has terminal outcomes; subsidy expiry no longer closes an open request; the cursor hash conflict stops explicitly with journals retained; the cancelled-callback race waits for finality. The exact-calldata check, the scan start at the module receipt block, the finalized-time expiry rule and the unchanged cancellation, reservation, nonce and signature handling all hold as described.\n- The retained limits (32-event page, 16 lifetime intents, about 0.189 gwei bootstrap ceiling, 0.00005 test ETH dispatch cap with about 0.04 gwei for a 1M-gas callback) are now documented in the diff and the arithmetic checks out. None of them is blocking for the bounded B launch: they are finite-capacity properties among admitted parties, and the canonical result remains readable without a callback.\n\n**Findings**\n\n1. **Medium, src/chain-deployments/index.mjs:181.** The new terminal mapping is keyed on the error code, so the post-confirmation latest-state checks after the executor's own confirmed transaction also become permanent blocks. One stale eth_call from a lagging backend turns a successful, gas-paid registration into a blocked job with no binding, and every later intent blocks on the exact-calldata check. The baseline retried and self-healed. Reproduced with a scratch worker test.\n2. **Low, adapters/chain-executor.mjs:81.** A single empty getLogs page for a finalized relay is committed to the cursor and then read as a conflict forever; the event block is never re-scanned. Reproduced.\n3. **Low, adapters/contract-delivery.mjs:62.** A callback whose resolve ran under a since-expired subsidy is rebound to that subsidy on every tick, so a later allocation never delivers it and the delivery never terminates. Reproduced.\n\n**Scope conclusions**\n\n- **B (public sponsored reviews, bounded Sepolia noncustodial readonly registration, Claus decisions, request/callback handling):** the two prior medium blockers are fixed, but the fix introduces the medium regression above on the same public registration path. I would not treat B as clear until the terminal mapping is narrowed to finalized-evidence throw sites. The two low findings can ship with a note. No authorization bypass, unbounded spend or custody exposure found.\n- **C (Mainnet profile preparation only):** the same code path applies on chain 1, so finding 1 applies there too and should be fixed before any activation decision. The release guards themselves are unchanged from the prior review and the documented fee ceiling is now written next to the 0.0046 ETH figure. Nothing here constitutes Mainnet authorization, which still requires the separate exact human approval.","createdAt":"2026-10-12T16:33:41.177Z","verdict":null,"findings":[{"severity":"medium","title":"registered_project_mismatch is now terminal for every throw site, including the post-confirmation latest-state check: one stale RPC read after a confirmed local registration permanently blocks the reg","path":"src/chain-deployments/index.mjs","line":181,"description":"0.12.2 adds registered_project_mismatch to runOne's terminal list so that a finalized conflicting registration blocks explicitly (the intent stated in docs/testnet.md). The mapping is keyed on the error code, not on whether the evidence was finalized. The same code is thrown by adapters/chain-executor.mjs:140-146 after the executor's OWN transaction is confirmed (3 confirmations on Sepolia, finalized on chain 1): `read(value.executor,'projects',...)` at the latest tag must already show owner/mandateHash/policyVersion/minDelaySeconds, and the new target must answer executor()/projectId(). Those reads go to whatever backend answers `latest`; a load-balanced public Sepolia endpoint whose eth_call backend lags the backend that served getTransactionReceipt/getBlockNumber by three or more blocks returns the pre-registration struct (zero owner), so requireThat throws registered_project_mismatch. On baseline 2b87522 that was a 60 s `waiting` retry which self-healed on the next tick; on 1f6f6d7 runOne finishes the job `blocked` and writes chain-registrations state blocked. The project is then registered onchain by this exact transaction (gas paid from the public pool) but has no chain-bindings row; every later prepareRegistration/submitRegistration produces a new intent whose relayedRegistration finds the first ProjectRegistered event, fails the exact-calldata check at chain-executor.mjs:92-93 (different nonce/expiresAt) and blocks again, so the project can never bind, and docs/operations.md states journals are never cleared. This is a regression of the 0.12.2 delta in the advertised B registration path, not an authorization bypass; no extra funds are spent. Fix: throw a distinct, non-terminal code from the post-confirmation state checks (or pin those reads to the confirmed receipt's blockNumber so they cannot lag), and keep the terminal mapping only for the finalized-evidence throw sites (chain-executor.mjs lines 81, 87-88, 92-93).","reproduction":"State: chain-transactions 'register:registration' exists with state 'confirmed', sender = executor signer, intent.chainId 11155111, receipt.blockNumber '105'; network-capabilities 'chain' enabled for executor E; chain-modules 'module' with receipt.blockNumber '80'; chain-registrations 'registration' queued with a project-register job enqueued. Mock client: getBlockNumber 110n, getBlock finalized 110n, readContract('projects') returns [0x0.., 0x0.., 0x0.., 0x0.., 0n] on the first tick (stale backend) and the registered struct [owner, target, mandateHash, policyVersion, 0n] afterwards. Expected (baseline 2b87522 behaviour): deployment.step rejects with registered_project_mismatch, runOne finishes {state:'waiting', retryAt: now+60000}, and the next tick returns 'verified' with the binding. Actual on 1f6f6d7: runOne returns state 'blocked' with result.error 'registered_project_mismatch', chain-registrations 'registration' is written state 'blocked', and the job is never claimed again; a direct deployment.step with the settled read returns 'verified', proving the block was premature. Scratch test test/scratch/confirmed-latest-mismatch.test.mjs (`node --test test/scratch/confirmed-latest-mismatch.test.mjs`) fails on this tree at `assert.equal(first.state, 'waiting')` with actual 'blocked'.","snippet":"        if (['registered_project_mismatch', 'registration_receipt_mismatch', 'recovery_block_changed', 'recovery_event_mismatch'].includes(error.code) ||","citation":"resolved","blocking":false},{"severity":"low","title":"A finalized relay whose single getLogs page comes back empty is committed to the recovery cursor and then read as a permanent registered_project_mismatch; the event block is never scanned again","path":"adapters/chain-executor.mjs","line":81,"description":"relayedRegistration treats `found.complete && logs.length === 0` plus a finalized `projects` read showing a non-zero target as proof of a conflicting registration. recoverChainEvent reaches `complete` on the strength of one eth_getLogs response for [start, finalizedHead] and persists nextBlock = head+1 before the finalized read runs (adapters/chain-event-recovery.mjs:16-21). If that single page is empty while the event actually sits in the range (a lagging getLogs backend that has not imported the newest finalized blocks, or a provider that silently truncates/returns [] under load, while eth_call at `head.number` is served by a synced backend), the finalized state read correctly shows the project registered and line 81 throws. On the next tick start = nextBlock > head (or a later empty range), so the event block is never re-queried: every tick throws the same code even after the RPC is healthy, and with the 0.12.2 mapping in src/chain-deployments/index.mjs:181 the first throw already finishes the job `blocked`. The exact signed registration IS the one onchain, so the outcome is the same permanent no-binding state as the medium finding: new intents block on the calldata check against the first event. Baseline 2b87522 had the cursor wedge judged low (it retried forever); 0.12.2 converts it into a terminal conflict with no re-scan. Fix: before concluding `registered_project_mismatch` from a finalized state read, discard the cursor and re-scan the full [fromBlock, head] range at least once (or query getLogs by the finalized block hash of the page end), and only block when the re-scan also returns no event.","reproduction":"State: chain-registrations intent with observedBlock '100', message.expiresAt 2000, module receipt.blockNumber '80', no local chain-transactions row. Mock client: getBlock finalized = {number:110n, timestamp:1900n}, readContract('projects') returns the registered struct (owner/mandate matching the intent) for latest and for blockNumber >= 101, zeros below 101; getTransaction returns the exact registerProject calldata; getLogs returns [] on its first call and the ProjectRegistered log at block 101 on every later call. Expected: the first step defers ('registering') or, at worst, the next step adopts the relay and returns 'verified'. Actual: first step rejects with code registered_project_mismatch and chain-event-recovery stores nextBlock '111'; the second and every later step also reject with registered_project_mismatch while getLogs is called exactly once in total (never for block 101). Scratch test test/scratch/transient-empty-logs.test.mjs (`node --test test/scratch/transient-empty-logs.test.mjs`) fails on this tree at the second step with code registered_project_mismatch.","snippet":"          requireThat(/^0x0{40}$/i.test(finalized[1]), 'registered_project_mismatch');","citation":"resolved","blocking":false},{"severity":"low","title":"A callback whose resolve was sent under a now-expired subsidy is pinned to that subsidy forever: a later admitted allocation never delivers it and the delivery never reaches a terminal state, even aft","path":"adapters/contract-delivery.mjs","line":62,"description":"createContractDelivery re-binds every step for a request to the budget recorded on its resolve transaction (lines 53-62) so that existing signed transactions keep their original allocation. That rebinding also governs the callback, which has no transaction yet. For the callback action expiresAt = budget.expiresAt of the ORIGINAL budget (line 154); with no transaction, line 188 now answers `pending` when that expiry has passed (the 0.12.2 change that replaced the prior terminal `closed`). So once the original subsidy lapses between the resolve and the callback, every later tick answers pending regardless of which budget the host selects: the later allocation is never consulted for the callback, nothing is signed, and nothing ever terminates (deliverCallback has no deadline in ClausRequestInbox.sol:86-90 and the callback iteration checks no request deadline). The worker re-steps every such request on every tick (chain.profile + chain.resolution + at least one deliveryState RPC read per request) for the life of the store. docs/contract-intake.md says an expired subsidy leaves delivery pending and that existing signed transactions retain their allocation; it does not say an unsigned callback is bound to a lapsed allocation. The prior job's judge finding 3 expected a later admitted allocation to deliver such work. No funds are at risk and the canonical result is onchain; this is a liveness/accuracy residue of the 0.12.2 fix. Fix: bind only transactions that already exist to their original budget (the resolve) and let a callback that has no transaction use the currently selected budget, or answer a terminal state once no selectable allocation can ever sign it.","reproduction":"Budget `first` (expiresAt T0+1d); request in result_ready with observation.deadline T0+3d, callbackGas>0; binding active to T0+10d. Tick 1 at T0: step broadcasts the resolve under `first` (pending). Confirm the resolve. Advance the clock to `first`.expiresAt + 1 ms; tick 2 under `first`: resolve confirmed, callback answers pending, broadcasts stay 1. Admit `second` (same inbox/dispatcher/codeHash, expiresAt T0+6d) and tick with createContractDelivery({budget: second}). Expected: one callback is signed and broadcast under `second` (broadcasts 2, `second`.reserved 135000000000). Actual: state pending, broadcasts 1, `second`.reserved and spent stay '0'. Advance past the request deadline by a day and tick three more times under `second`: still pending every time, never delivered, never closed/failed. Scratch test test/scratch/callback-expired-subsidy.test.mjs (`node --test test/scratch/callback-expired-subsidy.test.mjs`): the first test fails on this tree (broadcasts 1 !== 2, state pending); the second test passes and documents the non-terminal state.","snippet":"    return createBoundContractDelivery({ ...options, budget: prior }).step(input);","citation":"resolved","blocking":false}],"coverage":null},{"hash":"24763e5c2ee06fc0950eab43c3d054b6507342b62c077eb9602427cef1166f3e","nodeKey":"audit_permissions","role":"review","attempt":1,"deviceKey":"6b47d1bf92391c222bba8a8db12d0898cb52fd4d8f8214c4272b5faea62ff7e9","seat":{"tokenId":"632","agentId":"52214"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-opus-5-5","turns":41,"runtime":"claude","inputTokens":80,"wallClockMs":372682,"outputTokens":26395,"cachedInputTokens":3323478},"baseCommit":"1f6f6d7ebb30dab9e14edda5a34da20f74e565e8","startCommit":"1f6f6d7ebb30dab9e14edda5a34da20f74e565e8","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":[],"verificationErrors":0,"summary":"I found three defects in the 0.12.2 delta: one medium and two low. They are written to `.imd-findings.json`, and each one's path, line and quoted code was checked against the tree. No tracked files were changed, and the scratch tests, `node_modules` and Foundry build output are deleted.\n\n**Findings**\n\n1. **Medium (`adapters/chain-executor.mjs:92`): the new exact-calldata check lets anyone permanently block a project's registration.** `registerProject` can be called by anyone, and the contract ignores extra bytes added to the end of the call. So someone watching the public mempool can copy Claus's own registration transaction, add 32 zero bytes, and get it mined first.\n   - The project is registered onchain exactly as the owner signed it, and Claus's own transaction then reverts.\n   - Recovery compares the raw transaction bytes, so it rejects the relay. The job is then blocked permanently.\n   - The registration slot for that owner and project is now taken. Every later attempt fails as \"already registered\", and each one uses up one of the project's 16 lifetime registration attempts.\n   - Confirmed two ways: a Foundry test shows the padded call registers and Claus's exact call then reverts, and a node test shows recovery rejects it.\n   - This is a regression: 0.12.1 matched on the event alone and would have accepted this relay.\n   - Fix: decode the transaction and compare the decoded arguments, or match on the onchain result (the owner's nonce consumed plus the registered project's fields).\n\n2. **Low (`adapters/contract-delivery.mjs:200`): the cancelled-transaction edge case you asked me to check.** A resolve transaction that was signed but not sent can be cancelled purely because its subsidy expired. When that happens the request is marked `closed`, and the worker never retries it. The onchain request is still open. This contradicts the docs' statement that an expired subsidy alone does not close a valid request. It is reachable when the worker crashes or an RPC call fails between preparing and sending. Reproduced with a node test.\n\n3. **Low (`adapters/contract-delivery.mjs:188`): a callback can now stay `pending` forever.** Onchain, a callback has no deadline. But delivery stays tied to the allocation the resolve used. Once that allocation expires before the callback is prepared, every pass returns `pending`. That happens, for example, when callback preparation keeps hitting the cost cap. A newly admitted allocation cannot deliver it, and it never reaches a terminal state, so each such request costs RPC reads on every pass indefinitely. 0.12.1 returned `closed` here. Reproduced with a node test, including after a year of simulated time.\n\n**What I confirmed is fixed**\n- A pending relay no longer records an empty transaction hash.\n- The job commits are now awaited in the chain deployment and approval workers.\n- The recovery scan starts no later than the module deployment, and the finalized read uses the same block it scanned.\n- An intent expires only when finalized chain time passes its deadline. I checked this against the contract's own expiry rule and it holds.\n- A recovery block-hash conflict now stops the job explicitly and keeps its records.\n- An external callback that races a locally cancelled one now waits for finality.\n- Existing signed transactions stay bound to their original allocation.\n\nThe full suite gives 411 pass, 0 fail, 10 skipped, matching your source gate. I did not repeat the two real local-chain runs.\n\n**Retained limits 4, 7, 8 and 9** (shared scan page, 16 lifetime intents, the Mainnet bootstrap fee ceiling, the Sepolia dispatch cap): none of these blocks the bounded launch on its own. One interaction matters: limit 9 makes callbacks that fail on cost common, and those then become the permanent `pending` state in finding 3. Limit 7 also makes finding 1 worse, because each blocked retry uses up one of the 16 attempts.\n\n**Scope B (public Sepolia):** I would not reopen public registration until finding 1 is fix","createdAt":"2026-10-12T16:26:38.963Z","verdict":null,"findings":[{"severity":"medium","title":"Exact-calldata relay check lets any mempool observer permanently block a project's registration by front-running Claus's own registerProject with padded calldata","path":"adapters/chain-executor.mjs","line":92,"description":"0.12.2 adopts a relayed registration only if the relaying transaction's input is byte-identical to the locally encoded registerProject calldata. ClausApprovalExecutor.registerProject is permissionless and Solidity's ABI decoder ignores trailing calldata, so the same signed Registration/configuration/signature can be executed with extra bytes appended (or via any wrapper contract). The resulting registration is exactly the owner's signed intent (same owner, mandateHash, nonce, target), yet recovery throws registered_project_mismatch, which src/chain-deployments/index.mjs:181 now converts to a terminal 'blocked' job in every mode. Because projectKey = keccak(owner, keccak(projectId)) is fixed per owner/project and projects[projectKey].target is now non-zero, every later intent for that project also ends in registered_project_mismatch ('complete empty recovery for an already registered project'), so the project can never obtain a Claus binding for that owner; each retry also consumes one of its 16 lifetime intents. The signed calldata is public as soon as Claus broadcasts its own register:<id> transaction on Sepolia (scope B) or Mainnet (scope C), so the attacker needs no secret: copy calldata, append 32 zero bytes, pay higher priority fee. Under 0.12.1 the event-only match (owner+mandateHash) would have adopted this relay, so this is a regression introduced by the change. The documented 'nested contract relays unsupported' limit is stated as a recovery limitation, not as an attacker-triggerable permanent denial for every public registration. Fix: decode the relay transaction's input with decodeFunctionData and compare the decoded (message, configuration, signature) to the signed values (ignoring trailing bytes), or better, identify the intent by its onchain effect: registrationNonces[owner][nonce] consumed together with the ProjectRegistered event's owner/mandateHash and the finalized projects() tuple (owner, mandateHash, policyVersion, minDelaySeconds) and the target's implementation/configuration, which only the owner's exact signature can produce.","reproduction":"State: queued project-register job; Claus broadcasts register:<id> with calldata D = registerProject(message, configuration, sig). Attacker submits tx to the executor with input D || 0x00*32 and higher tip. Onchain: attacker tx succeeds and registers the exact signed intent; Claus tx reverts (target != 0). Verified with Foundry (contracts/test/scratch/PaddedRelay.t.sol: address(ex).call(bytes.concat(exact, new bytes(32))) returns true, projects(id).target != 0, then the exact call reverts). Off-chain: transact returns reverted -> relayedRegistration finds the single ProjectRegistered log in finalized history with matching owner/mandateHash -> getTransaction().input = D||00..00 != D -> throws registered_project_mismatch -> runOne finishes the job 'blocked'. Expected: state 'verified' with a binding to the registered target. Actual: registered_project_mismatch / blocked; verified with a node test using the registration-recovery fixture with getTransaction returning {to: executor, input: data + '00'.repeat(32)} (decodeFunctionData yields identical args for both inputs). Any new intent for the same project then fails as already-registered.","snippet":"    requireThat(transaction.to?.toLowerCase() === value.executor.toLowerCase() &&\n      transaction.input.toLowerCase() === data.toLowerCase(), 'registered_project_mismatch');","citation":"resolved","blocking":false},{"severity":"low","title":"Cancelled-transaction edge case: a signed-but-unsent resolve cancelled only because its subsidy expired is answered 'closed' (terminal) while the Inbox request is still open","path":"adapters/contract-delivery.mjs","line":200,"description":"0.12.2 distinguishes subsidy expiry from request expiry only when no transaction exists (lines 187-188). When a resolve transaction was already prepared/signed under budget A (intent expiresAt = min(request deadline, A.expiresAt)) but not broadcast, and A expires first, sepolia-transactions.submit cancels it (expiresAt <= now) and line 200 returns 'closed'. runtime/contract-intake-worker.mjs:29 never revisits 'closed', and createContractDelivery (line 55) keeps routing the request to A via the cancelled resolve's budgetId, so the request is permanently abandoned although resolve() would still succeed until the onchain deadline. This contradicts docs/contract-intake.md ('an expired subsidy alone does not close an otherwise valid onchain request'). Not cancelling the signed tx is correct (it must stay bound to A); the defect is the terminal answer. Reachable when a worker crashes, loses its lease, or an RPC call (getChainId in submit) fails between prepare() and submit() and the next successful tick is after A.expiresAt. Fix: in the cancelled branch for action 'resolve', return 'closed' only if chain.deliveryState(request).closed or resolution.expiresAt <= now(); otherwise return 'pending' (and decide explicitly whether a later allocation may prepare a new resolve under a new id, which the router currently forbids).","reproduction":"Fixture from test/contract-delivery.test.mjs: request deadline = budget.expiresAt/1000 + 3600 (stored request updated). Step 1 with getChainId failing on its 2nd call: prepare() writes the signed resolve (state 'prepared'), submit() throws 'rpc down'. Advance clock to budget.expiresAt + 1 (request still open for ~1h). Step 2: submit cancels the transaction (0 broadcasts) and step returns {state:'closed'}. Expected: 'pending' (only the subsidy expired). Actual: 'closed', which the intake worker treats as terminal. Verified with a node scratch test.","snippet":"        return answer(transaction.state === 'cancelled' ? 'closed' : 'failed');","citation":"resolved","blocking":false},{"severity":"low","title":"Callback whose resolve used an expired allocation now stays 'pending' forever: no deadline ends it and the router forbids any newer allocation from delivering it","path":"adapters/contract-delivery.mjs","line":188,"description":"For action 'callback', expiresAt is budget.expiresAt only; ClausRequestInbox.deliverCallback has no deadline, and the 'resolve'-only closure checks (lines 177, 187) never apply. createContractDelivery (lines 53-62) binds the whole delivery, including a callback that has no transaction yet, to the budget recorded on the resolve transaction. Once that budget A expires before the callback transaction is prepared (e.g. preparation keeps failing with transaction_cost_limit, which the retained limit 9 says happens above ~0.04 gwei for 1M-gas callbacks, or resolve finality outlasts A), every tick returns 'pending' with no retryAt, forever: a newly admitted allocation B can never deliver it and the request never becomes terminal. 0.12.1 returned 'closed' here; 0.12.2 replaces a terminal state with an unbounded non-terminal one, so delivery.state never reflects the real outcome and each such request costs chain.resolution plus two deliveryState RPC reads per worker pass indefinitely (accumulating with every affected request). Fix: for the callback action with no transaction and an expired original allocation, either return a distinct terminal state (e.g. 'closed' with reason 'callback_subsidy_expired', since the canonical result stays readable) or allow a currently active allocation to prepare the callback under its own authority, since no existing signed transaction would be replaced.","reproduction":"Fixture from test/contract-delivery.test.mjs with callback=true. Step 1 broadcasts resolve under budget A; confirm(false) (resolved, callback not delivered). Step 2 with estimateFeesPerGas = 1e12 wei throws transaction_cost_limit for the callback (no callback tx written). Admit budget B (same dispatcher/inbox/codeHash, expires 30 days later). Advance to A.expiresAt + 1: make(B).step -> 'pending'. Advance a further 365 days: make(B).step -> 'pending'; still 1 broadcast and 1 transaction. Expected: either delivery under B or a terminal state. Actual: permanent 'pending'. Verified with a node scratch test.","snippet":"        if (expiresAt <= now()) return answer('pending');","citation":"resolved","blocking":false}],"coverage":null}]}