This vulnerability was found in the TempleGold protocol on CodeHwaks contest platform. The auditor refers in his report to the RareSkills article that helped him find the problem in the code.
In short, stakers are always vested rewards using the total rewardPerToken without deducting how much rewardPerToken has already been accumulated before they stake. Any new staker will be rewarded with rewardData.rewardPerTokenStored that have been accumulated since creation of contract. That means a user who hasn't been staking from the beginning will be rewarded the same as a user who has been staking sing the beginning. Or in other words the contract assumes that every user staked when the rewardData.rewardPerTokenStored is 0.
stake calls updateReward modifier and we can see that every new staker's userRewardPerTokenPaid will be 0 since vesting rate is currently 0:
modifier updateReward(address _account, uint256 _index) {
{
// stack too deep
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;
}
}
_;
}
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;
}
}
Suppose,
- after 1 year the accumulated rewardPerTokenStored is 1000e18, which means every token staked since the beginning has earned 1000 tokens each.
- a new user Bob stakes 100 TEMPLE tokens.
- assuming the vesting period is 16 weeks, Bob claim his rewards after 16 weeks
- now his reward calculation is;
_perTokenReward = _rewardPerToken();
we can see that _perTokenReward for the new staker is the total _rewarsPerToken which has been accumulating since the beginning.
This is assuming no new rewards are added, where bob should've gotten 0 TGLD as rewards. Bob just earned rewards equivalent to staking 1 year + 16 weeks(for new rewards).
The issue here is that the contract does not account for the already accumulated rewardPerTokenStored from the past when calculating the new staker's userRewardPerTokenPaid and calculates it using the total rewardPerToken:
userRewardPerTokenPaid[_account][_index] = vestingRate * uint256(rewardData.rewardPerTokenStored) / 1e18;
As an impact: loss of reward tokens and unfair reward calculation for initial stakers or DOS(not enough reward tokens).
It also opens an attack path where a user can steal unclaimed rewards even when there are no new rewards added i.e. a user who enters after no new rewards are distributed will still get rewards from the past.
Read the full report and fix recommendations here:
Link: https://codehawks.cyfrin.io/c/2024-07-templegold/s/246And here you can find the RareSkills article that will help you find such bugs in future:
Link: https://rareskills.io/post/staking-algorithm/#vested
#rewards
#stake
Completely free courses
Learn more about the blockchain world
Free education videos
by RareSkills
by Jeiwan
by RareSkills
by RareSkills
by Andreas M. Antonopoulos, Gavin Wood
by Micah Dameron
Compare execution layer differences between chains
Dive deep into the storage of any contract