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

Dripping does not stop when nothing is staked #50

Closed
c4-bot-8 opened this issue Feb 29, 2024 · 3 comments
Closed

Dripping does not stop when nothing is staked #50

c4-bot-8 opened this issue Feb 29, 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 🤖_06_group AI based duplicate group recommendation unsatisfactory does not satisfy C4 submission criteria; not eligible for awards

Comments

@c4-bot-8
Copy link
Contributor

Lines of code

https://github.com/code-423n4/2024-02-uniswap-foundation/blob/5298812a129f942555466ebaa6ea9a2af4be0ccc/src/UniStaker.sol#L229-L234

Vulnerability details

Impact

Rewards can be dripped when nothing is staked, during which time rewards are permanently lost.

Proof of Concept

function rewardPerTokenAccumulated() public view returns (uint256) {
  if (totalStaked == 0) return rewardPerTokenAccumulatedCheckpoint;

  return rewardPerTokenAccumulatedCheckpoint
    + (scaledRewardRate * (lastTimeRewardDistributed() - lastCheckpointTime)) / totalStaked;
}

rewardPerTokenAccumulated() returns the cumulative distributed rewards per token. That is, if this is stationary/constant no rewards are being distributed. This happens either when totalStaked == 0 or when lastTimeRewardDistributed() == lastCheckpointTime, i.e. when the rewards distribution has ended.

A newly notified amount of rewards are distributed evenly over the next 30 days, and any currently remaining rewards are added to this amount. The end of this period is set in rewardEndTime. rewardEndTime can only be changed by again notifying a reward, upon which remaining rewards are added to the new 30 day distribution period. That is, the rewards are always distributed at a rate such that the entire amount is distributed in exactly 30 days. This means that any interruption to the distribution before rewardEndTime leads to the rewards not fully distributing, i.e. they are lost.

This happens when totalStaked == 0 during a distribution, i.e. if notifyRewardAmount() is called before anything has been staked, or if everything is unstaked before rewardEndTime.

As a more explicit example illustration, suppose nothing is staked (or never has been) and that notifyRewardAmount() is called. Suppose then 30 days pass and someone stakes. _stake() calls _checkpointGlobalReward() which sets lastCheckpointTime = rewardEndTime. Now consider the rewardPerTokenAccumulated() which is called on this stake and also some time later when rewards are claimed. In both cases it returns the same rewardPerTokenAccumulatedCheckpoint, first (on _stake()) since totalStaked == 0 and later (on _claimReward()) since lastTimeRewardDistributed() - lastCheckpointTime == 0. That is, rewardPerTokenAccumulated() - beneficiaryRewardPerTokenCheckpoint[_beneficiary] == 0 so unclaimedReward() returns 0 and no rewards could be claimed. The rewards which were notified were thus lost.

Recommended Mitigation Steps

A simple solution is to renotify Unistaker of its own balance, by adding in _stake()

if (totalStaked == 0) {
    uint256 lostRewards = REWARD_TOKEN.balanceOf(address(this));
    scaledRewardRate = (lostRewards * SCALE_FACTOR) / REWARD_DURATION;
    rewardEndTime = block.timestamp + REWARD_DURATION;
    lastCheckpointTime = block.timestamp;
}

The slight downside to this is that it restarts the distribution rather than pauses it.

Otherwise the distribution should be paused whenever totalStaked == 0. This may be achieved by keeping a variable startPause for the last time stamp totalStaked == 0 happened, set in _withdraw(). Then in _stake(), if totalStaked == 0 && _amount > 0, we extend rewardEndTime += block.timestamp - max(startPause, rewardEndTime - REWARD_DURATION).

Assessed type

Other

@c4-bot-8 c4-bot-8 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 Feb 29, 2024
c4-bot-1 added a commit that referenced this issue Feb 29, 2024
@c4-bot-12 c4-bot-12 added the 🤖_06_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
Copy link
Contributor

MarioPoneder marked the issue as unsatisfactory:
Invalid

@c4-judge c4-judge added the unsatisfactory does not satisfy C4 submission criteria; not eligible for awards label Mar 13, 2024
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 🤖_06_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

3 participants