Core Contracts

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

Boost Controller's Working Supply Desynchronization Enables Reward Manipulation

Summary

The working supply tracking in BoostController can become misaligned with the total boost amounts. This breaks the core boost accounting and could lead to incorrect reward distributions.

The Trigger

function updateUserBoost(address user, address pool) {
// 🚩 Working supply updated independently of total boost
poolBoost.workingSupply = newBoost;
poolBoost.totalBoost = poolBoost.totalBoost + (newBoost - oldBoost);
}

The code assumed working supply and total boost would naturally stay synchronized since they represent the same underlying value. However:

  1. The GaugeController uses these values for reward calculations

  2. The veRAACToken voting power affects boost calculations

  3. Independent updates create race conditions

Vulnerability Details

The updateUserBoost() function from BoostController.sol where the desynchronization occurs.

function updateUserBoost(address user, address pool) external override nonReentrant whenNotPaused {
// 🛡️ Security checks
if (paused()) revert EmergencyPaused();
if (user == address(0)) revert InvalidPool();
if (!supportedPools[pool]) revert PoolNotSupported();
// 📊 Load state
UserBoost storage userBoost = userBoosts[user][pool];
PoolBoost storage poolBoost = poolBoosts[pool];
// 🔄 Calculate boost changes
uint256 oldBoost = userBoost.amount;
// 🎯 Base calculation with 10000 (100%) as reference
uint256 newBoost = _calculateBoost(user, pool, 10000);
// ✍️ Update user state
userBoost.amount = newBoost;
userBoost.lastUpdateTime = block.timestamp;
// 🚨 Critical accounting section
if (newBoost >= oldBoost) {
poolBoost.totalBoost = poolBoost.totalBoost + (newBoost - oldBoost);
} else {
poolBoost.totalBoost = poolBoost.totalBoost - (oldBoost - newBoost);
}
// 💥 Desynchronization point: working supply set independently
poolBoost.workingSupply = newBoost;
poolBoost.lastUpdateTime = block.timestamp;
// 📢 Events
emit BoostUpdated(user, pool, newBoost);
emit PoolBoostUpdated(pool, poolBoost.totalBoost, poolBoost.workingSupply);
}

Think of the BoostController as a bank's accounting system, it must maintain perfect balance between its ledgers. The working supply represents the "available balance" while total boost tracks the "actual holdings." When these values diverge, it's like having two different amounts in your checking account.

In the RAAC protocol, this accounting system determines how much extra voting power and rewards users receive based on their veRAACToken holdings.

When a user interacts with the boost system. The BoostController calculates their new boost amount based on their veRAACToken balance. let's say they have 1000 veRAAC tokens. The contract then attempts to update both the user's boost and the pool's working supply. Here's where things go wrong:

// The contract updates these values independently
poolBoost.workingSupply = newBoost; // Set directly to 1500
poolBoost.totalBoost += (newBoost - oldBoost); // Increases by only 400

This creates a dangerous mismatch. The working supply now shows 1500 while total boost reflects 1400. This 100-token difference might seem small, but it compounds with each update, potentially leading to significant reward miscalculations.

For the RAAC protocol, which relies on precise boost calculations to incentivize long-term veRAACToken holders, this desynchronization directly impacts the core tokenomics. Users could receive up to 2.5x their normal rewards incorrectly, or have their voting power miscalculated in governance decisions.

Impact

  • Incorrect boost multipliers affect reward distribution

  • Undermines the veRAACToken voting power system

  • Destabilizes the gauge weight calculations

Recommendations

Treat the updates as a single atomic operation ensuring the fundamental accounting equation always holds true, working supply must equal total boost at all times.

function updateUserBoost(address user, address pool) external override nonReentrant whenNotPaused {
// 🛡️ Initial validations
if (paused()) revert EmergencyPaused();
if (user == address(0)) revert InvalidPool();
if (!supportedPools[pool]) revert PoolNotSupported();
// 📊 Load current state
UserBoost storage userBoost = userBoosts[user][pool];
PoolBoost storage poolBoost = poolBoosts[pool];
// 🔄 Calculate boost with bounds checking
uint256 oldBoost = userBoost.amount;
uint256 newBoost = _calculateBoost(user, pool, 10000);
// ⚖️ Validate boost delta
uint256 boostDelta = newBoost >= oldBoost ?
newBoost - oldBoost : oldBoost - newBoost;
if (boostDelta > MAX_BOOST_DELTA) revert ExcessiveBoostChange();
// ✨ Update user state
userBoost.amount = newBoost;
userBoost.lastUpdateTime = block.timestamp;
// 🔐 Synchronized pool updates
poolBoost.totalBoost = newBoost >= oldBoost ?
poolBoost.totalBoost + boostDelta :
poolBoost.totalBoost - boostDelta;
poolBoost.workingSupply = poolBoost.totalBoost;
// 🔍 Invariant check
assert(poolBoost.workingSupply == poolBoost.totalBoost);
// 📢 Emit events
emit BoostUpdated(user, pool, newBoost);
emit PoolBoostUpdated(pool, poolBoost.totalBoost, poolBoost.workingSupply);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

BoostController::updateUserBoost overwrites workingSupply with single user's boost value instead of accumulating, breaking reward multipliers and allowing last updater to capture all benefits

Support

FAQs

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

Give us feedback!