Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: high
Invalid

Unbounded Boost Multiplier Allows Governance Capture

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();
// 🔍 Get current weights without modifying state
(uint256 totalWeight, uint256 totalVotingPower, uint256 votingPower) = updateTotalWeight();
// 📊 Load user's voting power state
uint256 userBalance = IERC20(address(veToken)).balanceOf(user);
uint256 totalSupply = IERC20(address(veToken)).totalSupply();
// ⚡ Early return for edge cases
if (userBalance == 0 || totalSupply == 0) {
return amount;
}
// 🏗️ Setup boost calculation parameters
BoostCalculator.BoostParameters memory params = BoostCalculator.BoostParameters({...});
// 💥 Critical vulnerability point: Boost calculation before max check
(uint256 boostBasisPoints, uint256 boostedAmount) = BoostCalculator.calculateTimeWeightedBoost(...);
// ⚠️ Late validation allows boost exceeding MAX_BOOST
if (boostedAmount < amount) {
return amount;
}
uint256 maxBoostAmount = amount * MAX_BOOST / 10000;
if (boostedAmount > maxBoostAmount) {
return maxBoostAmount; // 🚨 Too late - boost already calculated
}
return boostedAmount;
}
function updateUserBoost(address user, address pool) external override nonReentrant whenNotPaused {
// 🔒 Basic security checks
if (paused()) revert EmergencyPaused();
if (user == address(0)) revert InvalidPool();
if (!supportedPools[pool]) revert PoolNotSupported();
// 📝 Load storage references
UserBoost storage userBoost = userBoosts[user][pool];
PoolBoost storage poolBoost = poolBoosts[pool];
uint256 oldBoost = userBoost.amount;
// 🎯 Calls vulnerable _calculateBoost
uint256 newBoost = _calculateBoost(user, pool, 10000);
// 📈 State updates with potentially inflated boost
userBoost.amount = newBoost;
userBoost.lastUpdateTime = block.timestamp;
// 🔄 Pool total updates propagate the excessive boost
if (newBoost >= oldBoost) {
poolBoost.totalBoost = poolBoost.totalBoost + (newBoost - oldBoost);
} else {
poolBoost.totalBoost = poolBoost.totalBoost - (oldBoost - newBoost);
}
poolBoost.workingSupply = newBoost;
poolBoost.lastUpdateTime = block.timestamp;
// 📢 Events emit potentially inflated values
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; // 2.5x maximum boost

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

// 🛠️ Fixed version of _calculateBoost
function _calculateBoost(
address user,
address pool,
uint256 amount
) internal view returns (uint256) {
if (amount == 0) revert InvalidBoostAmount();
if (!supportedPools[pool]) revert PoolNotSupported();
// 🔐 Calculate max boost limit first
uint256 maxBoostAmount = amount * MAX_BOOST / 10000;
// 🔍 Get current weights without modifying state
(uint256 totalWeight, uint256 totalVotingPower, uint256 votingPower) = updateTotalWeight();
// 📊 Load user's voting power state
uint256 userBalance = IERC20(address(veToken)).balanceOf(user);
uint256 totalSupply = IERC20(address(veToken)).totalSupply();
if (userBalance == 0 || totalSupply == 0) {
return amount;
}
// 🏗️ Setup boost calculation parameters
BoostCalculator.BoostParameters memory params = BoostCalculator.BoostParameters({
maxBoost: boostState.maxBoost,
minBoost: boostState.minBoost,
boostWindow: boostState.boostWindow,
totalWeight: totalWeight,
totalVotingPower: totalVotingPower,
votingPower: votingPower
});
// ⚖️ Calculate boost with early bounds
(uint256 boostBasisPoints, uint256 boostedAmount) = BoostCalculator.calculateTimeWeightedBoost(
params,
userBalance,
totalSupply,
amount
);
// ✅ Return the minimum of calculated boost and max allowed
return Math.min(boostedAmount, maxBoostAmount);
}
// 🔄 Updated updateUserBoost with expiry check
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];
// ⏰ Check delegation expiry
if (userBoost.expiry > 0 && block.timestamp >= userBoost.expiry) {
userBoost.amount = 0;
poolBoost.totalBoost -= userBoost.amount;
emit BoostUpdated(user, pool, 0);
return;
}
// 🔄 Continue with normal boost update
uint256 oldBoost = userBoost.amount;
uint256 newBoost = _calculateBoost(user, pool, 10000);
userBoost.amount = newBoost;
userBoost.lastUpdateTime = block.timestamp;
// 📊 Update pool totals with bounded values
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:

  1. Enforcing MAX_BOOST limit early in calculations

  2. Adding proper expiry validation

  3. Ensuring safe arithmetic in pool total updates

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.