52198 sc high balance manipulation between batches leading to inflated payout and dos
Submitted on Aug 8th 2025 at 16:55:41 UTC by @farman1094 for Attackathon | Plume Network
Report ID: #52198
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
The ArcToken::distributeYieldWithLimit function, which distributes rewards to token holders in batches, is vulnerable if token balances change between batch executions (due to mint, burn, transfers). An attacker can exploit this by transferring tokens from already-paid holders to unpaid holders between batches, inflating payouts and causing later batches to fail, leaving funds stuck.
Vulnerability Details (click to expand)
The ArcToken::distributeYieldWithLimit function is designed to distribute a fixed totalAmount of rewards among token holders in multiple batches to avoid exceeding gas limits when the number of holders is large.
The problem arises because each batch independently recalculates effectiveTotalSupply and reads the current balanceOf values directly from state at execution time. This creates a race condition between batches because balances can change in the time gap between executions.
For example, if a holder who has already been paid in batch 1 transfers tokens to a holder scheduled to be paid in batch 2, that second holder’s balance is inflated when batch 2 runs. Since the payout formula uses the updated balances and supply, the recipient’s proportional share is larger than it should be, effectively paying twice for the same tokens. This can deplete the remaining reward pool before all batches complete, causing later transfers to revert and leaving funds permanently stuck.
Relevant code snippet from ArcToken::distributeYieldWithLimit:
// `ArcToken::distributeYieldWithLimit`
for (uint256 i = 0; i < batchSize; i++) {
uint256 holderIndex = startIndex + i;
address holder = $.holders.at(holderIndex);
if (!_isYieldAllowed(holder)) {
continue;
}
uint256 holderBalance = balanceOf(holder);
if (holderBalance > 0) {
uint256 share = (totalAmount * holderBalance) / effectiveTotalSupply;
if (share > 0) {
yToken.safeTransfer(holder, share);
amountDistributed += share;
}
}
}Because the function expects the total of all batch payouts to exactly match the original totalAmount, any mid-distribution balance change breaks the calculation and enables intentional DoS and freezing of funds.
Impact Details
Denial of Service: Any transfer between batches can cause later batches to revert, halting yield distribution entirely.
Permanent Freezing of Funds: Rewards earmarked for unpaid holders can become locked and cannot be restored.
Possible Solution
Introduce a global state variable distributionInProcess.
Set this variable to true at the start of the batched distribution process, and keep it true until the process completes.
While distributionInProcess is true, all transfer, mint, and burn operations should be locked.
Proof of Concept
Inflated payout and failure
When batch 2 runs, the contract recalculates
effectiveTotalSupplyand readsuserB’s updatedbalanceOf(userB). The share calculation uses the updated balance and supply:
The inflated share can consume more than the remaining
totalAmount, causing transfers to revert or the distribution to be incomplete, leaving funds stuck.
Was this helpful?