TempleGold

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

Unprotected Stake/Unstake Mechanism in `TempleGoldStaking` Allows Reward Rate Manipulation Leading to Unfair Reward Distribution and Fund Drain

Summary

The TempleGoldStaking contract is susceptible to reward rate manipulation through large stake and unstake operations. This vulnerability allows an attacker to significantly reduce the reward rate for other stakers by manipulating the total supply just before reward distribution.

Vulnerability Details

The vulnerability exists in the reward calculation mechanism, specifically in the _rewardPerToken() function:

function _rewardPerToken() internal view returns (uint256) {
if (totalSupply == 0) {
return rewardData.rewardPerTokenStored;
}
return
rewardData.rewardPerTokenStored +
(((_lastTimeRewardApplicable(rewardData.periodFinish) -
rewardData.lastUpdateTime) *
rewardData.rewardRate * 1e18)
/ totalSupply);
}

This function calculates the reward per token based on the current totalSupply. The vulnerability arises from the fact that an attacker can manipulate this totalSupply just before reward distribution to significantly impact the reward rate.

The attack can be executed as follows:

1) The attacker waits until just before the reward distribution period.

2) They stake a large amount of tokens, significantly increasing the totalSupply:

function stakeFor(address _for, uint256 _amount) public whenNotPaused {
if (_amount == 0) revert CommonEventsAndErrors.ExpectedNonZero();
stakingToken.safeTransferFrom(msg.sender, address(this), _amount);
uint256 _lastIndex = _accountLastStakeIndex[_for];
_accountLastStakeIndex[_for] = ++_lastIndex;
_applyStake(_for, _amount, _lastIndex);
_moveDelegates(address(0), delegates[_for], _amount);
}

3) If the distributionStarter is set to address(0) they trigger the reward distribution:

function distributeRewards() updateReward(address(0), 0) external {
if (distributionStarter != address(0) && msg.sender != distributionStarter)
{ revert CommonEventsAndErrors.InvalidAccess(); }
if (totalSupply == 0) { revert NoStaker(); }
// Mint and distribute TGLD if no cooldown set
if (lastRewardNotificationTimestamp + rewardDistributionCoolDown > block.timestamp)
{ revert CannotDistribute(); }
_distributeGold();
uint256 rewardAmount = nextRewardAmount;
// revert if next reward is 0 or less than reward duration (final dust amounts)
if (rewardAmount < rewardDuration ) { revert CommonEventsAndErrors.ExpectedNonZero(); }
nextRewardAmount = 0;
_notifyReward(rewardAmount);
lastRewardNotificationTimestamp = uint32(block.timestamp);
}

4) Immediately after distribution, the attacker withdraws their large stake:

function withdraw(uint256 amount, uint256 index, bool claim) external override {
StakeInfo storage _stakeInfo = _stakeInfos[msg.sender][index];
_withdrawFor(_stakeInfo, msg.sender, msg.sender, index, amount, claim, msg.sender);
}

This sequence of actions results in a significantly reduced rewardRate for other stakers, as the rate is calculated based on the inflated totalSupply at the time of distribution.

POC

Detailed Exploit Scenario:

Initial State:

  • The TempleGoldStaking contract is deployed with a 7-day vesting period and a 7-day reward duration.

  • The attacker and victim each have 500,000 staking tokens.

  • The staking contract has 10,000 reward tokens available for distribution.

Step 1: The victim stakes 100 tokens, believing they will receive a fair share of rewards.

Step 2: Just before the reward distribution (1 hour before), the attacker stakes 500,000 tokens, dramatically increasing the total supply.

Step 3: The attacker triggers the reward distribution immediately after staking. The reward rate is calculated based on the inflated total supply.

Step 4: The attacker immediately unstakes their tokens after the reward distribution.

Step 5: Over the next reward period, rewards accrue based on the manipulated rate.

Outcome:

  • The victim receives significantly fewer rewards than expected, despite being staked for the entire period.

  • The attacker receives a disproportionately high amount of rewards for their brief staking period.

Impact

The exploitation of this vulnerability could have severe consequences:

  1. Honest stakers would receive substantially fewer rewards than they should, effectively having their rewards stolen by the attacker.

  2. The repeated exploitation of this vulnerability could lead to significant financial losses for legitimate stakers.

  3. As users become aware of the unfair reward distribution, they may lose faith in the protocol, potentially leading to a mass exodus of stakers.

Tools Used

Manual Review

Recommendations

To address this vulnerability, consider implementing the following mitigations:

  1. Time-Weighted Average Total Supply: Instead of using the current totalSupply for reward calculations, implement a time-weighted average of the total supply over the reward period. This would significantly reduce the impact of short-term supply manipulations.

struct TWASupply {
uint256 value;
uint256 lastUpdateTimestamp;
}
TWASupply private _twaSupply;
function updateTWASupply() internal {
uint256 timePassed = block.timestamp - _twaSupply.lastUpdateTimestamp;
_twaSupply.value = (_twaSupply.value * timePassed + totalSupply * (block.timestamp - _twaSupply.lastUpdateTimestamp)) / (timePassed + block.timestamp - _twaSupply.lastUpdateTimestamp);
_twaSupply.lastUpdateTimestamp = block.timestamp;
}
  1. Vesting Period for New Stakes: Implement a vesting period for newly staked tokens before they're eligible for rewards. This would prevent rapid stake-distribute-unstake attacks.

struct Stake {
uint256 amount;
uint256 timestamp;
}
mapping(address => Stake[]) private _stakes;
function _applyStake(address _for, uint256 _amount, uint256 _index) internal updateReward(_for, _index) {
totalSupply += _amount;
_balances[_for] += _amount;
_stakes[_for].push(Stake(_amount, block.timestamp));
emit Staked(_for, _amount);
}
function getVestedBalance(address account) public view returns (uint256) {
uint256 vestedBalance = 0;
for (uint i = 0; i < _stakes[account].length; i++) {
if (block.timestamp - _stakes[account][i].timestamp > vestingPeriod) {
vestedBalance += _stakes[account][i].amount;
}
}
return vestedBalance;
}
  1. Maximum Stake Size: Implement a maximum stake size relative to the current total supply to limit the impact of large stakes.

uint256 public constant MAX_STAKE_PERCENT = 10; // 10% of total supply
function stakeFor(address _for, uint256 _amount) public whenNotPaused {
require(_amount <= (totalSupply * MAX_STAKE_PERCENT) / 100, "Stake too large");
// ... rest of the function
}
  1. Delay Between Staking and Reward Distribution: Implement a mandatory delay between large stake changes and reward distribution to prevent rapid manipulation.

uint256 public constant LARGE_STAKE_THRESHOLD = 1e18; // 1% of total supply
uint256 public constant LARGE_STAKE_DELAY = 1 days;
mapping(address => uint256) private _lastLargeStakeTimestamp;
function stakeFor(address _for, uint256 _amount) public whenNotPaused {
// ... existing code
if (_amount >= (totalSupply * LARGE_STAKE_THRESHOLD) / 100) {
_lastLargeStakeTimestamp[_for] = block.timestamp;
}
}
function distributeRewards() updateReward(address(0), 0) external {
require(block.timestamp > _lastLargeStakeTimestamp[msg.sender] + LARGE_STAKE_DELAY, "Cannot distribute soon after large stake");
// ... rest of the function
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.