#41981 [SC-Critical] Loss of user funds during unstaking, while under the lockup period
Submitted on Mar 19th 2025 at 18:59:12 UTC by @Oxodus for Audit Comp | Yeet
Report ID: #41981
Report Type: Smart Contract
Report severity: Critical
Target: https://github.com/immunefi-team/audit-comp-yeet/blob/main/src/StakeV2.sol
Impacts:
Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Description
Brief/Intro
Users can lose their funds when they are unstaking and their funds are in lockup because the funds will appear as accumulated rewards and therefore can be distributed as rewards to other users, causing the users who have unstaked their tokens to lose their funds.
Vulnerability Details
In the StakeV2 contract, users are allowed to stake their yeet tokens and receive rewards. Users can also unstake their tokens by calling the startUnstake function, which prepares the funds for unstaking by locking the funds for the vesting period. However, while preparing for the unstaking, the function also has the following line of code totalSupply -= unStakeAmount with removes the amount to be unstaked from the contract's token supply of the staking token but doesn't send them to the user since they are still under lockup period. In the same contract, we also have a function StakeV2::accumulatedDeptRewardsYeet which according to the docs is meant to:
The function used to calculate the accumulated rewards that gets return by the zapper since swaps are not 100% efficient
and:
@return The accumulated rewards in YEET tokens
function accumulatedDeptRewardsYeet() public view returns (uint256) {
return stakingToken.balanceOf(address(this)) - totalSupply;
}The function above instead of only accounting for the staking tokens that are returned by the zapper, it will also include the user funds that are in lockup and are awaiting the vesting period to end. The issue with this function is that the return value(Which will include the users funds), is called by the executeRewardDistributionYeet which distributes the rewards to the users
uint256 accRevToken0 = accumulatedDeptRewardsYeet();This will effectively distribute the users funds as rewards to other users who have staked in the contract. Although the executeRewardDistributionYeet is a privileged function, this is still an issue because the function will always behave as not expected and cause the user to lose their funds.
Impact Details
The impact of this issue is critical since users with their stake awaiting the vesting period to end so that they can regain their funds will end up losing their funds when the manager calls the executeRewardDistributionYeet intending to distribute any rewards in the contract due to the incorrect accounting of the accumulatedDeptRewardsYeet() function.
References
https://github.com/immunefi-team/audit-comp-yeet/blob/main/src/StakeV2.sol#L148 https://github.com/immunefi-team/audit-comp-yeet/blob/main/src/StakeV2.sol#L158 https://github.com/immunefi-team/audit-comp-yeet/blob/main/src/StakeV2.sol#L255
Proof of Concept
Proof of Concept
Lets assume :
John has
100e18yeet tokensAlice has
80e18yeet tokens tooThe
StakeV2has 0 yeet token balance
John and Alice both deposit
100e18and80e18yeet tokens respectively into the contract by calling thestakefunction. Their new balances are0each, and thestakeV2yeet balance is180e18tokens ieaccumulatedDeptRewardsYeet()returns0andtotalsupplyis180e18Currently the
rewardIndexinStakeV2is 0As users yeet via the
Yeetcontract, a portion of their funds is transfered to theStakeV2contract as reward for the stakers (Won't focus on the amounts)The manager calls
executeRewardDistributionfunction to distribute the rewards to stakers, the function zaps Into the zapper and then shares minted by the vault to theStakeV2contract.In the zapping process:
yeet tokens gets returned by the zapper since swaps are not 100% efficient
As users continue to yeet, the
executeRewardDistributionis called by the manager to distribute the rewards and more yeet tokens continue to accumulate in theStakeV2contract and theaccumulatedDeptRewardsYeetfunction returns> 0say maybe500e18since there are yeet tokens accumulated in the contract. Now the manager want to call theexecuteRewardDistributionYeetto distribute the accumulated yeet token rewards but,John unstakes their
100e18yeet tokens by calling thestartUnstakewhich subtracts the amount to be unstaked from thetotalsupplyand the funds are locked for theVESTING_PERIODwhich is currently set to10 days.So now the
accumulatedDeptRewardsYeetwhich initially was returning500e18now returns600e18since John's tokens were subtracted from thetotalSupplybut are still in the contract.The manager calls the
accumulatedDeptRewardsYeetto see how many tokens have accumulated, and the function returns600e18.The manager proceeds to call
executeRewardDistributionYeetto distribute the accumulated yeet tokens to the stakers with theswap.inputamount as600e18. When the transaction finishes executing, the new yeet balance in the contract is80e18, thetotalSupplyis 80e18 ie Alices tokens . TheaccumulatedDeptRewardsYeetreturns 0 since
stakingToken.balanceOf(address(this)) - totalSupplyis 0.
John vesting time is over and heads over to
unstaketo finish the unstaking, however the transfer fails since the contract doesn't have john's funds. They were distributed as rewards to the users who have staked since the contract doesn't have a mechanism of accounting for the unstaked tokens that are under their vesting period. Therefore John loses their funds.This accounting mismatch will always cause users in their vesting period to lose funds when the manager calls
accumulatedDeptRewardsYeetsince they will always accounted asrewards form the zapper. Consider adding a mechanism to keep track of the total yeet tokens that are locked while awaiting their vesting period to elapse to avoid this issue
Was this helpful?