52303 sc insight incorrect yield distribution event emission
Submitted on Aug 9th 2025 at 16:03:03 UTC by @TheCarrot for Attackathon | Plume Network
Report ID: #52303
Report Type: Smart Contract
Report severity: Insight
Target: https://github.com/immunefi-team/attackathon-plume-network/blob/main/arc/src/ArcToken.sol
Impacts:
Contract fails to deliver promised returns, but doesn't lose value
Description
Brief/Intro
In distributeYieldWithLimit the contract emits YieldDistributed(totalAmount, yieldTokenAddr) when nextIndex == 0. However, the function actually transfers amountDistributed (which can be < totalAmount due to integer-division rounding or restricted holders). The event therefore can report an amount that was not actually sent.
Vulnerability Details
Inside distributeYieldWithLimit the code accumulates amountDistributed as it transfers per-holder shares:
uint256 share = (totalAmount * holderBalance) / effectiveTotalSupply;
if (share > 0) {
yToken.safeTransfer(holder, share);
amountDistributed += share;
}
...
if (nextIndex == 0) {
emit YieldDistributed(totalAmount, yieldTokenAddr); // <-- incorrect
}The bug: the emitted event uses totalAmount (the amount taken from the distributor at the start of a distribution run) rather than amountDistributed (the sum of actual transfers performed). When rounding (integer division) or restricted accounts reduce or zero-out per-holder shares, amountDistributed can be strictly less than totalAmount, producing a discrepancy between the event and on-chain token movements.
Impact Details
Emits incorrect distributed amount in final batch
Creates discrepancy between actual transfers and event logs
May mislead off-chain monitoring systems
References
https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/arc/src/ArcToken.sol#L551
Proof of Concept
A step-by-step reproducible scenario showing the mismatch between event and actual transfers:
Setup ArcToken holders
Ensure the ArcToken contract has two holders
H1andH2recorded in itsholdersset, each with ArcToken balances of1token (sobalanceOf(H1) = 1andbalanceOf(H2) = 1).Ensure
H1andH2are eligible for yield calculation (i.e., not both restricted) so thateffectiveTotalSupply = 2.
Observe execution behavior
At function start (because
startIndex == 0) the contract executesyToken.safeTransferFrom(D, address(this), 1), contract now holds 1YieldToken.For each holder: compute
share = (totalAmount * holderBalance) / effectiveTotalSupply. Hereshare = (1 * 1) / 2 = 0due to integer division truncation. Therefore neither holder receives anyYieldTokenandamountDistributedremains 0.
Event vs reality
Because
nextIndex == 0(we processed all holders in the single batch), the contract emits:
YieldDistributed(1, yieldTokenAddr)which claims 1 was distributed.
But in reality
amountDistributed == 0and nosafeTransfercalls were made toH1orH2. The contract balance remains 1YieldToken.
Was this helpful?