TempleGold

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

Some stakers may get more rewards after the distribution period ends

Summary

After one distribution period ends, some stakers can earn more rewards, this is unexpected.

Vulnerability Details

When we start one distribute reward period, we will set the distribution period's periodFinish. When the distribution period finishes, there will be no any reward for this stake period.

Stakers' rewards will be calculated via _earned(). In function _earned(), we will calculate the current rewards based on _rewardPerToken() and vestingRate. The _rewardPerToken() value will stop increasing when we reach periodFinish, but vestingRate can still increase even if we reach periodFinish. This will cause the final earned rewards will increase even if the distribution period ends.

function _notifyReward(uint256 amount) private {
......
rewardData.lastUpdateTime = uint40(block.timestamp);
rewardData.periodFinish = uint40(block.timestamp + rewardDuration);
}
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 {
_perTokenReward = _rewardPerToken() * vestingRate / 1e18;
}
return
(_stakeInfo.amount * (_perTokenReward - userRewardPerTokenPaid[_account][_index])) / 1e18 +
claimableRewards[_account][_index];
}
function _rewardPerToken() internal view returns (uint256) {
if (totalSupply == 0) {
return rewardData.rewardPerTokenStored;
}
return
rewardData.rewardPerTokenStored +
(((_lastTimeRewardApplicable(rewardData.periodFinish) -
rewardData.lastUpdateTime) *
rewardData.rewardRate * 1e18)
/ totalSupply);
}
function _getVestingRate(StakeInfo memory _stakeInfo) internal view returns (uint256 vestingRate) {
if (_stakeInfo.stakeTime == 0) {
return 0;
}
// vest period, if the time is less than fullyVestdAt, may lose some rewards
if (block.timestamp > _stakeInfo.fullyVestedAt) {
vestingRate = 1e18;
} else {
vestingRate = (block.timestamp - _stakeInfo.stakeTime) * 1e18 / vestingPeriod;
}
}

Poc

Add this test case into TempleGoldStaking.t.tsol.
When the distribution period ends, wait for another 2 weeks, the staker Bob can earn more rewards.

function test_Poc2_tgldStaking_single_stake_single_account() public {
// for distribution
skip(4 weeks);
uint32 _rewardDuration = 4 weeks;
_setVestingPeriod(_rewardDuration);
_setRewardDuration(_rewardDuration);
_setVestingFactor(templeGold);
//skip(4 weeks);
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);
uint256 stakeAmount = 100 ether;
staking.stake(stakeAmount);
uint256 stakeTime = block.timestamp;
uint256 goldBalanceBefore = templeGold.balanceOf(address(staking));
staking.distributeRewards();
uint256 goldRewardsAmount = templeGold.balanceOf(address(staking)) - goldBalanceBefore;
skip(2 weeks);
vm.stopPrank();
vm.startPrank(bob);
_approve(address(templeToken), address(staking), type(uint).max);
staking.stake(stakeAmount);
vm.stopPrank();
skip(2 weeks);
ITempleGoldStaking.Reward memory rewardData = staking.getRewardData();
uint256 earned = staking.earned(alice, 1);
console.log("Distribution period ends, Alice reward", earned);
earned = staking.earned(bob, 1);
console.log("Distribution period ends, Bob reward: ", earned);
skip(2 weeks);
earned = staking.earned(bob, 1);
console.log("Distribution period ends, Bob increased reward: ", earned);
}

The test case output is as below:

Logs:
Whole reward amount: 84000000000000000000000000
Distribution period ends, Alice reward 62999999999999999999596800
Distribution period ends, Bob reward: 31499999999999999999798400
Distribution period ends, Bob increased reward: 62999999999999999999596800

Impact

Stakers may get more rewards after the distribution period ends.

Tools Used

Manual

Recommendations

Revisit _getVestingRate() logic, if block.timestamp is larger than the distribution periodFinish, we cannot increase vestingRate.

Updates

Lead Judging Commences

inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Too generic

Support

FAQs

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