52285 sc high incorrect dust handling in yield distribution leads to permanent fund lock
Submitted on Aug 9th 2025 at 13:39:56 UTC by @vivekd for Attackathon | Plume Network
Report ID: #52285
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 distributeYield and previewYieldDistribution functions in ArcToken fail to properly redistribute rounding dust when the last holder in the holders array is restricted from receiving yield.
This causes yield tokens to become permanently locked in the ArcToken contract with no mechanism for recovery, leading to an accumulation of stuck funds over time.
Vulnerability Details
The vulnerability exists in the dust redistribution logic within both yield distribution functions.
The contract uses a common pattern to handle rounding errors where the last eligible holder receives totalAmount - sumOfPreviousDistributions to capture any dust from integer division:
// In distributeYield() - lines 448-457
if (holderCount > 0) {
address lastHolder = $.holders.at(lastProcessedIndex);
if (_isYieldAllowed(lastHolder)) {
uint256 lastShare = amount - distributedSum;
if (lastShare > 0) {
yToken.safeTransfer(lastHolder, lastShare);
distributedSum += lastShare;
}
}
// BUG: If lastHolder is restricted, lastShare is never distributed
}When the last holder in the array is blacklisted or sanctioned (fails _isYieldAllowed check), the dust amount (amount - distributedSum) is never transferred to anyone.
The yield tokens remain in the ArcToken contract permanently as there is no recovery mechanism.
The same issue exists in previewYieldDistribution():
// Lines 297-301
if (!_isYieldAllowed(lastHolder)) {
amounts[lastProcessedIndex] = 0; // Dust is not accounted for
} else {
amounts[lastProcessedIndex] = amount - totalPreviewAmount;
}Impact Details
Permanent Fund Lock: With realistic RWA parameters, significant value accumulates.
Accounting Discrepancy: The
YieldDistributedevent emitsdistributedSumwhich is less than the actual amount transferred into the contract, creating accounting inconsistencies that affect analytics and auditing.Preview Function Inaccuracy:
previewYieldDistributionreturns incorrect totals (example: shows 490,200 satoshis distributed when 500,000 were allocated).Scalability Impact: With multiple RWA tokens on the platform, locked funds multiply.
No Recovery Mechanism: The
ArcTokencontract has no function to sweep stuck yield tokens, making the lock permanent and growing indefinitely.
References
https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/arc/src/ArcToken.sol#L233-L305
https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/arc/src/ArcToken.sol#L384-L460
Proof of Concept
Setup
Deploy ArcToken with a realistic RWA tokenization scenario:
TVL: $120,000
10,000 holders each holding 12 tokens (120,000 total supply)
196 holders are yield-restricted (sanctioned/blacklisted)
One restricted holder is last in the
holdersarrayAPY: 6% with monthly distributions
Yield paid in WBTC at $120,000/BTC
Call previewYieldDistribution(500000)
9,804 eligible holders (10,000 - 196)
Each eligible holder calculation:
holderShare = (500,000 * 12) / 117,648 = 50.999592003264... satoshis
Integer division truncates to 50 satoshis
Total distributed to first 9,804 eligible holders: 9,804 * 50 = 490,200 satoshis
Dust that should go to last eligible holder: 500,000 - 490,200 = 9,800 satoshis
But if last holder is restricted: 9,800 satoshis not distributed
Execute distributeYield(500000)
Contract receives 500,000 satoshis via
transferFromProcess holders loop:
First 9,804 eligible holders each receive 50 satoshis
distributedSum = 9,804 * 50 = 490,200satoshisLast holder (restricted):
if (_isYieldAllowed(lastHolder)) { // Returns false // Skipped - 9,800 satoshis remain undistributed }
Event emitted:
YieldDistributed(490200, WBTC)— incorrect amount9,800 satoshis permanently locked in contract
Conclusion
The distribution logic assumes the last holder is eligible to receive the dust. When that holder is restricted, the dust is neither redistributed nor recoverable, causing permanent lock of yield tokens and incorrect accounting/reporting. The issue affects both actual distribution and preview calculations and scales with the number of tokens and distributions over time.
Was this helpful?