← yescrow security review

Published copy of reports/AI pentest, tenth run (2026-11-06).md from yescrow's private repository, unedited apart from local file paths. File and test references point into that repository; the contracts' source is verified on Arbiscan (see the contracts). Download as Markdown.

AI penetration test, tenth run (2026-11-06)

A short targeted step 4 of docs/ai-pentest.md on commit f5e3aab (current main, SPEC v0.37), the fixes for the ninth run (reports/AI pentest, ninth run (2026-11-06).md): area G (off-chain parts) only, scoped to what v0.37 changed in the service, the web app and docker-compose.yml since dc3e837, and whether each of the ninth run's five fixes holds. No Solidity changed (git diff --quiet dc3e837 f5e3aab -- contracts/src), so the contracts were out of scope. One fresh attack agent, with the procedure's prompt word for word and the brief narrowed to the fixes; it was asked to prove every finding on the real local stack (the service's harness: an Anvil fork of Arbitrum Sepolia with real Kleros, a scratch Postgres with the real migrations, the real app and keeper, and the real main.ts as a child process).

The terms of this round (the session's, after the user left the next steps to it on 2026-11-06). Attack only the v0.37 fixes: the sponsored-fee reservations under a lock and the cleanup of a fee reserved but never sent (service/src/sponsor.ts); the keeper's order, its payment of kept answers past the budget in the last third of their fee period, and admins' approvals within the same daily limit (service/src/answers.ts); the relayer's hash at broadcast, the shutdown that finishes the sends in flight, the database pool's error listener, the stop grace period, the rescue ops alert and the reminders for kept answers (relayer.ts, main.ts, db.ts, docker-compose.yml, ops.ts, alerts.ts); and the web app's kept-answer card, fee button, admin page and error texts. If it found nothing above Low, its findings would be fixed with regression tests and the loop closed; a Medium or worse would have been fixed and attacked once more.

Part 1. Summary

Result

No finding, at any severity, and no contract change. All five of the ninth run's fixes hold on the real stack:

One observation, not a finding: when a kept answer's party gets ETH in the last third of the fee period, the keeper records "your wallet has enough ETH to pay the fee" as the reason, so the urgent ops alert reads "the keeper can't pay it past the budget: your wallet has enough ETH…". It is accurate (yescrow leaves the fee to a party that can pay it), the party sees its fee button, and nothing is lost. Left as is.

All findings

None.

ID Finding Attacker Verifier Final Where the fix goes
— none — — — —

Three ideas stayed unconfirmed (in the area report): two service processes overlapping during a deploy (Compose stops the old one first; the lock and the chain checks would still prevent a double payment); whether the keeper's transfer and payout sends abandoned by a crash are reconciled after a restart, as answers and appeal stakes are; and an outsider growing the service's memory to an OOM kill (no mem_limit), which a restart now recovers from.

The regression run

The service's tests on f5e3aab before it was committed (in service/, node --test --test-concurrency=1 --test-timeout=900000 test/*.test.ts, with the database from scripts/test-db.sh): 157 tests, 157 pass, 0 fail, among them the four tests for the ninth run's fixes on the fork and the new alert and ops tests; the web app's unit tests, 84 pass; the 16 light off-chain proofs scripts/check.sh now runs, all pass; and scripts/pentest/ninth/G9_fixed_stop_and_restart.ts, PASS. With the fixes undone one at a time, each of the four fork tests failed. The attack agent ran the four fork tests, the web tests, G_sponsor_drain.ts and the stop-and-restart test again on its copy: all pass.

Limits


Part 2. Area report

Area G: off-chain parts

AI penetration test, tenth run, area G: off-chain parts (commit f5e3aab, the ninth run's fixes)

The attack agent's report, as it wrote it.

A short, targeted step 4 of docs/ai-pentest.md on commit f5e3aab (current main, SPEC v0.37), area G (off-chain), narrowed to the v0.37 fixes for the ninth run's five findings (R9-G-1…R9-G-5) and whether each holds. Work was done only in the exported copy. I read SPEC.md (§5, §8, §12, §14.17), docs/audit.md (§3, §5, §6f), docs/keys.md, the full CHANGES-since-dc3e837.diff, and the changed sources (service/src/sponsor.ts, answers.ts, relayer.ts, main.ts, db.ts, ops.ts, alerts.ts, config.ts, api/app.ts; web/lib/deal-state.ts, web/lib/errors.ts, web/components/deal/DealPage.tsx, steps.ts, web/components/admin/Admin.tsx; docker-compose.yml), plus the escrow fee-payer/authorize paths they depend on. I then ran the v0.37 regression proofs on the real stack (the service's Anvil-fork harness with real Kleros and a scratch Postgres from scripts/test-db.sh, the real main.ts as a child process, and the web pure-function tests). Contracts are unchanged and out of scope.

Result

No finding. No Critical, High, Medium, Low or Info weakness was confirmed. All five of the ninth run's fixes hold on the real stack, and the one residual that remains (R9-G-2's bounded, paid starvation) matches what SPEC §14.17 and docs/audit.md §5 already state. I did not change contracts/src, service/src or web/, and I wrote no new source. Because there was no new finding to prove, I wrote no new PoC under scripts/pentest/tenth/; instead I re-ran the project's own v0.37 regression proofs as the confirmation evidence below.

ID Finding Severity Proof command (from the copy's root)
— none — —

Unconfirmed

Checked and found sound

Each ninth-run fix, whether it holds, and why:

Commands run (from the copy's root; all printed PASS / exit 0): - eval "$(scripts/test-db.sh start)" then, in service/, systemd-run --user --scope -q -p MemoryMax=2500M -p MemorySwapMax=2G -- node --test --test-concurrency=1 --test-timeout=900000 --test-name-pattern="ninth run" test/api.test.ts → 4/4 pass (R9-G-1/2/4/5). - cd web && node --test lib/deal-state.test.ts lib/errors.test.ts → 20/20 pass (R9-G-2 web texts, errors). - cd service && TEST_DATABASE_URL=… node --no-warnings ../scripts/pentest/G_sponsor_drain.ts → exit 0 (R9-G-3 repair). - TEST_DATABASE_URL=… systemd-run --user --scope -q -p MemoryMax=2500M -p MemorySwapMax=2G -- node --no-warnings scripts/pentest/ninth/G9_fixed_stop_and_restart.ts → exit 0, PASS (R9-G-5 end to end).

Not done

The test database was started and stopped; no live system was touched, and no transaction was sent to any network (the Anvil fork read only from the public Arbitrum Sepolia RPC).