For the complete documentation index, see llms.txt. This page is also available as Markdown.

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?