Core Contracts

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

A flaw in `GaugeController::_updateGaugeWeight` causes miscalculation of the gauge's weight

Summary

The GaugeController::_updateGaugeWeight function is responsible for updating the total weight of a gauge when a user votes. It calculates the new total weight by subtracting the previously scaled weight the user voted with and adding the new user weight. The user's voting weight is determined by the formula:

veRAACToken balance * weight / precision, where:

  • weight (0 - 10000) is chosen by the user.

  • precision = 10000

However, the function only considers the current voting power, whereas it should also account for the voting power from the user's previous vote for that gauge. This results in an incorrect total gauge weight calculation.

Vulnerability Details

GaugeController::_updateGaugeWeight:

function _updateGaugeWeight(
address gauge,
uint256 oldWeight,
uint256 newWeight,
uint256 votingPower
) internal {
Gauge storage g = gauges[gauge];
uint256 oldGaugeWeight = g.weight;
@> uint256 newGaugeWeight = oldGaugeWeight - (oldWeight * votingPower / WEIGHT_PRECISION)
+ (newWeight * votingPower / WEIGHT_PRECISION);
g.weight = newGaugeWeight;
g.lastUpdateTime = block.timestamp;
}

As we can see, to calculate the new gauge weight, the old user weight should be subtracted from the current gauge total weight.
The votingPower is the current veRAACToken balance of the user. This can be a problem if a user votes multiple times for the same gauge because in this case the newGaugeWeight would be less than it should be.

PoC

import { time } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import hre from "hardhat";
const { ethers } = hre;
describe("GaugeControllerPoC", () => {
let gaugeController;
let rwaGauge;
let raacGauge;
let veRAACToken;
let rewardToken;
let owner;
let gaugeAdmin;
let user1;
let user2;
let user3;
beforeEach(async () => {
[owner, gaugeAdmin, user1, user2, user3] = await ethers.getSigners();
// 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();
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(
rewardTokenAddress,
veRAACAddress,
gaugeControllerAddress
);
await rwaGauge.waitForDeployment();
// Deploy RAACGauge with correct parameters
const RAACGauge = await ethers.getContractFactory("RAACGauge");
raacGauge = await RAACGauge.deploy(
rewardTokenAddress,
veRAACAddress,
gaugeControllerAddress
);
await raacGauge.waitForDeployment();
// Setup roles
const GAUGE_ADMIN_ROLE = await gaugeController.GAUGE_ADMIN();
await gaugeController.grantRole(GAUGE_ADMIN_ROLE, gaugeAdmin.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);
});
it("incorreclty calculates new gauge weight", async () => {
// mint 1e18 amount of veRAAC tokens to user1
await veRAACToken.mint(user1.address, ethers.parseEther("1"));
// mint 1e18 amount of veRAAC tokens to user2
await veRAACToken.mint(user2.address, ethers.parseEther("1"));
// user1 votes on a gauge with initial weight of 4000 out of 10000
await gaugeController.connect(user1).vote(await rwaGauge.getAddress(), 4000);
await gaugeController.connect(user2).vote(await rwaGauge.getAddress(), 10000);
// Later on user1 receives more veRAAC tokens and votes on the same gauge with more weight
await veRAACToken.mint(user1.address, ethers.parseEther("0.1"));
await gaugeController.connect(user1).vote(await rwaGauge.getAddress(), 4500);
// user1 initially voted with 1e18 veRAAC tokens and 4000 weight => 1e18 * 4000 / 10000 = 4e17
// user2 votes with 1e18 veRAAC tokens and 10000 weight => 1e18 * 10000 / 10000 = 1e18
// Now the gauge has total weight of 14e17
// user1 votes for the same gauge again with veRAAC token balance of 11e17 and 4500 weight => 11e17 * 4500 / 10000 = 495e15
// When calculating the new weight for the gauge the old weight is subtracted and the new weight is added
// The issue is that the new voting power is used to subtract the old weight
// newWeight = 14e17 - (4000 * 11e17 / 10000) + (4500 * 11e17 / 10000) = 1455e15
// Using the new voting power to calculate the old weight that the user previosly voted with we get:
// oldVotingUserWeight = 4000 * 11e17 / 10000 = 0.44e18
// Using the old votingPower (the correct calculation) we get:
// oldVotingUserWeight = 4000 * 1e18 / 10000 = 4e17 = 0.4e18
// 0.44e18 > 0.4e18
const newGaugeWeight = await gaugeController.getGaugeWeight(await rwaGauge.getAddress());
expect(newGaugeWeight).to.equal(ethers.parseEther("1.455"));
// the result with the current implementation is 1.455e18 but it should be 1.495e18
});
});

Output:

GaugeControllerPoC
✔ incorreclty calculates new gauge weight (1318ms)
1 passing (15s)

Impact

The issue disrupts the weight calculation functionality and leads to unfair rewards distribution across the gauges.

Tools Used

Manual Research, VSCode

Recommendations

To ensure accurate weight calculations, store each user's voting power at the time of their vote in a dedicated storage data structure, similar to how userGaugeVotes are stored.

Updates

Lead Judging Commences

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

GaugeController::_updateGaugeWeight uses current voting power for both old and new vote calculations, causing underflows when voting power increases and incorrect gauge weights

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

GaugeController::_updateGaugeWeight uses current voting power for both old and new vote calculations, causing underflows when voting power increases and incorrect gauge weights

Support

FAQs

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