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:
The leaf exists in the ZK tree.
The nullifier is correct.
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 usingtotal_balancepallets/wormhole/src/lib.rs:737-998—process_exit_bundlepallets/wormhole/src/lib.rs:828-847— exit minting viaUnbalanced::increase_balancepallets/wormhole/src/lib.rs:1280-1310—record_transferCommit audited:
94c4ca7a
Link to Proof of Concept
https://gist.github.com/jonah-space/c1f4bba446b0baa14649f1de5d515eaf
Proof of Concept
Steps to reproduce
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 = 1so the inserted leaf matches the committedprivate_batch.hexfixture 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 = 1while genesis leaves usetransfer_count = 0; the circuit validates both identically, only the proof needs to be regenerated.
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_bundleto mint the full 20 UNIT.Asserts the original reserved balance remains and the exit account receives 20 UNIT free balance.
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?