Summary
When distributeRewards
can be called by anyone, no new reward duration from setRewardDuration
or new vesting period from setVestingPeriod
can be called, because it will always be griefed, and periodFinish
will be reset, reverting the transactions.
## Vulnerability Details
The issue will happen when distributeRewards
is permissionless and everyone can call it, additionally setting a new vesting period and reward duration requires rewardData::periodFinish
to be strictly less than the current block.timestamp
:
function setVestingPeriod(uint32 _period) external override onlyElevatedAccess {
if (_period < WEEK_LENGTH) { revert CommonEventsAndErrors.InvalidParam(); }
if (rewardData.periodFinish >= block.timestamp) { revert InvalidOperation(); }
vestingPeriod = _period;
emit VestingPeriodSet(_period);
}
* @notice Set reward duration
* @param _duration Reward duration
*/
function setRewardDuration(uint256 _duration) external override onlyElevatedAccess {
if (_duration < WEEK_LENGTH) { revert CommonEventsAndErrors.InvalidParam(); }
if (rewardData.periodFinish >= block.timestamp) { revert InvalidOperation(); }
rewardDuration = _duration;
emit RewardDurationSet(_duration);
}
But in the distribute function we are always setting the periodFinish
to the current timestamp, which is the root cause of the issue:
internally called by distributeRewards
function _notifyReward(uint256 amount) private {
...MORE CODE
rewardData.lastUpdateTime = uint40(block.timestamp);
rewardData.periodFinish = uint40(block.timestamp + rewardDuration);
}
Here is a POC demonstrating that both functions will revert with InvalidOperation
:
forge test --match-test test_griefSetter
function test_griefSetter() public {
vm.startPrank(executor);
staking.setMigrator(alice);
uint256 _rewardDuration = 16 weeks;
uint32 _vestingPeriod = uint32(_rewardDuration);
_setVestingFactor(templeGold);
_setVestingPeriod(_vestingPeriod);
_setRewardDuration(_rewardDuration);
vm.startPrank(bob);
deal(address(templeToken), bob, 1000 ether, true);
_approve(address(templeToken), address(staking), type(uint).max);
staking.stake(100 ether);
vm.stopPrank();
skip(2 days);
staking.distributeRewards();
vm.startPrank(executor);
uint256 duration = 16 weeks;
staking.setRewardDuration(duration);
}
Impact
Permanently blocking authorized entities from setting new duration and vesting periods.
Tools Used
Manual review
Recommendations
The simplest approach allows setting these values when periodFinish
is equal to the current timestamp.