MultiHopper — “Break It Before Users Do” — Submission

Contact handle for follow-up: X/Twitter @hyunh543 · Telegram @hyunh543 · email [email protected] Environment: Solana Devnet — API base https://devnet.multihopper.com/api/v1, RPC https://api.devnet.solana.com. Test-mode key (mh_test_…, redacted). All findings reproduced without touching mainnet or third-party funds. Date of testing: 2026-07-02 (UTC).

What we built / used

Coverage of the suggested testing areas

Suggested testing area Covered Where
Agent understanding of create → prepare → sign/broadcast → confirm-broadcast → monitor ✅ LLM agent (src/agent.ts) + all scripts
Client-side signing, preserving server partial signatures ✅ signing-proof.ts (routeInitTxs[0] server presign preserved)
Strict broadcast sequencing (keeper → route → orchestrator → session) ◑ Partial keeper-funding broadcast tested; Finding “non-uniform blockhashes”. Full route/orchestrator/session ordering not broadcast (needs a devnet SPL-token balance) — noted, not claimed.
Two-step confirm-broadcast + missing keeper signature (MH_039) ◑ Partial confirm-broadcast flow + empty-body error tested; MH_039 not separately triggered
Idempotency-Key behavior, duplicates, conflicting retries, error handling ✅ idem.ts (MH_070/071, dedup) + Finding 2
Expiry & resume after interrupted / partial broadcasts ✅ Finding 2 (expiry) + Finding 1 (crash-window resume)
Status polling, webhooks, recovery paths, failure interpretation ◑ Partial polling (GET /transfers/:id) + recovery (rescue/prepare → MH_081) tested; webhook delivery not tested this round
Documentation gaps / unsafe assumptions / missing examples ✅ Findings 1–3 + supporting doc findings

(Webhook delivery and the full route→orchestrator→session broadcast ordering are the two areas we did not exercise end-to-end this round; flagged here rather than overclaimed.)


Finding 1 — CRITICAL: Crash-window double-funding (resume re-emits keeper funding already confirmed on-chain)

Severity: Critical (direct, repeatable fund loss; refutes a documented safety guarantee) Affected flow/actions: POST /transfers/:id/prepare (resume), the keeper-funding broadcast step, and the documented two-step confirm-broadcast protection.

Steps to reproduce

  1. POST /transfers → POST /transfers/:id/prepare to obtain preparedTxs.keeperFundingTx.
  2. Client-side sign and broadcast the keeper funding transaction; wait for on-chain confirmation.
  3. Do not call confirm-broadcast (simulates a crash / timeout / process restart in the exact window the docs say is protected).
  4. Resume per the documentation: call POST /transfers/:id/prepare again with a new Idempotency-Key.
  5. Observe preparedTxs.resume.keeperAlreadyFunded === false and a re-emitted keeperFundingTx.
  6. Sign and broadcast the re-emitted transaction → it confirms and moves funds again.

Repro scripts: src/doublefund.ts (shows re-emission), src/doublefund2.ts (measures the second debit).