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
Proof of Concept
A step-by-step reproducible scenario showing the mismatch between event and actual transfers:
Setup test tokens
- Deploy a simple ERC20 test token
YieldTokenand mint 1 unit to the distributor addressD. - Ensure
Dhas approved the ArcToken contract to spend 1YieldToken(approveArcTokenfor 1).
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.
Ensure distributor role
- Give
DtheYIELD_DISTRIBUTOR_ROLEon the ArcToken contract.
Call the function
From D, call:
distributeYieldWithLimit(1 /* totalAmount */, 0 /* startIndex */, 2 /* maxHolders */)
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.