Summary
If some stakers don't stake in the start of one staking period, they may get more rewards than expected because of the improper calculation in _rewardPerToken
.
Vulnerability Details
In Staking contract, when users want to stake, withdraw, getReward, function _rewardPerToken()
will be triggered to update reward per token. We will get the latest reward per token based on the previous reward per token and delta reward per token.
The vulnerability is that if some stakers stake in the middle of the whole staking period, they should not have some previous rewards. They may get more rewards than expected.
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 this case, Alice stakes 100 ether from start to the staking end, and Bob stakes 100 ether two weeks later.
function test_Poc2_tgldStaking_single_stake_single_account() public {
skip(4 weeks);
uint32 _rewardDuration = 4 weeks;
_setVestingPeriod(_rewardDuration);
_setRewardDuration(_rewardDuration);
_setVestingFactor(templeGold);
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("Alice reward", earned);
earned = staking.earned(bob, 1);
console.log("Bob reward: ", earned);
}
Logs:
Whole reward amount: 84000000000000000000000000
Alice reward 62999999999999999999596800
Bob reward: 31499999999999999999798400
From the output, the total amount for Alice and Bob is larger than the total reward amount.
Impact
The protocol will distribute more reward than expected. The protocol will lose funds. If the owner changes the distribution ratio to staking part, stakers may fail to get the rewards.
Tools Used
Manual
Recommendations