89999 w a critical multisig discovery accepts an indexer supplied address without verifying its derivation
Submitted on Aug 22nd 2026 at 12:00:40 UTC by @bytes for Audit Comp | Quantus
Report ID: #89999
Report Type: Websites & Apps
Report severity: Critical
Target: https://github.com/immunefi-team/audit-comp-quantus-apps/tree/audit-comp-ready/quantus_sdk
Impacts:
Malicious interactions with an already-connected wallet, such as:
Modifying transaction arguments or parameters
Substituting contract addresses
Submitting malicious transactions
Description
Brief/Intro
A Quantus multisig's on-chain address is fully determined by its signers, threshold, and nonce, and the wallet ships that derivation and runs it whenever it creates a multisig. When the wallet instead discovers a multisig that already exists, it accepts the address the indexer reports without recomputing it, even though the discovery record already carries the signers, threshold, and nonce the derivation needs. A compromised or malicious discovery indexer can return a record that lists the victim among the signers, so it appears to be the user's own multisig, while supplying an attacker-controlled address in the address field. The wallet stores, displays, and transacts against that address as the multisig, so funds the user sends to what they believe is their own shared account are sent to an account the attacker controls.
Vulnerability Details
The multisig address is computed as hash(pallet_id, sorted_signers, threshold, nonce). The SDK ships this derivation in quantus_sdk/rust/src/api/multisig.rs: predict_multisig_address (:31) implements it, and a golden-vector test (:76) pins that signers [5Grw..., 5FHn...], threshold 2, nonce 0 derive to qzkvQ2YBa7RrvYKNCXxJw7hJZPBSi6GzZxXGN2fN6DddaH5HJ. The Dart entry point is predictMultisigAddress in quantus_sdk/lib/src/services/multisig_service.dart:73.
When the wallet creates a multisig it uses that derivation and never trusts an externally supplied address: resolveMultisigCreationParams (multisig_service.dart:87) computes the address with predictMultisigAddress at :105.
Discovery does not. discoverForUser (multisig_service.dart:44) reads each record's address directly from record['id'] (:57) and builds a local account with multisigAccountFromIndexerRecord (:64). That mapper takes the address verbatim and stores it as the account's identity, while reading the signers, threshold, and nonce from the same record:
Every input predictMultisigAddress requires is present in this function, yet it is never called, and there is no point on the discovery path where record['id'] is compared against the derivation. The wallet authenticates the address it creates and accepts on trust the address it discovers, using the same data and the same primitive it already holds.
The accepted address is then used as the multisig throughout the app. The discovered account is persisted keyed on it (mobile-app/lib/v2/screens/multisig/discover_multisig_screen.dart:26 calls the accounts provider's add at :35, which stores it via SettingsService.addMultisigAccount, quantus_sdk/lib/src/services/settings_service.dart:269). It is displayed to the user as the multisig's address (discover_multisig_screen.dart:217). It becomes the account identity that later actions transact against: the approve, execute, and cancel confirmation sheets all key on msig.accountId (multisig_approve_confirm_sheet.dart:29, multisig_execute_confirm_sheet.dart:23, multisig_cancel_confirm_sheet.dart:25).
The broad risk of trusting indexer-sourced multisig data is already recognized; the review on PR #509 rates it High. What is specific here is narrower: the multisig address itself is never checked against the derivation the same SDK ships and already uses on the create path, so a hostile indexer controls the one field that determines where value settles while the signer list and threshold it also supplies serve only as reassuring display.
A runnable test that drives the wallet's real discovery flow against a controlled indexer response, and shows discovery returns an address that does not match the derivation for the same inputs, is provided in the proof-of-concept section.
Impact Details
A user who runs discovery against a compromised or malicious indexer can be shown, and can add, an account presented as their own multisig whose address is attacker-controlled. The signer list and threshold rendered in the UI are attacker-supplied display values; the address is the only field that determines where funds settle, and it belongs to an ordinary account the attacker controls. The multiple-approval protection the interface implies does not constrain that account, because it is not a multisig at all.
Any value routed to the substituted address is lost to the attacker: deposits the user makes to their multisig, and funds a counterparty sends after the user shares the address as their shared account. The loss is bounded only by how much value reaches the substituted address before the substitution is noticed, and nothing in the wallet surfaces the discrepancy, because the wallet held the signers, threshold, and nonce needed to detect it and never checked. This is a malicious interaction with an already-connected wallet in which an attacker-controlled address is substituted for the user's multisig, the in-scope impact selected for this report.
The precondition is control of the discovery indexer or its responses. The endpoint is served over HTTPS, so this is indexer compromise, not passive network interception.
References
Derivation and golden vector:
quantus_sdk/rust/src/api/multisig.rs:29,:31,:76Derivation entry point and create path:
quantus_sdk/lib/src/services/multisig_service.dart:73,:87,:105Discovery path and the unchecked mapping:
quantus_sdk/lib/src/services/multisig_service.dart:44,:57,:64,:164,:169,:176Persisted, displayed, and transacted against:
mobile-app/lib/v2/screens/multisig/discover_multisig_screen.dart:26,:35,:217;quantus_sdk/lib/src/services/settings_service.dart:269;mobile-app/lib/v2/screens/multisig/multisig_approve_confirm_sheet.dart:29Public context: PR #509 review
Assets:
quantus_sdkandmobile-app, commitd5fc4d6
Link to Proof of Concept
https://gist.github.com/therealbytes/7d871d6d38c799f0902fd341a00745fd
Proof of Concept
The proof of concept runs the wallet's real discovery flow against a controlled indexer response and shows the flow returns a multisig whose address is the one the indexer supplied, not the address the wallet's own derivation produces from the same signers, threshold, and nonce, and does not reject the record.
What it does
What it asserts
The wallet posted its real discover query to the endpoint (method and query body are checked).
Discovery returns an account whose address equals the attacker-selected address.
That address differs from the derived address for the same signers, threshold, and nonce.
Discovery returns successfully without rejecting the record.
The derived address is not computed by this test. It was established by the wallet's Rust golden-vector test for the same signers, threshold, and nonce, and is used here as the control value.
How to run
Result
The independently run Rust golden-vector test establishing the control address produced:
Was this helpful?