## Summary
`TempleGoldStaking._getVestingRate()` returns incorrect `vestingRate` as it calculates the rate based on the up-to-date `vestingPeriod` and not based on the actual vesting period of the stake.
## Vulnerability Details
- `TempleGoldStaking._getVestingRate()` function is used to update user rewards for a specific stake based on the elapsed time from the start of the stake:
```javascript
function _getVestingRate(
StakeInfo memory _stakeInfo
) internal view returns (uint256 vestingRate) {
if (_stakeInfo.stakeTime == 0) {
return 0;
}
if (block.timestamp > _stakeInfo.fullyVestedAt) {
vestingRate = 1e18;
} else {
vestingRate =
((block.timestamp - _stakeInfo.stakeTime) * 1e18) /
vestingPeriod;
}
}
```
as can be noticed, the maximum `vestingRate = 1e18` when the vesting time is finished (`block.timestamp > _stakeInfo.fullyVestedAt`), and if the vesting time is not finished yet; the `vestingRate` is calculated as a ratio of the elapsed vesting time / `vestingPeriod`.
- The stake info is stored when the user adds a stake ( `_applyStake()`):
```javascript
function _applyStake(
address _for,
uint256 _amount,
uint256 _index
) internal updateReward(_for, _index) {
totalSupply += _amount;
_balances[_for] += _amount;
_stakeInfos[_for][_index] = StakeInfo(
uint64(block.timestamp), //! stakeTime
uint64(block.timestamp + vestingPeriod), //! fullyVestedAt
_amount
);
emit Staked(_for, _amount);
}
```
So the `fullyVestedAt` is calculated based **on the current `vestingPeriod` at the time of the stake.**
- By knowing that **the `vestingPeriod` is not constant and can be updated** by the admin via `setVestingPeriod()`, then the vesting ratio calculated by the `_getVestingRate()` will return an incorrect vesting rate (if the vesting period is not over) as it divides the time elapsed by the current `vestingPeriod` that can be different from the one used to assign the `_stakeInfo.fullyVestedAt` (doesn't represent the actual vesting period of the stake), which would result in an incorrect ratio (could be greater than 1e18 if the `vestingPeriod` is reduced).
## Impact
Incorrect vestingRate calculation would result in an incorrect earned rewards calculation and possible DoS:
- `_earned()` function that is called whenever the rewards of the stake is updated could revert due to underflow when the calculated `vestingRate` of the stake is decreased if the globale state `vestingPeriod` is increased from the previous value, so the new `_perTokenReward` will be greater than the old value:
```js
function _earned(
StakeInfo memory _stakeInfo,
address _account,
uint256 _index
) internal view returns (uint256) {
uint256 vestingRate = _getVestingRate(_stakeInfo);
if (vestingRate == 0) {
return 0;
}
uint256 _perTokenReward;
if (vestingRate == 1e18) {
_perTokenReward = _rewardPerToken();
} else {
// @audit : the new _perTokenReward could be less than the `userRewardPerTokenPaid[_account][_index]` resulting in DoS of the function
_perTokenReward = (_rewardPerToken() * vestingRate) / 1e18;
}
return
(_stakeInfo.amount *
(_perTokenReward - userRewardPerTokenPaid[_account][_index])) /
1e18 +
claimableRewards[_account][_index];
}
```
knowing that the `userRewardPerTokenPaid[_account][_index]` is updated after `_earn()` is called:
```js
modifier updateReward(address _account, uint256 _index) {
{
//...
if (_account != address(0)) {
StakeInfo memory _stakeInfo = _stakeInfos[_account][_index];
//1. @audit : if `vestingPeriod` increased, this will be less than the previous value
uint256 vestingRate = _getVestingRate(_stakeInfo);
claimableRewards[_account][_index] = _earned(
_stakeInfo,
_account,
_index
);
userRewardPerTokenPaid[_account][_index] =
(vestingRate * uint256(rewardData.rewardPerTokenStored)) /
1e18;
}
}
_;
}
```
- if the globale state `vestingPeriod` is decreased from the previous value, users could get higher `vestingRate`, and claim more rewards than they are entitle to.
## Tools Used
Manual Review.
## Recommendations
Update `_getVestingRate()` to divide by the correct vesting period:
```diff
function _getVestingRate(
StakeInfo memory _stakeInfo
) internal view returns (uint256 vestingRate) {
if (_stakeInfo.stakeTime == 0) {
return 0;
}
if (block.timestamp > _stakeInfo.fullyVestedAt) {
vestingRate = 1e18;
} else {
vestingRate =
((block.timestamp - _stakeInfo.stakeTime) * 1e18) /
- vestingPeriod;
+ (_stakeInfo.fullyVestedAt - _stakeInfo.stakeTime)
}
}
```