52787 sc high batched yield distribution rounding in arctoken permanently freezes unclaimed funds and misreports payouts

Submitted on Aug 13th 2025 at 07:21:42 UTC by @manvi for Attackathon | Plume Network

  • Report ID: #52787

  • Report Type: Smart Contract

  • Report severity: High

  • Target: https://github.com/immunefi-team/attackathon-plume-network/blob/main/arc/src/ArcToken.sol

  • Impacts:

    • Permanent freezing of funds

Description

Brief/Intro

While testing ArcToken.distributeYieldWithLimit, I found that every batched yield distribution leaves behind a small amount of the yield token (“dust”) stuck in the contract due to integer division. This dust is never distributed to any holder and cannot be withdrawn, permanently freezing it. The function’s event emission also misreports payouts by claiming the full amount was distributed, even though part of it remains stuck. If this continues in production with frequent distributions, it will lead to a growing amount of inaccessible funds and misleading accounting data.

Vulnerability Details

In the smart contract, distributeYieldWithLimit processes yield payments in batches. For each holder in the batch, the share is calculated as:

share = (totalAmount * holderBalance) / effectiveTotalSupply;

Because integer division floors the result, any remainder for each holder is left undistributed in that batch. Unlike distributeYield, which tops off the final holder, this batched version never reconciles those remainders across batches.

Flow observed:

1

First batch: funds pulled

On the first batch (startIndex == 0), the entire totalAmount is transferred from the yield token into the ArcToken contract.

2

Per-holder distribution

Each batch distributes floored amounts to holders, leaving small “dust” remainders.

3

Dust remains in contract

The dust remains in the ArcToken contract with no function to sweep or recover it.

4

Misreported event

The function still emits YieldDistributed(totalAmount, token) when nextIndex == 0, misrepresenting the total payout.

Code excerpt demonstrating the issue

Impact Details

Even a remainder of a few tokens per batch can add up over hundreds of distributions per year. At this scale, this could mean thousands of USDC locked away annually.

References

  • Smart contract - ArcToken.sol

  • Function call - distributeYieldWithLimit

Proof of Concept

Show PoC (Hardhat test showing 1 unit stuck when splitting a 100-unit distribution across 2 batches)

I used Hardhat for this PoC. File: test/ArcToken.batched-dust.poc.js

I ran it with: npx hardhat test test/ArcToken.batched-dust.poc.js

Observed result:

  • h1, h2, h3 each end up with 33.

  • ArcToken retains 1 unit of the yield token.

  • Distributor’s USDC goes from 100 -> 0 (pulled once).

  • The leftover 1 is stuck on the contract with no way to pull it out.


If you want, I can:

  • Suggest minimal code fixes to avoid dust (e.g., carry remainders forward or top off last recipient of the entire distribution), or

  • Draft a suggested patch/PR compatible with the project style that reconciles leftover remainders and corrects the event emission.

Was this helpful?