89151 w a high primary only utility if else approval summary shows 1 quan while a 50 quan fallback drains the multisig
Submitted on Aug 17th 2026 at 14:25:34 UTC by @sl1de for Audit Comp | Quantus
Report ID: #89151
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
Brief/Intro
The mobile multisig approval screen can present a validly encoded and fully decodable Utility.if_else proposal with a prominent headline of 1 QUAN and a valid beneficiary even though the exact proposal contains a fallback that immediately transfers 50 QUAN to that same beneficiary. When the victim has enabled the normal Hide Balances setting, both nested branch amounts are replaced with -----, while the unmasked 1 QUAN headline remains and Yes, Approve stays enabled.
The attacker makes the primary 1 QUAN call fail deterministically by requesting a one-block reversible-transfer delay when the runtime minimum is two blocks. Utility.if_else then executes the 50 QUAN fallback. I verified this in an ordinary 2-of-2 multisig using the real Android approval flow and LocalAuth: the honest signer approved the exact 100-byte proposal shown by the app, after which the malicious signer executed it and drained the multisig from 50 QUAN to zero.
Vulnerability Details
Attack scenario
The verified scenario uses an ordinary 2-of-2 multisig:
Alice is an existing but malicious signer. Alice has one approval and cannot execute alone.
Bob is the honest second signer. Bob's software-wallet account is stored in the mobile app.
The multisig holds 50 QUAN.
The beneficiary is controlled by Alice.
Bob has enabled Hide Balances as a normal local privacy preference.
Alice proposes the following validly encoded and fully decodable call:
Both branches use the same beneficiary. The exact 100-byte SCALE payload used in the Android and current-runtime verification was:
Payload SHA-256:
The payload is not malformed. The mobile SDK and current runtime metadata decode both branches completely and re-encode them to the same 100 bytes.
Root cause
The decoder correctly creates separate transfer summaries for the two nested calls:
The primary summary contains 1 QUAN and the beneficiary.
The fallback summary contains 50 QUAN and the same beneficiary.
However, the Utility.if_else handler assigns the outer call only the primary call's summary:
The approval sheet builds its prominent headline from this outer summary. It therefore describes the proposal as a 1 QUAN transfer to the beneficiary even though either branch can be the economic result.
The nested call tree is rendered separately. Every native-token AmountField is passed through the global transaction amount display provider:
When Hide Balances is enabled, that provider replaces both token amounts with -----. This hides the 50 QUAN fallback amount but does not hide or qualify the primary-only 1 QUAN headline.
The final confirmation therefore shows:
The fallback call name and beneficiary remain visible. The security problem is that its exact amount is hidden while the screen affirmatively summarizes the transaction as 1 QUAN. Even after reviewing the nested fallback section, Bob cannot verify its amount from this screen.
This is also inconsistent with the application's other authorization screens. The normal send review and multisig proposal-creation review preserve the exact transaction amount even when balance privacy is enabled. The privacy preference is therefore being applied to a security-critical approval argument in this decoded-call tree, rather than merely concealing passive balance information.
The confirmation blocker only considers submission state, call-byte loading, and call-byte fetch failure:
A conditional call whose headline ignores one possible outcome, or whose authorization amounts are hidden, does not enter a blocked state. After Bob completes LocalAuth, _confirm passes the unchanged authoritative proposal bytes to the normal approval submission path.
This does not depend on a malicious indexer or RPC. The approval sheet obtains the proposal bytes from Multisig.Proposals chain storage. The chain then requires Bob's submitted inner call bytes to match the stored proposal byte for byte.
Why the fallback executes deterministically
The primary call requests BlockNumber(1). The current runtime defines the minimum reversible-transfer delay as two blocks:
schedule_transfer_with_delay validates the delay before scheduling a transfer. A one-block delay therefore returns DelayTooShort before the 1 QUAN transfer changes economic state.
Utility.if_else executes its fallback whenever the primary call returns an error. The fallback is a valid Balances.transfer_allow_death of 50 QUAN, so it succeeds and the outer if_else call returns success.
The resulting chain behavior is:
What the victim sees
On the same installed APK and the same stored proposal, the visible-state control showed:
Headline:
1 QUANPrimary amount:
-1 QUANFallback amount:
-50 QUANThe same beneficiary in the headline and both branches
With Hide Balances enabled, the production confirmation instead showed:
You are about to approve a transfer ofAn unmasked
1 QUANheadlineThe same valid beneficiary
Utility · if_elseReversibleTransfers · schedule_transfer_with_delay, delay1 blocks, and amount-----Balances · transfer_allow_deathand amount-----An enabled
Yes, Approvebutton
The fallback call name and beneficiary are visible, but its exact 50 QUAN amount is not. There is no warning that the primary is invalid or that the fallback will transfer fifty times the headline amount.
The Android API 36 reproduction mounted the audited production multisig proposal components, CallDecoder, nested call renderer, privacy provider, software-wallet signer, LocalAuth, transaction submission service, and SDK against a private spec-136 chain.
The local GraphQL fixture supplied only neutral proposal-discovery metadata. It was read-only, could not sign, and did not connect to RPC. The production SDK independently replaced its empty call bytes with the authoritative 100-byte proposal from local chain storage before display and approval.
Verified chain result
Run ID
if-else-api36-20260817-02
Android environment
API 36 evidence build mounting the production approval and signing components
Runtime
Spec 136, transaction version 3
Multisig
Ordinary 2-of-2, Alice initially at 1 of 2 approvals
Multisig address
qznR3ccVoLuGz2McYD2mr3n1mzyTjSSesp65jicUEzc9KzWFE
Beneficiary
qzntBpmqHZF1jxC8KJKpuxcYuHST892jyXBqRctpAxd1WQ9BL
Payload
Fully decoded, exactly 100 bytes
Payload SHA-256
0x425c2afad5639ea8329380333ee7a512a26782d01488385e6326e4af1d5d32fe
Starting multisig balance
50000000000000 base units, 50 QUAN
Bob approval
0xe647a0a05a9ea0770bb79baeaa3e2731e8a8eae44b42689778bed68ac85f7a4e
Approval inclusion
Block 208, extrinsic 1, block 0x5242fd11340475a975c1366c31446730cd5d4b0b31b7cc36567284ca6d3cf5e4
Alice execution
0xef731b0ae0449699dbbab9f138e0c968ba327dc8daa857e843ff8d342222df66
Execution inclusion
Block 318, extrinsic 1, block 0x2f1f3205e8fa1ad9e9c6f8d576aff2aeab29e1bc06edbcfdf057320f3eab6397
Primary result
ReversibleTransfers.DelayTooShort
Utility result
Utility.IfElseFallbackCalled
Transfer
Exactly 50000000000000 base units to the beneficiary
Ending multisig balance
0
Beneficiary gain
50000000000000 base units, exactly 50 QUAN
Final proposal state
Executed and removed
Bob completed the genuine Android device-credential prompt. Independent decoding located the app-reported approval in block 208, recomputed its hash, identified Bob as the signer, decoded Multisig.approve, and matched its inner call to the stored 100-byte payload. Alice's execution then emitted the exact 50 QUAN Balances.Transfer, Utility.IfElseFallbackCalled, Multisig.ProposalExecuted, and phase-scoped System.ExtrinsicSuccess.
Current runtime confirmation
I separately reproduced the same exact payload against the current audit-chain source at commit 3b243b870a5442a6e5f443056f67bbba87d472f5, runtime spec 143 and transaction version 3.
The current-runtime run confirmed:
The 100-byte call fully decodes as
Utility.if_else.The ordinary multisig begins below threshold with only Alice's approval.
Bob's exact approval is
0xe6dbfaeb13293002a386856d3881a0bf2ecbee8ea4d465181684d9528a32673f.Alice's execution is
0xf7ef6cc1e8e678daaa23c599a44c487293f715990e45dcb00640167bafa1fc51.The primary call fails with
ReversibleTransfers.DelayTooShort.IfElseFallbackCalledis emitted.The fallback transfers exactly 50 QUAN.
The multisig balance changes from 50 QUAN to zero.
The beneficiary gains exactly 50 QUAN.
The proposal is removed and the outer execution succeeds.
The same payload was tested against a high-security multisig and was rejected. The demonstrated vulnerable path is the ordinary multisig approval flow.
Preconditions and boundaries
The target is a funded ordinary multisig.
The attacker is a legitimate signer but controls fewer approvals than the threshold.
The victim uses a locally held software-wallet account and completes the normal approval interaction.
The victim has already enabled the user-facing Hide Balances feature. The attacker cannot enable it remotely.
The fallback call name, beneficiary, and amount placeholder remain visible.
No private key theft, physical device access, malicious RPC, compromised indexer, compromised backend, altered call bytes, or attacker-controlled quorum is required.
The attacker and victim need enough funds for normal proposal, approval, and execution fees.
I do not claim this payload bypasses high-security multisig filtering or a Keystone hardware-wallet display.
Controls that work but do not stop this exploit
Failure to fetch proposal bytes sets
_callBytesFailedand disables approval. This payload supplies valid authoritative bytes, so the fetch succeeds.Full recursive decoding succeeds. There is no decoder error for a fail-closed patch to catch.
Approval byte equality is enforced. This proves Bob approved the misleadingly displayed bytes, but it does not make the semantic presentation accurate.
With Hide Balances disabled, the same screen reveals both branch amounts. The vulnerable state is the normal privacy-enabled configuration.
Android LocalAuth prevents submission when authentication fails. Completing the normal prompt still submits the exact misleadingly presented call.
High-security multisig policy rejects this call. The verified target is an ordinary multisig.
Impact Details
This maps directly to both selected Critical impacts.
First, it enables direct theft of user funds. In the verified execution, the multisig held 50 QUAN and the fallback transferred all 50 QUAN to the attacker-controlled beneficiary. The multisig ended with a zero balance. The technique is not limited to the demonstrated amount. The fallback can request any chosen amount available to the ordinary multisig.
Second, it causes a malicious interaction with an already-connected wallet. Bob's software wallet approves exact attacker-chosen transaction arguments containing a 50 QUAN fallback while the confirmation headline affirmatively states 1 QUAN. The transaction is not presented as zero, blank, or undecodable. It shows a plausible nonzero amount and a valid beneficiary, and both branches use that same beneficiary.
The relevant preconditions are:
The target is a funded ordinary multisig.
The attacker is already a legitimate signer but remains below the approval threshold.
The victim has enabled Hide Balances.
The victim opens the normal proposal approval flow and completes LocalAuth using a software-wallet account.
The parties have enough funds for normal proposal, approval, and execution fees.
The attacker cannot remotely enable Hide Balances, and this is disclosed as a victim-side precondition. The fallback call name and beneficiary remain visible. However, the exact 50 QUAN value that the signer needs to authorize is replaced with a placeholder, while the screen's unmasked economic headline states 1 QUAN. A signer checking the displayed amount and destination can therefore approve what appears to be a 1 QUAN transaction while authorizing a deterministic 50 QUAN transfer.
No stolen key, physical device access, malicious RPC, compromised indexer, compromised backend, altered proposal bytes, or attacker-controlled quorum is required.
Recommended Remediation
References
Proof of Concept
Safety and environment
I reproduced this entirely in a private local environment using throwaway Alice, Bob, and beneficiary accounts. No public Quantus RPC, indexer, wallet, key, account, or funds were used.
The Android proof used the application source at commit d5fc4d6572ebc2244e95009a33f5da5048935867, an Android API 36 emulator, the production multisig approval components, production SDK decoder, software-wallet signer, genuine Android LocalAuth, and a private spec-136 chain. The APK was an isolated evidence entrypoint that mounted those production components, not an unmodified store release. The entrypoint supplied only throwaway account state and loopback endpoints. The application process was restricted to loopback RPC and fixture ports.
The completed Android and chain evidence run ID was if-else-api36-20260817-02. Its runtime reported spec 136 and transaction version 3.
I then executed the exact same 100-byte call independently against the current in-scope chain source at commit 3b243b870a5442a6e5f443056f67bbba87d472f5, runtime spec 143 and transaction version 3. This second run confirms that the payload and runtime behavior remain valid on the current chain.
Actors and preconditions
The realistic setup is an ordinary 2-of-2 multisig:
Alice is an existing authorized signer, but is malicious.
Alice begins with one approval and cannot execute alone.
Bob is the honest co-signer and holds his account as a software wallet in the mobile app.
The beneficiary is controlled by Alice.
The beneficiary address is identical in both branches of the call.
The multisig holds 50 QUAN.
Bob has enabled the normal Hide Balances setting in the app.
Bob completes the ordinary approval interaction and Android authentication.
I do not claim that Alice can remotely enable Hide Balances. The exploit applies when Bob has already enabled this normal user-facing privacy setting.
Malicious proposal
Alice creates the following validly encoded and fully decodable proposal:
Both branches use this same beneficiary:
The exact 100-byte SCALE call is:
Payload SHA-256:
The call is not malformed. It fully decodes as Utility.if_else, consumes all 100 bytes, and re-encodes to the identical payload.
The main branch requests a one-block reversible-transfer delay. The runtime minimum is two blocks, so the main branch deterministically returns ReversibleTransfers.DelayTooShort. Utility.if_else then dispatches the fallback, which immediately transfers 50 QUAN to the same beneficiary.
Reproduction steps
Observe the misleading confirmation
Observe that the prominent confirmation still says:
The same beneficiary is shown, but the nested amounts now render as:
The screen contains no visible 50 QUAN value and no warning that the fallback can transfer fifty times the headline amount. The fallback call name and beneficiary remain visible.
Verify the approval extrinsic
Independently locate the application-reported approval extrinsic on the local chain, recompute its hash, decode its signer and call, and compare its inner call field with the proposal stored in Multisig.Proposals.
The Android approval was:
The attester verified:
This proves that Bob's real software wallet approved the exact attacker-created bytes shown as a 1 QUAN transfer.
Verify events and balances
Verify the execution events and final balances. The main branch fails with DelayTooShort, then the fallback transfers exactly 50000000000000 base units. The relevant successful outcome is:
Final state:
The multisig therefore loses 50 QUAN even though Bob's confirmation affirmatively described the approval as a 1 QUAN transfer.
Current-runtime confirmation
I repeated the chain portion with the same 100-byte payload on current chain commit 3b243b870a5442a6e5f443056f67bbba87d472f5, runtime spec 143 and transaction version 3.
The current-runtime result was:
To execute the audited runtime without relying on a separately modified production node, this local check placed the audited spec-143 runtime Wasm into a private development genesis and ran it through the cached host node's Wasm executor. The live :code hash matched the audited Wasm, RPC reported spec 143 and transaction version 3, and the node had zero peers. Only stale or optional legacy development-genesis fields were removed. No production runtime source was changed.
Negative controls
With Hide Balances disabled, the same mobile confirmation reveals both
-1 QUANand-50 QUAN.The payload fully decodes and re-encodes exactly. No decoder exception or fail-open error path is involved.
Alice remains unable to execute before Bob supplies the second approval.
A high-security multisig rejects the exact same payload with
Multisig.CallNotAllowedForHighSecurityMultisig, and the rejected proposal is not stored. The demonstrated target is an ordinary multisig.Both branches use the same beneficiary. The exploit does not depend on hiding or substituting the recipient.
The app reads the authoritative stored bytes and submits those same bytes. No malicious RPC, indexer, fixture mutation, or backend compromise is required.
Verified final-run evidence
The six attached PNG files provide the following evidence:
The same proposal with both nested amounts replaced by
-----while the headline remains 1 QUAN.The visible control where the app reveals the real 1 QUAN and 50 QUAN branch amounts.
The enabled
Yes, Approvebutton while the 50 QUAN amount is hidden.The independently decoded Bob approval containing the exact 100-byte payload.
The final verifier showing the 50 QUAN drain.
The genuine Android authentication prompt before credential entry.
PoC video: Android multisig approval and verified 50 QUAN drain
The walkthrough shows the visible-amount control, the misleading privacy-enabled confirmation, the enabled approval action, genuine Android authentication, Bob's exact-byte approval, Alice's execution, and the final balance change.
Walkthrough SHA-256:
b4dc30054adc798afff9198bbbc2eea8fdaa0467b514f57acbcf5015f42d8a54
03-hidden-one-vs-fifty.png
604bbecff8d6a7e300cbee0dd8c3bb3a6a01b6ebb8371898357e44876c2a04a4
02-visible-confirmation-control.png
f6bd09273e4f58b52a5ac273ad165f59929a9c85500381522e6cbf908d324482
04-hidden-approve-enabled.png
9cf0684c5c5310a69eb1c734791bd333d44a9e973c5e126d465a0e4dbb53b5c5
06-approval-attestation.png
d940178fc8b9377f6ac03b6d8b40beb6d025908f732a81397734c049fc842718
07-final-drain-verification.png
2b4cf35cd8c85c92f629e2f1eb37388a11727f4a30f092c86dd85795eba86bba
05-real-android-auth-prompt-before-input.png
de41eb29e793e27101d022f6740a022797e41fe8cffb62d93b58a8db03553d85
Was this helpful?