Core Contracts

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

Gauges will become insolvent due to a faulty check

Summary

Gauges will become insolvent due to a faulty check

Vulnerability Details

notifyRewardAmount is used by the gauge controller to allocate rewards for gauges, where inside each gauge 2 very important checks are made.

First is if amount > periodState.emission to make sure we are not breaching the max emissions.
Second is if we have enough balance - rewardToken.balanceOf(address(this))

function notifyRewardAmount(uint256 amount) external override onlyController updateReward(address(0)) {
if (amount > periodState.emission) revert RewardCapExceeded();
// amount / 7 days
rewardRate = notifyReward(periodState, amount, periodState.emission, getPeriodDuration());
periodState.distributed += amount;
// This does not take into account all of the unclaimed balances
uint256 balance = rewardToken.balanceOf(address(this));
if (rewardRate * getPeriodDuration() > balance) { // 7 days
revert InsufficientRewardBalance();
}
lastUpdateTime = block.timestamp;
emit RewardNotified(amount);
}

The issue here is that the second check does not take into account users who have not yet claimed their rewards. These users will have their rewards inside this contract, contributing to a larger balance. And if notifyRewardAmount is called with an amount larger than the actual available balance then more rewards will be distributed that there are inside this contract, which would lead to insolvency as some users won't be able to claim.

This will likely happen with _distributeToGauges as it does not take into account any of those checks and distributes based on gauge weight. Where if our gauge has really big weight (is overweight :D) it would receive a larger portion of the distributed amount.

for (uint256 i = 0; i < _gaugeList.length; i++) {
address gauge = _gaugeList[i];
if (gauges[gauge].isActive && gauges[gauge].gaugeType == gaugeType) {
uint256 gaugeShare = (amount * gaugeWeights[i]) / totalTypeWeight;
if (gaugeShare > 0) {
IGauge(gauge).notifyRewardAmount(gaugeShare);
}
}
}

Same can be said for if we use distributeRewards, as it can too distribute more rewards than the current available balance.

function distributeRewards(
address gauge
) external override nonReentrant whenNotPaused {
if (!isGauge(gauge)) revert GaugeNotFound();
if (!gauges[gauge].isActive) revert GaugeNotActive();
uint256 reward = _calculateReward(gauge);
if (reward == 0) return;
IGauge(gauge).notifyRewardAmount(reward);
emit RewardDistributed(gauge, msg.sender, reward);
}
  1. There are 10k tokens inside the gauge, where 6k are left to be distributed (just sitting there) and 4k are already distributed, just users haven't claimed them yet

  2. Users call distributeRewards and since this gauge is heavier than the rest it gets allocated 8k rewards to distribute (there are not sent, just the gauge is configured to distribute them)

  3. notifyRewardAmount passes as the gauge has 10k balance (more than enough to cover the to-be distributed 8k)

Our gauge allocates 8k to be distributed and when the cycle finishes, there would be 12k rewards available to be claimed, but only 10k tokens inside the gauge. Our gauge is insolvent.

Impact

Gauge is insolvent
Some users receive more rewards while the last to claim are unable to since there are not enough tokens inside the contract.

Tools Used

Manual review

Recommendations

Add 2 variables - totalClaimed rewards and totalDistributed rewards, where in order to calculate how much are distributed, but not yet claimed just do totalDistributed - totalClaimed, then to find how much real balance the gauge has - rewardToken.balanceOf(address(this)) - (totalDistributed - totalClaimed).

And of course update those 2 variables whenever necessary.

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 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 4 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.