The `stake()` function in `BaseGauge.sol` is defined as:
```solidity
function stake(uint256 amount) external nonReentrant updateReward(msg.sender) {
if (amount == 0) revert InvalidAmount();
_totalSupply += amount;
_balances[msg.sender] += amount;
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
emit Staked(msg.sender, amount);
}
```
It uses the `updateReward` modifier, which calls `_updateReward(msg.sender)`.
`_updateReward` invokes `earned(msg.sender)`, which depends on `getUserWeight(msg.sender)`.
`getUserWeight` calls `_applyBoost` to adjust the base weight:
```solidity
function _applyBoost(address account, uint256 baseWeight) internal view virtual returns (uint256) {
if (baseWeight == 0) return 0;
IERC20 veToken = IERC20(IGaugeController(controller).veRAACToken());
uint256 veBalance = veToken.balanceOf(account);
uint256 totalVeSupply = veToken.totalSupply();
BoostCalculator.BoostParameters memory params = BoostCalculator.BoostParameters({
maxBoost: boostState.maxBoost,
minBoost: boostState.minBoost,
boostWindow: boostState.boostWindow,
totalWeight: boostState.totalWeight,
totalVotingPower: boostState.totalVotingPower,
votingPower: boostState.votingPower
});
uint256 boost = BoostCalculator.calculateBoost(veBalance, totalVeSupply, params);
return (baseWeight * boost) / 1e18;
}
```
BoostCalculator.calculateBoost computes:
```solidity
uint256 votingPowerRatio = (veBalance * 1e18) / totalVeSupply;
uint256 boostRange = params.maxBoost - params.minBoost;
uint256 boost = params.minBoost + ((votingPowerRatio * boostRange) / 1e18);
```
In the constructor, boostState is initialized with minBoost = 1e18 (from VeRAAC), maxBoost = 25000, and boostWindow = 7 days, while other fields (totalWeight, totalVotingPower, votingPower) remain 0 unless updated.
The issue arises because minBoost = 1e18 (1e18 basis points = 10000x) is in wei units, while maxBoost = 25000 (2.5x) is in basis points. This mismatch causes boostRange to be negative (25000 - 1e18 = -999,975,000), leading to a negative boost value (e.g., 1e18 - 999,975,000), which underflows when multiplied by baseWeight and divided by 1e18 in _applyBoost.
### Proof of Concept
The issue is demonstrated in BaseGaugeTest.t.sol:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../BaseGauge.sol";
import "../VeRAAC.sol";
import "../GaugeController.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract TestERC20 is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
_mint(msg.sender, 1_000_000 * 10**18);
}
}
contract TestGauge is BaseGauge {
constructor(address _rewardToken, address _stakingToken, address _controller, uint256 _maxEmission, uint256 _periodDuration)
BaseGauge(_rewardToken, _stakingToken, _controller, _maxEmission, _periodDuration) {}
function _getBaseWeight(address) internal view override returns (uint256) {
return GaugeController(controller).getGaugeWeight(address(this));
}
function getTotalWeight() external view override returns (uint256) {
return totalSupply();
}
function getTimeWeightedWeight() public view override returns (uint256) {
return GaugeController(controller).getGaugeWeight(address(this));
}
}
contract BaseGaugeTest is Test {
TestGauge gauge;
TestERC20 rewardToken;
TestERC20 stakingToken;
TestERC20 raacToken;
VeRAAC veToken;
GaugeController controller;
address admin = address(0x1);
address user = address(0x2);
address controllerAddr;
uint256 constant MAX_EMISSION = 10_000 * 10**18;
uint256 constant PERIOD_DURATION = 7 days;
function setUp() public {
rewardToken = new TestERC20("Reward Token", "RWD");
stakingToken = new TestERC20("Staking Token", "STK");
raacToken = new TestERC20("RAAC Token", "RAAC");
veToken = new VeRAAC(address(raacToken));
controller = new GaugeController(address(veToken));
controllerAddr = address(controller);
vm.prank(admin);
gauge = new TestGauge(address(rewardToken), address(stakingToken), controllerAddr, MAX_EMISSION, PERIOD_DURATION);
rewardToken.transfer(address(gauge), 50_000 * 10**18);
stakingToken.transfer(user, 10_000 * 10**18);
raacToken.transfer(user, 10_000 * 10**18);
vm.startPrank(user);
raacToken.approve(address(veToken), 1_000 * 10**18);
veToken.lock(1_000 * 10**18, 365 days);
vm.stopPrank();
vm.prank(admin);
controller.addGauge(address(gauge), controller.getRWAType(), 0);
vm.prank(user);
controller.vote(address(gauge), 5000);
vm.prank(controllerAddr);
gauge.notifyRewardAmount(7_000 * 10**18);
}
function testStakeUnderflow() public {
uint256 stakeAmount = 1_000 * 10**18;
vm.startPrank(user);
stakingToken.approve(address(gauge), stakeAmount);
gauge.stake(stakeAmount); // Fails with underflow
vm.stopPrank();
assertEq(gauge.balanceOf(user), stakeAmount, "User balance incorrect");
}
}
```
Test Result:
```yaml
forge test --match-test testStakeUnderflow -vvvv
Ran 1 test for test/foundry/Governance/gauges/BaseGaugeTest.t.sol:BaseGaugeTest
[FAIL: panic: arithmetic underflow or overflow (0x11)] testStakeUnderflow() (gas: 89041)
```
Scenario:
User locks 1000 RAAC, gets 250e18 veRAAC ((1000e18 * 365) / 1460).
Votes 50% (5000 basis points), setting gauge weight to 125e18.
stake(1e21) triggers _applyBoost with minBoost = 1e18, maxBoost = 25000.
boost = 1e18 - 999,975,000 (negative), causing (125e18 * boost) / 1e18 to underflow.
setBoostParameters Function
The contract includes a function to adjust boost parameters:
```solidity
function setBoostParameters(uint256 _maxBoost, uint256 _minBoost, uint256 _boostWindow) external onlyController {
boostState.maxBoost = _maxBoost;
boostState.minBoost = _minBoost;
boostState.boostWindow = _boostWindow;
}
```
Purpose: Allows the controller to set maxBoost, minBoost, and boostWindow at runtime.
Critical Dependency: Despite its existence, setBoostParameters isn’t called automatically after deployment. The default minBoost = 1e18 (set in the constructor via VeRAAC) remains unless explicitly updated. Without invoking this function with correct values (e.g., minBoost = 10000), users cannot stake or interact with the contract due to the underflow in _applyBoost.
Mandatory Initialization Post-Deployment:
Ensure setBoostParameters is called immediately after deployment by the controller:
solidity
// Example call by controller post-deployment
setBoostParameters(25000, 10000, 7 days);
Document this requirement clearly to prevent deployment without initialization.
Correct Boost Parameter Units in Constructor (if modification allowed):
Update the BaseGauge constructor to initialize boostState.minBoost in basis points:
```solidity
constructor(...) {
boostState.maxBoost = 25000; // 2.5x in basis points
boostState.minBoost = 10000; // 1x in basis points
boostState.boostWindow = 7 days;
}
```
This eliminates the need for an immediate setBoostParameters call, though runtime adjustment remains available.
Validate Boost Calculation:
Add runtime checks in _applyBoost to cap or floor the boost value (if modification allowed):
```solidity
uint256 boost = BoostCalculator.calculateBoost(veBalance, totalVeSupply, params);
if (boost < boostState.minBoost) boost = boostState.minBoost;
if (boost > boostState.maxBoost) boost = boostState.maxBoost;
return (baseWeight * boost) / 1e18;
```
Test-Driven Workaround:
Since contract modification isn’t permitted, ensure tests include:
```solidity
vm.prank(controllerAddr);
gauge.setBoostParameters(25000, 10000, 7 days);
```
This fixes the issue in testing and highlights the deployment dependency.
Conclusion
The stake function’s reliance on _applyBoost via updateReward ties staking to boost calculations, making it vulnerable to arithmetic errors if boost parameters are misconfigured. The setBoostParameters function provides a mechanism to correct this, but its absence from the initialization process leaves the contract inoperable by default. Users cannot stake or interact without this call, as the default minBoost = 1e18 triggers an underflow in _applyBoost. Ensuring this function is invoked with proper values (e.g., maxBoost = 25000, minBoost = 10000) post-deployment is critical to stabilize staking and maintain system integrity.