52371 sc high distributeyieldwithlimit is vulnerable to inter batch balance and holders array mutations
Submitted on Aug 10th 2025 at 09:43:16 UTC by @IronsideSec for Attackathon | Plume Network
Report ID: #52371
Report Type: Smart Contract
Report severity: High
Target: https://github.com/immunefi-team/attackathon-plume-network/blob/main/arc/src/ArcToken.sol
Impacts: Theft of unclaimed yield
Description / Brief
An attacker or even genuine token holders can move ARC between batches to receive yield twice for the same underlying stake, while another holder can be omitted due to index-swap semantics. Consequences: attacker gains excess yield at the expense of honest holders (misallocation/stealing of yield), undermining fairness of distributions.
Vulnerability Details
Root cause
Batching uses a mutable
EnumerableSetofholdersand current balances per call; there is no snapshot of the holder list or balances for the epoch.When a holder zeroes their balance, they’re removed and the last element is swapped into their index. New recipients are appended to the end.
The next batch uses indices on this mutated set and recomputed balances/denominator, so previously “covered” index ranges no longer correspond to the same addresses.
Attack path 1 (move-all between batches; omission via index swap; double-dip on new account)
Setup 100 holders; attacker at index 6.
Admin calls batch 1 with startIndex=1, max=50. Attacker gets paid.
Attacker moves all ARC to a new address. This removes attacker; the last holder is swapped into index 6; attacker’s new address is appended (~index 98).
Admin calls batch 2 with startIndex=51, max=50. Attacker’s new address (~98) gets paid again; the swapped-in holder now at index 6 is omitted for this epoch.
Attack path 2 (move-all-minus-one wei, between batches; double-pay for same stake)
Setup 100 holders; attacker at index 6; friend at index 16.
Admin calls batch 1 with startIndex=1, max=10. Attacker gets paid.
Attacker transfers all but 1 wei to friend. Attacker stays in set (dust), friend remains at index 16 (next batch range).
Admin calls batch 2 with startIndex=11, max=10. Friend gets paid too. Combined payout for attacker’s stake across two addresses exceeds a snapshot-fair pro-rata share for the epoch.
Proofs
Tests demonstrating both behaviors:
test_PoC1_100H_AttackerIdx6_MoveAll_Then51to100()test_PoC2_100H_AttackerIdx6_MoveAllMinusOne_ToIdx16()
Relevant code excerpts:
Transfer handler that mutates holders set (removal/add):
Distribution entrypoint:
Impact Details
Double-dipping: A single stake can receive yield twice by moving between batches, inflating the attacker’s payout beyond fair pro-rata.
Omission: A honest holder can be skipped in the epoch when swapped into an already-processed index range.
Zero-sum misallocation: Yield meant for all holders is redistributed unfairly, directly harming honest holders’ payouts. Repeating this each epoch yields ongoing economic loss for others. Severity: High (direct economic impact to holders).
References / Mitigation suggestion
Introduce ERC20 pausable feature.
Pause token transfers when distributor calls multiple
distributeYieldWithLimit.
Proof of Concept
References to source lines in the repository:
https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/arc/src/ArcToken.sol#L653-L712
https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/arc/src/ArcToken.sol#L466
Was this helpful?