#41974 [SC-Critical] Reducing `totalSupply` in `startUnstake` leads to protocol insolvency
Was this helpful?
Was this helpful?
Submitted on Mar 19th 2025 at 18:04:28 UTC by @Oxl33 for
Report ID: #41974
Report Type: Smart Contract
Report severity: Critical
Target: https://github.com/immunefi-team/audit-comp-yeet/blob/main/src/StakeV2.sol
Impacts:
Protocol insolvency
Description:
When a staker calls StakeV2::startUnstake
function, totalSupply
is decreased by unStakeAmount
:
The issue arises when manager calls StakeV2::executeRewardDistributionYeet
function, which calls accumulatedDeptRewardsYeet
:
As you can see, this function returns the difference between StakeV2
's YEET balance and totalSupply
. In the case where there is at least a single user in the process of unstaking (waiting for vesting to complete), the returned value of this function will be bigger than it should be, due to totalSupply
being decreased. This will lead to more YEET tokens being approved to Zapper
and more tokens transferred out (users that started unstaking process did NOT receive their YEET tokens yet).
This issue is only possible if the manager's inputted stakingParams.amount0Max
and swapData.inputAmount
parameters do not account for the wrong return value of accumulatedDeptRewardsYeet
function. I think manager will not account for this when calculating the input parameters, because the only logical way to determine the amount of YEET to transfer out is to use accumulatedDeptRewardsYeet
function. Because why would the manager use a different way to decide the amount of YEET to transfer, if this is the way used to decide how many tokens to approve? Approving more than actually gets transferred is also a dangerous practice and I strongly believe nobody would do this on purpose.
Tokens being transferred here:
In the above code snippet you can see that the tokens which are in unstaking process (alongside the actual reward amount) will be sent to Zapper
and ultimately they will end up being deposited into Beradrome farm and all other stakers will claim them as rewards as time goes by.
There is no way to emergency-withdraw these funds, because all the shares of MoneyBrinter
vault are held by StakeV2
contract, and can only be accessed by the stakers, when they claim rewards.
When the user, whose funds got sent to Zapper
, calls StakeV2::_unstake
(let's assume after the full vesting period), the outcome will be one of these:
The user will receive YEET that other users staked and, if everyone wants to unstake, the last staker's transaction will revert, due to StakeV2
being out-of-funds
If the user is the last staker left, the user's transaction will revert, due to StakeV2
being out-of-funds
If the user had staked a huge amount of YEET (more than all other stakers combined), the user's transaction will revert, due to StakeV2
being out-of-funds
If this situation were to happen, you would have no good way to fix it. Only thing you could do is to send the amount of YEET that is missing (out of your own pocket) to StakeV2
contract, and let the user(s) unstake it.
Impact:
Protocol insolvency, due to mismanagement of user funds.
Recommended Mitigation:
Consider not decreasing totalSupply
in startUnstake
function, and instead decrease it in _unstake
function. This way accumulatedDeptRewardsYeet
function will return the correct value and the issue will be solved.
Example fix:
Proof of Concept:
Consider this scenario:
Alice stakes 10_000_000e18
YEET tokens into StakeV2
, because she likes the project and wants to receive a big portion of the rewards
Some time passes, Alice participates in the project, many other users also stake their YEET tokens
Alice calls StakeV2::startUnstake
function, inputting unStakeAmount
equal to her initial staked amount
Few days (up to VESTING_PERIOD
) pass and manager decides to call executeRewardDistributionYeet
, because there are some (e.g. 100_000e18
) YEET tokens ready to be distributed as rewards
accumulatedDeptRewardsYeet
function should return 100_000e18
, but instead returns 10_100_000e18
, so the approved amount and the manager's input are much bigger (for the reasons I described above)
10_100_000e18
YEET tokens get sent to Zapper
and end up being deposited into Beradrome farm
Alice's vesting period ends and she calls StakeV2::_unstake
, but her transaction reverts, because her staked amount was more than the remaining YEET balance of StakeV2
contract, so the contract is out-of-funds
Alice gets left with nothing and her 10_000_000e18
YEET tokens get claimed as rewards by the remaining stakers