Rich Smoke Cheetah
Medium
There is a rounding issue in the way rewards are computed that underestimates the rewards and can lead to a loss of funds.
Whenever the state of a deposit is modified, two functions are called. The first one is _checkpointGlobalReward:
function _checkpointGlobalReward() internal virtual {
rewardPerTokenAccumulatedCheckpoint = rewardPerTokenAccumulated();
lastCheckpointTime = lastTimeRewardDistributed();
}
This function updates the rewardPerTokenAccumulatedCheckpoint variable:
function rewardPerTokenAccumulated() public view virtual returns (uint256) {
if (totalEarningPower == 0) return rewardPerTokenAccumulatedCheckpoint;
return rewardPerTokenAccumulatedCheckpoint
+ (scaledRewardRate * (lastTimeRewardDistributed() - lastCheckpointTime)) / totalEarningPower;
}
The problem is that rewardPerTokenAccumulatedCheckpoint represents the value of one earning power in reward tokens. After that, the function _checkpointReward updates the reward of the deposit:
function _checkpointReward(Deposit storage deposit) internal virtual {
deposit.scaledUnclaimedRewardCheckpoint = _scaledUnclaimedReward(deposit);
deposit.rewardPerTokenCheckpoint = rewardPerTokenAccumulatedCheckpoint;
}
The function _scaledUnclaimedReward
updates the scaledUnclaimedRewardCheckpoint by calling _scaledUnclaimedReward, as seen here:
function _scaledUnclaimedReward(Deposit storage deposit) internal view virtual returns (uint256) {
return deposit.scaledUnclaimedRewardCheckpoint
+ (deposit.earningPower * (rewardPerTokenAccumulated() - deposit.rewardPerTokenCheckpoint));
}
The function _scaledUnclaimedReward updates the scaledUnclaimedRewardCheckpoint by calling _scaledUnclaimedReward
, as seen here:
The rewards will be underestimated.
https://github.com/sherlock-audit/2024-11-tally/blob/main/staker/src/GovernanceStaker.sol#L748-L751
https://github.com/sherlock-audit/2024-11-tally/blob/main/staker/src/GovernanceStaker.sol#L759-L762
https://github.com/sherlock-audit/2024-11-tally/blob/main/staker/src/GovernanceStaker.sol#L522-L525
Foundry
The function rewardPerTokenAccumulated
should be updated as follows:
function rewardPerTokenAccumulated() public view virtual returns (uint256) {
if (totalEarningPower == 0) return rewardPerTokenAccumulatedCheckpoint;
return rewardPerTokenAccumulatedCheckpoint
+ (scaledRewardRate * (lastTimeRewardDistributed() - lastCheckpointTime));
}
And the function _scaledUnclaimedReward
should be updated as follows:
function _scaledUnclaimedReward(Deposit storage deposit) internal view virtual returns (uint256) {
return deposit.scaledUnclaimedRewardCheckpoint
+ (deposit.earningPower * (rewardPerTokenAccumulated() - deposit.rewardPerTokenCheckpoint))/totalEarningPower;
}