Summary
userRewardPerTokenPaid
is updated incorrectly inside the updateReward
function
Vulnerability Detail
The updateReward
sets the userRewardPerTokenPaid
for a stake to vestingRate * ....
link
modifier updateReward(address _account, uint256 _index) {
{
rewardData.rewardPerTokenStored = uint216(_rewardPerToken());
rewardData.lastUpdateTime = uint40(_lastTimeRewardApplicable(rewardData.periodFinish));
if (_account != address(0)) {
StakeInfo memory _stakeInfo = _stakeInfos[_account][_index];
uint256 vestingRate = _getVestingRate(_stakeInfo);
claimableRewards[_account][_index] = _earned(_stakeInfo, _account, _index);
=> userRewardPerTokenPaid[_account][_index] = vestingRate * uint256(rewardData.rewardPerTokenStored) / 1e18;
}
}
_;
For new stakes, the vestingRate
will be 0
link
function _getVestingRate(StakeInfo memory _stakeInfo) internal view returns (uint256 vestingRate) {
if (_stakeInfo.stakeTime == 0) {
return 0;
}
This will cause the next updateReward
call to result in the staker obtaining reward as if they have staked from the beginning hence stealing the reward from other deserving stakers who have actually staked earlier
POC
Add the following test to test/forge/templegold/TempleGoldStaking.t.sol
and run forge test --mt testAudit_staking_reward_period_start_zero -vv
. It is asserted that the sum of rewards earned becomes greater than the distributed rewards due to the inflated value in the stake done midway
function testAudit_staking_reward_period_start_zero() public {
uint32 _vestingPeriod = 16 weeks;
{
skip(3 days);
_setVestingPeriod(_vestingPeriod);
_setRewardDuration(_vestingPeriod);
_setVestingFactor(templeGold);
}
uint256 stakeAmount = 100 ether;
uint256 goldRewardsAmount;
ITempleGoldStaking.Reward memory rewardDataOne;
{
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);
templeGold.balanceOf(address(staking));
staking.distributeRewards();
goldRewardsAmount = templeGold.balanceOf(address(staking));
console2.log("goldRewardsAmount",goldRewardsAmount);
}
{
skip(8 weeks);
staking.stake(stakeAmount);
}
{
skip(10 weeks);
uint earned1 = staking.earned(alice, 1);
uint earned2 = staking.earned(alice, 2);
console.log("earned1",earned1);
console.log("earned2",earned2);
console.log("goldRewardsAmount",goldRewardsAmount);
assert(earned1 + earned2 > goldRewardsAmount); }
}
Impact
Inflated reward for new stakers
Tool used
Manual Review
Recommendation
Change the tracking of userRewardPerTokenPaid to track the actual rewardData.rewardPerTokenStored
instead of vestingRate * uint256(rewardData.rewardPerTokenStored)