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.
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
Proof of Concept
Setup and distribution start
There are 20 eligible holders. Distribution will be done in 2 batches of 10 each.
distributeYieldWithLimitis called with atotalAmountof rewards and will process holders 0–9 in batch 1 and 10–19 in batch 2.
Function signature:
function distributeYieldWithLimit(
uint256 totalAmount,
uint256 startIndex,
uint256 maxHolders
)
external
onlyRole(YIELD_DISTRIBUTOR_ROLE)
nonReentrant
returns (uint256 nextIndex, uint256 totalHolders, uint256 amountDistributed)
{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:
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;
}
}
}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?