Bounty: Break It Before Users Do — MultiHopper Agentic Flow Bugs & Fixes (Superteam Earn)

What I built / used: A small Python harness (requests, base58, solders, python-dotenv) driving the live devnet API through the full agentic transfer lifecycle (quote → prepare → confirm-broadcast), webhook registration and delivery, real on-chain Solana transaction signing/broadcasting, and authorization boundaries. Environment: Solana devnet only, mh_test_-prefixed API key. No mainnet endpoints or live user funds were touched at any point.

Repository: private (available on request — see contact below)

Contact: @takumixbt on X / @takumixbt on Telegram


Summary

Severity Count
Critical 1
High 4
Medium 15
Low 3
Documentation Blocker 3

One additional low-severity observation, F7, is noted separately at the end and isn't counted in this table since it's most likely an intended limitation of the devnet test environment rather than a real bug.

The two I'd flag as most interesting: premium-tier transfers get permanently stuck once quoted and can never be prepared (F3), and the fee tier itself is decided by a client-supplied, unvalidated decimals value that silently overcharges native-SOL transfers by ~10x and lets a malicious integrator evade protocol fees on purpose (F20), which also turns out to be the root cause of F3's "every transfer landed in premium tier" observation. Following F20's fix to its logical conclusion, forcing the correct decimals value gets a transfer past prepare, but that only exposes a second, stranger bug: the transfer then silently hangs forever, indistinguishable from a healthy one (F22). It turns out completing it actually requires the client to sign and broadcast five raw Solana transactions that are never documented or surfaced anywhere in the API, which I confirmed the hard way by driving one transfer through that undocumented flow to an actual status=completed, the only one in this whole engagement (F25). Actually doing that broadcast turns out to have its own sharp edges: one of the five transactions comes back partially pre-signed by the server, and a naive client that rebuilds its signature array clobbers that signature and fails on-chain (F29); broadcasting them out of the documented order can permanently brick a transfer within that prepared set (F30); and an agent that crashes mid-broadcast can resume, but only if it knows an undocumented trick, and the API's own "is the keeper funded" resume hint is unreliable (F31). /transfers/estimate separately understates both what the recipient will receive (F17) and what the sender needs to fund upfront (F19).


Finding Index

ID Severity Title
F1 Critical Webhook SSRF (internal and private-network URLs accepted)
F3 High Transfers get stuck permanently after quoting
F17 High /transfers/estimate understates the recipient's payout
F20 High Fee tier decided by client-supplied tokenDecimals
F22 High Standard tier gets past F3, straight into a silent dead end
F19 Medium sol.breakdown omits a real cost from its own total
F23 Medium prepare is non-idempotent and un-phase-gated
F28 Medium Repeated prepare mints genuinely independent on-chain accounts
F29 Medium preparedTxs includes a server-partial-signed transaction
F30 Medium Broadcast ordering not gated; out-of-order broadcast bricks a transfer
F31 Medium Resume after interrupted broadcast works but is fragile
F2 Medium No way to cancel a transfer
F14 Medium Empty events: [] on a webhook silently means "everything"
F24 Medium Webhook delivery retries once then drops the event forever
F9 Medium A raw DB error leaked schema details on a 500
F26 Medium rescue/confirm reproducibly returns a raw 504
F11 Medium amountRaw and amountTokens aren't cross-checked
F12 Medium Program addresses accepted as transfer recipients
F13 Medium GET /usage always 500s
F21 Medium Rate-limit state completely invisible, even on the 429 itself
F4 Low Raw Fastify errors leak instead of the MH error schema
F18 Low Idempotency-Key collisions cross endpoints
F27 Low progress.hopsTotal isn't stable or monotonic
F25 Documentation Blocker Standard-tier transfers complete only via an undocumented broadcast flow
F5 Documentation Blocker DELETE silently requires an Idempotency-Key
F6 Documentation Blocker Undocumented 5-webhook-per-key cap
F7 Note (unscored) Devnet SPL transfers don't work at all

Critical

F1: Webhook SSRF (internal and private-network URLs accepted)

URL Result
http://localhost:8080/webhook accepted
http://127.0.0.1:8080/webhook accepted
http://10.0.0.1:8080/webhook accepted
https://[email protected]/ (credentials embedded) accepted
https://webhook.site/ accepted

Impact: Anyone who can register a webhook (their own account, or a compromised upstream integration) can point MultiHopper's server-side requests at localhost, an internal RFC 1918 range, or a URL with embedded credentials. That's internal service probing, private network reconnaissance, or a credential-injection attack, all launched from MultiHopper's own infrastructure rather than the attacker's.

Proposed fix: Validate the URL at registration (reject non-HTTPS, embedded credentials, and any hostname that resolves to a private or loopback address) and re-validate on updates, since DNS can be repointed after the fact.


High