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