Core Contracts

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

Initial gauge weight floor in `GaugeController` prevents reduction of gauge weight below that floor, causing reward distribution failures and undermining user votes

Summary

The GaugeController::_updateGaugeWeight function prevents gauge weights from being reduced below their initial weight, causing gauges to permanently receive a significant portion of protocol rewards regardless of community voting. This creates inefficient capital allocation, prevents the protocol from adapting to changing market conditions, cause reward distribution failures and undermine user votes.

Vulnerability Details

In GaugeController::_updateGaugeWeight, the gauge weight calculation does not allow weights to decrease below the initial weight set when the gauge was added:

uint256 newGaugeWeight = oldGaugeWeight - (oldWeight * votingPower / WEIGHT_PRECISION) + (newWeight * votingPower / WEIGHT_PRECISION);

When users try to vote to reduce a gauge's weight, the voted weight is added to the initial weight instead, effectively creating a permanent weight floor. This could cause a revert in BaseGauge::notifyRewardAmount, when called by GaugeController::distributeRewards as at a point, the total calculated rewards would be greater than the actual rewards. i.e Two gauges with initial weight set at 5000 bips with total reward at 10000, since the weights can only increase when voted for, any vote will make the total greater than 10000 bips. This would cause the sum of the calculated rewards, greater than the total reward.

POC

To use foundry in the codebase, follow the hardhat guide here: Foundry-Hardhat hybrid integration by Nomic foundation

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {ERC20Mock} from "../../../../contracts/mocks/core/tokens/ERC20Mock.sol";
import {FeeCollector} from "../../../../contracts/core/collectors/FeeCollector.sol";
import {Treasury} from "../../../../contracts/core/collectors/Treasury.sol";
import {RAACToken} from "../../../../contracts/core/tokens/RAACToken.sol";
import {veRAACToken} from "../../../../contracts/core/tokens/veRAACToken.sol";
import {GaugeController, IGaugeController} from "../../../../contracts/core/governance/gauges/GaugeController.sol";
import {RAACGauge} from "../../../../contracts/core/governance/gauges/RAACGauge.sol";
import {RWAGauge} from "../../../../contracts/core/governance/gauges/RWAGauge.sol";
import {Test, console} from "forge-std/Test.sol";
contract TestSuite is Test {
FeeCollector feeCollector;
Treasury treasury;
RAACToken raacToken;
veRAACToken veRAACTok;
GaugeController gaugeController;
RAACGauge raacGauge;
RAACGauge raacGauge2;
RWAGauge rwaGauge;
ERC20Mock rewardToken;
address repairFund;
address admin;
uint256 initialSwapTaxRate = 100; //1%
uint256 initialBurnTaxRate = 50; //0.5%
uint256 initialWeight = 5000;
function setUp() public {
repairFund = makeAddr("repairFund");
admin = makeAddr("admin");
rewardToken = new ERC20Mock("Reward Token", "RWT");
treasury = new Treasury(admin);
raacToken = new RAACToken(admin, initialSwapTaxRate, initialBurnTaxRate);
veRAACTok = new veRAACToken(address(raacToken));
feeCollector = new FeeCollector(address(raacToken), address(veRAACTok), address(treasury), repairFund, admin);
vm.startPrank(admin);
raacToken.setFeeCollector(address(feeCollector));
raacToken.setMinter(admin);
gaugeController = new GaugeController(address(veRAACTok));
raacGauge = new RAACGauge(address(rewardToken), address(raacToken), address(gaugeController));
raacGauge2 = new RAACGauge(address(rewardToken), address(raacToken), address(gaugeController));
rwaGauge = new RWAGauge(address(rewardToken), address(raacToken), address(gaugeController));
gaugeController.addGauge(address(raacGauge), IGaugeController.GaugeType.RAAC, initialWeight);
gaugeController.addGauge(address(raacGauge2), IGaugeController.GaugeType.RAAC, initialWeight);
gaugeController.addGauge(address(rwaGauge), IGaugeController.GaugeType.RWA, initialWeight);
vm.stopPrank();
}
function testGaugeWeightFloorPreventsGaugeWeightReduction() public {
// Setup two legitimate users
address user1 = makeAddr("user1");
address user2 = makeAddr("user2");
uint256 userTokens = 100e18;
vm.startPrank(admin);
raacToken.mint(user1, userTokens);
raacToken.mint(user2, userTokens);
vm.stopPrank();
// Users lock tokens to get voting power
vm.startPrank(user1);
raacToken.approve(address(veRAACTok), userTokens);
veRAACTok.lock(userTokens, veRAACTok.MAX_LOCK_DURATION());
vm.stopPrank();
vm.startPrank(user2);
raacToken.approve(address(veRAACTok), userTokens);
veRAACTok.lock(userTokens, veRAACTok.MAX_LOCK_DURATION());
vm.stopPrank();
// Initially both users vote equally for both gauges
vm.prank(user1);
gaugeController.vote(address(raacGauge), 5000); // 50% //bug in votingPower precision here, which causes exponential gauge weight increase will make this test to fail. Divide the votingPower by 1e18
vm.prank(user2);
gaugeController.vote(address(raacGauge2), 5000); // 50%
// Record initial rewards distribution
uint256 gauge1InitialReward = gaugeController._calculateReward(address(raacGauge));
uint256 gauge2InitialReward = gaugeController._calculateReward(address(raacGauge2));
uint256 gauge1InitialWeight = gaugeController.getGaugeWeight(address(raacGauge));
uint256 gauge2InitialWeight = gaugeController.getGaugeWeight(address(raacGauge2));
console.log("Gauge 1 initial reward:", gauge1InitialReward);
console.log("Gauge 2 initial reward:", gauge2InitialReward);
console.log("Gauge 1 initial weight:", gauge1InitialWeight);
console.log("Gauge 2 initial weight:", gauge2InitialWeight);
// Simulate market change - users try to shift all voting power to gauge2
// as gauge1 becomes less profitable/useful
vm.startPrank(user1);
gaugeController.vote(address(raacGauge), 0); // Try to remove votes from gauge1
gaugeController.vote(address(raacGauge2), 10000); // Try to allocate max to gauge2
vm.stopPrank();
vm.startPrank(user2);
gaugeController.vote(address(raacGauge), 0); // Try to remove votes from gauge1
gaugeController.vote(address(raacGauge2), 10000); // Try to allocate max to gauge2
vm.stopPrank();
// Check new rewards distribution
uint256 gauge1FinalReward = gaugeController._calculateReward(address(raacGauge));
uint256 gauge2FinalReward = gaugeController._calculateReward(address(raacGauge2));
uint256 gauge1FinalWeight = gaugeController.getGaugeWeight(address(raacGauge));
uint256 gauge2FinalWeight = gaugeController.getGaugeWeight(address(raacGauge2));
console.log("Gauge 1 final reward:", gauge1FinalReward);
console.log("Gauge 2 final reward:", gauge2FinalReward);
console.log("Gauge 1 final weight:", gauge1FinalWeight);
console.log("Gauge 2 final weight:", gauge2FinalWeight);
// Even though users want to completely shift rewards to gauge2,
// gauge1 still receives significant rewards due to initial weight floor
assertGt(
gauge1FinalReward, gauge1InitialReward / 2, "Gauge 1 still gets significant rewards due to weight floor"
);
// Calculate how much of total rewards are still going to the deprecated gauge
uint256 totalRewards = gauge1FinalReward + gauge2FinalReward;
uint256 gauge1Share = (gauge1FinalReward * 100) / totalRewards;
console.log("Gauge 1 share despite zero votes:", gauge1Share, "%");
console.log("Initial weight acting as floor:", initialWeight);
// The deprecated gauge still receives a large share of rewards
// due to initial weight floor, preventing efficient capital allocation
assertGt(gauge1Share, 30, "Deprecated gauge still receives significant share due to weight floor");
}
}

Impact

The inability to reduce gauge weights below their initial values will make capital allocation inefficient and potentially cause reward distribution failures. It will also undermine the governance of the community in controlling the gauge weight value.

Tools Used

Manual review, foundry test suite

Recommendations

Modify the GaugeController::_updateGaugeWeight function to allow weights to be reduced below their initial values:

function _updateGaugeWeight(address gauge, uint256 oldWeight, uint256 newWeight, uint256 votingPower) internal {
Gauge storage g = gauges[gauge];
uint256 oldGaugeWeight = g.weight;
- uint256 newGaugeWeight = oldGaugeWeight - (oldWeight * votingPower / WEIGHT_PRECISION) + (newWeight * votingPower / WEIGHT_PRECISION);
+ uint256 weightReduction = (oldWeight * votingPower / WEIGHT_PRECISION);
+ uint256 weightAddition = (newWeight * votingPower / WEIGHT_PRECISION);
+ uint256 newGaugeWeight;
+ if (newWeight < oldGaugeWeight) {
+ newGaugeWeight = weightAddition; //@audit modify as needed
+ } else {
+ newGaugeWeight = oldGaugeWeight + weightAddition - weightReduction;
+ }
g.weight = newGaugeWeight;
g.lastUpdateTime = block.timestamp;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge
7 months ago
inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Appeal created

uddercover Submitter
6 months ago
inallhonesty Lead Judge
5 months ago
inallhonesty Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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