Description
_updateGaugeWeight() will revert when a user tries to vote again with an increased voting power because the logic incorrectly tries to deduct the old weight first by multiplying it with the current voting power instead of using the power used at vote time:
File: contracts/core/governance/gauges/GaugeController.sol
205:
206: * @notice Updates a gauge's weight based on vote changes
207: * @dev Recalculates gauge weight using voting power
208: * @param gauge Address of the gauge
209: * @param oldWeight Previous vote weight
210: * @param newWeight New vote weight
211: * @param votingPower Voter's voting power
212: */
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;
227: }
Consider: (See PoC for exact numbers)
User votes with power 100 and sets gauge weight to 50
User locks more raacTokens and receives more veTokens, increasing their voting power to 300
User votes again and oldWeight * votingPower / WEIGHT_PRECISION
evaluates to a figure greater than oldGaugeWeight
. L222 reverts with underflow.
It's important to note that even if the tx does not revert the current formula updates the weight incorrectly. If voting power has decreased in the second call, the tx won't revert but will update weight incorrectly.
Impact
User can't vote OR weights are incorrectly updated which will impact the rewards.
Proof of Concept
First, let's add some console statements inside _updateGaugeWeight()
for easier debugging. Remember to import "hardhat/console.sol";
at the top of file GaugeController.sol
:
function _updateGaugeWeight(
address gauge,
uint256 oldWeight,
uint256 newWeight,
uint256 votingPower
) internal {
Gauge storage g = gauges[gauge];
uint256 oldGaugeWeight = g.weight;
+ console.log("... oldGaugeWeight =", oldGaugeWeight);
+ console.log("... oldWeight =", oldWeight);
+ console.log("... power =", votingPower);
+ console.log("... minus =", (oldWeight * votingPower / WEIGHT_PRECISION));
uint256 newGaugeWeight = oldGaugeWeight - (oldWeight * votingPower / WEIGHT_PRECISION)
+ (newWeight * votingPower / WEIGHT_PRECISION);
+ console.log("... newGaugeWeight =", newGaugeWeight);
g.weight = newGaugeWeight;
g.lastUpdateTime = block.timestamp;
}
Now, add this inside the describe("Emission Direction Voting",
section of RAACGauge.test.js
and see it pass with the following output:
it("does not handle votes correctly when user voting power increases between votes", async () => {
console.log("\nInitial Weight before vote =", ethers.formatEther(await gaugeController.getGaugeWeight(await raacGauge.getAddress())));
console.log("user1 voting power =", ethers.formatEther(await veRAACToken.balanceOf(user1.address)), "\n");
const weight = 5000;
await expect(
gaugeController.connect(user1).vote(await raacGauge.getAddress(), weight)
).to.be.reverted;
});
Output:
RAACGauge
Emission Direction Voting
... oldGaugeWeight = 10000
... oldWeight = 0
... power = 1000000000000000000000
... minus = 0
... newGaugeWeight = 1000000000000000010000 1️⃣
Initial Weight before vote = 1000.00000000000001
user1 voting power = 1900.0
... oldGaugeWeight = 1000000000000000010000
... oldWeight = 10000
... power = 1900000000000000000000
... minus = 1900000000000000000000 2️⃣
✔ does not handle votes correctly when user voting power increases between votes (39ms)
1 passing
Mitigation
To fix this, the contract needs to track how much voting power was used at the time of the voting. Then later on do something along the lines of:
uint256 newGaugeWeight = oldGaugeWeight + (newWeight * votingPower / WEIGHT_PRECISION) - (oldWeight * userVote.votingPowerUsed / WEIGHT_PRECISION);