Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: medium
Valid

Gauge rewards don't account for boosts

Summary

Rewards distributed to gauges don't account for user boosts leading to the last users claiming unable to receive their rewards.

Vulnerability Details

When gauges are notified of rewards, the rewardRate is updated.

The updateRewards() modifier (akin to Synthetix staking rewards) then syncs the user state:

function _updateReward(address account) internal {
rewardPerTokenStored = getRewardPerToken();
lastUpdateTime = lastTimeRewardApplicable();
if (account != address(0)) {
UserState storage state = userStates[account];
state.rewards = earned(account);
state.rewardPerTokenPaid = rewardPerTokenStored;
state.lastUpdateTime = block.timestamp;
emit RewardUpdated(account, state.rewards);
}
}

The state.rewards variable is assigned to earned(account), which applies the user's boost:

function earned(address account) public view returns (uint256) {
return (getUserWeight(account) *
(getRewardPerToken() - userStates[account].rewardPerTokenPaid) / 1e18
) + userStates[account].rewards;
}
function getUserWeight(address account) public view virtual returns (uint256) {
uint256 baseWeight = _getBaseWeight(account);
return _applyBoost(account, baseWeight);
}

Thus, the user's base rewards per token will be multiplied by the boost and weight from the BoostCalculator.
But when users try to claim rewards, the function will check whether reward > balance and if it is, revert:

function getReward() external virtual nonReentrant whenNotPaused updateReward(msg.sender) {
...
lastClaimTime[msg.sender] = block.timestamp;
UserState storage state = userStates[msg.sender];
uint256 reward = state.rewards;
if (reward > 0) {
state.rewards = 0;
uint256 balance = rewardToken.balanceOf(address(this));
@> if (reward > balance) {
revert InsufficientBalance();
}
...
}
}

This creates an issue, let's follow it with an example:

  1. 10,000 rewards notified in gauge to be distributed

  2. 10 participants each eligible for 1000 rewards

  3. The updateRewards() modifier will assign the user earned rewards as baseReward * boost

  4. 10k distributed, but due to boosts, users are eligible for 15k rewards

  5. There are 10k rewards in the contract but 15k to be claimed, creating a race condition among users for who claims

Impact

First users that claim rewards will receive boosted amounts but last users claiming will be unable to claim rewards due to notify rewards not taking into account the boosted rewards. With current implementation it will be challenging to calculate the correct rewards to notify ahead of time with ever-changing user boosts and weights.

Tools Used

Manual Review

Recommendations

Refactor the rewards distribution to gauges.

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

BaseGauge::notifyRewardAmount checks token balance without accounting for unclaimed rewards, allowing allocation of more rewards than available tokens

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

BaseGauge::notifyRewardAmount checks token balance without accounting for unclaimed rewards, allowing allocation of more rewards than available tokens

Support

FAQs

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

Give us feedback!