Skinny Mocha Dragon
High
Reward tokens can become permanently locked in the GovernanceStaker
contract if rewards are notified when there are no active stakers (totalEarningPower == 0
). The protocol lacks a mechanism to recover these tokens, leading to a permanent loss of funds.
The vulnerability exists in the reward notification flow:
- The notifier transfers reward tokens to the contract
- Then calls
notifyRewardAmount()
- If
totalEarningPower == 0
(no active stakers):
function notifyRewardAmount(uint256 _amount) external virtual {
if (!isRewardNotifier[msg.sender]) {
revert GovernanceStaker__Unauthorized("not notifier", msg.sender);
}
// No check for totalEarningPower > 0
rewardPerTokenAccumulatedCheckpoint = rewardPerTokenAccumulated();
if (block.timestamp >= rewardEndTime) {
scaledRewardRate = (_amount * SCALE_FACTOR) / REWARD_DURATION;
}
}
The issue arises because:
- The contract requires rewards to be transferred before notification
- No validation of
totalEarningPower > 0
before accepting rewards - No mechanism to recover tokens if they get stuck
HIGH. If rewards are notified when no one is staking:
- Reward tokens become permanently locked in the contract
- No users can claim these rewards (even future stakers)
- No recovery mechanism exists
- Loss of funds for the protocol
function rewardPerTokenAccumulated() public view virtual returns (uint256) {
if (totalEarningPower == 0) return rewardPerTokenAccumulatedCheckpoint; // @audit rewards lost
return rewardPerTokenAccumulatedCheckpoint
+ (scaledRewardRate * (lastTimeRewardDistributed() - lastCheckpointTime)) / totalEarningPower;
}
function notifyRewardAmount(uint256 _amount) external virtual {
if (!isRewardNotifier[msg.sender]) {
revert GovernanceStaker__Unauthorized("not notifier", msg.sender);
}
// @audit no check for totalEarningPower > 0
rewardPerTokenAccumulatedCheckpoint = rewardPerTokenAccumulated();
}
Manual Review
implement a recovery mechanism