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 EnumerableSet of holders and 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

1

Paste tests into repository

  1. Paste the following code at the end of arc/test/ArcToken.t.sol.

2

Run tests

  1. Run:

  • forge t --mt test_PoC1_100H_AttackerIdx6_MoveAll_Then51to100 -vvvv --via-ir

  • forge t --mt test_PoC2_100H_AttackerIdx6_MoveAllMinusOne_ToIdx16 -vvvv --via-ir

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?