Summary
The _updateReward function in the BaseGauge contract uses block.timestamp to update state.lastUpdateTime instead of using `lastTimeRewardApplicable()@ which is used for other reward time calculations.
Vulnerability Details
In the _updateReward function:
function _updateReward(address account) internal {
rewardPerTokenStored = getRewardPerToken();
lastUpdateTime = lastTimeRewardApplicable();
if (account != address(0)) {
UserState storage state = userStates[account];
state.rewards = earned(account);
state.rewardPerTokenPaid = rewardPerTokenStored;
state.lastUpdateTime = block.timestamp;
emit RewardUpdated(account, state.rewards);
}
}
The problem is that in future calculations we will calculate getRewardPerToken inside the earned: this function calculates:
function getRewardPerToken() public view returns (uint256) {
if (totalSupply() == 0) {
return rewardPerTokenStored;
}
return rewardPerTokenStored + (
(lastTimeRewardApplicable() - lastUpdateTime) * rewardRate * 1e18 / totalSupply()
);
}
and we have
function lastTimeRewardApplicable() public view returns (uint256) {
return block.timestamp < periodFinish() ? block.timestamp : periodFinish();
}
This means after when block.timestamp > periodFinish() block.timestamp will be bigger than LastTimeRewardApplicable() which means the getRewardPerToken will underflow
Impact
When reward period ends, the function will revert due to underflow
Users can't claim rewards after period ends
All functions using the updateReward modifier will be blocked
Tools Used
Manual Review
Recommendations
Use LastTimeRewardApplicable:
function _updateReward(address account) internal {
rewardPerTokenStored = getRewardPerToken();
lastUpdateTime = lastTimeRewardApplicable();
if (account != address(0)) {
UserState storage state = userStates[account];
state.rewards = earned(account);
state.rewardPerTokenPaid = rewardPerTokenStored;
state.lastUpdateTime = lastTimeRewardApplicable();
emit RewardUpdated(account, state.rewards);
}
}```