49941 sc low permanent freezing of yield tokens due to flawed check in distribution logic
Submitted on Jul 20th 2025 at 16:52:50 UTC by @perseverance for Attackathon | Plume Network
Report ID: #49941
Report Type: Smart Contract
Report severity: Low
Target: https://github.com/immunefi-team/attackathon-plume-network/blob/main/arc/src/ArcToken.sol
Impacts:
Permanent freezing of funds
Description
Short summary
The distributeYield functions in ArcToken.sol unconditionally pull yield tokens into the contract before checking for edge case conditions (such as no eligible holders or effectiveTotalSupply is 0). If these conditions are met, the functions exit prematurely, leaving the transferred yield tokens permanently locked within the contract, as there is no function to withdraw them. This leads to an irreversible loss of funds for the yield distributor.
Background Information
The ArcToken.sol contract provides functionality to distribute yield tokens (e.g., USDC) to its holders. This is handled by two primary functions: distributeYield for a one-shot distribution and distributeYieldWithLimit for a paginated distribution to avoid gas limits. Both functions are intended to be called by an account with the YIELD_DISTRIBUTOR_ROLE. The core logic involves calculating each holder's share based on their token balance and transferring the yield token to them.
The critical part of the logic is as follows: https://github.com/immunefi-team/attackathon-plume-network/blob/main/arc/src/ArcToken.sol#L411-L428
// File: arc/src/ArcToken.sol
function distributeYield(
uint256 amount
) external onlyRole(YIELD_DISTRIBUTOR_ROLE) nonReentrant {
// ...
// The funds are transferred IN before any checks
yToken.safeTransferFrom(msg.sender, address(this), amount);
uint256 holderCount = $.holders.length();
uint256 holderCount = $.holders.length();
if (holderCount == 0) { // <-- @audit-issue VULNERABILITY HERE
emit YieldDistributed(0, yieldTokenAddr);
return;
}
uint256 effectiveTotalSupply = 0;
for (uint256 i = 0; i < holderCount; i++) {
address holder = $.holders.at(i);
if (_isYieldAllowed(holder)) {
effectiveTotalSupply += balanceOf(holder);
}
}
if (effectiveTotalSupply == 0) { // <-- @audit-issue VULNERABILITY HERE
emit YieldDistributed(0, yieldTokenAddr);
return;
}
//...
}The vulnerability
Vulnerability Details
The vulnerability lies in a critical logic flaw where funds are accepted before verifying that they can be distributed.
Note
The function distributeYieldWithLimit correctly pulls yield token after the check effectiveTotalSupply is 0. The vulnerability exists in distributeYield specifically.
Potential Failure Scenario
The Consequence
The
distributeYieldfunction first pulls the 1,000,000 USDC from the distributor into theArcTokencontract.The function then loops through all holders to calculate
effectiveTotalSupply. Because every holder is now restricted (_isYieldAllowedreturnsfalsefor all), the calculatedeffectiveTotalSupplyis0.The check
if (effectiveTotalSupply == 0)becomes true.The function emits
YieldDistributed(0, yieldTokenAddr)andreturns.
Severity assessment
Bug Severity: Medium
Impact category: Critical
Permanent freezing of funds: although the likelihood is low, the consequence is catastrophic if it occurs. The reporter assesses overall severity as Medium.
Suggested Fix / Remediation
Follow the logic in distributeYieldWithLimit: perform all sanity checks (holders count, effectiveTotalSupply) before pulling tokens from the distributor. Alternatively, add a safe rescue mechanism (e.g., recoverERC20) restricted to a governance/admin role to recover tokens accidentally sent to the contract—but only if this is acceptable given the protocol's security model and trust assumptions.
Proof of Concept
The sequence below illustrates how the Yield Distributor loses their funds.
Consequence
The
distributeYieldfunction first pulls the 1,000,000 USDC from the distributor into theArcTokencontract.The function then loops through all holders to calculate
effectiveTotalSupply. Because every holder is now restricted (_isYieldAllowedreturnsfalsefor all), the calculatedeffectiveTotalSupplyis0.The check
if (effectiveTotalSupply == 0)becomes true.The function emits
YieldDistributed(0, yieldTokenAddr)andreturns.
Was this helpful?