50527 sc high attacker can steal yield during batch distribution

Submitted on Jul 25th 2025 at 17:49:51 UTC by @wellbyt3 for Attackathon | Plume Network

  • Report ID: #50527

  • 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 / Intro

An attacker can steal yield being distributed through ArcToken::distributeYieldWithLimit() by transferring ArcTokens between addresses in different batches.

Vulnerability Details

distributeYieldWithLimit allows the YIELD_DISTRIBUTOR_ROLE to distribute yield in batches for tokens with a large number of token holders.

Example scenario (simplified):

  • 100e18 of yield token distributed in 2 batches.

  • Four ArcToken holder addresses, each with 10e18 ArcTokens (totalSupply = 40e18).

  • Holders enumerated as:

    • Index 0: Holder 1

    • Index 1: Attacker (Address 1)

    • Index 2: Attacker (Address 2)

    • Index 3: Holder 2

During batch 1:

  • Yield tokens are transferred to the ArcToken contract, then distributed pro rata based on current balances vs totalSupply.

  • Holder 1 receives 25e18 yield tokens; Attacker (Address 1) receives 25e18.

Exploit (backrun and transfer between batches):

  • The attacker backruns the first batch's distributeYieldWithLimit() and transfers their ArcTokens from Attacker (Address 1) to Attacker (Address 2) after Address 1 receives yield.

  • Attacker (Address 1) balance becomes 0 and is removed from the holders enumerated list. When removed, the last index holder is moved into the removed index position.

  • New enumerated list becomes:

    • Index 0: Holder 1

    • Index 1: Holder 2

    • Index 2: Attacker (Address 2)

  • In batch 2, Attacker (Address 2) now holds 20e18 (50% of totalSupply) and can receive the remaining 50e18 yield tokens.

  • Holder 2 is skipped because their index moved and the batch iteration misses them.

POC provided models this exact scenario.

Impact Details

High — an attacker can steal yield by manipulating holder enumeration across batches.

References

  • https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/arc/src/ArcToken.sol#L466

Proof of Concept

Below is the PoC test. Add a new .t.sol file in arc/test, paste the code, then run: forge test --mt test_attackerCanStealYieldDuringBatchDistribution -vv

1

Setup and deploy contracts

The test deploys RestrictionsFactory, RestrictionsRouter, ArcTokenFactory, a mock yield token, ArcTokenPurchase, and registers module types.

2

Create ArcToken and mint holders

  • Create an ArcToken via the factory.

  • Mint 10e18 ArcTokens to four addresses: user1, attackerAddr1, attackerAddr2, user2.

  • The attacker owns attackerAddr1 and attackerAddr2.

3

Distribute first batch

  • The yield (100e18) is approved and distributeYieldWithLimit(100e18, 0, 4) is called to distribute in batches (include factory and creator slots that are present as zero-balance holders).

  • After the first batch, attackerAddr1 receives its share (25e18).

4

Attacker transfers tokens between their addresses (backrun)

  • attackerAddr1 transfers its 10e18 ArcTokens to attackerAddr2.

  • attackerAddr1 becomes 0 balance and is removed from the holders enumeration, causing an index swap that moves Holder 2 into the removed slot.

5

Distribute second batch and observe theft

  • Call distributeYieldWithLimit(100e18, 4, 2) for the 2nd batch.

  • attackerAddr2 now receives disproportionate yield (total 75e18 across both attacker addresses), while user2 receives 0.

Proof-of-concept test code:

Was this helpful?