TempleGold

TempleDAO
Foundry
25,000 USDC
View results
Submission Details
Severity: high
Valid

Incorrect Reward Calculation in `TempleGoldStaking`

Summary

In TempleGoldStaking, when a user first stakes, their userRewardPerTokenPaid is set to 0 since stakeInfo is not initialized as updateReward is being called first. Later, in the _earned function, when the user has passed the vestingPeriod, _perTokenReward would equal _rewardPerToken(). And the final reward calculation _stakeInfo.amount * (_perTokenReward - userRewardPerTokenPaid[_account][_index]) / 1e18 suggests that the current reward is _stakeInfo.amount * _rewardPerToken() / 1e18, which means the reward for amount is calculated for the whole staking period, far exceeding the actual staking time.

Vulnerability Details

The updateReward modifier is used to update the rewardPerTokenStored and userRewardPerTokenPaid[_account][_index].

modifier updateReward(address _account, uint256 _index) {
{
// stack too deep
@=> rewardData.rewardPerTokenStored = uint216(_rewardPerToken());
rewardData.lastUpdateTime = uint40(_lastTimeRewardApplicable(rewardData.periodFinish));
if (_account != address(0)) {
StakeInfo memory _stakeInfo = _stakeInfos[_account][_index];
@=> uint256 vestingRate = _getVestingRate(_stakeInfo);
claimableRewards[_account][_index] = _earned(_stakeInfo, _account, _index);
@=> userRewardPerTokenPaid[_account][_index] = vestingRate * uint256(rewardData.rewardPerTokenStored) / 1e18;
}
}
_;
}

When a user first stakes, _applyStake is called, triggering updateReward.

In the calculation:

  • The vestingRate is calculated as _getVestingRate(_stakeInfo), but since _stakeInfo is not initialized (_stakeInfo.stakeTime == 0), vestingRate is 0.

  • claimableRewards[_account][_index] is 0 because vestingRate is 0.

  • As a result, userRewardPerTokenPaid[_account][_index] = vestingRate * uint256(rewardData.rewardPerTokenStored) / 1e18 is also 0.

Later, when the user’s vesting period has passed, the user calls getReward, invoking updateReward again.

  • The vestingRate is now 1e18 because the condition block.timestamp > _stakeInfo.fullyVestedAt is true.

if (block.timestamp > _stakeInfo.fullyVestedAt) {
vestingRate = 1e18;
}
  • In the _earned function, _perTokenReward is equal to _rewardPerToken() as vestingRate == 1e18.

if (vestingRate == 1e18) {
_perTokenReward = _rewardPerToken();
}
  • The final earned calculation in _earned is (_stakeInfo.amount * (_perTokenReward - userRewardPerTokenPaid[_account][_index])) / 1e18 + claimableRewards[_account][_index], which simplifies to _stakeInfo.amount * _rewardPerToken() / 1e18. This results in the reward being calculated based on rewardData.rewardPerTokenStored for the entire staking period, exceeding the user’s actual staking time.

return
(_stakeInfo.amount * (_perTokenReward - userRewardPerTokenPaid[_account][_index])) / 1e18 +
claimableRewards[_account][_index];
  • This leads to claimableRewards[_account][_index] being updated to a higher amount, allowing the user to claim more rewards than they should.

Impact

The reward calculation is incorrect, allowing users to receive more rewards than they are entitled to.

Tools Used

Manual

Recommendations

Reconsider the design of the reward calculation. userRewardPerTokenPaid should always record the rewardPerTokenStored without any further modification to ensure accurate reward calculation based on the actual staking time.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Future stakers are paid with rewards that have been accrued from the past due to miscalculation in userRewardPerTokenPaid and _perTokenReward

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.