Hidden Crepe Cormorant
Medium
When the totalEarningPower
is zero, lastCheckpointTime
is updated current time, but there is no one to receive the reward.
https://github.com/sherlock-audit/2024-11-tally/blob/main/staker/src/GovernanceStaker.sol#L750
N/A
N/A
N/A
Rewards may be locked in this contract indefinitely.
GovernanceStaker.sol
294: function lastTimeRewardDistributed() public view virtual returns (uint256) {
if (rewardEndTime <= block.timestamp) return rewardEndTime;
else return block.timestamp;
}
function _checkpointGlobalReward() internal virtual {
rewardPerTokenAccumulatedCheckpoint = rewardPerTokenAccumulated();
750: lastCheckpointTime = lastTimeRewardDistributed();
}
430: function notifyRewardAmount(uint256 _amount) external virtual {
if (!isRewardNotifier[msg.sender]) {
revert GovernanceStaker__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) {
440: scaledRewardRate = (_amount * SCALE_FACTOR) / REWARD_DURATION;
} else {
uint256 _remainingReward = scaledRewardRate * (rewardEndTime - block.timestamp);
443: scaledRewardRate = (_remainingReward + _amount * SCALE_FACTOR) / REWARD_DURATION;
}
rewardEndTime = block.timestamp + REWARD_DURATION;
lastCheckpointTime = block.timestamp;
if ((scaledRewardRate / SCALE_FACTOR) == 0) revert GovernanceStaker__InvalidRewardRate();
// This check cannot _guarantee_ sufficient rewards have been transferred to the contract,
// because it cannot isolate the unclaimed rewards owed to stakers left in the balance. While
// this check is useful for preventing degenerate cases, it is not sufficient. Therefore, it is
// critical that only safe reward notifier contracts are approved to call this method by the
// admin.
if (
(scaledRewardRate * REWARD_DURATION) > (REWARD_TOKEN.balanceOf(address(this)) * SCALE_FACTOR)
) revert GovernanceStaker__InsufficientRewardBalance();
emit RewardNotified(_amount, msg.sender);
}
This protocol provides rewards over time, with a set start and end time, and scaledRewardRate
(L440,L443).
After totalEarningPower
goes to zero, the protocol still attempts to provide rewards, and the lastCheckpointTime
is updated to the current time, but there is no one to receive these rewards.
As a result, these rewards locked in this contract indefinitely.
When totalEarningPower
is zero,
lastCheckpointTime
should not be updated to the current time, or alternatively,
rewardEndTime
should be increased by the same value as the increment of lastCheckpointTime
.
function _checkpointGlobalReward() internal virtual {
rewardPerTokenAccumulatedCheckpoint = rewardPerTokenAccumulated();
+ uint256 _lastCheckpointTime = lastTimeRewardDistributed();
+ if (totalEarningPower == 0) rewardEndTime += (_lastCheckpointTime - lastCheckpointTime);
+ lastCheckpointTime = _lastCheckpointTime;
-750: lastCheckpointTime = lastTimeRewardDistributed();
}