Core Contracts

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

Boost Calculation Relies on Outdated Balance, Allowing Unfair Advantage in Rewards and Governance Manipulation.

Summary

A vulnerability has been identified in the BoostController::_calculateBoost function, which incorrectly calculates the user's boost based on the user's veRaac token balance instead of the user's current voting power. This discrepancy leads to inaccurate boost calculations, potentially allowing users to gain unintended advantages in the boost mechanism. Additionally, the BoostController::delegateBoost function also relies on balanceOf, enabling users to delegate more veTokens than they currently possess, further exacerbating the issue.

Vulnerability Details

The BoostController contract includes several functions for managing user boosts, primarily updateUserBoost and delegateBoost. These functions depend on boost calculations to determine the boost amount for users. However, the boost calculation relies on the stale veRaac balance instead of the current voting power, leading to inaccurate results.

Affected Functions:

  1. BoostController::updateUserBoost

    • Uses _calculateBoost, which relies on IERC20.balanceOf(user) instead of veToken.getVotingPower(user, block.timestamp).

    • This results in outdated balance references, leading to incorrect boost calculations.

  2. BoostController::_calculateBoost (internal function)

    • Fetches the user's balance using IERC20.balanceOf(user).

    • This does not account for veRaac’s decay mechanism, which dynamically changes voting power over time.

  3. BoostController::delegateBoost

    • Uses IERC20.balanceOf(user), allowing users to delegate more veTokens than they effectively have in terms of voting power.

    • This discrepancy can be exploited to delegate an inflated boost amount.

Contextual Code Snippets

BoostController::_calculateBoost (Flawed Calculation)

function _calculateBoost(address user, address pool, uint256 amount) internal view returns (uint256) {
if (amount == 0) revert InvalidBoostAmount();
if (!supportedPools[pool]) revert PoolNotSupported();
// Get current weights without modifying state
(uint256 totalWeight, uint256 totalVotingPower, uint256 votingPower) = updateTotalWeight();
@> // @info: using stale voting power == user balance
@> uint256 userBalance = IERC20(address(veToken)).balanceOf(user);
@> uint256 totalSupply = IERC20(address(veToken)).totalSupply();
if (userBalance == 0 || totalSupply == 0) {
return amount;
}
// Create parameters struct for calculation
BoostCalculator.BoostParameters memory params = BoostCalculator.BoostParameters({
maxBoost: boostState.maxBoost,
minBoost: boostState.minBoost,
boostWindow: boostState.boostWindow,
totalWeight: totalWeight,
totalVotingPower: totalVotingPower,
votingPower: votingPower
});
(uint256 boostBasisPoints, uint256 boostedAmount) =
@> BoostCalculator.calculateTimeWeightedBoost(params, userBalance, totalSupply, amount);
if (boostedAmount < amount) {
return amount;
}
uint256 maxBoostAmount = amount * MAX_BOOST / 10000;
if (boostedAmount > maxBoostAmount) {
return maxBoostAmount;
}
return boostedAmount;
}

BoostController::calculateBoost (Correct Calculation)

function calculateBoost(address user, address pool, uint256 amount)
external
view
override
returns (uint256 boostBasisPoints, uint256 boostedAmount)
{
if (!supportedPools[pool]) revert UnsupportedPool();
// Get current weights without modifying state
(uint256 totalWeight, uint256 totalVotingPower, uint256 votingPower) = updateTotalWeight();
@> uint256 userVotingPower = veToken.getVotingPower(user, block.timestamp);
// Create parameters struct for calculation
BoostCalculator.BoostParameters memory params = BoostCalculator.BoostParameters({
maxBoost: boostState.maxBoost,
minBoost: boostState.minBoost,
boostWindow: boostState.boostWindow,
totalWeight: totalWeight,
totalVotingPower: totalVotingPower,
votingPower: votingPower
});
return BoostCalculator.calculateTimeWeightedBoost(params, userVotingPower, totalVotingPower, amount);
}

BoostController::delegateBoost

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();
}
@> // @info: delegator can delegate more veToken than they currently have
@> // because veToken has a decay rate slope
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();
delegation.amount = amount;
delegation.expiry = block.timestamp + duration;
delegation.delegatedTo = to;
delegation.lastUpdateTime = block.timestamp;
emit BoostDelegated(msg.sender, to, amount, duration);
}

Proof of Concept (PoC)

  1. Scenario Setup: A user has 1000 veRaac tokens but a decayed voting power of only 500 due to locking and time decay.

  2. Incorrect Boost Calculation: _calculateBoost uses balanceOf(user) = 1000 instead of getVotingPower(user) = 500, giving the user an unfair boost.

  3. Correct Calculation: calculateBoost correctly considers votingPower and provides a lower, fair boost.

  4. Exploit Impact: A user could manipulate the system by holding a high balance without actually having the corresponding voting power, leading to an unfair advantage in boosted rewards.

PoC Test Suite

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 = 10_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 testBalanceOfUsedToUpdateUserBoost() public {
vm.startPrank(BOOST_CONTROLLER_OWNER);
boostController.modifySupportedPool(BOB, true);
vm.stopPrank();
vm.warp(block.timestamp + 365 days / 2);
vm.startPrank(ALICE);
(uint256 boostBasisPoints, uint256 calculatedBoost) = boostController.calculateBoost(ALICE, BOB, 10000);
vm.stopPrank();
console.log("boostBasisPoints: ", boostBasisPoints);
console.log("calculatedBoost : ", calculatedBoost);
uint256 boostBalance = boostController.getWorkingBalance(ALICE, BOB);
console.log("boostBalance : ", boostBalance);
vm.startPrank(ALICE);
boostController.updateUserBoost(ALICE, BOB);
vm.stopPrank();
console.log("after updating userBoost..");
boostBalance = boostController.getWorkingBalance(ALICE, BOB);
console.log("boostBalance : ", boostBalance);
}
  1. Step 6: To run the test, execute the following commands in your terminal:

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

[⠒] Compiling...
No files changed, compilation skipped
Ran 1 test for test/GovernanceTest.t.sol:GovernanceTest
[PASS] testBalanceOfUsedToUpdateUserBoost() (gas: 219974)
Logs:
boostBasisPoints: 10806
calculatedBoost : 10806
boostBalance : 0
after updating userBoost..
boostBalance : 11612
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 9.89ms (509.20µs CPU time)
Ran 1 test suite in 14.03ms (9.89ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

As demonstrated, the test confirms that the user's boost is incorrectly updated.

Impact

  • Protocol Imbalance: Users with high veRaac balances but low voting power receive disproportionately high boosts.

  • Unfair Advantage: Users can exploit the boost system by artificially maintaining a high balance.

  • Governance Integrity Risk: Boosting mechanisms tied to governance participation may be compromised.

  • Boost Manipulation: Users may receive incorrect boost amounts due to outdated balance references.

  • Delegation Exploit: Users can delegate more veTokens than they effectively possess.

  • Governance Manipulation: Boosting mechanics that rely on veRaac voting power can be distorted.

Tools Used

  • Manual Review

  • Foundry

Recommendations

To mitigate this vulnerability, the following changes should be implemented:

  1. Modify _calculateBoost to Use Voting Power:

    • Replace IERC20(veToken).balanceOf(user) with veToken.getVotingPower(user, block.timestamp) or,

  2. Refactor updateUserBoost to Use calculateBoost:

    • Instead of _calculateBoost, call calculateBoost directly in updateUserBoost.

  3. Ensure delegateBoost verifies the user’s actual voting power before allowing delegation.

  4. Audit Other Related Functions:

    • Ensure that all boost calculations consistently use voting power rather than token balance.

Example Fix:

function _calculateBoost(address user, address pool, uint256 amount) internal view returns (uint256) {
if (amount == 0) revert InvalidBoostAmount();
if (!supportedPools[pool]) revert PoolNotSupported();
(uint256 totalWeight, uint256 totalVotingPower, uint256 votingPower) = updateTotalWeight();
- uint256 userBalance = IERC20(address(veToken)).balanceOf(user);
+ // Corrected: Using voting power instead of balance
+ uint256 userBalance = veToken.getVotingPower(user, block.timestamp);
- uint256 totalSupply = IERC20(address(veToken)).totalSupply();
- if (userVotingPower == 0 || totalVotingPower == 0) {
+ if (userBalance == 0 || totalSupply == 0) {
return amount;
}
BoostCalculator.BoostParameters memory params = BoostCalculator.BoostParameters({
maxBoost: boostState.maxBoost,
minBoost: boostState.minBoost,
boostWindow: boostState.boostWindow,
totalWeight: totalWeight,
totalVotingPower: totalVotingPower,
votingPower: votingPower
});
(uint256 boostBasisPoints, uint256 boostedAmount) =
- BoostCalculator.calculateTimeWeightedBoost(params, userBalance, totalSupply, amount);
+ BoostCalculator.calculateTimeWeightedBoost(params, userVotingPower, totalVotingPower, amount);
if (boostedAmount < amount) {
return amount;
}
uint256 maxBoostAmount = amount * MAX_BOOST / 10000;
if (boostedAmount > maxBoostAmount) {
return maxBoostAmount;
}
return boostedAmount;
}
Updates

Lead Judging Commences

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

BaseGauge::_applyBoost, GaugeController::vote, BoostController::calculateBoost use balanceOf() instead of getVotingPower() for vote-escrow tokens, negating time-decay mechanism

Support

FAQs

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