TempleGold

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

Some reward tokens may be stuck in staking contract

Summary

If staking amount becomes 0 in the staking period, some reward tokens will be left in the staking contract. Although some left reward tokes can be shifted to next reward period, there still is some left reward tokens. These tokens will be locked in this contract forever, because there is no recover method for reward token.

Vulnerability Details

Staking contract should make sure that there is some staking supply before we start one new round distribution via distributeRewards(). However, stakers can withdraw in the distribution period, this may cause the staking supply decreased to 0. When we try to withdraw all staking supply before the end of this distribution period, the left supply will be 0. And the earning rewards will be calculated via vestingRate. This means the earned reward is not calculated by one liner way. This mechanism aims to encourage users stake long time.
The vulnerability is that the left reward tokens can not be completed shifted to next distribution period. This will cause some tokens stuck in the contract.

For example:

  • wait for 4 weeks to get some reward tokens for staking contract. Assume the rewards are 100 TempleGold.

  • assume vesting period and reward duration are 4 weeks.

  • Alice stakes 100 Temple. And start distribute via distributeRewards()

  • Alice withdraw all stakes in 2 weeks. Earned rewards will be 100/2/2 = 25 TempleGold.

  • Other 75 TempleGold will be left in contract in this distribution period.

function distributeRewards() updateReward(address(0), 0) external {
if (distributionStarter != address(0) && msg.sender != distributionStarter)
{ revert CommonEventsAndErrors.InvalidAccess(); }
if (totalSupply == 0) { revert NoStaker(); }
......
}
function _earned(
StakeInfo memory _stakeInfo,
address _account,
uint256 _index
) internal view returns (uint256) {
uint256 vestingRate = _getVestingRate(_stakeInfo);
if (vestingRate == 0) {
return 0;
}
uint256 _perTokenReward;
if (vestingRate == 1e18) {
_perTokenReward = _rewardPerToken();
} else {
_perTokenReward = _rewardPerToken() * vestingRate / 1e18;
}
return
(_stakeInfo.amount * (_perTokenReward - userRewardPerTokenPaid[_account][_index])) / 1e18 +
claimableRewards[_account][_index];
}

Poc

Add this test case into TempleGoldStaking.t.sol.

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);
_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);
}

The test case's output is

Logs:
Whole reward amount: 84000000000000000000000000
Alice earned: 20999999999999999999865600

From the output, the staker's whole earned rewards in this distribution period are less than the amount of reward distribution.

Impact

Tools Used

Manual

Recommendations

Considering to add these left reward tokens into the next distribution period.

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.