Core Contracts

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

Lack of decimal scaling for veRAACToken voting power in `GaugeController::vote` leads to reward distribution DOS

Summary

The GaugeController::vote function uses the raw veRAACToken balance (18 decimals) without scaling, causing gauge weights to become exponentially large. This leads to arithmetic precision loss in reward calculations, resulting in zero rewards being distributed to gauges.

Vulnerability Details

In GaugeController::vote, the user's voting power is taken directly from veRAACToken.balanceOf(msg.sender) without accounting for the token's 18 decimal places. This inflated voting power is then used in _updateGaugeWeight to calculate gauge weights.

The vulnerability exists in the interaction between three key functions:

  • GaugeController::vote: Uses unscaled veRAACToken balance as voting power

function vote(address gauge, uint256 weight) external override whenNotPaused {
uint256 votingPower = veRAACToken.balanceOf(msg.sender); // @audit - uses raw 18 decimal balance
_updateGaugeWeight(gauge, oldWeight, weight, votingPower);
}
  • GaugeController::_updateGaugeWeight: Calculates new gauge weight using the inflated voting power

function _updateGaugeWeight(address gauge, uint256 oldWeight, uint256 newWeight, uint256 votingPower) internal {
uint256 newGaugeWeight = oldGaugeWeight - (oldWeight * votingPower / WEIGHT_PRECISION) +
(newWeight * votingPower / WEIGHT_PRECISION);
g.weight = newGaugeWeight;
}
  • GaugeController::_calculateReward: Attempts to calculate rewards using the inflated gauge weight

function _calculateReward(address gauge) public view returns (uint256) {
uint256 gaugeShare = (g.weight * WEIGHT_PRECISION) / totalWeight;
uint256 typeShare = (typeWeights[g.gaugeType] * WEIGHT_PRECISION) / MAX_TYPE_WEIGHT;
return (periodEmission * gaugeShare * typeShare) / (WEIGHT_PRECISION * WEIGHT_PRECISION);
}

The core issue is that votingPower includes 18 decimal places from veRAACToken, but is used directly in weight calculations. For example:

  • 1 veRAACToken = 1e18 units

  • With weight = 1 (minimum), the gauge weight becomes: 1 * 1e18 / 10000 = 1e14

  • With weight = 10000 (maximum), the gauge weight becomes: 10000 * 1e18 / 10000 = 1e18

This causes two critical problems:

  1. Gauge weights become astronomically large (1e14 to 1e18 range)

  2. The reward calculation divides by WEIGHT_PRECISION twice (once for gaugeShare, once for typeShare), causing precision loss

Proof of Concept

  • Initial Setup

// User has 1 veRAACToken (1e18 units)
// Two gauges exist: gaugeA and gaugeB
// Initial gauge weights are 0
  • User votes with minimum weight (1) for gaugeA

votingPower = 1e18
newGaugeWeight = 0 - (0 * 1e18 / 10000) + (1 * 1e18 / 10000)
= 0 + 1e14
// gaugeA.weight is now 1e14
  • User votes with maximum weight (10000) for gaugeB

votingPower = 1e18
newGaugeWeight = 0 - (0 * 1e18 / 10000) + (10000 * 1e18 / 10000)
= 0 + 1e18
// gaugeB.weight is now 1e18
  • Reward calculation for gaugeA fails

totalWeight = 1e14 + 1e181e18
gaugeShare = (1e14 * 10000) / 1e18 = 0 // Rounds to 0 due to division
typeShare = (5000 * 10000) / 10000 = 5000
reward = (250000e18 * 0 * 5000) / (10000 * 10000) = 0

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 = 0;
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 testSmallAmountVotingPowerMakesGaugeWeightExponentiallyLargerDOSingRewardDistribution() public {
address user = makeAddr("user");
uint256 mintAmount = 1e18; // 1 token with e18 precision
uint256 minDuration = veRAACTok.MIN_LOCK_DURATION();
uint256 minWeight = 1;
uint256 maxWeight = 10000;
vm.startPrank(admin);
raacToken.mint(user, mintAmount);
vm.stopPrank();
//user mints veRaacTokens and votes for 2 different raacGauges with varying weights
vm.startPrank(user);
raacToken.approve(address(veRAACTok), mintAmount);
veRAACTok.lock(mintAmount, minDuration);
gaugeController.vote(address(raacGauge), minWeight);
gaugeController.vote(address(raacGauge2), maxWeight);
vm.stopPrank();
//call _calculateRewards but it always returns 0 reward regardless of the gauge weight and emission amount
uint256 reward = gaugeController._calculateReward(address(raacGauge));
uint256 gaugeWeight = gaugeController.getGaugeWeight(address(raacGauge));
console.log("Gauge weight: ", gaugeWeight);
assertEq(reward, 0);
}
}

Impact

The vulnerability causes:

  1. Gauge weights to be inflated by 1e18

  2. Zero rewards distribution due to arithmetic precision loss

  3. Permanent denial of service as weights cannot be reduced once inflated

Tools Used

Manual review, foundry test suite

Recommendations

Scale down the voting power by 1e18 before using it in weight calculations:

function vote(address gauge, uint256 weight) external override whenNotPaused {
- uint256 votingPower = veRAACToken.balanceOf(msg.sender);
+ uint256 votingPower = veRAACToken.balanceOf(msg.sender) / 10 ** ERC20(address(veRAACToken)).decimals();
_updateGaugeWeight(gauge, oldWeight, weight, votingPower);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Design choice
inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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