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;
}