SIP-77: StakingRewards bug fix's and Pausable stake()
| Author | |
|---|---|
| Status | Implemented |
| Type | Governance |
| Network | Ethereum |
| Implementor | TBD |
| Release | TBD |
| Created | 2020-08-06 |
Simple Summary
stake() needs to be pausable for completed incentives and two bug fixes.
Abstract
Enhancements include:
- Inheriting
Pausablecontract and addnotPausedmodifer tostake()to prevent staking into deprecated pools - Fix a potential overflow bug in the reward notification function reported by samcsun
- Fix to
setRewardsDurationto allowrewardsDurationto be updated after the initial setting
Motivation
Pause stake when rewards completed
When a StakingRewards campaign has completed the contract needs to prevent anyone from staking into it. The staker will not accrue any rewards and can cause blocking issues with inverse Synths that need to be purged so that they can be relanced.
Adding Pausable.sol and modifier notPaused to stake() will allow the admin to set paused to true preventing anyone from staking. SelfDestructible has not been implemented and given the amount of value in these contracts probably best not to implement.
Potential overflow bug fix
Summary
There is a multiplication overflow that can occur inside the rewardPerToken function, on line 66:
lastTimeRewardApplicable().sub(lastUpdateTime).mul(rewardRate).mul(1e18).div(_totalSupply)
An overflow occurs whenever rewardRate >= 2^256 / (10^18 * (lastTimeRewardApplicable() - lastUpdateTime)).
This can happen when the updateReward modifier is invoked, which will cause the following functions to revert:
earnedstakewithdrawgetRewardexitnotifyRewardAmount
The reward rate is set inside notifyRewardAmount, if a value that is too large is provided to the function.
Of particular note is that notifyRewardAmount is itself affected by this problem, which means that if the provided
reward is incorrect, then the problem is unrecoverable.
Solution
The notifyRewardAmount transaction should be reverted if a value greater than 2^256 / 10^18 is provided.
As an additional safety mechanism, this value will be required to be no greater than the remaining
balance of the rewards token in the contract. This will both prevent the overflow, and also provide an additional check
that the reward rate is being set to a value in the appropriate range (for example, no extra/missing zeroes).
Details
Specifically, this problem occurs when rewardRate is too high; it is set inside the notifyRewardAmount function on
lines 114 and 118.
rewardRate = floor(reward / rewardsDuration) = (reward - k) / rewardsDuration
for some 0 <= k < rewardsDuration.
For the bug to occur, we need:
(reward - k) / rewardsDuration >= 2^256 / (10^18 * (lastTimeRewardApplicable - lastUpdateTime))
reward >= rewardsDuration * 2^256 / (10^18 * (lastTimeRewardApplicable - lastUpdateTime)) + k
Hence, we can ensure the bug does not occur if we force:
reward < rewardsDuration * 2^256 / (10^18 * (lastTimeRewardApplicable - lastUpdateTime))
So we should constrain reward to be less than the minimum value of the RHS.
The smallest possible value of lastUpdateTime is the block timestamp when notifyRewardAmount was last called.
The largest possible value of lastTimeRewardApplicable is periodFinish,
and periodFinish = notificationBlock.timestamp + rewardsDuration (line 121).
Putting these together we have:
(lastTimeRewardApplicable - lastUpdateTime) <= rewardsDuration
Ergo, we need:
reward < rewardsDuration * 2^256 / (10^18 * rewardsDuration)
= 2^256 / 10^18
So the problem will not emerge whenever we require
reward < uint(-1) / UNIT
Fix to setRewardsDuration to allow updates after the initial setting
setRewardsDuration was intended to allow the rewardsDuration to be set after the duration had completed. However a flaw in the require meant it could be changed after the initial setting.
Current code
require(periodFinish == 0 || block.timestamp > periodFinish);
Proposed change
require(block.timestamp > periodFinish);
Technical Specification
- Inherit the
Pausable.solcontract and add modifiernotPausedtostake() - Revert the
notifyRewardAmounttransaction if the computer reward rate would pay out more than the balance of the contract over the reward period. - Change the
requireinsetRewardsDurationto only check the period has finished
Test Cases
- Pausable
- should revert when stake is called when paused is true
- should allow a call to stake to succeed when paused is false
- Overflow bugfix
- should revert
notifyRewardAmountif reward is greater than the contract balance - should revert
notifyRewardAmountif reward plus any leftover from the previous period is greater than the contract balance - should not revert
notifyRewardAmountif reward is equal to the contract balance
- should revert
- setRewardsDuration bug fix
- should revert when setting setRewardsDuration before the period has finished
- should update rewardsDuration when calling setRewardsDuration after the rewards period has finished
Configurable Values (Via SCCP)
Please list all values configurable via SCCP under this implementation.
Copyright
Copyright and related rights waived via CC0.