TempleGold

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

Stakers may get more rewards in next distribution

Summary

We don't clear the variable rewardPerTokenStored, and this variable will be used in next distribution period. This will impact the reward calculation.

Vulnerability Details

In staking contract, we will calculate rewards based on function _rewardPerToken(). We will calculate the rewards based on previous reward per token. The vulnerability is that we don't reset the variable rewardPerTokenStored when last distribution period ended. We will calculate the price based on last distribution period rewardPerTokenStored. This could cause incorrect reward results.

function _rewardPerToken() internal view returns (uint256) {
if (totalSupply == 0) {
return rewardData.rewardPerTokenStored;
}
return
rewardData.rewardPerTokenStored +
(((_lastTimeRewardApplicable(rewardData.periodFinish) -
rewardData.lastUpdateTime) *
rewardData.rewardRate * 1e18)
/ totalSupply);
}

Poc

Add this test case into TempleGoldStaking.t.sol.
In below test case, Alice stake 100 ether in the first distribution period and withdraw all staking amount when the first distribution period finished. Bob stakes 100 ether in the second distribution. From the output of the test case, bob should earn double rewards compared with Alice's rewards.

function test_Poc_tgldStaking_single_stake_single_account() public {
// for distribution
skip(4 weeks);
uint32 _rewardDuration = 4 weeks;
_setVestingPeriod(_rewardDuration);
_setRewardDuration(_rewardDuration);
_setVestingFactor(templeGold);
// Cannot start if there is no staker
//staking.distributeRewards();
//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);
// Can we withdraw before we reach the reward duration ???
//staking.withdrawAll(1, false);
skip(2 weeks);
ITempleGoldStaking.Reward memory rewardData = staking.getRewardData();
uint256 earned = staking.earned(alice, 1);
console.log("Alice earned: ", earned);
staking.getReward(alice, 1);
staking.withdrawAll(1, false);
vm.stopPrank();
vm.startPrank(bob);
_approve(address(templeToken), address(staking), type(uint).max);
//
console.log(" ----------- Distribute again ---------------");
staking.stake(stakeAmount);
staking.distributeRewards();
skip(4 weeks);
console.log("----------- Before second earn ");
earned = staking.earned(bob, 1);
console.log("----------- Bob earned in second distribution: ", earned);
}
Logs:
Whole reward amount: 84000000000000000000000000
Alice earned: 83999999999999999999462400
----------- Distribute again ---------------
Whole reward amount: 84000000000000000000537600
----------- Before second earn
----------- Bob earned in second distribution: 167999999999999999998924800

Impact

Stakers may get more rewards if there is enough rewards or stakers cannot get their rewards.

Tools Used

Manual

Recommendations

Clear the related variables when one distribution period ends.

Updates

Lead Judging Commences

inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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