# Audit report

> Review exact source bce9c415e337fc1e4ffccd03d3f711907e5d0a63 for the already accepted public noncustodial Claus Network runtime. Prior reviewed runtime c9894f4b586332f5a0b4a9576502a3630d5d9a8b is live. This follow-up changes only adapters/source-verification.mjs to submit constructorArguements alongside constructorArguments, with identical encoded bytes, because Etherscan accepted the existing exact-compiled registration after this compatibility retry. No source, signatures, policy, authority, amount, verification predicate or transaction semantics change. Inspect the diff, the matching source-verification tests and its immediate callers for any real regression. Version is 0.12.6. Existing Solidity source and all prior history/evidence remain exact. Exact Sourcify creation/runtime match plus Etherscan verification remain required. Docs cursor clarification is nonruntime. Do not treat source verification as financial safety or override the independent Claus decision. Public scope remains free projects/services, bounded sponsored reviews and readonly Ethereum/Sepolia contracts; no custody, public inference or token launch. Report concrete actionable findings without speculative expansion.

| | |
|---|---|
| Repository | https://github.com/contractclaus/claus-network |
| Commit | `bce9c415e337fc1e4ffccd03d3f711907e5d0a63` |
| Job | `6c73fa09-6f89-452b-9b64-878a54897795` |
| Judged | 2026-10-12 19:52 UTC |
| Findings | 1 medium · 2 low |

Four agents audited the code as it is at `bce9c41`, 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: Etherscan GUID is never cleared after a terminal check failure, so the 0.12.6 constructorArguements field is never resubmitted for an already-attempted row and the deployment stays 'verifying' forever

`adapters/source-verification.mjs:56`

```
      if (!value.etherscanVerified && !value.etherscanGuid && (!value.etherscanAttemptAt || value.etherscanAttemptAt < now() - 60000)) {
```

Merged from audit_math, audit_flow, audit_permissions and audit_economics (same function, same mechanism, same fix). The verifysourcecode submission is gated on `!value.etherscanGuid`. A GUID is stored when verifysourcecode answers status '1' (line 62) and is never deleted. When checkverifystatus later answers status '0' with a terminal result such as 'Fail - Unable to verify' (line 67), only `etherscanError` is recorded; the GUID remains, so every later `step` call skips submission (line 56), re-polls the dead GUID (lines 65-67) and returns state 'pending'. The 60 s throttle on `etherscanAttemptAt` is unreachable for such a row. Nothing else in the tree writes or resets 'chain-verifications' rows. This is pre-existing in c9894f4 and is not a regression introduced by the diff, but it bounds what the diff achieves: the new `constructorArguements` field at line 60 only travels on a submission, and any Sepolia row whose 0.12.5 attempt was queued by Etherscan and then failed for missing constructor bytes already holds a GUID, so the compatibility retry only helps brand-new addresses or rows whose first POST was refused outright with status '0'. Downstream: adapters/chain-executor.mjs:136, adapters/wallet-executor.mjs:49 and adapters/vault-executor.mjs:50 map any non-'verified' proof to 'verifying'; src/chain-deployments/index.mjs:166-170 then keeps the job 'waiting' with a 30 s retry indefinitely and lines 103/128 refuse a new registration for the project with 'registration_in_progress'; src/vaults/index.mjs:18 refuses vault creation while the policy binding is not 'verified'; integration/mainnet-release.mjs:74 refuses to stage a Mainnet release without a 'verified' proof. The only exit is an out-of-band manual verification on Etherscan that makes getsourcecode return the source. No funds are at risk and the Claus decision is unaffected; this is a liveness gap in the verification state machine. The existing test only exercises 'Pending in queue' and never a terminal check result. Minimal behaviour-preserving fix: at line 67, when checkverifystatus reports a terminal (non pending/queue) failure, keep `etherscanError` and `etherscanAttemptAt` but delete `value.etherscanGuid`, so line 56 resubmits after the existing 60 s backoff with both field spellings; optionally bound the number of resubmissions with a reportable terminal error. Add a test with a terminal checkverifystatus result asserting a second verifysourcecode POST.

**Reproduction**

Run against this tree with an in-memory store (no network): fetcher answers the Sourcify POST with {verificationId:'v1'}, the Sourcify GET with {runtimeMatch:'exact_match', creationMatch:'exact_match'}, getsourcecode with {status:'1', result:[{SourceCode:'', ContractName:'', SimilarMatch:''}]}, verifysourcecode with {status:'1', result:'guid-N'} (counting N), and checkverifystatus with {status:'0', result:'Fail - Unable to verify'}. Input: address 0x11..11 (20 bytes), creationTransaction 0x22..22 (32 bytes), constructorArguments '0x1234', artifact {entry:'src/Example.sol', contract:'Example', profile:{compiler:'0.8.27+commit.40a35a09.Emscripten.clang'}, standardInput:{language:'Solidity', sources:{}}}. Call step(input) once, then advance the injected clock by 10 minutes and call step(input) ten more times. Expected: after the terminal failure at least one further verifysourcecode POST (now carrying constructorArguements) or a terminal state. Actual: verifysourcecode called exactly 1 time, checkverifystatus called 11 times, etherscanGuid stays 'guid-1', etherscanError stays 'Fail - Unable to verify', state stays 'pending'. Saved as test/scratch/stuck-guid.test.mjs; `node --test test/scratch/stuck-guid.test.mjs` fails on this tree with 'expected a resubmission after a terminal failure, got 1'.

### 2. Low: Sourcify POST body is JSON.parsed before the HTTP status is checked, so a non-JSON 5xx body throws an uncoded SyntaxError instead of verification_service_unavailable

`adapters/source-verification.mjs:43`

```
        const result = JSON.parse(await boundedResponse(response));
        requireThat(response.ok || response.status === 409, 'verification_service_unavailable');
```

Line 43 parses the Sourcify verify response as JSON before line 44 checks `response.ok || status === 409`. The Etherscan helper `request` (lines 7-11) checks `response.ok` first and so does not have this problem. When Sourcify or a proxy in front of it returns a 502/503 with an HTML or empty-text body, JSON.parse throws a SyntaxError with no `.code`. Callers fall back to a generic code: src/chain-deployments/index.mjs:190 records `error.code ?? 'chain_execution_unavailable'`, and the wallet and vault loops have equivalent fallbacks. The retry still happens, but the recorded reason is wrong and the intended `verification_service_unavailable` branch is unreachable for this input, which misleads operators diagnosing a stalled Sepolia deployment. Pre-existing in c9894f4; unchanged by this diff. Minimal fix: move the `requireThat(response.ok || response.status === 409, 'verification_service_unavailable')` above the parse, or wrap the parse and rethrow with that code.

**Reproduction**

Run against this tree with an in-memory store: fetcher = async () => new Response('<html>502 Bad Gateway</html>', { status: 502 }); call step with address 0x11..11, creationTransaction 0x22..22, constructorArguments '0x1234' and the Example artifact described above. Expected: rejection whose code is 'verification_service_unavailable'. Actual: rejection is SyntaxError, message "Unexpected token '<', \"<html>502 \"... is not valid JSON", code undefined, thrown from adapters/source-verification.mjs:43. Covered by the second test in test/scratch/stuck-guid.test.mjs, which fails on this tree.

### 3. Low: Packaged OpenAPI schema and API reference still describe 0.12.5 after the 0.12.6 version bump

`docs/openapi.json:5`

```
    "version": "0.12.5"
```

This commit bumps API_VERSION in interfaces/contract.mjs and package.json to 0.12.6 but did not run `npm run docs:generate`. docs/openapi.json (info.version 0.12.5) and docs/api-reference.md line 3 ('This source describes API 0.12.5 with 105 operations') are generated from that constant by scripts/generate-docs.mjs and now disagree with the live /openapi.json route and the MCP serverInfo, which report 0.12.6. At c9894f4 the packaged files matched the source. Non-runtime; no behaviour change and no effect on verification or signing. Fix: run `node scripts/generate-docs.mjs` and commit docs/openapi.json and docs/api-reference.md (only the version string changes; the 105-operation table is unchanged).

**Reproduction**

From the repository root: `node -e "import('./interfaces/contract.mjs').then(m=>console.log(m.openapi({origin:'https://claus-network-api.vercel.app'}).info.version))"` prints 0.12.6; `node -e "console.log(require('./docs/openapi.json').info.version)"` prints 0.12.5. `node scripts/generate-docs.mjs && git diff --stat` shows docs/api-reference.md and docs/openapi.json each modified by one line. Expected: packaged schema version equals API_VERSION as it did at c9894f4. Actual: 0.12.5 vs 0.12.6.

---

Judge's submission `c90cafff1dc2df97523bb1cb825def3e1461f1f56682893560e85373dd273e1b`, 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.
