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

Any UniStaker fees accruing when no one is staking would be lost #333

Closed
c4-bot-7 opened this issue Mar 4, 2024 · 3 comments
Closed

Any UniStaker fees accruing when no one is staking would be lost #333

c4-bot-7 opened this issue Mar 4, 2024 · 3 comments
Labels
3 (High Risk) Assets can be stolen/lost/compromised directly 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-7
Copy link
Contributor

c4-bot-7 commented Mar 4, 2024

Lines of code

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

Vulnerability details

Description

When fees are added to the UniStaker, notifyRewardAmount() is called.

function notifyRewardAmount(uint256 _amount) external {
  if (!isRewardNotifier[msg.sender]) revert UniStaker__Unauthorized("not notifier", msg.sender);
  // We checkpoint the accumulator without updating the timestamp at which it was updated, because
  // that second operation will be done after updating the reward rate.
  rewardPerTokenAccumulatedCheckpoint = rewardPerTokenAccumulated();
  if (block.timestamp >= rewardEndTime) {
    scaledRewardRate = (_amount * SCALE_FACTOR) / REWARD_DURATION;
  } else {
    uint256 _remainingReward = scaledRewardRate * (rewardEndTime - block.timestamp);
    scaledRewardRate = (_remainingReward + _amount * SCALE_FACTOR) / REWARD_DURATION;
  }
  rewardEndTime = block.timestamp + REWARD_DURATION;
  lastCheckpointTime = block.timestamp;

It sets scaledRewardRate so that the total pending fees would be split uniformally over the next 30 days.
rewardEndTime is set to now + 30 days, while lastCheckpointTime = now

Suppose there's no stakers when the first rewards are sent to the UniStaker, for example since yield is zero right now. Some time passes and the first staker joins. All interactions with beneficiaries including stake() run the following line to update global rewards:

_checkpointGlobalReward();

Which is:

/// @notice Checkpoints the global reward per token accumulator.
function _checkpointGlobalReward() internal {
  rewardPerTokenAccumulatedCheckpoint = rewardPerTokenAccumulated();
  lastCheckpointTime = lastTimeRewardDistributed();
}

The first line updates the total unlocked reward/token:

function rewardPerTokenAccumulated() public view returns (uint256) {
  if (totalStaked == 0) return rewardPerTokenAccumulatedCheckpoint;
  return rewardPerTokenAccumulatedCheckpoint
    + (scaledRewardRate * (lastTimeRewardDistributed() - lastCheckpointTime)) / totalStaked;
}

In case totalStaked is zero, it returns the original checkpoint without accounting for the new rewards.

The second line updates lastCheckpointTime:

function lastTimeRewardDistributed() public view returns (uint256) {
  if (rewardEndTime <= block.timestamp) return rewardEndTime;
  else return block.timestamp;
}

Note that it would update regardless if totalStaked==0. Therefore, it would store the current timestamp in lastCheckpointTime.

At this point, the tokens weren't distributed, but lastCheckpointTime is updated. So in the future, the accumulation of tokens in rewardPerTokenAccumulated() will never include the days when there weren't any stakers.

This results in loss of fees which are permanently stuck in the contract, whenver there are no stakers. That could be at any part of the contract's lifetime.

Impact

Permanent freeze of yield which currently belongs to the Uniswap governance.

Proof of Concept

  • Uniswap deploys UniStaker + FactoryOwner
  • Someone instantly calls claimFees() to get fees and give rewards, before the first stake deposits into UniStaker.
  • Rewards from this point and until the first staker, permanently stuck in the contract.

Tools Used

Manual audit

Recommended Mitigation Steps

Consider not advancing lastCheckpointTime in case totalStaked == 0 in _checkpointGlobalReward().

Assessed type

Math

@c4-bot-7 c4-bot-7 added 3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working labels Mar 4, 2024
c4-bot-7 added a commit that referenced this issue Mar 4, 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 6, 2024
@c4-judge
Copy link
Contributor

c4-judge commented Mar 6, 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 duplicate-369 unsatisfactory does not satisfy C4 submission criteria; not eligible for awards and removed duplicate-9 labels Mar 7, 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
3 (High Risk) Assets can be stolen/lost/compromised directly 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