sherlock-audit / 2024-07-kwenta-staking-contracts-judging

4 stars 4 forks source link

LonWof-Demon - Reward loss for the `staker` Due to Manual `stakeEscrow` Function in `stakingRewardV2.sol`. #136

Closed sherlock-admin2 closed 3 months ago

sherlock-admin2 commented 3 months ago

LonWof-Demon

High

Reward loss for the staker Due to Manual stakeEscrow Function in stakingRewardV2.sol.

Summary

If two users stake the same amount of tokens and then claim rewards, their rewards are vested in to rewardEscrowV2 contract. Users can compound rewards by staking rewards by calling stakeEscrow. This function should allow users to earn more by staking rewards, but in fact, it also dilutes rewards for those who do not want to restake reward tokens. It happens because stakeEscrow increases total supply and thus decreases reward per one token. It is fair for the usual staking flow if users stake more, rewards per token decrease. But in case of stakeEscrow users are not transferring actual KWENTA tokens, they just call escrow balance inside the contract

Vulnerability Detail

https://www.rareskills.io/post/staking-algorithm https://github.com/sherlock-audit/2024-07-kwenta-staking-contracts/blob/main/token/contracts/StakingRewardsV2.sol#L278 https://github.com/sherlock-audit/2024-07-kwenta-staking-contracts/blob/main/token/contracts/StakingRewardsV2.sol#L435 https://github.com/sherlock-audit/2024-07-kwenta-staking-contracts/blob/main/token/contracts/StakingRewardsV2.sol#L447 https://github.com/sherlock-audit/2024-07-kwenta-staking-contracts/blob/main/token/contracts/StakingRewardsV2.sol#L464 https://github.com/sherlock-audit/2024-07-kwenta-staking-contracts/blob/main/token/contracts/StakingRewardsV2.sol#L472

The contract distribute the reward based on totalSuppply and individual balance inside the system to calculate there part of reward share they own, but due to contract current reward distribution logic which is inspired from synthetix due to changes made in that can result in legitimate staker facing reward losses, suppose there are 2 stakers, staker A and B, where both have deposited 50% into the pool lets assume 100 tokens by A and 100 tokens by B,totalSupply = 200, now after sometime they harvest some rewards for simplicity lets assume 10 tokens of USDC and 10 tokens KWENTA, the USDC tokens for both the user are sent to there respective addressess and KWENTA tokens are sent to RewardEscrowV2 which will be Vested there for 1 year.

The issue arises when staker A decide to call stakeEscrow and result in changing state variable which track user A deposit and totalSupply = 210 and B is didn't call stakeEscrow as he is not interested in more rewards which changes the percentage they stake in vault compare to A which was same intially. Assume B is staking from starting of protocol so he should be getting more rewards but as user A called stakeEscrow result in changing totalSupply which reduces the RewardPerToken as the supply increases rewards will get more dilute.

As stakeEscrow is not automated process the staker who don't call this function will face huge amount of loss as number of user call stakeEscrow, the intention of stakeEscrow is to give more profit to Staker but not reducing rewards for then who didn't call it as a result user will also receive less reward for USDC

Impact

Staker who did not call the stakeEscrow function will end up facing huge reward losses as number of staker grows in the system.

Code Snippet

  function _stakeEscrow(address _account, uint256 _amount)
        internal
        whenNotPaused
        updateReward(_account)
    {
        if (_amount == 0) revert AmountZero();
        uint256 unstakedEscrow = unstakedEscrowedBalanceOf(_account);
        if (_amount > unstakedEscrow) revert InsufficientUnstakedEscrow(unstakedEscrow);

        // update state
        userLastStakeTime[_account] = block.timestamp;
        _addBalancesCheckpoint(_account, balanceOf(_account) + _amount);
        _addEscrowedBalancesCheckpoint(_account, escrowedBalanceOf(_account) + _amount);

        // updates total supply despite no new staking token being transfered.
        // escrowed tokens are locked in RewardEscrow
        _addTotalSupplyCheckpoint(totalSupply() + _amount);

        // emit escrow staking event and index account
        emit EscrowStaked(_account, _amount);
    }

Tool used

Manual Review

Recommendation

After _getreward function is called by user, call stakeEscrow for everyone as they all vesting KWENTA for a year. Make escrow automatic for everyone