88671 bc medium permissionless vesting claim forces payout fragmentation leading to loss of beneficiary funds
Submitted on Aug 14th 2026 at 12:57:06 UTC by @zealynx for Audit Comp | Quantus
Report ID: #88671
Report Type: Blockchain/DLT
Report severity: Medium
Target: https://github.com/immunefi-team/audit-comp-quantus-chain
Impacts:
Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)
Description
Brief/Intro
Vesting::claim is permissionless and pays the largest amount vested at call time. Every payout becomes a separate Wormhole leaf, and each leaf independently loses value to circuit rounding. An unrelated account can therefore call claim once per permitted interval to fragment a beneficiary's grant into many small leaves, multiplying the rounding loss. The beneficiary permanently receives less than their grant, the loss is irreversible, and the attacker needs nothing but transaction fees and no relationship to the schedule.
Vulnerability Details
Vesting::claim calls ensure_signed(origin) and deliberately permits any signer to claim any schedule. claim_plan then pays the largest amount vested at that moment.
Every payout becomes a separate Wormhole leaf quantized to 0.01 QUAN. A Wormhole leaf must satisfy:
(output * 10_000) <= (input * 9_996)Rounding occurs independently for every leaf. Fragmenting one payout into many therefore multiplies the rounding loss rather than amortising it.
Aggregation cannot recover it. The inequality is enforced inside each leaf circuit before private-batch aggregation. The batch circuit may group identical exit accounts and sum their outputs, but it only ever sums outputs that already satisfied the per-leaf inequality — so batching fragmented leaves cannot restore what each leaf already rounded away.
Because the claim is permissionless, the choice of fragmentation belongs to the attacker, not the beneficiary. The one-day MinClaimInterval sets the maximum fragmentation rate but does nothing to prevent it: for a linear 365-day, 365-QUAN schedule an attacker calls once per day and forces 365 one-QUAN leaves, each able to output at most 0.99 QUAN. Claimed once at completion, the same grant is a single leaf able to output 364.85 QUAN.
Vulnerable scenario:
Attacker authority
Only an arbitrary funded account able to submit a signed extrinsic. The caller need not be the beneficiary, schedule creator, treasury, administrator, or a governance member. The RuntimeOrigin::root() calls in the PoC construct identical legitimate victim schedules as test fixtures only — every harmful daily claim is submitted by unrelated account(8).
Relationship to the other permissionless-claim finding
This shares only its precondition — Vesting::claim accepting any signer — with the separate report on blocked schedule termination. They are distinct vulnerabilities with different victims, mechanisms and fixes, and neither fix resolves the other:
Victim
the beneficiary
the treasury
Mechanism
repeated claims fragment the grant into many Wormhole leaves; per-leaf rounding compounds
a single claim leaves a sub-minimum vested remainder
Attacker calls
once per interval, repeatedly
exactly once, at a chosen vesting point
Loss
~3.50 QUAN of a 365-QUAN grant, destroyed
< 1 MinimumPayout, denied to the treasury
Fix
constrain who may claim, or amortise leaf rounding
correct the claim_plan non-final cap
Capping claim_plan (the other report's fix) does not prevent fragmentation; constraining the caller here does not repair the dust remainder that blocks termination.
Impact Details
Irreversible loss of a beneficiary's vested funds, inflicted by any third party.
Amount at risk: an additional 3.50 QUAN destroyed on a 365-QUAN grant — the maximum withdrawable falls from 364.85 QUAN to 361.35 QUAN. The loss scales with the number of forced fragments, i.e. with schedule duration.
Who bears the loss: the beneficiary. The funds are not transferred to the attacker; they become permanently unwithdrawable through the Wormhole exit path.
Reversibility: none. Each leaf's rounding is fixed at the moment the payout is recorded, and aggregation provably cannot recover it.
Attacker prerequisites: a funded account and transaction fees. No control over the schedule, the beneficiary, or any privileged origin.
Profit motive: none — this is griefing. The attacker pays fees to destroy another user's funds, which is why it is filed as Medium rather than higher.
References
Frozen revision: quantus-chain@3b243b870a5442a6e5f443056f67bbba87d472f5
pallets/vesting/src/lib.rs#L319-L350—claim,ensure_signedwith the result discardedpallets/vesting/src/lib.rs#L530-L559—claim_plan, pays the largest currently-vested amountruntime/src/configs/mod.rs#L562-L571—VestingMinClaimInterval,VestingPayoutQuantum,VestingMinimumPayoutWormhole leaf fee inequality
(output * 10_000) <= (input * 9_996), enforced per leaf in the withdrawal circuit before private-batch aggregation
Proof of Concept
Environment
Runs entirely locally. No test was performed against mainnet or any public testnet.
Requires a Rust toolchain matching the repository's rust-toolchain and SKIP_WASM_BUILD=1 (native-only test build). The test is added to the existing runtime/tests/governance/vesting.rs, which already supplies the account(), signers(), treasury_multisig() and new_test_ext() helpers it uses. No RPC endpoint, no fork, no external service.
Use frozen chain commit 3b243b870a5442a6e5f443056f67bbba87d472f5. Save and apply this patch from the repository root:
The test uses the production runtime configuration, records actual ZK-tree leaves, and compares identical schedules whose only difference is the unrelated caller's claim cadence. RuntimeOrigin::root() only establishes the victim's legitimate pre-existing vesting schedule; the attack itself is every Vesting::claim(RuntimeOrigin::signed(account(8)), 0) call, made by an unrelated ordinary account with no administrative or treasury authority.
Observed output:
Baseline:
After applying the recommendation below, the unchanged harm-mode PoC fails at the first daily claim with NothingToClaim. A positive sweep comparing periodic and final-only claims for seven schedule shapes produced identical maximum outputs in every case. The complete runtime Vesting suite passed 7/7 and cargo test -p pallet-vesting passed 58/58.
Recommendations
Quantize every non-final payout to the smallest multiple of PayoutQuantum whose Wormhole fee is an exact number of leaf quanta, while preserving an exact final payout for the remainder. At 4 bps and a 0.01-QUAN leaf quantum, that alignment is 2,500 leaf quanta, or 25 QUAN.
The core of the verified remediation is:
Was this helpful?