Dripping does not stop when nothing is staked #50
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
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
rewardPerTokenAccumulated()
returns the cumulative distributed rewards per token. That is, if this is stationary/constant no rewards are being distributed. This happens either whentotalStaked == 0
or whenlastTimeRewardDistributed() == 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 beforerewardEndTime
leads to the rewards not fully distributing, i.e. they are lost.This happens when
totalStaked == 0
during a distribution, i.e. ifnotifyRewardAmount()
is called before anything has been staked, or if everything is unstaked beforerewardEndTime
.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 setslastCheckpointTime = rewardEndTime
. Now consider therewardPerTokenAccumulated()
which is called on this stake and also some time later when rewards are claimed. In both cases it returns the samerewardPerTokenAccumulatedCheckpoint
, first (on_stake()
) sincetotalStaked == 0
and later (on_claimReward()
) sincelastTimeRewardDistributed() - lastCheckpointTime == 0
. That is,rewardPerTokenAccumulated() - beneficiaryRewardPerTokenCheckpoint[_beneficiary] == 0
sounclaimedReward()
returns0
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()
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 variablestartPause
for the last time stamptotalStaked == 0
happened, set in_withdraw()
. Then in_stake()
, iftotalStaked == 0 && _amount > 0
, we extendrewardEndTime += block.timestamp - max(startPause, rewardEndTime - REWARD_DURATION)
.Assessed type
Other
The text was updated successfully, but these errors were encountered: