Summary
The BaseGauge
contract and BoostCalculator
library implement a reward distribution system with boost multipliers based on user staking and voting power. However, there is a critical issue in the initialization of minBoost
and maxBoost
parameters in the BaseGauge
contract. Specifically, minBoost
is set to 1e18
(1,000,000,000,000,000,000), while maxBoost
is set to 25000
. This causes the calculateBoost
function in the BoostCalculator
library to behave incorrectly, leading to unintended reward distribution logic.
Vulnerability Details
In the BaseGauge
constructor, the boostState
is initialized as follows:
boostState.maxBoost = 25000;
boostState.minBoost = 1e18;
boostState.boostWindow = 7 days;
Here, minBoost
is set to 1e18
, while maxBoost
is set to 25000
. This means minBoost
is significantly larger than maxBoost
, which violates the intended logic of the boost calculation.
The _applyBoost
function will use these params to call calculateBoost
function:
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
);
The calculateBoost
function in the BoostCalculator
library is designed to calculate a user's boost multiplier based on their veToken balance and total supply. The function uses minBoost
and maxBoost
to determine the boost range. Here is the relevant code:
function calculateBoost(
uint256 veBalance,
uint256 totalVeSupply,
BoostParameters memory params
) internal pure returns (uint256) {
if (totalVeSupply == 0) {
return params.minBoost;
}
uint256 votingPowerRatio = (veBalance * 1e18) / totalVeSupply;
uint256 boostRange = params.maxBoost - params.minBoost;
uint256 boost = params.minBoost + ((votingPowerRatio * boostRange) / 1e18);
if (boost < params.minBoost) {
return params.minBoost;
}
if (boost > params.maxBoost) {
return params.maxBoost;
}
return boost;
}
In calculateBoost
function, boostRange
is calculated as params.maxBoost - params.minBoost
. Since minBoost
(1e18
) is much larger than maxBoost
(25000
), this results in an underflow in Solidity's uint256
type, as a result, this function will revert.
The _applyBoost
function is used in getUserWeight
:
function getUserWeight(address account) public view virtual returns (uint256) {
uint256 baseWeight = _getBaseWeight(account);
return _applyBoost(account, baseWeight);
}
The getUserWeight
function is used in earned
:
function earned(address account) public view returns (uint256) {
return (getUserWeight(account) *
(getRewardPerToken() - userStates[account].rewardPerTokenPaid) / 1e18
) + userStates[account].rewards;
}
The earned
is used in _updateReward
:
function _updateReward(address account) internal {
rewardPerTokenStored = getRewardPerToken();
lastUpdateTime = lastTimeRewardApplicable();
if (account != address(0)) {
UserState storage state = userStates[account];
state.rewards = earned(account);
state.rewardPerTokenPaid = rewardPerTokenStored;
state.lastUpdateTime = block.timestamp;
emit RewardUpdated(account, state.rewards);
}
}
As a result, the _updateReward
function will always revert due to underflow issues.
POC
add following test case into RAACGauge.test.js
it("getUserWeight should not revert", async () => {
const weight = await raacGauge.getUserWeight(user1.address);
expect(weight).to.be.gte(0);
});
comment the following code in beforeEach hook so that the initialization of minBoost and maxBoost parameters will not be changed:
run npx hardhat test --grep "getUserWeight should not revert"
1) RAACGauge
Initialization
getUserWeight should not revert:
Error: VM Exception while processing transaction: reverted with panic code 0x11 (Arithmetic operation overflowed outside of an unchecked block)
at RAACGauge.calculateBoost (contracts/libraries/governance/BoostCalculator.sol:89)
at RAACGauge._applyBoost (contracts/core/governance/gauges/BaseGauge.sol:246)
at RAACGauge.getUserWeight (contracts/core/governance/gauges/BaseGauge.sol:596)
at EdrProviderWrapper.request (node_modules/hardhat/src/internal/hardhat-network/provider/provider.ts:444:41)
at staticCallResult (node_modules/ethers/src.ts/contract/contract.ts:337:22)
at staticCall (node_modules/ethers/src.ts/contract/contract.ts:303:24)
The transaction reverts due to Arithmetic operation overflowed.
Impact
The getUserWeight
function will always revert if the minBoost is not reset.
The impact is Medium because the minBoost can be reset by calling setBoostParameters. The likelihood is High, so the severity is Medium.
Tools Used
Maunal Review
Recommendations
Consider following fix in BaseGauge
constructor(
address _rewardToken,
address _stakingToken,
address _controller,
uint256 _maxEmission,
uint256 _periodDuration
) {
rewardToken = IERC20(_rewardToken);
stakingToken = IERC20(_stakingToken);
controller = _controller;
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(CONTROLLER_ROLE, _controller);
boostState.maxBoost = 25000;
boostState.minBoost = 10000;
boostState.boostWindow = 7 days;
...
...