Core Contracts

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

`GaugeController::_calculateReward` implementation will cause smaller shares to be allocated to every gauge

Summary

The GaugeController::_calculateReward function incorrectly calculates rewards by using the combined total weight of all gauge types instead of calculating rewards relative to the total weight of each gauge type separately. This causes rewards to be diluted across different gauge types, resulting in gauges receiving significantly fewer rewards than intended.

Vulnerability Details

The GaugeController::getTotalWeight function sums up all the weights of every active gauge, regardless of gaugeType, in the gaugeController and returns the result.

function getTotalWeight() public view override returns (uint256) {
uint256 total = 0;
// This could be optimized by maintaining a running total
@> for (uint256 i = 0; i < _gaugeList.length; i++) {
if (gauges[_gaugeList[i]].isActive) {
total += gauges[_gaugeList[i]].weight;
}
}
return total;
}

And the GaugeController::_calculateReward function relies on the amount returned to calculate the shares for each gauge, but the periodEmission calculated is done based on the gaugeType.

function _calculateReward(address gauge) public view returns (uint256, uint256) {
Gauge storage g = gauges[gauge];
@> uint256 totalWeight = getTotalWeight();
if (totalWeight == 0) return (0, 0);
@> uint256 gaugeShare = (g.weight * WEIGHT_PRECISION) / totalWeight;
uint256 typeShare = (typeWeights[g.gaugeType] * WEIGHT_PRECISION) / MAX_TYPE_WEIGHT;
// Calculate period emissions based on gauge type
@> uint256 periodEmission = g.gaugeType == GaugeType.RWA ? _calculateRWAEmission() : _calculateRAACEmission();
return ((periodEmission * gaugeShare * typeShare) / (WEIGHT_PRECISION * WEIGHT_PRECISION), gaugeShare);
}

This means that, gauge types that shouldn't have a share of a particular emission are technically allocated shares as well, effectively reducing the available shares for the actual deserving gauge type.

This effect is heightened as more gauges are added to the controller.

Proof of Code

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;
address repairFund;
address admin;
address rewardToken;
uint256 initialSwapTaxRate = 100; //1%
uint256 initialBurnTaxRate = 50; //0.5%
function setUp() public {
repairFund = makeAddr("repairFund");
admin = makeAddr("admin");
rewardToken = address(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(rewardToken, address(veRAACTok), address(gaugeController));
raacGauge2 = new RAACGauge(rewardToken, address(veRAACTok), address(gaugeController));
rwaGauge = new RWAGauge(rewardToken, address(veRAACTok), address(gaugeController));
vm.stopPrank();
}
function testTotalGaugeWeightImplementationReducesGaugeShares() public {
//1. add 3 gauges, 2 of 1 type and 1 of another
uint256 initialWeightRAAC = 4000; //40%
uint256 initialWeightRWA = 2000; //20%
//admin creates and adds gauges
vm.startPrank(admin);
gaugeController.addGauge(address(raacGauge), IGaugeController.GaugeType.RAAC, initialWeightRAAC);
gaugeController.addGauge(address(raacGauge2), IGaugeController.GaugeType.RAAC, initialWeightRAAC);
gaugeController.addGauge(address(rwaGauge), IGaugeController.GaugeType.RWA, initialWeightRWA);
vm.stopPrank();
//2. calculate the reward for 1 of the two, it should equal half of the emissions
// gaugeShare of one RAAC gauge should be half of the total RAAC gaugeType weight i.e 4000 / 8000 = 0.5 or 5000 bips
(, uint256 actualGaugeShare) = gaugeController._calculateReward(address(raacGauge)); //function modified to return gaugeShare for ease of access
uint256 expectedGaugeShare = 5000;
console.log("Actual gauge share: ", actualGaugeShare);
console.log("Expected gauge share: ", expectedGaugeShare);
assertLt(actualGaugeShare, expectedGaugeShare);
}

Impact

Lesser rewards distributed to each gauge in the gauge controller

Tools Used

Manual review, foundry test suite

Recommendations

Create a function that takes into consideration the gauge type when calculating the total weight and use that to calculate the rewards for each gauge. Alternatively, modify the existing `totalWeight` function to do so instead.

Updates

Lead Judging Commences

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

GaugeController::_calculateReward uses combined weight of all gauge types instead of type-specific weights, causing incorrect reward distribution between RWA and RAAC gauges

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

GaugeController::_calculateReward uses combined weight of all gauge types instead of type-specific weights, causing incorrect reward distribution between RWA and RAAC gauges

Appeal created

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

GaugeController::_calculateReward uses combined weight of all gauge types instead of type-specific weights, causing incorrect reward distribution between RWA and RAAC gauges

Support

FAQs

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