Block: 92

Timestamp: 02:32:47

AuditProfile

Security blog

Read the RareSkills blog and it could help you win $2500!

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/246

And 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

Connent with me:

Регистрация прошла успешно! Спасибо за внимание!

loader