Core Contracts

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

The `GaugeController::vote()` function incorrectly updates `gauges[gauge].weight` due to improper handling of dynamic voting power calculations.

Summary

The GaugeController::vote() function incorrectly updates gauges[gauge].weight due to improper handling of dynamic voting power calculations.

Vulnerability Details

In the GaugeController::vote() function, the _updateGaugeWeight() function is called with the dynamically retrieved votingPower to update the gauge weight:

// GaugeController::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];
userGaugeVotes[msg.sender][gauge] = weight;
@> _updateGaugeWeight(gauge, oldWeight, weight, votingPower);
emit WeightUpdated(gauge, oldWeight, weight);
}

However, in the _updateGaugeWeight() function, oldWeight * votingPower is used to determine the user's previous voting weight. This approach introduces a critical issue: if the user's veRAACToken balance changes between votes, the computed weight can be significantly distorted.

// GaugeController::_updateGaugeWeight()
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;
}

Poc

Add the following test to test/unit/core/governance/gauges/GaugeController.test.js and execute it:

describe("weight calculation error", () => {
it("veRAACToken balance changes", async () => {
await veRAACToken.mint(user1.address, ethers.parseEther("500"));
await veRAACToken.mint(user2.address, ethers.parseEther("500"));
// user1 calls vote with parameter 5000
await gaugeController.connect(user1).vote(await rwaGauge.getAddress(), 5000);
const wigthAfterUser1Vote = await gaugeController.getGaugeWeight(await rwaGauge.getAddress());
console.log("wigthAfterUser1Vote:",wigthAfterUser1Vote);
// user2 calls vote with parameter 5000
await gaugeController.connect(user2).vote(await rwaGauge.getAddress(), 5000);
const wigthAfterUser2Vote = await gaugeController.getGaugeWeight(await rwaGauge.getAddress());
console.log("wigthAfterUser2Vote:",wigthAfterUser2Vote);
// user1's balance increases, then they vote again
await veRAACToken.mint(user1.address, ethers.parseEther("500"));
// user1 calls vote again with parameter 1
await gaugeController.connect(user1).vote(await rwaGauge.getAddress(), 1);
// Expected result: 250e18 + 1e17, but incorrect value is returned ❌
const wigthEnd = await gaugeController.getGaugeWeight(await rwaGauge.getAddress());
console.log("wigthEnd:",wigthEnd);
});
});

output:

GaugeController
weight calculation error
wigthAfterUser1Vote: 250000000000000000000n
wigthAfterUser2Vote: 500000000000000000000n
wigthEnd: 100000000000000000n

Impact

The dynamic retrieval of votingPower within _updateGaugeWeight() leads to incorrect gauge weight calculations whenever a user's veRAACToken balance changes. This can result in misallocation of governance power, leading to unfair voting outcomes and potential manipulation of gauge weights.

Tools Used

Manual Review

Recommendations

To ensure accurate gauge weight calculations, store and use the user's actual voting weight at the time of voting instead of dynamically fetching votingPower. This prevents errors arising from balance changes.

for example:

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;
+ userGaugeVotes[msg.sender][gauge] = weight * votingPower;
_updateGaugeWeight(gauge, oldWeight, weight, votingPower);
emit WeightUpdated(gauge, oldWeight, weight);
}
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);
+ uint256 newGaugeWeight = oldGaugeWeight - (oldWeight / WEIGHT_PRECISION) + (newWeight * votingPower / WEIGHT_PRECISION);
g.weight = newGaugeWeight;
g.lastUpdateTime = block.timestamp;
}

test again:

GaugeController
weight calculation error
wigthAfterUser1Vote: 250000000000000000000n
wigthAfterUser2Vote: 500000000000000000000n
wigthEnd: 250100000000000000000n
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.