For the complete documentation index, see llms.txt. This page is also available as Markdown.

88914 w a high hide balances masks executable multisig amounts enabling deceptive utility if else approvals

Submitted on Aug 15th 2026 at 15:55:18 UTC by @zkbm for Audit Comp | Quantus

  • Report ID: #88914

  • 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 application's privacy setting for hiding account balances is also applied to amounts inside the decoded runtime call shown on a multisig authorization screen. Native-token AmountField values are -----, but the Approve button remains enabled.

Composite calls make this exploitable rather than merely inconvenient. For utility.if_else, the confirmation headline is derived only from the primary branch. A malicious cosigner can therefore propose a call whose primary branch appears to transfer 1 QUAN but is guaranteed to fail, while its fallback transfers a much larger amount to the same expected recipient. The screen headlines the proposal as 1 QUAN, masks the fallback amount, and permits the victim to approve it. Once the approval threshold is reached, the attacker can execute the proposal and the fallback transfer runs. The PoC proves that fallback transfers of 2 QUAN and 999,999.999999999999 QUAN have different SCALE-encoded call bytes but produce identical authorization text.

I think this could reasonably be scored as a High or Critical - since it does involve some level of user interaction, but to me this is a transaction authorization integrity flaw, not just a cosmetic UX issue. You can present materially different signed payloads with identical authorization text that conceals the amount that can execute and leaves approval enabled. Considering the ByBit hack root cause (manipulation of safe signing), I think it could be considered critical but didn't want to oversell the rating. Hide balances is a global, persistent privacy setting, so it's reasonable that a user would have this enabled while viewing another account and later enter an authorization flow with the fallback amounts concealed.

Vulnerability Details

mobile-app/lib/v2/components/decoded_call_view.dart renders a native-token AmountField through the general transaction display provider and retains only its primaryAmount:

mobile-app/lib/providers/currency_display_provider.dart makes that provider dependent on the global isBalanceHiddenProvider. When the privacy option is active, all amounts collapse to the same ----- string. The same provider also defaults to two token decimals and may select rounded fiat as the primary display, making it unsuitable for authorization even when privacy mode is off.

mobile-app/lib/v2/screens/multisig/multisig_action_confirm_sheet.dart displays the decoded call but only blocks approval while call bytes are loading or unavailable. It does not block approval when signed fields are redacted:

Finally, quantus_sdk/lib/src/chain/call_decoder.dart assigns utility.if_else the summary of its primary branch:

This combination creates a misleading 1 QUAN headline while the amount in the branch that will actually execute is hidden.

Attack scenario

1

Establish the multisig boundary

The victim and attacker are members of a 2-of-2 multisig. This is the security boundary multisig is intended to enforce. One malicious signer must not be able to move funds without the honest signer's informed authorization.

2

Enable Hide balances

The victim enables the wallet's Hide balances privacy option.

3

Propose the malicious inner call

The attacker proposes the following inner call, using the documented Quantus CLI or any SCALE-capable client:

4

Trigger the fallback

dispatch_as_fallible requires a Root origin. Multisig execution supplies the multisig's signed origin, so the primary branch fails with BadOrigin and utility.if_else dispatches the fallback.

5

Approve the misleading proposal

The victim opens the proposal in the mobile app. The headline says 1 QUAN. Both nested amount rows say -----; after scrolling, the fallback amount is still ----- and Approve is enabled.

6

Execute the approved proposal

The victim approves what appears to be the expected 1 QUAN payment. The attacker executes the now-approved proposal. The fallback sends the larger amount, limited only by the multisig's available balance.

Impact

An attacker who controls one multisig member can obtain another member's approval for materially different transfer arguments than the authorization UI communicates. Once the threshold is reached, this results in direct theft of the multisig's funds.

This maps to the in-scope Critical impacts Direct theft of user funds and Malicious interactions with an already-connected wallet, including modifying transaction arguments or parameters.

1

Render exact native-token amounts

Render every native-token AmountField in canonical token units at full chain precision, regardless of Hide balances, fiat-first mode, or account-balance visibility.

2

Use an authorization-specific formatter

Never use txAmountDisplayProvider in a decoded-call authorization view. Use an authorization-specific formatter that cannot redact or round signed values.

3

Block incomplete confirmations

Disable confirmation if any security-sensitive field cannot be rendered exactly.

4

Clarify conditional calls

For conditional calls, make it explicit that either branch may execute and avoid using only the primary branch as the transaction headline.

5

Add injectivity testing

Add an injectivity test so that changing any amount, destination, branch, or nested call must change the authorization text.

Proof of Concept

Save the following as hidden_multisig_amount_poc_test.dart in the test directory (mobile-app/test/unit/hidden_multisig_amount_poc_test.dart). Run it from the mobile-app directory after your Flutter setup is done.

Expected result:

The test constructs and SCALE-encodes real batch_all and if_else runtime calls using the generated runtime types. It decodes those bytes with the production CallDecoder and renders the production MultisigActionConfirmSheet.

The test demonstrates the following:

  • The signed 999,999.999999999999 QUAN amount is replaced by -----.

  • Both amounts in the conditional call are hidden.

  • The Approve control has a non-null tap handler.

  • The authorization headline says 1 QUAN based only on the primary branch.

  • Scrolling to the fallback does not reveal its amount.

  • Changing the fallback from 2 QUAN to 999,999.999999999999 QUAN changes the signed SCALE bytes but produces identical rendered authorization text.

Attached is a screenshot from the victim's point of view. As you can see, this is extremely deceptive since the victim reasonably believes that they are authorizing the transfer of 1 QUAN. The executable fallback amount can't be verified by the victim.

Was this helpful?