Core Contracts

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

Stale boost values persist after lock expiry during pause/unpause transitions

Summary

The BoostController contract fails to properly update user boost values during pause/unpause transitions, allowing users to maintain inflated boost values even after their veRAACToken locks have expired. This issue stems from the contract's reliance on manual updates to refresh boost values.

Vulnerability Details

The BoostController contract is responsible for calculating and managing boost multipliers based on users' veRAACToken voting power. When a user's lock expires, their voting power drops to 0, but their boost value remains unchanged until updateUserBoost is called:

function _calculateBoost(
address user,
address pool,
uint256 amount
) internal view returns (uint256) {
// ...
(
uint256 totalWeight,
uint256 totalVotingPower,
uint256 votingPower
) = updateTotalWeight();
uint256 userVotingPower = veToken.getVotingPower(user, block.timestamp);
uint256 totalSupply = IERC20(address(veToken)).totalSupply();
if (userVotingPower == 0 || totalSupply == 0) {
return amount;
}
// ...
}

While the _calculateBoost function correctly uses getVotingPower, the issue arises because:

  1. User boost values are only updated when updateUserBoost is called

  2. During a pause, no updates can be made

  3. After unpause, boosts remain at their old values until manually updated

This creates a window of opportunity where users can maintain inflated boost values after their locks expire, particularly during pause/unpause transitions.

Impact

High severity. This vulnerability allows users to:

  1. Maintain maximum boost multipliers (up to 2.5x) even after their veRAACToken locks expire

  2. Receive higher rewards than they should in protocol pools

  3. Exploit the pause/unpause functionality to extend the duration of their inflated boosts

The economic impact scales with:

  • The duration of the pause

  • The number of users with expired locks

  • The size of the affected pools

  • The difference between minimum and maximum boost multipliers (1x to 2.5x)

Proof of Concept

The following test demonstrates the vulnerability:

function testPauseExploit() public {
// 1. Initial setup - user has high voting power
veToken.setVotingPower(user, 1000e18);
vm.startPrank(user);
boostController.updateUserBoost(user, pool);
vm.stopPrank();
// Initial state - 2.5x boost
(uint256 initialBoostBasisPoints,) = boostController.calculateBoost(user, pool, 100e18);
uint256 initialWorkingBalance = boostController.getWorkingBalance(user, pool);
// initialWorkingBalance = 25000 (2.5x)
// 2. Admin pauses contract
vm.startPrank(admin);
boostController.setEmergencyShutdown(true);
// 3. During pause, lock expires
vm.warp(block.timestamp + 366 days);
// Voting power is now 0
// 4. Admin unpauses
boostController.setEmergencyShutdown(false);
vm.stopPrank();
// 5. Check boost - still at 2.5x despite 0 voting power
uint256 workingBalanceAfterUnpause = boostController.getWorkingBalance(user, pool);
// workingBalanceAfterUnpause = 25000 (2.5x)
// 6. Only drops to 1x after manual update
vm.startPrank(user);
boostController.updateUserBoost(user, pool);
vm.stopPrank();
uint256 finalWorkingBalance = boostController.getWorkingBalance(user, pool);
// finalWorkingBalance = 10000 (1x)
}

Test output shows:

=== Initial Setup ===
Initial voting power: 1000
Initial boost multiplier: 2 x
Initial working balance: 25000
=== During Pause ===
Contract paused
Time advanced past lock expiry
Voting power after expiry: 0
Expected boost multiplier after expiry: 1 x
=== After Unpause ===
Contract unpaused
Working balance after unpause: 25000
Boost multiplier still at: 2 x
=== After User Update ===
Final working balance: 10000
Final boost multiplier: 1 x

Tools Used

  • Manual code review

  • Foundry for testing

Recommendations

Add automatic boost updates during unpause by modifying the setEmergencyShutdown function:

function setEmergencyShutdown(bool paused) external onlyRole(EMERGENCY_ADMIN) {
if (paused) {
_pause();
} else {
_unpause();
// Update all active user boosts
for (uint i = 0; i < activeUsers.length; i++) {
address user = activeUsers[i];
for (uint j = 0; j < activePools.length; j++) {
address pool = activePools[j];
if (userBoosts[user][pool].amount > 0) {
_updateUserBoost(user, pool);
}
}
}
}
emit EmergencyShutdown(msg.sender, paused);
}

Additionally:

  1. Consider implementing a global boost update mechanism that can be triggered by admins

  2. Add a function to batch update boosts for multiple users

  3. Implement automatic boost updates when locks expire

References

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

BoostController maintains stale boost values when veRAACToken locks expire during protocol pauses, temporarily allowing higher-than-deserved rewards until values are manually updated

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

BoostController maintains stale boost values when veRAACToken locks expire during protocol pauses, temporarily allowing higher-than-deserved rewards until values are manually updated

Support

FAQs

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