#41549 [SC-Critical] users funds can get lost when the executeRewardDistributionYeet function invoked after users unstake
Submitted on Mar 16th 2025 at 13:29:42 UTC by @zeroK for Audit Comp | Yeet
Report ID: #41549
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
Smart contract unable to operate due to lack of token funds
Description
Brief/Intro
when users invokes the startUnstake function they request unstake, invoking this function decrease the balance of user and the totalSupply by the unstake amount, then after 10 days of invoking this function, users are able to transfer their yeet tokens back, however this is not always the case when the executeRewardDistributionYeet function get invoked after unstake function called by users, since the executeRewardDistributionYeet swaps the specified input amount + amount0Max which can be less or equal to the accumulated rewards, this function can transfer the unstake amount of the users by mistake by the manager since the totalSupply decreased but yet the users token not transferred to unstake addresses. more details in Vulnerability Details
Vulnerability Details
if users want to unstake, they have two way to do so, rageQuit and lose around 50% of the staked amount(not very desired) and the second method is invoking the startUnstake function below:
function startUnstake(uint256 unStakeAmount) external {
require(unStakeAmount > 0, "Amount must be greater than 0");
require(stakedTimes[msg.sender] < STAKING_LIMIT, "Amount must be less then the STAKING_LIMIT constant"); // DOS protection https://github.com/Enigma-Dark/Yeet/issues/12
_updateRewards(msg.sender);
uint256 amount = balanceOf[msg.sender];
require(amount >= unStakeAmount, "Insufficient balance");
balanceOf[msg.sender] -= unStakeAmount;
totalSupply -= unStakeAmount;
uint256 start = block.timestamp;
uint256 end = start + VESTING_PERIOD;
vestings[msg.sender].push(Vesting(unStakeAmount, start, end));
stakedTimes[msg.sender]++;
emit VestingStarted(msg.sender, unStakeAmount, vestings[msg.sender].length - 1);
}
this function updates some important storage, one of them which is our core issue is totalSupply that get decreased by amount, but the amount will not get transferred until the vesting period passed(10 days), this will increase the returned amount by the accumulatedDeptRewardsYeet function:
then, the manager invokes call to the executeRewardDistributionYeet function by setting the input + amount0Max == accumulatedDeptRewardsYeet, (keep in mind current logic have issue with verifying and using the yeet token, the issue mentioned here so i explain this report keeping that issue in mind):
the executeRewardDistributionYeet flow including the swapping steps
the issue in above flow is that the manger can by mistake or in purpose invoke the executeRewardDistributionYeet function and setting the swap.inputAmount equal to half of the accumulated amount and the amount0Max equal to the another half of the accumulated amount which includes users unstaked request balance, to make this more clear, expect the scenario below:
current
balanceOf(address(stakeV2)) == $10k worth of yeetand the totalSupply is $9k worth of yeet.Bob decided to unstake and take his yeet token back($3k worth of yeet) by invoking startUnstake, this will decrease the totalSupply to $6k, but, the balanceOf still at $10k since the tokens not transferred yet and bob should wait 10 days to transfer tokens
the manager by mistake invoke calls to the executeRewardDistributionYeet function and setting the
swap.input == 2k and amount0max == 2kso that it can invoke the executeRewardDistributionYeet correctly(due to approve issue in this link https://drive.google.com/file/d/1JK91TgoE_t62RI7lu66V_N517P3AkA9c/view) this way the executeRewardDistributionYeet will swap the yeet token included bob yeet that added to unstake and change it to share token that benefits other stakers expect bob, this is becausebalanceOfbob became zero and he can accumulate rewards anymore:
due to all the flow, there is nothing prevent the manager to invoke call and get shares amount equal to the total of accumulated rewards debt.
this will lead to direct lose of tokens funds that users unstake by manager, its not possible for manager to keep track and calculate the amount without including the unstake change that happened to totalSupply by unstakers.
Impact Details
direct lose of funds due to incorrect calculation in the accumulated reward function
recommend
add a map that track the unstake amount and add its value to the calculation in in the accumulatedDeptRewardsYeet or do not decrease totalSupply until vesting period ends.
Proof of Concept
Proof of Concept
the steps for running valid poc:
first thing stake $10k worth of yeet by different addresses(alice 5k, bob 3k, eve 2k) so that the balanceOf and totalSupply equal to 10k.
eve rageQuit or unstake first so that the totalSupply became 8k and balanceOf became 8k.
the manager or owner or any address transfer 2k worh of yeet directly to the vault, this way the accumulatedRewardDebt function return 10k - 8k = 2k worth of yeet.
bob decide to unstake by invoking the startUnstake, he unstake all his balance which is 3k, the totalSupply became 5k but the yeet still in the stakeV2 contract(not transferred yet)
manager checks the accumulatedRewardDebt and see that the value to use to get share is 5k worht of yeet and decide to invoke call to the executeRewardDistributionYeet funciton with
swap.input = 2.5k and the amount0max = 2.5kwhich later the input amount swapped to token0/token1 and it get used alongside the amount0Max/amount1Max to add liquidity and then adding this liquidity to get shares which later used to calculate rewards for users or stakers.bob now can not transfer his tokens back, even if he does these tokens are alice tokens in the stake contract and the lose goes to alice in this case
Was this helpful?