Summary
Function GaugeController::vote() allows an user to vote for gauge and update the gauge weight based on user's current veToken balance. However, users can be unable to vote to update gauge weight because of arithmetic underflow.
Vulnerability Details
The function GaugeController::vote() allows an user to vote for a gauge many times. In case, the user already voted for that gauge, the next vote will consider the old vote weight with user's current voting power. The gauge's total weight is recalculated with user's old voted weight, new voted weight and the current voting power, handled by function _updateGaugeWeight(). This function computes the gauge's new total weight: uint256 newGaugeWeight = oldGaugeWeight - (oldWeight * votingPower / WEIGHT_PRECISION) + (newWeight * votingPower / WEIGHT_PRECISION). This operation can be failed when oldGaugeWeight < (oldWeight * votingPower / WEIGHT_PRECISION), causing arithmetic underflow. Hence, the function vote() reverts.
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);
}
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 test to test/unit/core/governance/gauges/GaugeController.test.js
describe("Period Management", () => {
it.only("vote reverts", async () => {
await gaugeController.connect(user1).vote(await raacGauge.getAddress(), 5000);
await veRAACToken.mint(user1.address, ethers.parseEther("1000"));
await gaugeController.connect(user1).vote(await raacGauge.getAddress(), 7000);
});
Run the test and console shows
GaugeController
Period Management
1) vote reverts
0 passing (2s)
1 failing
1) GaugeController
Period Management
vote reverts:
Error: VM Exception while processing transaction: reverted with panic code 0x11 (Arithmetic operation overflowed outside of an unchecked block)
at GaugeController._updateGaugeWeight (contracts/core/governance/gauges/GaugeController.sol:235)
at GaugeController.vote (contracts/core/governance/gauges/GaugeController.sol:210)
Impact
Tools Used
Manual
Recommendations
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);
+ int256 newGaugeWeight;
+ unchecked {
+ newGaugeWeight = int(oldGaugeWeight - (oldWeight * votingPower / WEIGHT_PRECISION)
+ + (newWeight * votingPower / WEIGHT_PRECISION));
+ }
- g.weight = newGaugeWeight;
+ g.weight = newGaugeWeight < 0 ? 0 : uint(newGaugeWeight);
g.lastUpdateTime = block.timestamp;
}