#41365 [SC-Critical] Vested tokens are counted as accumulated revenue
Submitted on Mar 14th 2025 at 11:42:08 UTC by @armormadeofwoe for Audit Comp | Yeet
Report ID: #41365
Report Type: Smart Contract
Report severity: Critical
Target: https://github.com/immunefi-team/audit-comp-yeet/blob/main/src/StakeV2.sol
Impacts:
Protocol insolvency
Theft of unclaimed yield
Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Description
Brief/Intro
accumulatedDeptRewardsYeet
incorrectly counts all vested staking tokens as revenue, allowing them to be swapped for vault shares and distributed as rewards to all stakers.
Vulnerability Details
accumulatedDeptRewardsYeet
considers the difference between the live stakingToken.balanceOf(address(this))
and the user-deposited totalSupply
as revenue.
Afterwards this amount can be swapped for additional rewards via executeRewardDistributionYeet
The issue here lies within startUnstake
, due to it subtracting the unstaked tokens from the totalSupply
without transferring or accounting the stakingToken
they should receive.
So what happened is:
User started and unstake which vests their token for 10 days
totalSupply
went down, however no token transfers took placeCalling
accumulatedDeptRewardsYeet
will return value equal to the vested amount
Let's see a practical example.
Assume there is no revenue, balanceOf(address(StakingV2)) == totalSupply == 100 tokens
Bob unstakes his 60 tokens by calling
startUnstake
totalSupply = 100 - 60 = 40
, 10 day vesting period starts for BobAdmin calls
executeRewardDistributionYeet
with input amount ==accumulatedDeptRewardsYeet == 60
60 staking tokens are swapped through the Zapper for vault shares
_handleVaultShares
handles reward distribution by updating the reward index.10 days pass
Bob calls
unstake
, however the method reverts sincebalanceOf(address(StakingV2)) = 40
whileunlockedAmount = 60
NB! This example is severely simplified for illustrative purposes. One might argue that the admin would not perform the call, knowing that there is no current revenue. However under normal working conditions, the protocol will have hundreds/thousands of actors and it is not possible to track which funds are part of the vestings and which are accumulated revenue.
Impact Details
Protocol becomes insolvent since balanceOf(address(StakingV2))
< owed amount to unstaking parties.
Last person to withdraw their funds will be unable to due to insufficient balance.
Rewards had been stolen and distributed to other Yeet participants.
References
https://github.com/immunefi-team/audit-comp-yeet/blob/da15231cdefd8f385fcdb85c27258b5f0d0cc270/src/StakeV2.sol#L148-L150
https://github.com/immunefi-team/audit-comp-yeet/blob/da15231cdefd8f385fcdb85c27258b5f0d0cc270/src/StakeV2.sol#L158-L160
https://github.com/immunefi-team/audit-comp-yeet/blob/da15231cdefd8f385fcdb85c27258b5f0d0cc270/src/StakeV2.sol#L255
Proof of Concept
Proof of Concept
Attaching step-by-step from main body
Let's see a practical example.
Assume there is no revenue, balanceOf(address(StakingV2)) == totalSupply == 100 tokens
Bob unstakes his 60 tokens by calling
startUnstake
totalSupply = 100 - 60 = 40
, 10 day vesting period starts for BobAdmin calls
executeRewardDistributionYeet
with input amount ==accumulatedDeptRewardsYeet == 60
60 staking tokens are swapped through the Zapper for vault shares
_handleVaultShares
handles reward distribution by updating the reward index.10 days pass
Bob calls
unstake
, however the method reverts sincebalanceOf(address(StakingV2)) = 40
whileunlockedAmount = 60
Was this helpful?