# Audit report

> Independently assess only the 0.12.3 delta against baseline 1f6f6d7ebb30dab9e14edda5a34da20f74e565e8 in this repository. Reuse the completed prior reports, especially job d80d33c1-afdd-48fc-96bc-88570c9d7a48 at https://api.imd.fun/jobs/d80d33c1-afdd-48fc-96bc-88570c9d7a48/submissions and report.md. Its findings 1 and 2 block public B and prepared C; 3 through 7 concern recovery and delivery liveness. Report remaining material defects honestly and distinguish them from already accepted bounded limits. Do not re-audit unrelated unchanged code.
>
> B means finite sponsored reviews and Sepolia noncustodial readonly projects, true Claus decisions and the bounded Inbox/callback flow. C means preparation of the same readonly mainnet profile only, requiring a separate exact human approval. No public B or C activation has occurred. No Solidity, authority, financial ceilings, custody, inference, token launch or signing algorithm changed.
>
> Finding 1: direct relays are decoded and re-encoded before comparison, so ABI-equivalent trailing bytes are accepted while changed signed arguments are rejected. Both real local EVM integrations now mine the padded calldata before the worker recovers it. Nested contract relays remain explicitly unsupported.
> Finding 2: all post-registration project/target state reads use the receipt block number; a missing/mismatched state response there uses retryable registration_state_unavailable, separate from definitive relay conflicts. The original job remains recoverable.
> Findings 3/4: failed sponsored delivery is distinct from a closed onchain request. Expired subsidy with no unsent transaction returns delivery failed / dispatch_subsidy_expired. A cancelled unsent transaction returns delivery failed / dispatch_cancelled unless actual resolve deadline or chain state proves closure. The request result and original transaction history remain intact; no replacement spend or rebinding to another budget. An external callback seen but not final still waits for finality. Worker persists those bounded reason codes, stops failed attempts and backs off ordinary pending delivery by one minute. Users can still read a finalized canonical result or externally deliver its callback; a stopped sponsorship is explicitly not promised automatic recovery.
> Finding 5: registration event recovery uses a new cursor namespace while retaining old journals. Before committing an empty page it checks finalized project state at the page end and before its start. A registration transition in an allegedly empty page returns retryable recovery_logs_inconsistent without advancing the cursor. A project that already existed before the eligible module history remains a conflict. No cursor is deleted.
> Finding 6: an unsent expired registration is not terminal merely from local time when finalized chain time is still before its expiry; recovery waits without signing another transaction.
> Finding 7: late submission of an expired prepared intent is allowed to queue original-signature recovery. Current API authority and one-active-intent limits still apply; the worker cannot freshly broadcast the expired signature.
>
> Regressions exercise all these cases. Full source gate: 419 pass, 10 optional environment skips. Real local chain1 and Sepolia padded-relay flows pass. Prior 32-event shared scan fairness, 16 lifetime intents, fee ceilings and small callback budget are unchanged and documented; the previous judge accepted these as non-blocking limits. Please provide separate B and C scope conclusions for this final bounded candidate, with any remaining findings. Test evidence is operator evidence, never a claim of independent acceptance. Nothing in a successful assessment authorizes mainnet activation.

| | |
|---|---|
| Repository | https://github.com/contractclaus/claus-network |
| Commit | `2b31b6674677383e6d5bd2fb0a2e02e1a150ec8b` |
| Job | `e340d6dd-adb4-44ef-b4de-fea90abf2620` |
| Judged | 2026-10-12 17:47 UTC |
| Findings | 1 medium · 5 low · 1 info |

Four agents audited the code as it is at `2b31b66`, each in one area (math, permissions, economics, control flow),
and a judge reproduced, merged and ranked what they found, then read the code once more itself. Nothing in the repository was changed or deployed.

## Findings

### 1. Medium: Relay recovery and the pinned post-registration reads need historical state at old blocks, which the hard-wired Sepolia endpoint does not serve beyond about 10,000 blocks, so a relayed registration fo

`adapters/chain-executor.mjs:74`

```
        if (toBlock < deployed || /^0x0{40}$/i.test((await projectAt(toBlock))[1])) return;
```

Merged from audit_math (medium), audit_flow (medium), audit_permissions (info) and audit_economics (info); one mechanism, reproduced against the code and against the live endpoint. The 0.12.3 delta adds three state reads at fixed historical blocks on the relayed-registration path: validateEmpty reads projects() at the end of every empty log page whose end is at or after the module receipt block (line 74) and, when inconsistent, getCode/projects() at the block before the page (lines 77-78); and the Finding 2 fix pins the post-registration projects()/executor()/projectId() reads to the relay receipt block (lines 158-167). The executor worker talks to one hard-wired endpoint, https://ethereum-sepolia-rpc.publicnode.com with retryCount 0 (runtime/chain-worker.mjs:28). Measured on 2026-10-12 at head 11,900,395 against the active executor 0x01a8d2e011dcb3fa65395e0343e1ab73d562b0f6: eth_call projects(bytes32), eth_getCode and a 10,000-block eth_getLogs range all succeed at depths 100, 1,000, 3,000 and 8,000 and all answer error 4444 "pruned history unavailable: requested N, earliest available 11,890,xxx" at depths 11,000, 12,000 and 25,000 (4 of 4 calls each). viem rethrows that as an error whose .code is 4444 (not a string the job runner knows), so src/chain-deployments/index.mjs:189 records waiting/chain_execution_unavailable and retries every 60 s with no bound, the cursor is never written (validateEmpty runs before the cursor commit in adapters/chain-event-recovery.mjs:15-21), and the registration row stays queued/registering so prepareRegistration keeps answering registration_in_progress for the project. relayedRegistration is entered exactly in the cases 0.12.3 set out to recover: the owner's direct relay, the padded front-run that Finding 1 was fixed for (Claus's own transaction reverts and line 152 falls into the scan), and the late submission of an expired intent allowed by Finding 7. The scan starts at the module receipt block (line 72), so once a verified module is more than about 20,000 blocks (about 2.8 days) old, the first page [deployed, deployed+9999] ends deeper than the retention window and line 74 throws on every tick even when the relay itself is recent. The log fetch for that page already depended on the endpoint's log retention under the baseline (measured 4444 for logs at the same depths; audit_math saw logs served intermittently at depth), but the baseline committed an empty page without any state read and never read state older than latest; the delta makes the dependency deterministic and extends it to the receipt-pinned reads, which fail for any relay mined more than about 10,000 blocks before the worker processes it (late submission of an expired intent, the Finding 7 case, or a worker outage after Claus's own confirmed registration). Consequences: for the attacker-triggered padded-relay case this restores the original Finding 1 outcome (a permanently unbindable project for the price of one front-run registration) whenever the victim's module is more than about a day old, which is the ordinary case for a reviewed module; for the honest relay and late-submission cases the owner is locked out of a new intent while the one onchain is never bound. No spend, custody or authority effect. The same code serves chain id 1 through CLAUS_MAINNET_RPC_URL, so the C profile needs the same guarantee. Fix (minimal): make the executor RPC URL a configured value, require an endpoint that serves eth_call, eth_getCode and eth_getLogs at arbitrary historical blocks for the Sepolia worker and the C checklist, and map pruned/historical-state errors (code 4444, -32000 "missing trie node") to an explicit diagnosable job error rather than a silent 60 s loop. Fix (structural): start the relay scan at the intent observedBlock rather than the module block, detect a pre-existing registration with one projects() read at observedBlock (non-zero target there is the terminal conflict the delta wants to keep), and for the receipt-pinned read

**Reproduction**

Scratch test test/scratch/judge-registration.test.mjs (node --test test/scratch/judge-registration.test.mjs), fixture copied from test/registration-recovery.test.mjs with a mock client that answers readContract/getCode/getLogs below latest-10,000 with Error("pruned history unavailable ...") code 4444, as the live endpoint does. (1) Control, archive mode: module receipt 100000, observedBlock 100020, owner's exact relay mined at 130101, finalized head 130110: four deployment.step calls return registering, registering, registering, verified. (2) Pruned logs and state: the same state rejects with code 4444 on five consecutive ticks, chain-event-recovery stays empty, the only log range ever requested is [100000,109999]. (3) Pruned state, logs served: three ticks reject with 4444, the only state block ever requested is 109999 (the page end), no cursor is written and the same page is fetched three times. (4) Relay mined at 100050 (late submission): the log is found but the pinned projects() read at 100050 rejects with 4444 on every tick and no chain-transactions row is written. Expected in every case: verified, or a diagnosable terminal state. Live evidence: eth_call projects(0x0) on 0x01a8d2e011dcb3fa65395e0343e1ab73d562b0f6, eth_getCode and eth_getLogs over a 10,000-block range at head-11,000 and deeper on https://ethereum-sepolia-rpc.publicnode.com returned {"code":4444,"message":"pruned history unavailable ..."} on 12 of 12 attempts; at head-8,000 and shallower all 12 succeeded.

### 2. Low: validateEmpty reads projects() at a page end where the pinned executor has no code yet, so a relay for a module deployed more than one 10,000-block page before the executor is never recovered

`adapters/chain-executor.mjs:74`

```
        if (toBlock < deployed || /^0x0{40}$/i.test((await projectAt(toBlock))[1])) return;
```

From audit_permissions, reproduced. The guard skips only pages that end before the module receipt block; it does not check that the executor the intent targets exists at the page end. The scan starts at min(module receipt block, observedBlock) and pages by 10,000 blocks. When the module was deployed more than 9,999 blocks before the executor, the first page [moduleBlock, moduleBlock+9999] ends before the executor's creation; eth_call against an address with no code answers 0x and viem's readContract throws ContractFunctionExecutionError ("returned no data") with no .code, which src/chain-deployments/index.mjs:189 maps to a 60 s wait. The same page is scanned on every tick, the cursor is never written (validateEmpty runs before the commit), the exact mined relay is never adopted, and the owner cannot prepare another intent (registration_in_progress). The state is reachable: executor rotation is supported (network-capabilities.retainedExecutors; contracts/DEPLOYMENTS.json lists one retained and one active Sepolia executor, created 1,212 blocks apart, so the deployed set does not trigger it today), prepareRegistration requires only module.chainId === capability.chainId and unchanged module code, and chain.registration() pins the current default executor. Any module deployed about 33 h (Sepolia) or more before a later executor rotation and then registered through a direct relay hits it; the same code serves chain id 1. Baseline 1f6f6d7 committed the page as empty and found the relay on a later page. Liveness only. Fix: in validateEmpty return early when client.getCode({ address: value.executor, blockNumber: toBlock }) is 0x (nothing can have been registered on a page that ends before the executor exists), or treat the zero-data answer from projectAt(toBlock) as "not registered at this block".

**Reproduction**

test/scratch/judge-registration.test.mjs, test "executor created more than one page after the module". State: chain-modules module receipt block 80, intent observedBlock 85, executor code present only from block 20000 (getCode answers 0x below it and readContract("projects") with blockNumber < 20000 throws the viem zero-data error), ProjectRegistered log with matching owner/mandateHash at block 20100 whose transaction input is the exact signed calldata, finalized head 20200. Expected: tick 1 commits [80,10079] as empty, tick 2 commits [10080,20079], tick 3 returns verified. Actual: three ticks reject with "returned no data", chain-event-recovery stays empty and the log range requested is [80,10079] on all three ticks. Control test in the same file with the executor present from block 50 returns registering, registering, verified.

### 3. Low: The empty-page guard was added only to registration recovery; execution recovery still commits an empty finalized page and then reads a consumed nonce as a terminal approval_cancelled

`adapters/chain-executor.mjs:236`

```
        const found = await recoverChainEvent({store,client,chainId,key:'execution:'+value.id,
          fromBlock:input.evidence?.chain?.blockNumber??queued?.receipt?.blockNumber??0,
          query:{address:p.executor,event,args:{projectId:p.projectKey,approvalHash:signed.approvalHash}}});
```

From audit_permissions, reproduced. Prior finding 5 (one empty eth_getLogs page advances the cursor past the event, after which the finalized state read is terminal) was fixed in 0.12.3 for ProjectRegistered recovery by passing validateEmpty. The sibling consumer of recoverChainEvent for UpgradeExecuted (an approval a relayer executed first) passes no validateEmpty. Sequence: executionNonces is true at latest (line 231); the finalized page holding the UpgradeExecuted event comes back empty from a lagging log index; recoverChainEvent writes the cursor to head+1 and returns complete; line 242 reads executionNonces at finalized (true, the real execution consumed it) and line 243 returns blocked/approval_cancelled, which src/approvals/index.mjs:188-189 finishes as a blocked job and a blocked chain-proposals row. The cursor key execution:<id> now starts past the event, so a later tick with a correct log index returns the same answer. The upgrade executed onchain exactly as Claus signed it; the proposal is recorded as cancelled. This is unchanged code, but it is the mechanism the delta set out to close and the fix reached one of the two call sites. The registration guard transfers directly: before committing an empty page that ends at or after the evidence block, read executionNonces at toBlock; if consumed, require it to be consumed at fromBlock-1 as well, otherwise throw a retryable code without advancing the cursor. No spend or authority effect.

**Reproduction**

test/scratch/judge-execution.test.mjs (node --test test/scratch/judge-execution.test.mjs). State: network-capabilities chain enabled with approvalSigner = a generated key; claus-signed-approvals row with a real EIP-712 Approval signature from that key (hashTypedData and recoverTypedDataAddress pass); chain-bindings for the project; artifacts.get returns the proposal with evidence.chain.blockNumber 100; no chain-transactions rows; readContract executionNonces answers true; finalized head 200; UpgradeExecuted log at block 150 that getLogs omits on the first tick and serves afterwards. Actual: the first execution.step returns {state:"blocked", error:"approval_cancelled"} and writes chain-event-recovery nextBlock 201; after the log becomes visible the second step returns the same and the log range [100,200] is never requested again. Control test with the log visible from the start returns executed with the relayed transaction hash.

### 4. Low: The target codeHash written into the write-once verified binding is still read at latest, not at the receipt block, so a lagging getCode persists the hash of empty code permanently

`adapters/chain-executor.mjs:175`

```
          owner: value.message.owner, target, codeHash: keccak256(await client.getCode({ address: target })),
```

From audit_flow, reproduced. 0.12.3 pins every post-registration project/target state read to the receipt block and makes a missing answer the retryable registration_state_unavailable (lines 158-167). The one remaining post-registration chain read, the target's code for binding.codeHash, is taken at latest with no block pin and no non-empty check. The same lagging-backend answer that Finding 2 was fixed for (a load-balanced public node that has not yet applied the registration block) answers 0x for the just-created target; keccak256("0x") = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 is written into chain-bindings, which src/chain-deployments/index.mjs:174-177 writes exactly once and never corrects, and the step still returns verified so the job completes. The value is exported through binding() and portability and is part of the binding digest that src/vaults/index.mjs:19 compares with the operator's vault policy admission (admission.bindingHash === digest(value)), so a later vault admission would be keyed to a hash of empty code. No spend, authority or custody effect; the claim that all post-registration state reads use the receipt block is not complete. Fix: read getCode with blockNumber: tx.receipt.blockNumber (the same registeredRead pin) and requireThat the code is non-empty, throwing registration_state_unavailable otherwise so the job retries instead of persisting.

**Reproduction**

test/scratch/judge-registration.test.mjs, test "a lagging latest getCode for the target persists the hash of empty code in a verified binding". State: the registration-recovery fixture (relay confirmed at block 101, every pinned read answers correctly); only getCode({ address: target }) with no blockNumber answers 0x. Actual: deployment.step returns {state:"verified", binding:{codeHash:"0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"}}, which runOne persists as the project's permanent binding. Expected: the real target code hash, or a retryable registration_state_unavailable.

### 5. Low: A finalized resolve whose callback funding lapsed is recorded as delivery failed without its resolve transaction hash, so the request view cannot be told from a request that was never resolved

`adapters/contract-delivery.mjs:188`

```
        if (expiresAt <= now()) return answer('failed', { reason: 'dispatch_subsidy_expired' });
```

Merged from audit_math and audit_economics (one mechanism), reproduced through the worker. step() handles resolve and then callback. When the resolve is confirmed and finalized (lines 208-211) and the budget has since expired, the callback iteration has no transaction and returns answer("failed", { reason: "dispatch_subsidy_expired" }) at line 188; the same shape is returned at lines 203 and 205 for a cancelled or reverted callback and at line 212 (no reason at all) for a confirmed callback that did not deliver. answer() carries only state, resultHash and reason; the resolve hash is attached only to the delivered answer at line 214, and resolveTransaction is set only after the loop completes. runtime/contract-intake-worker.mjs:63-69 persists exactly that shape, copies transactionHash/finalized only for delivered, and never revisits a failed row (line 29). contract_request_get (src/contract-intake/index.mjs request()) returns the stored delivery object unchanged, and chain-transactions rows are internal. So the public record for a request that IS resolved onchain (immutable result stored in the Inbox) is byte-for-byte the same as for one that was never resolved: { state: "failed", reason: "dispatch_subsidy_expired", resultHash, observedAt }. docs/contract-intake.md:106 promises "the immutable result and original transaction history remain available; a finalized result can be read directly or its callback delivered externally", but the API gives the caller no signal that the resolve exists or which transaction carried it. State-accuracy gap in the bounded Inbox/callback flow, not a spend or authorization defect. Fix: when the resolve action completed in this step or a confirmed contract-delivery:<id>:resolve row exists, include its hash (and a callback-specific reason such as dispatch_callback_subsidy_expired / dispatch_callback_cancelled / dispatch_callback_not_delivered) in the failed answer, and let the worker persist transactionHash for failed callbacks.

**Reproduction**

test/scratch/judge-delivery.test.mjs, first test (node --test test/scratch/judge-delivery.test.mjs; fixture copied from test/contract-delivery.test.mjs with callback = true, observation.chainId set and the request deadline a day past the allocation expiry, driven through createContractIntakeWorker). Tick 1 prepares and broadcasts the resolve under fixture-budget (broadcasts 1); confirm() mines and finalizes it (chain-transactions resolve row state confirmed). Advance now to budget.expiresAt + 1 and tick again. Actual on 2b31b66 (the test fails here): contract-requests.delivery = { state: "failed", reason: "dispatch_subsidy_expired", resultHash, observedAt } with transactionHash undefined while the confirmed resolve row still exists; the second test shows the never-resolved request under the same expired allocation persists exactly the same four keys. Expected: a delivery record that names the finalized resolve transaction.

### 6. Low: Accepted-limit consequence, broader than documented: a request that was never attempted when the pinned allocation lapsed is permanently failed, a replacement allocation never delivers it, and intake

`adapters/contract-delivery.mjs:188`

```
        if (expiresAt <= now()) return answer('failed', { reason: 'dispatch_subsidy_expired' });
```

Merged from audit_permissions (low), audit_flow (info) and audit_economics (info); one mechanism, reproduced. The brief states this behaviour for Findings 3/4 ("expired subsidy with no unsent transaction returns delivery failed / dispatch_subsidy_expired") and the operator test asserts it, so it is intended; it is recorded so the requester decides it knowingly. For the attempt cases (a resolve already signed under the lapsed budget, or a callback bound to the resolve's allocation) the terminal answer is exactly the documented no-replacement rule. For a request that was merely waiting (no transaction row, nothing reserved or signed) the same answer is produced, line 188 runs before the allowSubmit/active check at line 189 (so a paused tick produces it too), and runtime/contract-intake-worker.mjs:29 makes it terminal. createContractDelivery.step rebinds only when an original resolve row exists, so a replacement allocation would deliver such a request normally (control in the reproduction). The hosted worker pins one allocation at startup (runtime/contract-intake-host.mjs:23) and its enabled() is ports.verifyAdmission, which does not read the allocation's expiry, so after it lapses the host keeps scanning, ingesting and deciding public requests (inference cost incurred) and every result_ready row it visits, including those whose results become ready after the expiry, is marked failed within one tick. Baseline 1f6f6d7 left these rows pending and the next allocation delivered them. docs/contract-intake.md:106 describes the attempt case ("ends that sponsored attempt"); it does not say that decisions completed while no allocation is live are never sponsored. No spend, custody or authority effect; the onchain request stays open until its own deadline and the canonical result remains readable, but only the dispatcher can call Inbox.resolve, so the requester cannot self-serve. If the requester keeps this behaviour: state it in the docs, and have verifyAdmission (or the host) refuse to run active with an expired pinned allocation so intake stops admitting work it will never deliver. Otherwise: on the no-transaction path answer pending with the existing retryAt while the request is open onchain and no attempt exists, keeping failed for prepared/cancelled rows and for callbacks bound to a lapsed resolve allocation.

**Reproduction**

test/scratch/judge-delivery.test.mjs, tests 3 and 4. Test 3: advance now to budget.expiresAt + 1 and tick createContractIntakeWorker bound to the lapsed allocation: contract-requests.delivery becomes { state: "failed", reason: "dispatch_subsidy_expired" } with zero chain-transactions rows; admit a replacement allocation (expiresAt one day later) and tick a worker bound to it: broadcasts stays 0 and delivery.step is never called for the row. Control in the same test: stepping the delivery adapter directly with the replacement allocation returns pending, broadcasts 1 and reserves 135000000000 wei on the replacement, proving the request was deliverable. Test 4: the same state ticked with enabled() false (operator pause) also persists failed. Expected (baseline behaviour): pending until an unexpired allocation delivers it before the Inbox deadline.

### 7. Info: Accepted limit, broader trigger than documented: a signed-but-unsent delivery met by one disabled or paused tick becomes a permanent dispatch_cancelled failure while budget, binding and request are al

`adapters/contract-delivery.mjs:203`

```
          return answer('failed', { reason: 'dispatch_cancelled' });
```

From audit_flow, reproduced. This is the cancelled-unsent edge of prior findings 3/4, now labelled honestly as failed/dispatch_cancelled instead of closed, and it is recorded as an accepted bounded limit: the one-attempt-per-request rule (line 47 comment) and docs/contract-intake.md:106 ("cancellation of a signed but unsent delivery ends that sponsored attempt as failed") cover it. What the documentation does not say is that the cancellation is not limited to subsidy expiry or lost authority. sepolia-transactions.submit cancels a prepared row whenever the authorize callback returns false (adapters/sepolia-transactions.mjs:98-107), and the callback at line 166 returns false for !allowSubmit, i.e. whenever the intake host ticks while enabled() is false or throws an admission code (a disabled contract-intake capability during a release rollover is enough). The worker then persists failed and never revisits the row, so a request whose Inbox deadline, binding and budget are all open loses its only sponsored attempt because an operator pause coincided with a crash or RPC failure between prepare and submit. The cancellation itself is correct (never broadcast while unauthorized); only the terminal label is a choice. If pauses should be survivable, the minimal change is to leave the prepared row untouched (answer pending with retryAt) when allowSubmit is false and the intent is unexpired, cancelling only on expiry or an authority denial from active().

**Reproduction**

test/scratch/judge-delivery.test.mjs, last test. State: budget valid for 24 h, request result_ready with an open deadline, binding active. Step 1: delivery.step with allowSubmit true; prepare commits a prepared chain-transactions row, then submit's getChainId fails (mock RPC error), nothing is broadcast. Step 2: tick the worker with enabled() false. Actual: the row becomes cancelled, the reservation is released, contract-requests.delivery = { state: "failed", reason: "dispatch_cancelled" }, and a later tick with enabled() true never re-steps the row (broadcasts stays 0). Expected if pauses are meant to be survivable: pending with retryAt, resumed when the host is re-enabled.

---

Judge's submission `766b07181fb06c65cc9ddfe5621c2db66e24bf7e687f2f49414603e3213566f3`, accepted on the IdentityMD network. Acceptance means the report met the job's checks;
it is not a guarantee that the code has no other defects.
