Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: high
Valid

GaugeController vote() multiplies current voting power to weight while subtracting instead of using the power used at vote time

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)

  1. User votes with power 100 and sets gauge weight to 50

  2. User locks more raacTokens and receives more veTokens, increasing their voting power to 300

  3. 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; // 50% in basis points
await expect(
gaugeController.connect(user1).vote(await raacGauge.getAddress(), weight)
).to.be.reverted; // @audit-issue : reverts with underflow
});

Output:

RAACGauge
Emission Direction Voting
... oldGaugeWeight = 10000
... oldWeight = 0
... power = 1000000000000000000000
... minus = 0
... newGaugeWeight = 1000000000000000010000 1️⃣ // <---- vote() called during setup of existing tests on L70 of `RAACGauge.test.js`. See https://github.com/Cyfrin/2025-02-raac/blob/main/test/unit/core/governance/gauges/RAACGauge.test.js#L70
Initial Weight before vote = 1000.00000000000001 // <---- same as 1️⃣, just formatted
user1 voting power = 1900.0
... oldGaugeWeight = 1000000000000000010000 // <---- same as 1️⃣
... oldWeight = 10000
... power = 1900000000000000000000
... minus = 1900000000000000000000 2️⃣ // <---- 2️⃣ > 1️⃣, hence reverts with underflow
✔ 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:

// Remove old vote using OLD voting power
uint256 newGaugeWeight = oldGaugeWeight + (newWeight * votingPower / WEIGHT_PRECISION) - (oldWeight * userVote.votingPowerUsed / WEIGHT_PRECISION);
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

GaugeController::_updateGaugeWeight uses current voting power for both old and new vote calculations, causing underflows when voting power increases and incorrect gauge weights

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

GaugeController::_updateGaugeWeight uses current voting power for both old and new vote calculations, causing underflows when voting power increases and incorrect gauge weights

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.