51910 sc low inconsistent yield token transfer logic causes permanent loss of yield in distributeyield
- Report ID: #51910
- Report Type: Smart Contract
- Report severity: Low
- Target: https://github.com/immunefi-team/attackathon-plume-network/blob/main/arc/src/ArcToken.sol
- Impacts:
- Theft of unclaimed yield
Description
Brief / Intro
When distributing yield by calling ArcToken::distributeYield() while effectiveTotalSupply == 0, the yield tokens are still sent to the contract even though there are no addresses to receive them. These tokens will be stuck in the contract.
Vulnerability Details
Both yield distribution functions ensure totalSupply() is greater than zero (otherwise they revert). However, effectiveTotalSupply can still be zero when all addresses holding ArcTokens are yield-restricted.
ArcToken::distributeYieldWithLimit()checks thateffectiveTotalSupplyis greater than zero before transferring the yield tokens to the contract, and returns early if it's zero.ArcToken::distributeYield()transfers the yield token amount to the contract first, then checks ifeffectiveTotalSupply == 0and may return early.
Because distributeYield() performs the token transfer before the effectiveTotalSupply check, tokens can be sent into the contract when there are no eligible recipients, leaving them permanently stuck. This behavior is inconsistent with distributeYieldWithLimit().
Impact Details
ArcToken::distributeYield() transfers the yield token amount to the contract even when effectiveTotalSupply == 0. This results in yield tokens being permanently stuck in the contract and is inconsistent with the guarded flow used in ArcToken::distributeYieldWithLimit().
References
Code references are provided throughout the original report and implementation:
- distributeYield source: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/arc/src/ArcToken.sol#L388-L460
- totalSupply() check: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/arc/src/ArcToken.sol#L397-L400
- yield transfer in distributeYield: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/arc/src/ArcToken.sol#L407-L408
- effectiveTotalSupply early return in distributeYield: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/arc/src/ArcToken.sol#L425-L428
- distributeYieldWithLimit conditional transfer: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/arc/src/ArcToken.sol#L518-L528
Proof of Concept
Reproduce distribution when no eligible holders exist
Call ArcToken::distributeYield() to distribute yield to token holders while skipping restricted accounts.
Assume there are currently no non-restricted holders.
totalSupply() check passes and tokens are transferred
totalSupply() of ArcToken is greater than zero, so the call proceeds.
The yield token amount is then sent to the contract:
effectiveTotalSupply == 0 triggers early return but tokens are already stuck
After the transfer, the function checks effectiveTotalSupply. If it equals zero, the function returns early:
Because the tokens were transferred to the contract before this check, they remain stuck with no eligible recipients. This contrasts with distributeYieldWithLimit(), which performs the effective supply check before transferring tokens.
Suggested Mitigation (non-exhaustive)
- Make the
effectiveTotalSupply == 0check before performing any token transfers indistributeYield(), aligning its flow withdistributeYieldWithLimit(). - Alternatively, revert early (or skip transfer) if there are no eligible recipients to avoid tokens being stranded in the contract.