#42469 [SC-Critical] Incorrect computation of excess rewards leads to permanent freezing of user funds
Submitted on Mar 24th 2025 at 07:07:53 UTC by @Oxgee001 for Audit Comp | Yeet
Report ID: #42469
Report Type: Smart Contract
Report severity: Critical
Target: https://github.com/immunefi-team/audit-comp-yeet/blob/main/src/StakeV2.sol
Impacts:
Permanent freezing of funds
Description
Brief/Intro
The StakeV2 contract contains a critical accounting error in its unstaking mechanism where tokens under vesting are incorrectly counted as excess rewards. This allows these vesting tokens to be distributed as rewards through executeRewardDistributionYeet
, permanently freezing user funds that are supposed to be released after the vesting period.
Vulnerability Details
The vulnerability stems from an accounting mismatch between the contract's totalSupply
and actual token balance handling during the unstaking process. When users call startUnstake()
, the function reduces totalSupply
immediately but keeps the actual tokens in the contract until the vesting period ends:
function startUnstake(uint256 unStakeAmount) external {
// ... checks ...
balanceOf[msg.sender] -= unStakeAmount;
totalSupply -= unStakeAmount; // Reduces totalSupply but tokens remain in contract
vestings[msg.sender].push(Vesting(unStakeAmount, start, end));
// ... other logic ...
}
However, the accumulatedDeptRewardsYeet()
function calculates excess rewards incorrectly:
function accumulatedDeptRewardsYeet() public view returns (uint256) {
return stakingToken.balanceOf(address(this)) - totalSupply;
}
This calculation incorrectly considers vesting tokens as excess rewards since they're included in the contract's balance but excluded from totalSupply
. These "excess" tokens can then be distributed through executeRewardDistributionYeet
for reward distribution:
function executeRewardDistributionYeet(
IZapper.SingleTokenSwap calldata swap,
IZapper.KodiakVaultStakingParams calldata stakingParams,
IZapper.VaultDepositParams calldata vaultParams
) external onlyManager nonReentrant {
uint256 accRevToken0 = accumulatedDeptRewardsYeet();
require(accRevToken0 > 0, "No rewards to distribute");
stakingToken.approve(address(zapper), accRevToken0);
// ... proceeds to transfer tokens ...
}
These tokens are permanently converted to LP positions and vault shares through the zapper, with no mechanism to convert them back for unstaking. Once distributed as rewards, the tokens become inaccessible to the original vesting users who are waiting for their vesting period to end.
Impact Details
All users with active vesting entries will permanently lose access to their tokens if executeRewardDistributionYeet is called during their vesting period. The tokens are irreversibly converted to LP positions and vault shares, making them permanently frozen and inaccessible to the original vesting users. This could affect the entire contract balance if multiple users are in the vesting state when rewards are distributed, leading to a complete freeze of vested funds with no possibility of recovery.
References
https://github.com/immunefi-team/audit-comp-yeet/blob/da15231cdefd8f385fcdb85c27258b5f0d0cc270/src/StakeV2.sol#L255 https://github.com/immunefi-team/audit-comp-yeet/blob/da15231cdefd8f385fcdb85c27258b5f0d0cc270/src/StakeV2.sol#L148 https://github.com/immunefi-team/audit-comp-yeet/blob/da15231cdefd8f385fcdb85c27258b5f0d0cc270/src/StakeV2.sol#L153
Proof of Concept
Proof of Concept
Consider this scenario:
Alice stakes 1000 tokens
Alice calls
startUnstake(1000)
, which:Reduces totalSupply by 1000
Creates a vesting entry for 1000 tokens
The 1000 tokens remain in the contract
accumulatedDeptRewardsYeet()
now returns 1000 (contract balance 1000 - totalSupply 0)Manager calls
executeRewardDistributionYeet
, which:Sees 1000 tokens as "excess rewards"
Converts these 1000 tokens to LP positions and vault shares
After vesting period ends, Alice tries to unstake but fails because:
Her 1000 tokens were already converted and distributed
The contract has 0 tokens to fulfill her withdrawal
Was this helpful?