Summary
The vote function in the GaugeController contract uses the latest veRAACToken.balanceOf(msg.sender) as votingPower to call _updateGaugeWeight. However, in _updateGaugeWeight, this votingPower is incorrectly used to calculate the old weight contribution. If the user's votingPower has increased since their last vote, this can lead to incorrect calculations and potential arithmetic overflow. This issue arises because the old weight contribution should be calculated using the votingPower at the time of the previous vote, not the current votingPower.
Vulnerability Details
The vote function retrieves the user's current veRAACToken balance as votingPower and passes it to _updateGaugeWeight:
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);
}
In _updateGaugeWeight, the votingPower is used to calculate both the old and new weight contributions:
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;
}
The votingPower passed to _updateGaugeWeight is the user's current veRAACToken balance. This votingPower is used to calculate both the old and new weight contributions, which is incorrect because the old weight contribution should be calculated using the votingPower at the time of the previous vote. If the user's votingPower has increased since their last vote, the calculation of the old weight contribution can result in a negative value, causing an underflow in Solidity's uint256.
Example Scenario
-
First Vote:
-
Second Vote:
votingPower = 20000 (increased from 10000)
weight = 100
newGaugeWeight = 100 - (100 * 20000 / 10000) + (100 * 20000 / 10000)
The subtraction 100 - (100 * 20000 / 10000) results in 100 - 200 = -100, which causes an underflow in Solidity's uint256.
POC
add following test case in GaugeController.test.js
it("vote should not revert after the balance of veRAACToken increase", async () => {
await gaugeController.connect(user1).vote(await rwaGauge.getAddress(), 5000);
await veRAACToken.mint(user1.address, ethers.parseEther("1000"));
await gaugeController.connect(user1).vote(await rwaGauge.getAddress(), 5000);
});
run npx hardhat test --grep "vote should not revert"
1) GaugeController
Weight Management
vote should not revert:
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:224)
at GaugeController.vote (contracts/core/governance/gauges/GaugeController.sol:200)
at EdrProviderWrapper.request (node_modules/hardhat/src/internal/hardhat-network/provider/provider.ts:444:41)
at HardhatEthersSigner.sendTransaction (node_modules/@nomicfoundation/hardhat-ethers/src/signers.ts:125:18)
at send (node_modules/ethers/src.ts/contract/contract.ts:313:20)
The vote transaction will revert due to Arithmetic operation overflowed error.
Impact
The gauge weight calculation is totally incorrect. The impact is High, the likelihood is High, so the severity is High.
Tools Used
Manual Review
Recommendations
To fix this issue, we need to track the votingPower used in each vote and use it to calculate the old weight contribution in _updateGaugeWeight.
Add a mapping to store the votingPower used in each vote:
mapping(address => mapping(address => uint256)) public userVotingPower;
Store the votingPower used in the current vote:
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];
uint256 oldVotingPower = userVotingPower[msg.sender][gauge];
userGaugeVotes[msg.sender][gauge] = weight;
userVotingPower[msg.sender][gauge] = votingPower;
_updateGaugeWeight(gauge, oldWeight, weight, oldVotingPower, votingPower);
emit WeightUpdated(gauge, oldWeight, weight);
}
Use the old votingPower to calculate the old weight contribution and the new votingPower to calculate the new weight contribution:
function _updateGaugeWeight(
address gauge,
uint256 oldWeight,
uint256 newWeight,
uint256 oldVotingPower,
uint256 newVotingPower
) internal {
Gauge storage g = gauges[gauge];
uint256 oldGaugeWeight = g.weight;
uint256 newGaugeWeight = oldGaugeWeight - (oldWeight * oldVotingPower / WEIGHT_PRECISION)
+ (newWeight * newVotingPower / WEIGHT_PRECISION);
g.weight = newGaugeWeight;
g.lastUpdateTime = block.timestamp;
}