Summary
The GaugeController contract uses an incorrect calculation of voting power when uses vote for gauges
Vulnerability Details
The veRAACToken contract is a vote-escrowed token contract where voting power decays linearly over time.
* @title Vote Escrowed RAAC Token
* @author RAAC Protocol Team...
* @notice A vote-escrowed token contract that allows users to lock RAAC tokens to receive voting power and boost capabilities
* @dev Implementation of vote-escrowed RAAC (veRAAC) with time-weighted voting power, emergency controls, governance integration and boost calculations
* Key features:
* - Users can lock RAAC tokens for voting power
* - Voting power decays linearly over time
* - Includes emergency withdrawal mechanisms
* - Integrates with governance for proposal voting
* - Provides boost calculations for rewards
*/
The problem is that when calculatung a users voting power, the GaugeConroller contract gets the user's veRAACToken contract balance which seems to provide different results compared to
* @notice Core voting functionality for gauge weights
* @dev Updates gauge weights based on user's veToken balance
* @param gauge Address of gauge to vote for
* @param weight New weight value in basis points (0-10000)
*/
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);
}
The function veRAACToken::balanceOf provides different results compared to veRAACToken::getVotingPower.
Add this POC to test/unit/core/tokens/veRAACToken.test.js::Voting Power Calculations. The POC shows that these two functions deliver different results as time passes:
it("should decay voting power linearly over time", async () => {
const amount = ethers.parseEther("1000");
console.log("Test Amount: ", amount);
const duration = 365 * 24 * 3600;
console.log("Test Duration: ", duration);
await veRAACToken.connect(users[0]).lock(amount, duration);
const initialPower = await veRAACToken.getVotingPower(users[0].address);
console.log("Test Initial Power: ", initialPower);
await time.increase(duration / 2);
const midPower = await veRAACToken.getVotingPower(users[0].address);
console.log("Test Mid Power: ", midPower);
const midBalance = await veRAACToken.balanceOf(users[0].address);
expect(midPower).to.be.lt(initialPower);
expect(midPower).to.be.gt(0);
expect(midBalance).to.be.gt(midPower);
});
Impact
The voting power calculation using veRAACToken::balanceOf does not decay with time therefore gives higher values than expected. I am evaluating this as a high because this calculation scheme will report higher values than expected therefore leading to incorrect weights allocated to gauges compared users who will have locked their tokens in the same block as they cast votes.
Tools Used
Manual review
Recommendations
Consider using the function veRAACToken::getVotingPower to determine votes to allocate to gauges.