Core Contracts

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

Incorrect calculation in BoostController breaks veRAACToken rewards and attacker to capture the entire boost allocation

Author Revealed upon completion

Summary

The BoostController contract incorrectly sets workingSupply equal to only the latest user's boost, instead of baseSupply + totalBoost. As baseSuuply is also never set, it is impossible to calculate the correct workingSupply. This causes all accumulated boosts from previous users to be erased, directly affecting the reward multiplier calculations for RAAC's lending pools.

Vulnerability Details

In the BoostController's updateUserBoost function:

// updateUserBoost
// Updates totalBoost correctly
if (newBoost >= oldBoost) {
poolBoost.totalBoost = poolBoost.totalBoost + (newBoost - oldBoost);
} else {
poolBoost.totalBoost = poolBoost.totalBoost - (oldBoost - newBoost);
}
// @audit: Overwrites workingSupply with just newBoost
poolBoost.workingSupply = newBoost; // <-- bug

The contract correctly tracks totalBoost but incorrectly sets workingSupply. As workingSupply is used to calculate the effective boost for reward distributions in RAAC's lending pools, this causes all previous user boosts to be ignored.

Another issue is that the baseSupply is never set.

Let's check the NatSpec from workingSupply:

/**
* @notice Struct to track pool-wide boost metrics
* @param totalBoost The total boost amount for the pool
@> * @param workingSupply The total working supply including boosts
@> * @param baseSupply The base supply without boosts
* @param lastUpdateTime The last time pool boosts were updated
*/
struct PoolBoost {
uint256 totalBoost;
uint256 workingSupply;
uint256 baseSupply;
uint256 lastUpdateTime;
}

Let's take a look at the following scenario describing the correct and incorrect case:

Given:
- baseSupply = 1000 tokens
- User A gets a boost of 200
- User B gets a boost of 300
Current (Incorrect) Result:
- totalBoost = 500 (correct: 200 + 300)
- workingSupply = 300 (incorrect: only shows User B's boost)
- baseSuuply = 0
Expected Result:
- totalBoost = 500 (200 + 300)
- baseSuuply = 1000
- workingSupply = 1500 (baseSupply 1000 + totalBoost 500)

Besides, sponsor(@Alex Werner) confirmed: "the intended behavior is the one described with base supply + boost".

There is also a second issue: theremoveBoostDelegation logic:

if (poolBoost.totalBoost >= delegation.amount) {
poolBoost.totalBoost -= delegation.amount;
}
if (poolBoost.workingSupply >= delegation.amount) {
poolBoost.workingSupply -= delegation.amount;
}

  1. If a delegation exists, totalBoost MUST have increased by that amount when it was created. Therefore, checking if totalBoost >= delegation.amount is redundant.

  2. The workingSupply check is entirely incorrect since workingSupply should be derived from baseSupply + totalBoost, not manipulated directly.

Impact

  • Protocol's economic model is severely compromised as the boost mechanism fails to properly incentivize long-term token locking

  • Unfair advantage to the last user updating their boost as they effectively capture the entire boost allocation

  • Reward calculations for veRAACToken holders will only consider the last user's boost, erasing multipliers from all previous lockers

  • The protocol's boost incentivization mechanism becomes ineffective as early adopters lose their boost benefits

Tools Used

Manual Review

Recommendations

  1. When updating user boost, set the workingSupplyand baseSupply properly:

    function updateUserBoost(address user, address pool) external override {
    ...
    - poolBoost.workingSupply = newBoost;
    + uint256 currentBaseSupply = IERC20(pool).totalSupply();
    + poolBoost.baseSupply = currentBaseSupply;
    + poolBoost.workingSupply = currentBaseSupply + poolBoost.totalBoost;
    }
  2. Perform the same logic in removeBoostDelegation:

    function removeBoostDelegation(address from) external override nonReentrant {
    ...
    - if (poolBoost.totalBoost >= delegation.amount) {
    - poolBoost.totalBoost -= delegation.amount;
    - }
    - if (poolBoost.workingSupply >= delegation.amount) {
    - poolBoost.workingSupply -= delegation.amount; // @audit-issue: Can skip deduction
    - }
    + uint256 currentBaseSupply = IERC20(msg.sender).totalSupply();
    + // Always decrement totalBoost and recalculate workingSupply
    + poolBoost.totalBoost -= delegation.amount;
    + poolBoost.baseSupply = currentBaseSupply;
    + poolBoost.workingSupply = currentBaseSupply + poolBoost.totalBoost;
    poolBoost.lastUpdateTime = block.timestamp;
    }
Updates

Lead Judging Commences

inallhonesty Lead Judge 8 days 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.