Core Contracts

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

Asymmetric Delegation Control in BoostController Creates Delegation Lock Risk

Summary

The BoostController contract implements an asymmetric permission model for boost delegations where only delegation recipients can remove delegations, while delegators have no control over their delegated boosts after creation. This creates a significant risk where delegators could have their boosts locked if recipients refuse or are unable to release the delegation.

Vulnerability Details

  • No mechanism for delegators to revoke their delegations

  • Delegations can only be removed by recipients

  • Delegators lose control of their boost until expiry

function removeBoostDelegation(address from) external override nonReentrant {
UserBoost storage delegation = userBoosts[from][msg.sender];
// @audit - only recipient of the delegation can remove it
if (delegation.delegatedTo != msg.sender) revert DelegationNotFound();
if (delegation.expiry > block.timestamp) revert InvalidDelegationDuration();
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";
import {IBoostController} from "../../contracts/interfaces/core/governance/IBoostController.sol";
contract BoostControllerTest 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_removeBoost() public {
uint256 boostAmount = 100e18;
vm.startPrank(alice);
veToken.lock(LOCK_AMOUNT, YEAR);
controller.delegateBoost(bob, boostAmount, 10 days);
vm.warp(block.timestamp + 10 days);
// Alice can't remove here delegation to bob
vm.expectRevert(IBoostController.DelegationNotFound.selector);
controller.removeBoostDelegation(bob);
vm.expectRevert(IBoostController.DelegationNotFound.selector);
controller.removeBoostDelegation(alice);
vm.stopPrank();
vm.prank(bob);
// Only bob can remove delegation from alice
// He can take her boost hostage
controller.removeBoostDelegation(alice);
}
}

Impact

  • Delegators can lose access to their boost if recipients become unresponsive

  • Potential for malicious recipients to hold delegations hostage

  • No recourse for delegators in case of disputes

Tools Used

  • Manual code review

Recommendations

Add delegation revocation capabilities for delegator

Updates

Lead Judging Commences

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

BoostController: Users unable to remove their own expired boost delegations, creating dependency on recipients and preventing efficient reallocation of boosts

Support

FAQs

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

Give us feedback!