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

89077 bc low incorrect genesis balance accounting in wormhole on initialize allows reserved locked funds to be exited as free balance leading to direct loss of funds and unauthorized minting

Submitted on Aug 17th 2026 at 00:57:29 UTC by @argusdev for Audit Comp | Quantus

  • Report ID: #89077

  • Report Type: Blockchain/DLT

  • Report severity: Low

  • Target: https://github.com/immunefi-team/audit-comp-quantus-chain

  • Impacts:

    • Direct loss of funds

    • Inflation bugs (unauthorized minting)

Description

Brief / Intro

pallet-wormhole records genesis ZK leaves using Currency::total_balance, which includes reserved and locked funds. Because exits are minted as free balance without reducing the original reserved balance, an attacker can convert locked genesis funds into freely spendable funds while keeping the original lock intact.

Vulnerability Details

During the genesis block, pallet-wormhole::on_initialize(1) iterates every account and creates a ZK leaf for each non-zero balance:

// pallets/wormhole/src/lib.rs:478-487
for who in frame_system::Account::<T>::iter_keys() {
    let amount = <T::Currency as Currency<_>>::total_balance(&who);
    if amount.is_zero() { continue; }
    let to: T::WormholeAccountId = who.into();
    Self::record_transfer(T::AssetId::default(), &minting_account, &to, amount);
}

The leaf amount is total_balance, so any reserved or locked balance is included in the leaf. The ZK leaf circuit only checks:

  1. The leaf exists in the ZK tree.

  2. The nullifier is correct.

  3. Output ≤ input after the fee.

It does not distinguish between free and reserved balance.

Later, process_exit_bundle mints the exit output as free balance:

increase_balance credits the exit account without consuming or unlocking the original account's reserved balance. Consequently, an attacker who controls a genesis account with reserved funds can keep the reserved balance locked on the original account and receive an equivalent, minus fee, amount of free balance on an exit account.

This violates the invariant that a wormhole leaf should represent only spendable value.

Impact Details

  • Double-liquidity of locked funds: Reserved/locked balance stays on the original account while an equivalent amount of free balance is minted to the exit account, effectively inflating spendable supply.

  • Circumvention of economic locks: Vesting, staking bonds, treasury locks, and governance locks become ineffective against wormhole exits.

  • Conditional severity: Exploitable whenever any account holds reserved balance at leaf-generation time. If genesis contains no reserved balances today, the code is unsafe by design and silently becomes exploitable as soon as any lock appears. Rated High; Critical if material reserved balances exist at genesis.

References

  • pallets/wormhole/src/lib.rs:478-487 — genesis leaf recording using total_balance

  • pallets/wormhole/src/lib.rs:737-998 — process_exit_bundle

  • pallets/wormhole/src/lib.rs:828-847 — exit minting via Unbalanced::increase_balance

  • pallets/wormhole/src/lib.rs:1280-1310 — record_transfer

  • Commit audited: 94c4ca7a

https://gist.github.com/jonah-space/c1f4bba446b0baa14649f1de5d515eaf

Proof of Concept

Steps to reproduce

1

Build the test environment

2

Run the end-to-end PoC that uses a real ZK proof

What it does:

  • Endows the test account with 20 UNIT (15 UNIT free + 5 UNIT reserved).

  • Pre-sets TransferCount = 1 so the inserted leaf matches the committed private_batch.hex fixture proof.

  • Records a leaf for the full 20 UNIT via Wormhole::record_transfer.

  • Submits the real fixture proof through Wormhole::verify_private_batch.

  • Asserts the original account still holds 5 UNIT reserved and the exit account receives 19.98 UNIT free balance.

  • Note: the fixture uses transfer_count = 1 while genesis leaves use transfer_count = 0; the circuit validates both identically, only the proof needs to be regenerated.

3

Run the dispatch-path isolation PoC

This PoC bypasses the ZK verifier to isolate the accounting bug.

What it does:

  • Endows the test account with 20 UNIT (10 UNIT free + 10 UNIT reserved).

  • Triggers on_initialize(1) to record the genesis leaf.

  • Directly calls process_exit_bundle to mint the full 20 UNIT.

  • Asserts the original reserved balance remains and the exit account receives 20 UNIT free balance.

4

Run the full test suite

PoC Test

Add the following test to pallets/wormhole/src/tests.rs (requires making test_account() / TEST_SECRET pub(crate) and adding Currency, ReservableCurrency, Inspect imports):

Run:

A second test (genesis_reserved_balance_is_exitable_as_free_balance) bypasses the ZK verifier and calls process_exit_bundle directly to isolate the same accounting bug.

Was this helpful?