Summary
The GaugeController::addGauge function is responsible for adding new gauges to the system, initializing gauge-specific parameters, and creating an associated time period using the TimeWeightedAverage library. In this process, a non-zero minimum weight is enforced for period tracking by using the expression:
uint256 periodWeight = initialWeight == 0 ? 1 : initialWeight;
However, while periodWeight is correctly computed and used for period creation, the gauge's own stored weight is mistakenly set to initialWeight instead of periodWeight. As a result, if initialWeight is zero, the gauge’s stored weight becomes zero. Later, when the updatePeriod function is called, it passes g.weight (which is zero) to the TimeWeightedAverage.createPeriod function. Since that function checks that weight is not zero (reverting with ZeroWeight if it is), every attempt to update the gauge period results in a revert. Consequently, the gauge’s period state cannot be updated, effectively causing a permanent denial of service (DoS) for that gauge.
Vulnerability Details
Incorrect Weight Storage in addGauge
In the addGauge function, the minimum period weight is determined to ensure that period creation does not fail due to a zero weight:
uint256 periodWeight = initialWeight == 0 ? 1 : initialWeight;
This ensures that if the admin passes initialWeight == 0, the period used for time-weighted calculations will have a weight of 1.
However, when the gauge is stored in the contract’s mapping, the code incorrectly assigns the gauge’s weight using initialWeight rather than periodWeight:
gauges[gauge] = Gauge({
weight: initialWeight,
typeWeight: 0,
lastUpdateTime: block.timestamp,
gaugeType: gaugeType,
isActive: true,
lastRewardTime: block.timestamp
});
Impact on updatePeriod Function
Later, the updatePeriod function attempts to update the gauge’s time period state. It retrieves the gauge’s stored weight (which may be zero) and passes it to the TimeWeightedAverage period creation call:
TimeWeightedAverage.createPeriod(
period,
block.timestamp + 1,
duration,
0,
g.weight
);
The TimeWeightedAverage library’s createPeriod function includes a check that prevents period creation if weight is zero:
if (weight == 0) revert ZeroWeight();
Thus, if initialWeight was zero at the time of gauge creation, g.weight remains zero, and every subsequent call to updatePeriod will revert with a ZeroWeight error. This failure to update the gauge’s period state causes a permanent denial of service (DoS) for that gauge, disrupting time-weighted average calculations, reward distributions, and governance mechanisms that depend on up-to-date gauge data.
Visual Code Highlights
Proof of Concept
The following Foundry test suite demonstrates the vulnerability. The test creates a gauge with an initialWeight of zero, causing the gauge’s stored weight to be zero. Later, when attempting to update the period, the call reverts with ZeroWeight, confirming the DoS condition.
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {BaseGauge} from "../src/core/governance/gauges/BaseGauge.sol";
import {GaugeController} from "../src/core/governance/gauges/GaugeController.sol";
import {RAACGauge} from "../src/core/governance/gauges/RAACGauge.sol";
import {RWAGauge} from "../src/core/governance/gauges/RWAGauge.sol";
import {veRAACToken} from "../src/core/tokens/veRAACToken.sol";
import {RAACToken} from "../src/core/tokens/RAACToken.sol";
import {RewardTokenMock} from "./mocks/RewardTokenMock.m.sol";
import {StakingTokenMock} from "./mocks/StakingTokenMock.m.sol";
import {IGaugeController} from "../src/interfaces/core/governance/gauges/IGaugeController.sol";
import {IGauge} from "../src/interfaces/core/governance/gauges/IGauge.sol";
contract GaugeTest is Test {
GaugeController gaugeController;
RAACGauge raacGauge;
RWAGauge rwaGauge;
veRAACToken veToken;
RAACToken raacToken;
RewardTokenMock rewardToken;
StakingTokenMock stakingToken;
address GAUGE_CONTROLLER_OWNER = makeAddr("GAUGE_CONTROLLER_OWNER");
address RAAC_GAUGE_OWNER = makeAddr("RAAC_GAUGE_OWNER");
address RWA_GAUGE_OWNER = makeAddr("RWA_GAUGE_OWNER");
address RAAC_OWNER = makeAddr("RAAC_OWNER");
address RAAC_MINTER = makeAddr("RAAC_MINTER");
uint256 initialRaacSwapTaxRateInBps = 200;
uint256 initialRaacBurnTaxRateInBps = 150;
uint256 MAX_LOCK_AMOUNT = 10_000_000e18;
uint256 MAX_LOCK_DURATION = 1460 days;
address VE_RAAC_OWNER = makeAddr("VE_RAAC_OWNER");
address ALICE = makeAddr("ALICE");
address BOB = makeAddr("BOB");
address CHARLIE = makeAddr("CHARLIE");
address DEVIL = makeAddr("DEVIL");
function setUp() public {
vm.startPrank(RAAC_OWNER);
raacToken = new RAACToken(RAAC_OWNER, initialRaacSwapTaxRateInBps, initialRaacBurnTaxRateInBps);
vm.stopPrank();
vm.startPrank(VE_RAAC_OWNER);
veToken = new veRAACToken(address(raacToken));
vm.stopPrank();
rewardToken = new RewardTokenMock();
stakingToken = new StakingTokenMock();
vm.startPrank(GAUGE_CONTROLLER_OWNER);
gaugeController = new GaugeController(address(veToken));
vm.stopPrank();
vm.startPrank(RWA_GAUGE_OWNER);
rwaGauge = new RWAGauge(address(rewardToken), address(stakingToken), address(gaugeController));
vm.stopPrank();
}
function testGaugeControllerUpdatePeriodFailsDueToZeroGaugeWeight() public {
uint256 initialWeight = 0;
vm.startPrank(GAUGE_CONTROLLER_OWNER);
gaugeController.addGauge(address(rwaGauge), IGaugeController.GaugeType.RWA, initialWeight);
vm.stopPrank();
vm.warp(block.timestamp + 30 days);
vm.startPrank(ALICE);
vm.expectRevert(bytes4(keccak256("ZeroWeight()")));
gaugeController.updatePeriod(address(rwaGauge));
vm.stopPrank();
}
}
How to Run the Test
Create a Foundry Project:
forge init my-foundry-project
Remove Unnecessary Files:
Delete extraneous files to keep the project clean.
Convert or Import Contracts:
Place your contract files in the src directory.
Create Test and Mocks Folders:
Create a test directory adjacent to src and include any necessary mock contracts.
Add the Test Suite:
Create a file named GaugeTest.t.sol in the test directory and paste the above test suite.
Run the Test:
forge test --mt testGaugeControllerUpdatePeriodFailsDueToZeroGaugeWeight -vv
Expected Output:
The test should revert with ZeroWeight(), confirming that the gauge period update fails due to the stored gauge weight being zero.
Impact
Immediate Denial of Service (DoS):
Gauges added with an initial weight of zero will never have their period state updated, effectively locking the gauge in a stale state.
Disrupted Reward Distribution:
Since gauge periods are used to calculate time-weighted averages for reward distribution, failing to update these periods leads to incorrect reward allocations.
Governance and System Instability:
Inaccurate gauge information affects governance decisions and user incentives, undermining the trust and stability of the entire protocol.
Potential Exploitation:
Malicious admins or attackers might deliberately set gauges with zero weight to disrupt the system or manipulate reward and governance outcomes.
Tools Used
Recommendations
To resolve this vulnerability, the gauge’s stored weight must be initialized with the non-zero periodWeight value rather than the raw initialWeight. This ensures that subsequent calls to updatePeriod use a valid weight and do not revert.
Diff Recommendations for GaugeController::addGauge
function addGauge(address gauge, GaugeType gaugeType, uint256 initialWeight) external onlyGaugeAdmin {
if (gauges[gauge].lastUpdateTime != 0) revert GaugeAlreadyExists();
if (gaugeType != GaugeType.RWA && gaugeType != GaugeType.RAAC) {
revert InvalidGaugeType();
}
// Use minimum weight (1) for period tracking if initialWeight is 0
uint256 periodWeight = initialWeight == 0 ? 1 : initialWeight;
uint256 duration = gaugeType == GaugeType.RWA ? 30 days : 7 days;
- gauges[gauge] = Gauge({
- weight: initialWeight,
- typeWeight: 0,
- lastUpdateTime: block.timestamp,
- gaugeType: gaugeType,
- isActive: true,
- lastRewardTime: block.timestamp
- });
+ gauges[gauge] = Gauge({
+ weight: periodWeight, // Use non-zero minimum weight for gauge storage
+ typeWeight: 0,
+ lastUpdateTime: block.timestamp,
+ gaugeType: gaugeType,
+ isActive: true,
+ lastRewardTime: block.timestamp
+ });
// Initialize period with current timestamp
TimeWeightedAverage.Period storage period = gaugePeriods[gauge];
TimeWeightedAverage.createPeriod(
period,
block.timestamp, // Start from current timestamp
duration,
periodWeight,
periodWeight
);
_gaugeList.push(gauge);
emit GaugeAdded(gauge, gaugeType);
}