Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

calcaultion of scaledRewardRate will make the rewards stuck in the contract when totalStaked = 0 #134

Closed
c4-bot-7 opened this issue Mar 3, 2024 · 3 comments
Labels
2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value bug Something isn't working duplicate-369 edited-by-warden 🤖_14_group AI based duplicate group recommendation unsatisfactory does not satisfy C4 submission criteria; not eligible for awards

Comments

@c4-bot-7
Copy link
Contributor

c4-bot-7 commented Mar 3, 2024

Lines of code

https://github.com/code-423n4/2024-02-uniswap-foundation/blob/5a2761c8277541a24bc551fbd624413b384bea94/src/UniStaker.sol#L577-L582

Vulnerability details

Impact

scaledRewardRate is only calculated in notifyRewardAmount without considering if totalStaked is 0 or not. And scaledRewardRate is not updated anywhere else. So if totalStaked = 0, this will cause the waste of reward tokens, as these tokens are considered to be already distributed during the period but they are actually not. This leads to the waste and lock of funds.

Proof of Concept

scaledRewardRate is used to represent the global rate at which rewards are currently being distributed to stakers. It is updated every time there is a new reward coming in and notifying via notifyRewardAmount.

    if (block.timestamp >= rewardEndTime) {
      scaledRewardRate = (_amount * SCALE_FACTOR) / REWARD_DURATION;
    } else {
      uint256 _remainingReward = scaledRewardRate * (rewardEndTime - block.timestamp);
      scaledRewardRate = (_remainingReward + _amount * SCALE_FACTOR) / REWARD_DURATION;
    }

The problem is that, if totalStaked = 0, rewards have been distributed according to the calculation _remainingReward = scaledRewardRate * (rewardEndTime - block.timestamp) but they are not rewarded since no one is staking yet. The root cause is that rewards are still distributed even though no one is staking.

Since scaledRewardRate can not be updated elsewhere, these reward tokens are certainly wasted as they won't be counted in the next reward period. This leads to the waste and lock of funds.

Note: Several cases will all cause totalStaked=0, like no one has staked before, or the only staker decides to withdraw all shares.

The PoC is shown below (in UniStaker.t.sol::NotifyRewardAmount):

  function test_FundLoss() public {
    uint256 _amount = 1 ether;
    _mintTransferAndNotifyReward(_amount);
    uint256 _expectedRewardRate = (SCALE_FACTOR * _amount) / uniStaker.REWARD_DURATION();
    assertEq(uniStaker.scaledRewardRate(), _expectedRewardRate);
    emit log_named_uint("totalStake during 1st pharse of reward", uniStaker.totalStaked());
    emit log_named_uint("scaledRewardRate in the first phase",uniStaker.scaledRewardRate());
    vm.warp(block.timestamp + 15 days);
    _mintTransferAndNotifyReward(_amount);
    emit log_named_uint("scaledRewardRate in the second phase",uniStaker.scaledRewardRate());
    emit log_named_uint("scaledRewardRate with 1.5 * _amount in the second phase",_expectedRewardRate * 3 / 2);
    emit log_string("0.5 * _amount reward has been lost");
  }

The log is shown below, even though there is no staker, half of the _amount (15 days / 30 days) is still considered to be consumed:

Logs:
  totalStake during 1st pharse of reward: 0
  scaledRewardRate in the first phase: 385802469135802469135802469135802469135802469135
  scaledRewardRate in the second phase: 578703703703703703703703703703703703703703703703
  scaledRewardRate with 1.5 * _amount in the second phase: 578703703703703703703703703703703703703703703702
  0.5 * _amount reward has been lost

Tools Used

Foundry

Recommended Mitigation Steps

I would suggest the following:

  1. If totalStaked is 0, the rewarding process should not be open, and the fund could be recorded in a variable nextRoundRewards for the next reward process.
  2. If the only staker quits, the rewarding process should also stop, and the remaining fund should also be recorded in nextRoundRewards.

Assessed type

Math

@c4-bot-7 c4-bot-7 added 2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value bug Something isn't working labels Mar 3, 2024
c4-bot-3 added a commit that referenced this issue Mar 3, 2024
@c4-bot-12 c4-bot-12 added the 🤖_14_group AI based duplicate group recommendation label Mar 5, 2024
@c4-judge c4-judge closed this as completed Mar 7, 2024
@c4-judge
Copy link
Contributor

c4-judge commented Mar 7, 2024

MarioPoneder marked the issue as duplicate of #9

@c4-judge
Copy link
Contributor

c4-judge commented Mar 7, 2024

MarioPoneder marked the issue as duplicate of #369

@c4-judge c4-judge added the unsatisfactory does not satisfy C4 submission criteria; not eligible for awards label Mar 13, 2024
@c4-judge
Copy link
Contributor

MarioPoneder marked the issue as unsatisfactory:
Invalid

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value bug Something isn't working duplicate-369 edited-by-warden 🤖_14_group AI based duplicate group recommendation unsatisfactory does not satisfy C4 submission criteria; not eligible for awards
Projects
None yet
Development

No branches or pull requests

4 participants