Summary
In BoostController.sol, there is a critical inconsistency between how voting power is calculated in internal vs external functions, leading to different boost values for the same user.
Vulnerable Code
function _calculateBoost(address user, address pool, uint256 amount) internal view returns (uint256) {
uint256 userBalance = IERC20(address(veToken)).balanceOf(user);
uint256 totalSupply = IERC20(address(veToken)).totalSupply();
(uint256 boostBasisPoints, uint256 boostedAmount) = BoostCalculator.calculateTimeWeightedBoost(
params,
userBalance,
totalSupply,
amount
);
}
function calculateBoost(address user, address pool, uint256 amount)
external view override returns (uint256 boostBasisPoints, uint256 boostedAmount)
{
uint256 userVotingPower = veToken.getVotingPower(user, block.timestamp);
return BoostCalculator.calculateTimeWeightedBoost(
params,
userVotingPower,
totalVotingPower,
amount
);
}
Impact
Calculation Discrepancy
function demonstrateDiscrepancy() public {
address user = address(1);
address pool = address(2);
uint256 amount = 1000e18;
controller.updateUserBoost(user, pool);
uint256 internalBoost = controller.getWorkingBalance(user, pool);
(_, uint256 externalBoost) = controller.calculateBoost(user, pool, amount);
assert(internalBoost != externalBoost);
}
Economic Impact
Users receive different boosts depending on which function is called
Rewards distribution becomes unpredictable
Gaming opportunity through function call selection
Proof of Concept
contract BoostDiscrepancyTest {
IveRAACToken veToken;
BoostController controller;
function testBoostDiscrepancy() public {
address user = address(1);
address pool = address(2);
uint256 lockAmount = 1000e18;
uint256 lockDuration = 365 days;
veToken.createLock(lockAmount, lockDuration);
uint256 rawBalance = IERC20(address(veToken)).balanceOf(user);
uint256 votingPower = veToken.getVotingPower(user, block.timestamp);
assert(votingPower > rawBalance);
uint256 amount = 1000e18;
controller.updateUserBoost(user, pool);
(_, uint256 externalBoost) = controller.calculateBoost(user, pool, amount);
uint256 storedBoost = controller.getWorkingBalance(user, pool);
assert(storedBoost < externalBoost);
}
}
Recommended Mitigation
function _calculateBoost(
address user,
address pool,
uint256 amount
) internal view returns (uint256) {
if (amount == 0) revert InvalidBoostAmount();
if (!supportedPools[pool]) revert PoolNotSupported();
(uint256 totalWeight, uint256 totalVotingPower, uint256 votingPower) = updateTotalWeight();
uint256 userVotingPower = veToken.getVotingPower(user, block.timestamp);
if (userVotingPower == 0 || totalVotingPower == 0) {
return amount;
}
BoostCalculator.BoostParameters memory params = BoostCalculator.BoostParameters({
maxBoost: boostState.maxBoost,
minBoost: boostState.minBoost,
boostWindow: boostState.boostWindow,
totalWeight: totalWeight,
totalVotingPower: totalVotingPower,
votingPower: votingPower
});
return BoostCalculator.calculateTimeWeightedBoost(
params,
userVotingPower,
totalVotingPower,
amount
);
}
Additional Recommendations
Add voting power caching:
mapping(address => VotingPowerSnapshot) public votingPowerSnapshots;
struct VotingPowerSnapshot {
uint256 power;
uint256 timestamp;
}
Implement consistency checks:
function validateBoostCalculation(uint256 internalBoost, uint256 externalBoost) internal pure {
require(
(internalBoost * 100) / externalBoost >= 95 &&
(internalBoost * 100) / externalBoost <= 105,
"Boost calculation deviation too high"
);
}
Add events for monitoring:
event BoostCalculationPerformed(
address indexed user,
address indexed pool,
uint256 votingPower,
uint256 boost,
bool isInternal
);