50195 sc low unfair yield distribution due to remainder allocation to last holder
Submitted on Jul 22nd 2025 at 13:05:53 UTC by @AasifUsmani for Attackathon | Plume Network
Report ID: #50195
Report Type: Smart Contract
Report severity: Low
Target: https://github.com/immunefi-team/attackathon-plume-network/blob/main/arc/src/ArcToken.sol
Impacts:
Theft of unclaimed yield
Description
Brief/Intro
The distributeYield() function in ArcToken contains a systematic flaw where any rounding remainder from yield distribution calculations is allocated to the last holder in the iteration order, rather than being distributed proportionally. Token owners can set any ERC20 token as the yield token, so token decimals can be 0, 6, 8, 18, etc., magnifying the unfair advantage. Yield token decimals used by RWA token owners can amplify this error significantly.
Vulnerability Details
Root Cause Analysis
The vulnerability exists specifically in the distributeYield() function where holders are processed sequentially, and any remainder from rounding errors is implicitly given to the last processed holder:
function distributeYield(uint256 amount) external onlyRole(YIELD_DISTRIBUTOR_ROLE) nonReentrant {
// ... validation logic ...
uint256 distributedSum = 0;
uint256 lastProcessedIndex = holderCount > 0 ? holderCount - 1 : 0;
// Process all holders except the last one
for (uint256 i = 0; i < lastProcessedIndex; i++) {
address holder = $.holders.at(i);
if (!_isYieldAllowed(holder)) continue;
uint256 holderBalance = balanceOf(holder);
if (holderBalance > 0) {
uint256 share = (amount * holderBalance) / effectiveTotalSupply; // ❌ Rounds down
if (share > 0) {
yToken.safeTransfer(holder, share);
distributedSum += share; // ❌ Accumulates rounding losses
}
}
}
// ❌ CRITICAL FLAW: Last holder gets remainder instead of proportional share
if (holderCount > 0) {
address lastHolder = $.holders.at(lastProcessedIndex);
if (_isYieldAllowed(lastHolder)) {
uint256 lastShare = amount - distributedSum; // ❌ Gets ALL remainder
if (lastShare > 0) {
yToken.safeTransfer(lastHolder, lastShare);
}
}
}
}Here, the distributed sum is calculated based on rounded-down shares. The last user gets the remainder, which can be substantially larger than their rightful proportional share.
Impact Details
Critical Impact: Token Owner Controls Yield Token Precision
ArcToken owners can set any ERC20 as the yield token:
Because decimals are not validated, yield tokens can have:
0 decimals (whole units only)
1-6 decimals (limited precision)
18+ decimals (high precision)
Consequences:
If yield token has 0 decimals, rounding losses are severe.
With low decimal tokens (0–6), distribution rounding causes significant unfairness, especially if distributions are in whole tokens or small units relative to supply.
References
https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/arc/src/ArcToken.sol#L388
Proof of Concept
Note: Add the tests into ArcToken.t.sol file and run test-specific commands to run the PoC.
In tests that use LowDecimalToken, include this contract at the top of ArcToken.t.sol:
Was this helpful?