#41640 [SC-High] Stuck Rewards in StakeV2 Contract Due to Improper Handling of Leftover Tokens
Was this helpful?
Was this helpful?
Submitted on Mar 17th 2025 at 07:59:42 UTC by @DoD4uFN for
Report ID: #41640
Report Type: Smart Contract
Report severity: High
Target: https://github.com/immunefi-team/audit-comp-yeet/blob/main/src/StakeV2.sol
Impacts:
Permanent freezing of unclaimed yield
The StakeV2
contract interacts with the Zapper
contract to facilitate reward claiming. However, the zapOut
, zapOutToToken0
, and zapOutToToken1
functions in Zapper
send leftover tokens to both the user and _msgSender()
, which in this case is StakeV2
. Since StakeV2
has no mechanism to retrieve or use these tokens, they remain stuck in the contract indefinitely. This results in an unintended loss of user rewards and inefficient token management.
The issue arises from how the Zapper
contract distributes leftover tokens after executing a reward claim. The zapOutToToken0
, zapOutToToken1
, and zapOut
functions send a portion of the tokens to the receiver (the user) and another portion to _msgSender()
, which is StakeV2
. However, StakeV2
does not have a function to recover or utilize these tokens, leading to a permanent loss of funds.
In StakeV2
, the claim functions call zapOut
functions from Zapper
:
Similarly, in Zapper
, the zapOutToToken0
function distributes leftover token1Debt
to _msgSender()
:
Since _msgSender()
is StakeV2
, these tokens get stuck in the contract indefinitely.
User funds are partially lost: A portion of rewards intended for the user is permanently locked in StakeV2
.
Contract accumulates unusable tokens: Over time, the contract accumulates stuck tokens, which can affect the efficiency of reward distribution.
This issue could result in significant financial losses depending on the volume of rewards being claimed over time.
Modify the Zapper
contract to ensure that all leftover tokens are returned exclusively to the user, not _msgSender()
. Example fix:
Deploy StakeV2
and Zapper
contracts.
Stake tokens and accumulate rewards.
Call claimRewardsInToken0()
, claimRewardsInToken1()
, or claimRewardsInToken()
with swapData.inputAmount
less than tokenXDebt
, which leads to tokenXDebt
being non-zero.
Observe that part of the tokens sent to _msgSender()
(i.e., StakeV2
) are never recoverable.
Check the balance of StakeV2
—tokens remain stuck indefinitely.
This PoC demonstrates that rewards are not fully transferred to users, leading to locked funds.