# #41938 \[SC-Critical] Unstake process manipulation and reward distribution vulnerability

**Submitted on Mar 19th 2025 at 14:07:25 UTC by @pontifex for** [**Audit Comp | Yeet**](https://immunefi.com/audit-competition/audit-comp-yeet)

* **Report ID:** #41938
* **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

The `StakeV2.accumulatedDeptRewardsYeet` function wrongly considers tokens locked for the `VESTING_PERIOD` as the accumulated rewards. So locked tokens can be distributed to the vault and then claimed as rewards. This causes that part of users can't unstake the full amount of their staked tokens.

## Vulnerability Details

The `StakeV2` tracks the virtual amount of staked tokens in the `totalSupply` variable:

```solidity
    function stake(uint256 amount) external {
        require(amount > 0, "Amount must be greater than 0");
        _updateRewards(msg.sender);

        stakingToken.transferFrom(msg.sender, address(this), amount);

        balanceOf[msg.sender] += amount;
>>      totalSupply += amount;
        emit Stake(msg.sender, amount);
    }
```

The contract owner can distribute excess rewards to the vault via the `executeRewardDistributionYeet` function. This function calculates the distributed amount as a difference between the contract balance an the `totalSupply` variable:

```solidity
    function accumulatedDeptRewardsYeet() public view returns (uint256) {
>>      return stakingToken.balanceOf(address(this)) - totalSupply;
    }
```

The problem is that when users start the unstake process the `totalSupply` variable is also decreased by the `unStakeAmount`, but the contract balance is not changed.

```solidity
    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 way the `unStakeAmount` value can be distributed via the `executeRewardDistributionYeet` function.\
When users finalize the unstake process the contract balance becomes less than the `totalSupply` and users' stakes becomes undercollateralized.

```solidity
    function _unstake(uint256 index) private {
        Vesting memory vesting = vestings[msg.sender][index];

        (uint256 unlockedAmount, uint256 lockedAmount) = calculateVesting(vesting);
        require(unlockedAmount != 0, "No unlocked amount");

>>      stakingToken.transfer(msg.sender, unlockedAmount);
>>      stakingToken.transfer(address(0x000000dead), lockedAmount);
        _remove(msg.sender, index);
        if (lockedAmount > 0) {
            emit RageQuit(msg.sender, unlockedAmount, lockedAmount, index);
        } else {
            emit Unstake(msg.sender, unlockedAmount, index);
        }
        stakedTimes[msg.sender]--;
    }
```

I suggest tracking locked amounts in a separate variable and taking it into account in the `accumulatedDeptRewardsYeet` function.

## Impact Details

Loss of user funds: Users may not be able to retrieve their staked tokens, resulting in a loss of funds.\
Undercollateralization: The contract's balance becomes less than the totalSupply, causing users' stakes to become undercollateralized.

## References

<https://github.com/immunefi-team/audit-comp-yeet/blob/da15231cdefd8f385fcdb85c27258b5f0d0cc270/src/StakeV2.sol#L149\\>
<https://github.com/immunefi-team/audit-comp-yeet/blob/da15231cdefd8f385fcdb85c27258b5f0d0cc270/src/StakeV2.sol#L255>

## Proof of Concept

## Proof of Concept

1. Suppose the `totalSupply` is 1000 tokens stacked by innocent users.
2. Suppose there are 100 tokens as accumulated rewards. So the `stakingToken.balanceOf(address(this))` is 1100.
3. An attacker stakes another 1000 tokens.
4. Now the `totalSupply` is 2000 tokens and the `stakingToken.balanceOf(address(this))` is 2100.
5. The attacker starts the unstake process for 500 tokens.
6. Now the `totalSupply` is 1500 tokens and the `stakingToken.balanceOf(address(this))` is still 2100.
7. The attacker waits for the moment when the contract owner distributes excess rewards (100) and locked (500) to the vault.
8. Now the `totalSupply` is 1500 tokens and the `stakingToken.balanceOf(address(this))` is 1500.
9. Then the attacker finalizes the unstake process for the first 500 tokens.
10. Now the `totalSupply` is 1500 tokens and the `stakingToken.balanceOf(address(this))` is 1000.
11. Then the attacker claims rewards and receives about 500 / 1500 part of distributed rewards (600), i.e. \~200 tokens.
12. Then the attacker starts the unstake process for another 500 tokens and finalizes it after the `VESTING_PERIOD`.
13. Now the `totalSupply` is 1000 tokens and the `stakingToken.balanceOf(address(this))` is 500.
14. The attacker has profit and the `totalSupply` is undercollateralized.
