Summary
After analyzing the getBoostMultiplier function in BoostController.sol, I can confirm a critical mathematical flaw that causes incorrect boost multiplier calculations.
Vulnerable Code
function getBoostMultiplier(
address user,
address pool
) external view override returns (uint256) {
if (!supportedPools[pool]) revert PoolNotSupported();
UserBoost storage userBoost = userBoosts[user][pool];
if (userBoost.amount == 0) return MIN_BOOST;
uint256 baseAmount = userBoost.amount * 10000 / MAX_BOOST;
return userBoost.amount * 10000 / baseAmount;
}
Mathematical Proof
Let's substitute:
amount = userBoost.amount
MAX_BOOST = 25000
Step 1: baseAmount = amount * 10000 / MAX_BOOST
Step 2: return (amount * 10000) / baseAmount
Substituting Step 1 into Step 2:
return (amount * 10000) / (amount * 10000 / 25000)
Simplifying:
= (amount * 10000 * 25000) / (amount * 10000)
= 25000 (MAX_BOOST)
Impact
Economic Implications
function testBoostMultiplier() {
address user1 = address(0x1);
address user2 = address(0x2);
assert(controller.getBoostMultiplier(user1, pool) ==
controller.getBoostMultiplier(user2, pool));
}
Reward Distribution Distortion
Small stakers receive same boost as large stakers
Negates veToken staking incentives
Undermines protocol tokenomics
System Manipulation
function exploitMaxBoost() {
veToken.stake(1);
uint256 boost = controller.getBoostMultiplier(address(this), pool);
assert(boost == MAX_BOOST);
pool.claim();
}
Proof of Concept
contract BoostMultiplierTest {
BoostController controller;
function testAlwaysMaxBoost() public {
address user = address(1);
address pool = address(2);
uint256 smallStake = 1e18;
uint256 largeStake = 1000e18;
vm.mockCall(
address(veToken),
abi.encodeWithSelector(IERC20.balanceOf.selector, user),
abi.encode(smallStake)
);
uint256 multiplier = controller.getBoostMultiplier(user, pool);
assertEq(multiplier, MAX_BOOST);
}
}
Recommended Mitigation
function getBoostMultiplier(
address user,
address pool
) external view override returns (uint256) {
if (!supportedPools[pool]) revert PoolNotSupported();
UserBoost storage userBoost = userBoosts[user][pool];
if (userBoost.amount == 0) return MIN_BOOST;
uint256 userVotingPower = veToken.getVotingPower(user, block.timestamp);
uint256 totalVotingPower = veToken.getTotalVotingPower();
if (totalVotingPower == 0) return MIN_BOOST;
uint256 boostRatio = (userVotingPower * BASIS_POINTS) / totalVotingPower;
uint256 multiplier = MIN_BOOST +
((MAX_BOOST - MIN_BOOST) * boostRatio) / BASIS_POINTS;
return Math.min(multiplier, MAX_BOOST);
}
Additional Recommendations
Add input validation:
require(userVotingPower <= totalVotingPower, "Invalid voting power");
Implement boost decay:
uint256 timeSinceLastUpdate = block.timestamp - userBoost.lastUpdateTime;
multiplier = multiplier * (MAX_DURATION - timeSinceLastUpdate) / MAX_DURATION;
Add events for monitoring:
event BoostMultiplierCalculated(
address indexed user,
address indexed pool,
uint256 multiplier,
uint256 votingPower
);