51802 sc low temporary freeze of rewards is possible if efficientsupply 0

Submitted on Aug 5th 2025 at 21:33:32 UTC by @Santi for Attackathon | Plume Network

  • Report ID: #51802

  • 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 rewards

Description

Brief/Intro

Rewards tokens may be stuck on the contract. This can happen if the effectiveSupply for distribution is equal to 0.

Vulnerability Details

For yield token distribution, the user with role YIELD_DISTRIBUTOR_ROLE must call ArcToken.distributeYield() or ArcToken.distributeYieldWithLimit().

The function ArcToken.distributeYield() transfers rewards token before the effectiveTotalSupply == 0 check, which returns from the function if it is true.

Code snippet:

function distributeYield(
        uint256 amount
    ) external onlyRole(YIELD_DISTRIBUTOR_ROLE) nonReentrant {
        ArcTokenStorage storage $ = _getArcTokenStorage();

.... 

        ERC20Upgradeable yToken = ERC20Upgradeable(yieldTokenAddr);
        yToken.safeTransferFrom(msg.sender, address(this), amount); // <- transfer yield tokens to contract

        uint256 distributedSum = 0;
        uint256 holderCount = $.holders.length();
        if (holderCount == 0) {
            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); // <- calculate effectiveTotalSupply
            }
        }

        if (effectiveTotalSupply == 0) {
            emit YieldDistributed(0, yieldTokenAddr);
            return; // <- return  from function if effectiveSupply == 0, but contract stores yield tokens.
        }

        
...
    }

It is incorrect to transfer yield tokens before the effectiveTotalSupply check. If effectiveSupply is equal to zero (for example, if all holders are blacklisted for yield distribution), the yield tokens will be stuck on the contract.

For comparison, ArcToken.distributeYieldWithLimit() checks that effectiveSupply != 0 before tokens transfer. Link: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/arc/src/ArcToken.sol#L518-L528

Impact Details

It is possible to lose all yield tokens that the YIELD_DISTRIBUTOR_ROLE attempted to distribute. This is a rare case because:

  • All token holders must be ineligible for yield tokens (e.g., blacklisted).

  • YIELD_DISTRIBUTOR_ROLE must call this function when all holders are ineligible.

Recovery is possible via ArcToken.distributeYieldWithLimit(), but with constraints:

  • A new token holder must appear, and the new token holder must be at index > 0 (for example, it can be admin).

  • The new token holder must not be blacklisted.

Then YIELD_DISTRIBUTOR_ROLE can call ArcToken.distributeYieldWithLimit() and pass the correct totalAmount, startIndex, maxHolders.

Recovery is possible, but yield tokens will be temporarily frozen and distribution will be incorrect. That is why this finding is rated Low and ArcToken.distributeYield() should handle this case correctly.

References

  • distributeYield() function: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/arc/src/ArcToken.sol#L388

Proof of Concept

Was this helpful?