88830 bc low missing constraint private batch allows real outputs with canonical zero exit account
Submitted on Aug 15th 2026 at 05:18:28 UTC by @bugtester for Audit Comp | Quantus
Report ID: #88830
Report Type: Blockchain/DLT
Report severity: Low
Target: https://github.com/immunefi-team/audit-comp-quantus-qp-zk-circuits
Impacts:
Modification of transaction fees outside of design parameters
Description
Brief/Intro
The private-batch aggregation circuit does not enforce that a real output with a non-zero amount must have a non-zero exit account.
The leaf-level circuit exposes the exit accounts as public inputs without constraining them to be non-zero. The private-batch circuit distinguishes dummy proofs using the block-hash sentinel and only masks exits to zero for those dummy proofs. For a real proof, the exit account is forwarded unchanged.
As a result, the circuit accepts the following state:
real proof
output_amount > 0
exit_account = [0, 0, 0, 0]The private-batch aggregation logic then treats the zero exit account as a normal grouping key, aggregates its associated amount, and places the resulting (amount, zero-exit) pair into the aggregated public inputs.
This breaks the invariant that real positive-value exits must have a valid destination account.
Vulnerability Details
The private-batch circuit first determines whether a child proof is a dummy based on the block-hash sentinel.
For each output slot, dummy proofs are masked to:
For a real proof, is_dummy_i is false and the original exit_raw value is returned unchanged.
The subsequent aggregation logic compares exit accounts, groups matching exits, sums their amounts, and emits the resulting exit and amount.
There is no constraint enforcing:
Therefore, the aggregation circuit does not prevent a real positive-value output from using the same canonical zero value that is otherwise used for dummy/unused exits.
Impact Details
The missing constraint allows a real, positive-value exit to be represented with the canonical zero exit account.
The private-batch circuit uses the zero exit account as the canonical representation for dummy/unused outputs, but does not enforce that the same value cannot be used by a real output carrying a non-zero amount.
A real leaf can contain:
The private-batch logic does not reject this combination. Because real slots are forwarded without validating the exit account, the positive amount enters the aggregation logic under the zero exit key and is exposed in the aggregated public inputs as:
This breaks the expected invariant that every real positive-value exit has a valid destination account.
The ZK circuit therefore accepts and proves an invalid settlement state: a positive-value output is associated with the canonical zero destination.
If the downstream settlement layer processes the aggregated output without independently rejecting the zero exit account, the associated value can be sent to the zero/ownerless destination, resulting in permanent loss or misdirection of funds.
Even where downstream validation rejects the zero destination, the circuit still permits an invalid positive-value zero-exit state to become part of a valid aggregated proof, allowing malformed settlement batches to reach the settlement layer and potentially cause settlement failure or batch-level disruption.
Proof of Concept
The issue can be reproduced by constructing a real leaf proof with a non-zero output amount and a zero exit account, then passing that proof through the private-batch aggregation circuit.
The relevant code paths are:
Private-batch masking + grouping:
This shows that dummy slots are masked using
select(is_dummy_i, zero, exit_raw)andselect(is_dummy_i, zero, amount_raw), while real slots are forwarded without a check enforcing(real && amount > 0) => exit != zero.The subsequent grouping/sum logic treats the exit account as a grouping key without validating that a positive amount has a non-zero destination.
SubstrateAccount leaf-level handling:
The exit accounts are exposed as public inputs without a constraint requiring them to be non-zero.
Formal specification:
The specification notes that
exit_account_1/2are unconstrained at the leaf level and that the binding obligation is intended to be enforced at the private-batch layer.
Recommended Fix
Add a circuit constraint requiring every real positive-value output to have a non-zero exit account:
For example:
This ensures that:
cannot satisfy the circuit.
Was this helpful?