89806 w a high frozen vesting decode failure leaves an opaque multisig approval actionable and allows a 999 999 quan drain
Submitted on Aug 21st 2026 at 09:56:16 UTC by @legat for Audit Comp | Quantus
Report ID: #89806
Report Type: Websites & Apps
Report severity: High
Target: https://github.com/immunefi-team/audit-comp-quantus-apps/tree/audit-comp-ready/mobile-app
Impacts:
Direct theft of user funds
Malicious interactions with an already-connected wallet, such as:
Modifying transaction arguments or parameters
Substituting contract addresses
Submitting malicious transactions
Description
Summary of the technically proven frozen issue
The frozen chain is runtime spec 143 and registers Vesting at RuntimeCall pallet index 22. The frozen SDK declares bundled spec 136 and its generated RuntimeCall switch has no case 22. Any canonical composite call containing Vesting therefore throws in the official SDK.
The mobile multisig confirmation successfully loads the authoritative storage bytes, catches the semantic decode exception, falls back to generic/non-authoritative top-level information, and leaves Approve/Execute enabled. Its blocked predicate covers byte-loading failure but not the state “bytes loaded, authoritative semantic decode failed.”
The E2E constructs a canonical 49-byte call:
A real 2-of-2 proposal stores the exact bytes. Bob's real signed approval preserves them and crosses threshold. Alice executes. Vesting.claim(u64::MAX) returns the ordinary NoSchedule error, force_batch continues, and the attacker receives 999.999 QUAN from a 1,000 QUAN multisig while the existential deposit remains.
Description
Intended behavior
An actionable multisig review must be bound to the connected runtime and must successfully decode every nested authoritative call/parameter. If the client cannot understand exact stored bytes, it must show a blocking incompatibility error and refuse Approve/Execute.
Frozen actual behavior
The frozen generated RuntimeCall decoder recognizes indices:
and otherwise throws:
The frozen chain composition contains:
CallDecoder.decodeBytes() allows this generated exception to propagate. MultisigProposal.decodedCall catches it and returns null. The authoritative hydration then clears transfer summary details when decodedCall?.summary is absent.
The confirmation does not translate that semantic failure into _callBytesFailed. The relevant gate is:
Therefore successful RPC byte retrieval is treated as sufficient even when those bytes cannot be reviewed.
Exact 49-byte layout
For attacker AccountId A:
Exact continuous reference-run form using Crystal Charlie's documented local-development AccountId:
This is canonical and contains no trailing bytes, alternate compact encoding, or ignored suffix. It is distinct from the older noncanonical trailing-byte trigger.
Root cause
The frozen root crosses three Quantus-controlled boundaries:
Binding drift: chain spec 143 has Vesting/index 22, while the checked-in bundled SDK is spec 136 and omits it.
Error-state loss: the authoritative decoder exception becomes nullable display data rather than an actionable hard error.
Fail-open authorization: the button gate requires successful byte loading but not successful semantic decoding.
Immutable affected source:
Frozen technical risk
Impact
Measured result:
The victim's approval is necessary to cross a genuine threshold, but the official actionable path cannot display any child or material parameter from the exact bytes. The proven frozen-target behavior maps directly to the listed Critical impacts: direct theft and malicious interaction with an already-connected wallet.
The report does not claim that the victim sees a benign recipient or decoded safe transfer. The actual frozen failure is an opaque/generic wrapper with missing children and no blocking warning.
Likelihood
Ordinary member of a normal non-high-security 2-of-2
One 49-byte canonical proposal
Arbitrary real attacker AccountId
Normal proposal fees/deposit and ordinary expiry
No private key theft, malformed payload, indexer corruption, oracle, admin, governance, Technical Collective, hardware wallet, race, or harmful trusted configuration
Attack path
The PoC directly proves steps 1–6 and 8–11 with real signatures/storage/execution/balances. Step 7 is established by the exact frozen mobile source. No device UI automation is claimed.
Preconditions
External
The normal proposal discovery flow surfaces the active proposal.
The victim uses the official software-signer approval flow and chooses Approve.
Internal
Frozen/released vulnerable SDK binding.
Chain runtime containing Vesting index 22.
Normal non-high-security multisig with reducible balance.
No malicious indexer behavior is required; authoritative bytes create the failure.
Affected code and complete flow
Runtime/SDK schema mismatch
quantus_sdk/lib/generated/planck/types/quantus_runtime/runtime_call.dart:100-135
Omits active Vesting index 22 and throws
Bundled-version evidence
quantus_sdk/lib/src/constants/app_constants.dart:60-65
Pins generated review binding to spec 136
Authoritative decoder
quantus_sdk/lib/src/chain/call_decoder.dart:48-59
Propagates unknown-variant failure
Nullable model sink
quantus_sdk/lib/src/models/multisig_proposal.dart:268-274
Converts security-relevant failure to null
Authoritative hydration
quantus_sdk/lib/src/services/multisig_service.dart:293-325
Clears actionable summary fields after null decode
Mobile action gate
mobile-app/lib/v2/screens/multisig/multisig_action_confirm_sheet.dart:156-210,345-408
Catches decode error but blocks only on byte-load failure
Exact-byte approval
quantus_sdk/lib/src/services/multisig_service.dart:395-448
Submits the same concealed bytes
Runtime ground truth
runtime/src/lib.rs:283-287
Registers Vesting at pallet index 22
Threshold/dispatch
pallets/multisig/src/lib.rs:779-825,1123-1166
Counts approval and dispatches as the shared account
Utility.force_batch, Vesting's ordinary error, and Balances transfer_all behave as designed; they are impact carriers rather than separately alleged defects.
Recommended mitigation
Proof of Concept
Proof boundary and topology
The PoC exercises the exact frozen release node/runtime spec 143, Quantus Rust key derivation, SDK signatures, real balances, multisig creation, funding, 49-byte canonical proposal storage, authoritative retrieval, exact index-22 decoder failure, Bob's exact-byte approval, threshold crossing, Alice execution, and final balances.
One exact release node is sufficient. There is no consensus, validator-divergence, network-partition, P2P, or cluster claim. The proof boundary is canonical storage, signatures/threshold, one runtime's deterministic dispatch, frozen SDK/mobile behavior, and balances. A cluster would execute identical call semantics and add no evidence.
Test-local preferences/secure storage only provide documented disposable development mnemonics. No proposal, decoder result, signature, storage value, call result, token, or balance is mocked.
File layout
Place these files in the frozen apps checkout; keep sibling audit-comp-quantus-chain:
Complete Dart E2E PoC
Save as quantus_sdk/test/poc/vesting_fail_open_node_e2e_test.dart:
Complete verifier and runner
Save as poc/quantus-report-2/run.sh and make executable:
Run the frozen-target E2E:
Captured reference node fingerprint
The runner records the complete runtime-version response including its apis array.
Captured reference E2E output
These are real signed create, funding, propose, Bob approve, and Alice execute transaction hashes in order.
Determinism and falsification
The frozen test fails unless the generated SDK throws specifically on active runtime index 22, the runtime accepts/stores exactly 49 bytes, Bob reaches two approvals, execution removes the proposal, the multisig ends below one QUAN at the existential deposit, and recipient gain exactly equals multisig loss above 999 QUAN.
u64::MAX makes the Vesting failure deterministic; force_batch makes continuation deterministic; keep_alive=true eliminates account-reaping/replay. A corrected binding containing Vesting/index 22 is the negative control: the decode-error assertion must fail.
Was this helpful?