Core Contracts

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

Incorrect Boost Delegation Flow in BoostController

Summary

The delegateBoost() function in the BoostController contract incorrectly handles the boost delegation flow. Instead of allowing users to delegate their existing boost to another address, it creates a new boost entry without properly linking or transferring the user's existing boost.

Vulnerability Details

  1. Doesn't check if user has existing boost to delegate

  2. Creates new boost entry instead of transferring existing boost

  3. No reduction in delegator's boost amount

  4. No proper linking between delegator and delegate (The boost isn't actually applied to any pool's calculations)

  5. Doesn't handle pool working supplies correctly

function delegateBoost(address to, uint256 amount, uint256 duration) external override nonReentrant {
if (paused()) revert EmergencyPaused();
if (to == address(0)) revert InvalidPool();
if (amount == 0) revert InvalidBoostAmount();
if (duration < MIN_DELEGATION_DURATION || duration > MAX_DELEGATION_DURATION) {
revert InvalidDelegationDuration();
}
uint256 userBalance = IERC20(address(veToken)).balanceOf(msg.sender);
if (userBalance < amount) revert InsufficientVeBalance();
// @audit - Current implementation only creates a new UserBoost entry
UserBoost storage delegation = userBoosts[msg.sender][to];
if (delegation.amount > 0) revert BoostAlreadyDelegated();
delegation.amount = amount; // @audit - Direct assignment without pool integration
delegation.expiry = block.timestamp + duration;
delegation.delegatedTo = to;
delegation.lastUpdateTime = block.timestamp;
// @audit-issue Missing critical steps:
// - No check for existing user boost in pools
// - No modification of pool's working supply
// - No update to delegator's existing boost allocations
emit BoostDelegated(msg.sender, to, amount, duration);
}

If we look at the removeBoostDelegation() function we can see that it tries to do some arithmetics based on the poolBoost mapping which never got updated in the delegateBoost function:

function removeBoostDelegation(address from) external override nonReentrant {
UserBoost storage delegation = userBoosts[from][msg.sender];
if (delegation.delegatedTo != msg.sender) revert DelegationNotFound();
if (delegation.expiry > block.timestamp) revert InvalidDelegationDuration();
//@audit Attempts to modify pool metrics that were never set
PoolBoost storage poolBoost = poolBoosts[msg.sender];
if (poolBoost.totalBoost >= delegation.amount) {
poolBoost.totalBoost -= delegation.amount;
}
if (poolBoost.workingSupply >= delegation.amount) {
poolBoost.workingSupply -= delegation.amount;
}
poolBoost.lastUpdateTime = block.timestamp;
emit DelegationRemoved(from, msg.sender, delegation.amount);
delete userBoosts[from][msg.sender];
}

PoC

In order to run the test you need to:

  1. Run foundryup to get the latest version of Foundry

  2. Install hardhat-foundry: npm install --save-dev @nomicfoundation/hardhat-foundry

  3. Import it in your Hardhat config: require("@nomicfoundation/hardhat-foundry");

  4. Make sure you've set the BASE_RPC_URL in the .env file or comment out the forking option in the hardhat config.

  5. Run npx hardhat init-foundry

  6. There is one file in the test folder that will throw an error during compilation so rename the file in test/unit/libraries/ReserveLibraryMock.sol to => ReserveLibraryMock.sol_broken so it doesn't get compiled anymore (we don't need it anyways).

  7. Create a new folder test/foundry

  8. Paste the below code into a new test file i.e.: FoundryTest.t.sol

  9. Run the test: forge test --mc FoundryTest -vvvv

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import {console2} from "forge-std/console2.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC20Mock} from "../../contracts/mocks/core/tokens/ERC20Mock.sol";
import {GaugeController} from "../../contracts/core/governance/gauges/GaugeController.sol";
import {RAACGauge} from "../../contracts/core/governance/gauges/RAACGauge.sol";
import {RWAGauge} from "../../contracts/core/governance/gauges/RWAGauge.sol";
import {veRAACToken} from "../../contracts/core/tokens/veRAACToken.sol";
import {IGaugeController} from "../../contracts/interfaces/core/governance/gauges/IGaugeController.sol";
import {IGauge} from "../../contracts/interfaces/core/governance/gauges/IGauge.sol";
import {BoostController} from "../../contracts/core/governance/boost/BoostController.sol";
contract FoundryTest is Test {
// Contracts
ERC20Mock public raacToken;
veRAACToken public veToken;
BoostController public controller;
// Test addresses
address public admin = address(this);
address public alice = address(0x1);
address public bob = address(0x2);
address public carol = address(0x3);
address public pool = address(0x4);
// Constants
uint256 public constant INITIAL_SUPPLY = 1_000_000e18;
uint256 public constant LOCK_AMOUNT = 100_000e18;
uint256 public constant YEAR = 365 days;
function setUp() public {
// Deploy mock tokens
raacToken = new ERC20Mock("RAAC Token", "RAAC");
// Deploy veToken
veToken = new veRAACToken(address(raacToken));
// Deploy controller
controller = new BoostController(address(veToken));
// Setup initial token balances
raacToken.mint(alice, INITIAL_SUPPLY);
raacToken.mint(bob, INITIAL_SUPPLY);
// Setup approvals
vm.startPrank(alice);
raacToken.approve(address(veToken), type(uint256).max);
vm.stopPrank();
vm.startPrank(bob);
raacToken.approve(address(veToken), type(uint256).max);
vm.stopPrank();
controller.modifySupportedPool(pool, true);
}
function test_delegateBoost() public {
uint256 lockAmount = 100e18;
vm.startPrank(alice);
veToken.lock(LOCK_AMOUNT, YEAR);
controller.delegateBoost(bob, lockAmount, 10 days);
// delegate again
controller.delegateBoost(carol, lockAmount, 10 days);
// has no effect to bob
(uint256 amount, , , ) = controller.getUserBoost(bob, pool);
assertEq(amount, 0);
// has no effect to carol
(uint256 amount2, , , ) = controller.getUserBoost(carol, pool);
assertEq(amount2, 0);
}
}

Impact

  • Users can delegate non-existent boosts

  • Original boost remains unchanged

  • Double-counting of boost amounts

  • Incorrect boost accounting in the system

  • Potential for boost amount manipulation

Tools Used

  • Foundry

  • Manual Review

Recommendations

  1. Check if delegator has an existing boost

  2. Check if recipient already has delegated boost

  3. Reduce delegators boost

  4. Delegate boost to recipient

Updates

Lead Judging Commences

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

BoostController::delegateBoost lacks total delegation tracking, allowing users to delegate the same veTokens multiple times to different pools for amplified influence and rewards

Support

FAQs

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

Give us feedback!