Summary
In case of vote cast we check that if user has voted on given gauge earlier than we subtract the pervius vote from total weight and than add new votes to total weight , how ever this logic is not correct as we subtract the current power into oldWeight from old weight decpit the fact that the user power has changed from old weight.
Vulnerability Details
The normal flow to vote on gauge is that user give weight <= 10000 and gauge address. we first fetch the old vote weight of user to this gauge and pass the oldWeight aling with the new weight to update weight function.
/contracts/core/governance/gauges/GaugeController.sol:190
190: function vote(address gauge, uint256 weight) external override whenNotPaused {
...
196: uint256 votingPower = veRAACToken.balanceOf(msg.sender);
197: uint256 oldWeight = userGaugeVotes[msg.sender][gauge];
198: userGaugeVotes[msg.sender][gauge] = weight;
199:
200: _updateGaugeWeight(gauge, oldWeight, weight, votingPower);
...
203: }
_updateGaugeWeight subtract the oldWeight from gauge weight and add new weight to the gauge weight.
213: function _updateGaugeWeight(
214: address gauge,
215: uint256 oldWeight,
216: uint256 newWeight,
217: uint256 votingPower
218: ) internal {
219: Gauge storage g = gauges[gauge];
220:
221: uint256 oldGaugeWeight = g.weight;
222: uint256 newGaugeWeight = oldGaugeWeight - (oldWeight * votingPower / WEIGHT_PRECISION)
223: + (newWeight * votingPower / WEIGHT_PRECISION);
224:
225: g.weight = newGaugeWeight;
226: g.lastUpdateTime = block.timestamp;
...
230: }
The issue arises when a user who initially had lower voting power later mints more voting power or loses voting power by burning veRAACToken. However, the system still multiplies the current voting power with the old weight, leading to inaccurate calculations.
Example flow :
User First Vote :
g.weight =0;
user balance = 10e18
newWeight = 6000
so the total weight of gauge will be g.weight : 6000000000000000000
Now after some time the user receive more veRAACToken tokens and than vote again with current weight or different weight
so new the g.weight : 6000000000000000000
user balance = 15e18
newWeight : 6000
Now it will either revert if the user is only one who has voted or decrease the g.weight with a wrong value if there are multiple users who have voted.
This part of the calculation will revert : oldGaugeWeight - (oldWeight * votingPower / WEIGHT_PRECISION).
6000000000000000000 - (6000 * 15e18 / 10000) => -3000000000000000000 : so it will revert in this case.
POC:
Add following test case GaugeCantroller.test.js and run with command : npx hardhat test.
it.only("vote caste will revert ", async () => {
await veRAACToken.mint(user1.address, ethers.parseEther("1000"));
await gaugeController.connect(user1).vote(await rwaGauge.getAddress(), 5000);
const userVote = await gaugeController.userGaugeVotes(user1.address, await rwaGauge.getAddress());
expect(userVote).to.equal(5000);
await veRAACToken.mint(user1.address, ethers.parseEther("1000"));
await gaugeController.connect(user1).vote(await rwaGauge.getAddress(), 4000);
});
Logs:
Error: VM Exception while processing transaction: reverted with panic code 0x11 (Arithmetic operation overflowed outside of an unchecked block)
Impact
Due to incorrect weight subtraction, the gauge's weight will be wrongly reduced if the user has already voted or may revert, as demonstrated in the provided POC.
Tools Used
Recommendations
Track the weight of user with his voting power , and use in case of subtraction.