TempleGold

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

Changes to vesting period is not handled inside `_getVestingRate`

Summary

Changes to vesting period is not handled inside _getVestingRate

Vulnerability Detail

The vesting period end time of a stake is fixated at the time of staking itself while the vesting period of the contract can change after an ongoing draw

link

function _applyStake(address _for, uint256 _amount, uint256 _index) internal updateReward(_for, _index) {
totalSupply += _amount;
_balances[_for] += _amount;
=> _stakeInfos[_for][_index] = StakeInfo(uint64(block.timestamp), uint64(block.timestamp + vestingPeriod), _amount);

In case the vesting period is reduced, this allows for scenario's where the calculated vestingRate of a stake can be greater than 1e18 ie. the ideal maximum value for vestingRate leading to inflated returns for the staker initially and a potential timeframe where the reward claiming could revert due to underflow caused by the previosuly stored inflated userRewardPerTokenPaid

link

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

In case the vesting period is increased, there will be a sudden drop in the vestingRates of the stakers whose vesting time has not ended and this will cause the reward claiming to revert until the vesting period fully finishes

Example

Draw period == initial vesting period == 16weeks
User stakes at 15th week
For the stake, fullyVestedAt = 15week + 16weeks == 31 weeks
After draw ends, vesting period is reduced to 8 weeks
Now after 9 weeks the vesting rate of the stake will be,
(10 weeks/8 weeks) * 1e18 > 1e18

POC

Apply the follwing diff and run forge test --mt testAudit_vesting_period_influence_inflated_reward
The function getVestingRate is added to the TempleGoldStaking contract in order to access the vesting rate publicly

diff --git a/protocol/contracts/templegold/TempleGoldStaking.sol b/protocol/contracts/templegold/TempleGoldStaking.sol
index b2d95ae..4397f26 100644
--- a/protocol/contracts/templegold/TempleGoldStaking.sol
+++ b/protocol/contracts/templegold/TempleGoldStaking.sol
@@ -460,6 +460,9 @@ contract TempleGoldStaking is ITempleGoldStaking, TempleElevatedAccess, Pausable
}
}
+ function getVestingRate(StakeInfo memory _stakeInfo) public view returns (uint256 vestingRate) {
+ return _getVestingRate(_stakeInfo);
+ }
function _earned(
StakeInfo memory _stakeInfo,
address _account,
diff --git a/protocol/test/forge/templegold/TempleGoldStaking.t.sol b/protocol/test/forge/templegold/TempleGoldStaking.t.sol
index 705e3d4..4330691 100644
--- a/protocol/test/forge/templegold/TempleGoldStaking.t.sol
+++ b/protocol/test/forge/templegold/TempleGoldStaking.t.sol
@@ -1111,6 +1111,52 @@ contract TempleGoldStakingTest is TempleGoldStakingTestBase {
assertEq(staking.earned(alice, 2), 0);
}
+ function testAudit_vesting_period_influence_inflated_reward() public {
+ uint32 _vestingPeriod = 16 weeks;
+ {
+ skip(3 days);
+ _setVestingPeriod(_vestingPeriod);
+ _setRewardDuration(_vestingPeriod);
+ _setVestingFactor(templeGold);
+ }
+
+ uint256 stakeAmount = 100 ether;
+ uint256 goldRewardsAmount;
+ ITempleGoldStaking.Reward memory rewardDataOne;
+ {
+ // initial distribution and draw
+ vm.startPrank(alice);
+ deal(address(templeToken), alice, 1000 ether, true);
+ deal(address(templeToken), bob, 1000 ether, true);
+ _approve(address(templeToken), address(staking), type(uint).max);
+ staking.stake(stakeAmount);
+ assertEq(staking.earned(alice, 1), 0);
+ staking.distributeRewards();
+ }
+
+ {
+ // 2nd stake, 1 week left for the draw end
+ skip(15 weeks);
+ staking.stake(stakeAmount);
+ }
+
+ {
+ // current draw finished
+ skip(2 weeks);
+ // new vesting period is set to half
+ _setVestingPeriod(_vestingPeriod / 2);
+ // new draw started
+ staking.distributeRewards();
+ // after 8 weeks, the vestingRate will return > 1e18 instead of 1e18 since still attached will the old vest end period
+ skip(9 weeks);
+ ITempleGoldStaking.StakeInfo memory _stakeInfo = staking.getAccountStakeInfo(alice, 2);
+ uint vestingRate = staking.getVestingRate(_stakeInfo);
+ // 1.375000000000000000
+ // console.log("vestingRate",vestingRate);
+ assert(vestingRate > 1e18);
+ }
+ }
+
function test_getReward_tgldStaking_multiple_stakes_multiple_rewards_distribution() public {
uint32 _vestingPeriod = 16 weeks;
{

Impact

Incorrect reward allocation in case of vesting period changes causing loss of rewards for deserving stakers

Tool used

Manual Review

Recommendation

Limit the maximum to the 1e18 inside the _getVestingRate function

Updates

Lead Judging Commences

inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Changes to vesting period is not handled inside `_getVestingRate`

Support

FAQs

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