Summary
The GaugeController::vote()
function incorrectly updates gauges[gauge].weight
due to improper handling of dynamic voting power calculations.
Vulnerability Details
In the GaugeController::vote()
function, the _updateGaugeWeight()
function is called with the dynamically retrieved votingPower
to update the gauge weight:
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);
}
However, in the _updateGaugeWeight()
function, oldWeight * votingPower
is used to determine the user's previous voting weight. This approach introduces a critical issue: if the user's veRAACToken
balance changes between votes, the computed weight can be significantly distorted.
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;
}
Poc
Add the following test to test/unit/core/governance/gauges/GaugeController.test.js
and execute it:
describe("weight calculation error", () => {
it("veRAACToken balance changes", async () => {
await veRAACToken.mint(user1.address, ethers.parseEther("500"));
await veRAACToken.mint(user2.address, ethers.parseEther("500"));
await gaugeController.connect(user1).vote(await rwaGauge.getAddress(), 5000);
const wigthAfterUser1Vote = await gaugeController.getGaugeWeight(await rwaGauge.getAddress());
console.log("wigthAfterUser1Vote:",wigthAfterUser1Vote);
await gaugeController.connect(user2).vote(await rwaGauge.getAddress(), 5000);
const wigthAfterUser2Vote = await gaugeController.getGaugeWeight(await rwaGauge.getAddress());
console.log("wigthAfterUser2Vote:",wigthAfterUser2Vote);
await veRAACToken.mint(user1.address, ethers.parseEther("500"));
await gaugeController.connect(user1).vote(await rwaGauge.getAddress(), 1);
const wigthEnd = await gaugeController.getGaugeWeight(await rwaGauge.getAddress());
console.log("wigthEnd:",wigthEnd);
});
});
output:
GaugeController
weight calculation error
wigthAfterUser1Vote: 250000000000000000000n
wigthAfterUser2Vote: 500000000000000000000n
wigthEnd: 100000000000000000n
Impact
The dynamic retrieval of votingPower
within _updateGaugeWeight()
leads to incorrect gauge weight calculations whenever a user's veRAACToken
balance changes. This can result in misallocation of governance power, leading to unfair voting outcomes and potential manipulation of gauge weights.
Tools Used
Manual Review
Recommendations
To ensure accurate gauge weight calculations, store and use the user's actual voting weight at the time of voting instead of dynamically fetching votingPower. This prevents errors arising from balance changes.
for example:
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;
+ userGaugeVotes[msg.sender][gauge] = weight * votingPower;
_updateGaugeWeight(gauge, oldWeight, weight, votingPower);
emit WeightUpdated(gauge, oldWeight, weight);
}
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);
+ uint256 newGaugeWeight = oldGaugeWeight - (oldWeight / WEIGHT_PRECISION) + (newWeight * votingPower / WEIGHT_PRECISION);
g.weight = newGaugeWeight;
g.lastUpdateTime = block.timestamp;
}
test again:
GaugeController
weight calculation error
wigthAfterUser1Vote: 250000000000000000000n
wigthAfterUser2Vote: 500000000000000000000n
wigthEnd: 250100000000000000000n