Skip to content

Latest commit

 

History

History
86 lines (69 loc) · 3.7 KB

002.md

File metadata and controls

86 lines (69 loc) · 3.7 KB

Hidden Crepe Cormorant

Medium

When The totalEarningPower Is Zero, The Rewards Are Locked

Summary

When the totalEarningPower is zero, lastCheckpointTime is updated current time, but there is no one to receive the reward.

Root Cause

https://github.com/sherlock-audit/2024-11-tally/blob/main/staker/src/GovernanceStaker.sol#L750

Internal pre-conditions

N/A

External pre-conditions

N/A

Attack Path

N/A

Impact

Rewards may be locked in this contract indefinitely.

PoC

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.

Mitigation

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();
        }