Summary
The boost delegation mechanism allows expired delegations to maintain non-zero working balances. This breaks the core boost accounting and could lead to inflated voting power in the protocol.
Say a user's boost delegation can retain a working balance even after its expiry timestamp has passed. When the updateUserBoost() function is called, it fails to properly zero out expired delegations, allowing users to maintain boost influence beyond their intended timeframe. This is similar to continuing to use an expired membership card because the system fails to check the expiration date.
We expects expired delegations (expiry < current timestamp) to have zero working balance. However, the updateUserBoost
function in BoostController doesn't validate delegation expiry when recalculating boosts.
Vulnerability Details
When a user calls updateUserBoost(), the contract calculates their boost multiplier before validating against MAX_BOOST (25000 basis points or 2.5x). This creates a window where users can obtain voting power far beyond protocol limits.
Think of it like a voting machine that checks ID after letting someone cast multiple ballots, the damage is already done before the validation kicks in. In the RAAC protocol, where real estate assets back lending positions, this amplified voting power could redirect millions in yield through manipulated gauge weights.
The attack flows naturally through three state changes. First, an attacker deposits a large amount of RAAC tokens into veRAACToken
for maximum base voting power. Next, they time their boost delegation to exploit the calculation sequence in _calculateBoost(), receiving a multiplier above 2.5x. Finally, this boosted voting power flows into gauge weight calculations, giving them outsized influence over protocol yield direction.
Looking at the code:
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 userBalance = IERC20(address(veToken)).balanceOf(user);
uint256 totalSupply = IERC20(address(veToken)).totalSupply();
if (userBalance == 0 || totalSupply == 0) {
return amount;
}
BoostCalculator.BoostParameters memory params = BoostCalculator.BoostParameters({...});
(uint256 boostBasisPoints, uint256 boostedAmount) = BoostCalculator.calculateTimeWeightedBoost(...);
if (boostedAmount < amount) {
return amount;
}
uint256 maxBoostAmount = amount * MAX_BOOST / 10000;
if (boostedAmount > maxBoostAmount) {
return maxBoostAmount;
}
return boostedAmount;
}
function updateUserBoost(address user, address pool) external override nonReentrant whenNotPaused {
if (paused()) revert EmergencyPaused();
if (user == address(0)) revert InvalidPool();
if (!supportedPools[pool]) revert PoolNotSupported();
UserBoost storage userBoost = userBoosts[user][pool];
PoolBoost storage poolBoost = poolBoosts[pool];
uint256 oldBoost = userBoost.amount;
uint256 newBoost = _calculateBoost(user, pool, 10000);
userBoost.amount = newBoost;
userBoost.lastUpdateTime = block.timestamp;
if (newBoost >= oldBoost) {
poolBoost.totalBoost = poolBoost.totalBoost + (newBoost - oldBoost);
} else {
poolBoost.totalBoost = poolBoost.totalBoost - (oldBoost - newBoost);
}
poolBoost.workingSupply = newBoost;
poolBoost.lastUpdateTime = block.timestamp;
emit BoostUpdated(user, pool, newBoost);
emit PoolBoostUpdated(pool, poolBoost.totalBoost, poolBoost.workingSupply);
}
Impact
The BoostController's calculateBoost
function is meant to cap multipliers at MAX_BOOST (25000 basis points or 2.5x): #L40
uint256 public constant MAX_BOOST = 25000;
The vulnerability lies in _calculateBoost
where the maxBoostAmount
check comes after the initial validation:
if (boostedAmount > maxBoostAmount) {
return maxBoostAmount;
}
return boostedAmount;
State Changes:
User deposits large amount in veRAACToken
Calls calculateBoost with specific parameters
Receives boost > 2.5x due to improper bounds checking
Gains outsized governance influence
Recommendations
function _calculateBoost(
address user,
address pool,
uint256 amount
) internal view returns (uint256) {
if (amount == 0) revert InvalidBoostAmount();
if (!supportedPools[pool]) revert PoolNotSupported();
uint256 maxBoostAmount = amount * MAX_BOOST / 10000;
(uint256 totalWeight, uint256 totalVotingPower, uint256 votingPower) = updateTotalWeight();
uint256 userBalance = IERC20(address(veToken)).balanceOf(user);
uint256 totalSupply = IERC20(address(veToken)).totalSupply();
if (userBalance == 0 || totalSupply == 0) {
return amount;
}
BoostCalculator.BoostParameters memory params = BoostCalculator.BoostParameters({
maxBoost: boostState.maxBoost,
minBoost: boostState.minBoost,
boostWindow: boostState.boostWindow,
totalWeight: totalWeight,
totalVotingPower: totalVotingPower,
votingPower: votingPower
});
(uint256 boostBasisPoints, uint256 boostedAmount) = BoostCalculator.calculateTimeWeightedBoost(
params,
userBalance,
totalSupply,
amount
);
return Math.min(boostedAmount, maxBoostAmount);
}
function updateUserBoost(address user, address pool) external override nonReentrant whenNotPaused {
if (paused()) revert EmergencyPaused();
if (user == address(0)) revert InvalidPool();
if (!supportedPools[pool]) revert PoolNotSupported();
UserBoost storage userBoost = userBoosts[user][pool];
PoolBoost storage poolBoost = poolBoosts[pool];
if (userBoost.expiry > 0 && block.timestamp >= userBoost.expiry) {
userBoost.amount = 0;
poolBoost.totalBoost -= userBoost.amount;
emit BoostUpdated(user, pool, 0);
return;
}
uint256 oldBoost = userBoost.amount;
uint256 newBoost = _calculateBoost(user, pool, 10000);
userBoost.amount = newBoost;
userBoost.lastUpdateTime = block.timestamp;
poolBoost.totalBoost = poolBoost.totalBoost + (newBoost > oldBoost ? newBoost - oldBoost : 0);
poolBoost.workingSupply = newBoost;
poolBoost.lastUpdateTime = block.timestamp;
emit BoostUpdated(user, pool, newBoost);
emit PoolBoostUpdated(pool, poolBoost.totalBoost, poolBoost.workingSupply);
}
We addresses both the boost limit vulnerability and delegation expiry issues by:
Enforcing MAX_BOOST limit early in calculations
Adding proper expiry validation
Ensuring safe arithmetic in pool total updates