Security review

A network holding other people's money will be attacked, so before launch the code you have deployed was attacked on purpose. What was found, what changed and what held is what stands between your money and the world. The operating rules the review produced are folded into Deploying your own network.

The review ran on 2026-06-13 and covered AIOUM (contracts + offchain), aioum-app and the private node: an adversarial multi-agent review across 6 attack surfaces, every finding re-verified by a skeptic prompted to refute it, then a synthesis. Of 35 raw findings, 21 were confirmed (5 High, 9 Medium, 7 Low) and 14 were refuted.

Verdict

The protocol's money-custody logic is trustworthy — no theft path survived verification, escrow solvency is intact, and the privacy boundary's consent gate works.

Every "theft" candidate (AIOUM-02/03/04/05) resolved to griefing or self-harm with no extractable theft: an operator cannot extract value without the client's voucher, and the client always recovers the unspent deposit.

Fixed (commits a0a8399 offchain, c0139c2 app)

All 13 non-contract findings are fixed and verified: the offchain build is tsc clean, the boundary suites pass 11/11 offchain and 8/8 in the app, next build is green, and the two HIGH private-node fixes were verified live.

IDSevFix
DTB-2 / F1 / PN-04HIGHPrivate-node bearer token now required on every state-touching route (only /health and the OAuth callback /oauth/:provider/callback, which stores the provider's tokens after Google redirects back, are open); loopback-trust fast-path removed (opt-in PRIVATE_NODE_TRUST_LOOPBACK=1, default off). Live: GET /memory no-token → 401.
PN-01 / F5HIGHNo hardcoded aioum-dev-passphrase. Keys derive from PRIVATE_NODE_PASSPHRASE or an auto-generated 0600 .secretkey; compose default removed. Live: memory.json is a ZPB1 encrypted envelope, no plaintext.
F3HIGHhandover calls setArbitrator(DAO) before transferOwnership; deploy requires ARBITRATOR_ADDRESS on a live chain; verify-deploy asserts arbitrator()==Timelock.
F6MEDDeploy requires TREASURY_ADDRESS on a live chain; verify-deploy asserts treasury() matches.
DTB-1MEDuntrusted→remember downgrade enforced at the sink (addMemory trusted flag), not the forgeable executor flag; a /mcp remember can only write note.
DTB-3MEDsensitive taint is sticky through paid hops — re-triggers the egress consent gate at every subsequent paid step (consent to one node ≠ consent to all).
PN-02MEDscrypt N=2¹⁷ + versioned ZPB1 envelope (legacy blobs still decrypt); backup/export passphrase floor raised to 12.
PN-03MEDMagic-header ciphertext detection ends the ~2.3%-of-restarts memory wipe; an undecryptable encrypted store is backed up (.bak), never overwritten.
MP-1MEDSettle loop commits results before settle (commitResults needs Open), so dispute evidence is actually anchored.
MP-2MEDExecutor persists the opened session in the checkpoint before serving the call, so a crash no longer re-opens and double-pays the deposit.
MP-4LOWSettle/commit tx.wait() bounded by timeout; the broadcast hash is recorded durably first, so a tx still pending in the mempool is waited out — never duplicated (NonceManager assigns fresh nonces, so a "retry" cannot same-nonce replace). A dropped tx gets a nonce resync + fee-bumped rebroadcast; a session found settled/reclaimed on-chain is cleaned up instead of retried. One stuck tx can't wedge other sessions.
PN-06LOWOAuth callback validates r.ok/access_token (no longer persists an error JSON as a secret); stamps expires_at; store writes are atomic.
F7LOWMonitor binds 127.0.0.1 by default; non-loopback requires MONITOR_HOST + a MONITOR_SCRAPE_TOKEN that gates /metrics.

Contract-level findings

Five findings concern the rules the chain enforces on every session. Each is bounded to griefing or self-harm; none allows theft.

AIOUM-01 (HIGH) — gas sponsorship is signature-gated

src/aa/AIOUMPaymaster.sol:21-28 — _validatePaymasterUserOp returns ("", 0) with no checks, so anyone pointing paymasterAndData at a funded copy would drain its EntryPoint deposit. That is the local development paymaster. The network's gas sponsor, AIOUMVerifyingPaymaster (src/aa/AIOUMVerifyingPaymaster.sol), sponsors only UserOps carrying a time-boxed signature from sponsorSigner over the op's sender, nonce, initCode, callData and gas fields, the paymaster gas limits, the chain and the paymaster instance, so a sponsorship cannot be replayed for a different op, gas budget, chain or paymaster; rotating the signer voids every outstanding sponsorship. Deploy installs it on a real chain whenever SPONSOR_SIGNER_ADDRESS is set, unfunded (USE_VERIFYING_PAYMASTER=1 selects it on the local development network); the bundler relay (A12) relays only openSessionFor from AIOUMAccount senders with per-epoch gas caps; and verify-deploy refuses a funded accept-everything paymaster on a live chain.

AIOUM-02 (MED) — session-key vouchers are validated at settle time

src/aa/AIOUMAccount.sol:175-197 · src/payments/PaymentManager.sol:356 — a client can revokeSessionKey (or let validUntil lapse) after delivery; isValidSignature then fails, settle reverts, and the client can reclaim() the deposit. Exposure is one session's unsettled balance: the operator loses payment for calls it served, never funds. Griefing, not theft. Fixed: openedBy[sessionId] is set only for a then-valid key, and isValidSignature accepts that key's vouchers for that session whatever happens to the key afterwards.

AIOUM-03 (LOW) — Permit2 / EIP-3009 authorization is not bound to serviceId

src/payments/PaymentManager.sol:227-263 — a relayer can lock a victim's captured authorization into a service of its choosing. The deposit stays the client's and comes back through reclaim() after the inactivity timeout. Griefing, no theft.

AIOUM-04 (LOW) — session-key spent is consumed during validation

src/aa/AIOUMAccount.sol:153-158 — the budget decrements during validation and is not refunded if the open reverts, so spendCap is a lifetime allowance: a reverted open consumes part of it, and the owner raises it again with authorizeSessionKey (any cap ≥ spent). No funds move.

AIOUM-05 (LOW) — an operator can unstake to zero while sessions are open

src/staking/StakingManager.sol:242-258 · src/payments/PaymentManager.sol:449,500-516 — settlement reverts when a delegator share is owed but total stake is 0. Self-harm only: the operator bricks its own settlement; the client's deposit remains reclaimable. Listing requires active stake, and an operator keeps stake above zero while sessions are open.

What held under attack (probed, not re-litigated)

Escrow solvency and fund custody held: deposits are escrowed under the real client, spent only advances inside _settle behind a valid voucher, reclaim() refunds deposited − spent after the timeout, and the four StdInvariants (per-asset solvency, no-overpay, split-conservation, active-count integrity) pass. Signature and voucher binding held: vouchers are bound to the client and verified via SignatureChecker/ERC-1271; AIOUM-02 is over-strict re-validation, a liveness property, not forgery. The privacy boundary held: sensitive memory never leaves the node without an explicit per-step allowContextEgress consent gate, and the taint fixes (DTB-1 downgrade-on-write, DTB-3 consent-laundering) never touched that first consent. Remote exposure held: the private node is not internet-reachable by default (compose publishes 127.0.0.1:8770), and the now-closed loopback bypass required a local foothold.

Operating rules that follow from the review

Four rules, each a line on the launch checklist. Gas sponsorship runs through AIOUMVerifyingPaymaster with its sponsor signer, and the deployer, governance and relayer keys live in a KMS, never as EOAs on a laptop. Governance keys sit in the Timelock/DAO: after npm run handover, npm run verify-deploy confirms owner(), arbitrator() and treasury() resolve to governance on-chain before anything is funded. The private node is single-tenant (or socket-gated); its trust model assumes an unshared host. Operators use strong passphrases and protect their backup blobs; the export passphrase floor is 12 characters.

The full playbook is Deploying your own network.

Next: Testing guide — the checks, static and live, that keep this record true on every change.