Core Contracts

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

veRAACToken Contract Uses `balanceOf` Instead of `getVotingPower`: Severe Impact on Minting/Burning, Rewards, and Governance

Summary

The RAAC Protocol’s governance and reward mechanisms rely on the accurate measurement of users’ voting power, which is time-weighted due to decay and incremental changes from additional token locks. However, in the veRAACToken contract and related modules (GaugeController, BoostCalculator, BaseGauge, BoostController, etc.), the balanceOf function is used in lieu of the correct getVotingPower function. The balanceOf function returns a stale, non-time-weighted balance, whereas getVotingPower computes the current, time-weighted voting power based on decay and lock duration.

This misimplementation propagates across multiple functions:

  • Minting and Burning: In the increase and extend functions, using balanceOf(msg.sender) instead of getVotingPower(msg.sender) leads to an incorrect calculation of new vs. old voting power. Consequently, users receive an excessive number of veRAAC tokens (or have too many burned) during lock modifications.

  • Reward Distribution and Boost Calculation: Functions such as calculateBoost, getCurrentBoost, and getLockPosition use the stale balance, leading to distorted boost factors and erroneous reward allocations.

  • Governance & Voting: Modules like GaugeController::vote, BaseGauge::_applyBoost, and BaseGauge::voteDirection depend on these miscalculated values, which can undermine governance decisions and quorum calculations.

The root cause is the widespread use of balanceOf to determine a user’s voting power, despite the existence of a correct helper function getVotingPower that applies the decay slope and time-weighting. This oversight severely undermines the protocol’s tokenomics and the fairness of its incentive and governance mechanisms.

Vulnerability Details

Incorrect Use of balanceOf vs. getVotingPower

In several key functions, the contract uses balanceOf to fetch a user’s veRAAC token balance. However, because veRAAC tokens are subject to a decay mechanism over time, the “raw” balance from balanceOf does not accurately reflect a user’s current voting power. The correct approach is to use the getVotingPower function, which calculates the time-weighted balance:

function getVotingPower(address account) public view returns (uint256) {
return _votingState.getCurrentPower(account, block.timestamp);
}

Below are specific examples where balanceOf is misused:

1. Minting and Burning in Lock Increase/Extend

  • veRAACToken::increase:

    // Incorrect: Using balanceOf(msg.sender)
    _mint(msg.sender, newPower - balanceOf(msg.sender));

    Issue: The non-time-weighted balance causes excessive minting when new veRAAC tokens are issued, distorting the user’s effective voting power.

  • veRAACToken::extend:

    uint256 oldPower = balanceOf(msg.sender);
    ...
    if (newPower > oldPower) {
    _mint(msg.sender, newPower - oldPower);
    } else if (newPower < oldPower) {
    _burn(msg.sender, oldPower - newPower);
    }

    Issue: Using balanceOf here results in an inaccurate comparison, potentially causing improper burning or minting actions.

2. Boost Calculation and Governance Position

  • veRAACToken::calculateBoost:

    // Incorrect: Using balanceOf(user) instead of getVotingPower(user)
    return _boostState.calculateTimeWeightedBoost(balanceOf(user), totalSupply(), amount);
  • veRAACToken::getCurrentBoost:

    return _boostState.calculateTimeWeightedBoost(balanceOf(account), totalSupply(), _lockState.locks[account].amount);
  • veRAACToken::getLockPosition:

    return LockPosition({amount: userLock.amount, end: userLock.end, power: balanceOf(account)});

    Issue: All of the above lead to misrepresentations in boost factors, resulting in inaccurate reward distribution and misaligned governance weight.

3. Other Modules Misusing balanceOf

Several other functions in related modules contain similar issues:

  • GaugeController::vote:

    // @info: using balanceOf instead of getVotingPower to get votingPower
    uint256 votingPower = veRAACToken.balanceOf(msg.sender);
  • BoostCalculator::calculateBoost:

    // @info: using balanceOf instead of getVotingPower
    uint256 userBalance = veRAACToken.balanceOf(user);
    // @info: also fetching stale totalSupply
    uint256 totalSupply = veRAACToken.totalSupply();
  • BaseGauge::_applyBoost:

    // @info: using balanceOf instead of getVotingPower and stale totalSupply
    uint256 veBalance = veToken.balanceOf(account);
    uint256 totalVeSupply = veToken.totalSupply();
  • BaseGauge::voteDirection:

    // @info: using balanceOf instead of getVotingPower
    uint256 votingPower = IERC20(IGaugeController(controller).veRAACToken()).balanceOf(msg.sender);
  • BoostController::delegateBoost & _calculateBoost:
    Similar issues occur where balanceOf is used to gauge the user’s boost balance.

Furthermore, the veRAACToken::getTotalVotingPower function returns totalSupply() which can be stale because veRAAC tokens are rebasable. An internal helper to update each user's voting power dynamically is needed.

Proof of Concept

The following Foundry test suite demonstrates how the misuse of balanceOf leads to incorrect and stale voting power values across the contract, thereby affecting minting, burning, and reward calculations.

// 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 {TimeWeightedAverage} from "../src/libraries/math/TimeWeightedAverage.sol";
import {LockManager} from "../src/libraries/governance/LockManager.sol";
import {IveRAACToken} from "../src/interfaces/core/tokens/IveRAACToken.sol";
contract VeRAACTokenTest is Test {
veRAACToken veRaacToken;
RAACToken raacToken;
address RAAC_OWNER = makeAddr("RAAC_OWNER");
address RAAC_MINTER = makeAddr("RAAC_MINTER");
uint256 initialRaacSwapTaxRateInBps = 200; // 2%
uint256 initialRaacBurnTaxRateInBps = 150; // 1.5%
address VE_RAAC_OWNER = makeAddr("VE_RAAC_OWNER");
address ALICE = makeAddr("ALICE");
address BOB = makeAddr("BOB");
address CHARLIE = makeAddr("CHARLIE");
address DEVIL = makeAddr("DEVIL");
function setUp() public {
raacToken = new RAACToken(RAAC_OWNER, initialRaacSwapTaxRateInBps, initialRaacBurnTaxRateInBps);
vm.startPrank(VE_RAAC_OWNER);
veRaacToken = new veRAACToken(address(raacToken));
vm.stopPrank();
}
}

Steps to Run the Test Suite

  1. Create a Foundry Project:

    forge init my-foundry-project
  2. Remove Unnecessary Files:
    Delete any extraneous files to streamline the project.

  3. Convert Your Hardhat Project (if applicable):
    Move your contracts into the src directory.

  4. Create Test and Mocks Folders:
    Create a test directory (adjacent to src) and a mocks directory if needed.

  5. Add the Test Suite:
    In the test directory, create a test file (e.g., VeRAACTokenTest.t.sol) and paste the above test suite.

  6. Run the Test:
    Execute:

    forge test --mt testStaleVotingPowerUsedAccrossVeRAACTokenContract -vv
  7. Expected Output:
    The output should reveal that the values retrieved using balanceOf are stale compared to those computed by getVotingPower, confirming the vulnerability. For example, you might see:

    alice current Voting Power: 250000000000000000000000
    ...
    diff between expected and actual : 125342465753424667379200

Impact

  • Incorrect Minting and Burning of veRAACTokens:
    Users may receive an inflated number of veRAACTokens when increasing or extending their locks, thereby distorting their effective voting power and skewing governance outcomes.

  • Inaccurate Reward Distribution:
    Boost and reward calculations based on stale voting power lead to unfair reward allocations. Some users may receive rewards far beyond their entitlement while others are undercompensated.

  • Governance Manipulation and Quorum Distortion:
    Misrepresenting users’ voting power undermines the integrity of governance. Voting and quorum calculations become unreliable, potentially enabling attackers to dominate decision-making or block legitimate proposals.

  • Misrepresentation of User Lock Positions:
    Functions like getLockPosition return outdated information, leading users to make poor decisions based on incorrect data.

  • Overall Protocol Instability:
    The systemic reliance on stale data can destabilize multiple layers of the protocol—from tokenomics and reward distribution to governance and boost mechanics.

Tools Used

  • Manual Review

  • Foundry

  • Console Log (Foundry)

Recommendations

To mitigate this vulnerability, all instances where balanceOf is used to obtain a user’s voting power must be replaced with getVotingPower. Additionally, any function retrieving total supply values (which are subject to decay and rebase) should use an updated mechanism that reflects current voting power.

Diff Recommendations

1. In veRAACToken::increase:

- _mint(msg.sender, newPower - balanceOf(msg.sender));
+ _mint(msg.sender, newPower - getVotingPower(msg.sender));

2. In veRAACToken::extend:

- uint256 oldPower = balanceOf(msg.sender);
+ uint256 oldPower = getVotingPower(msg.sender);

3. In veRAACToken::calculateBoost:

- return _boostState.calculateTimeWeightedBoost(balanceOf(user), totalSupply(), amount);
+ return _boostState.calculateTimeWeightedBoost(getVotingPower(user), totalSupply(), amount);

4. In veRAACToken::getCurrentBoost:

- return _boostState.calculateTimeWeightedBoost(balanceOf(account), totalSupply(), _lockState.locks[account].amount);
+ return _boostState.calculateTimeWeightedBoost(getVotingPower(account), totalSupply(), _lockState.locks[account].amount);

5. In veRAACToken::getLockPosition:

- return LockPosition({amount: userLock.amount, end: userLock.end, power: balanceOf(account)});
+ return LockPosition({amount: userLock.amount, end: userLock.end, power: getVotingPower(account)});

6. In GaugeController::vote:

- uint256 votingPower = veRAACToken.balanceOf(msg.sender);
+ uint256 votingPower = veRAACToken.getVotingPower(msg.sender);

7. In BoostCalculator::calculateBoost:

- uint256 userBalance = veRAACToken.balanceOf(user);
- uint256 totalSupply = veRAACToken.totalSupply();
+ uint256 userBalance = veRAACToken.getVotingPower(user);
+ uint256 totalSupply = /* Implement a dynamic total voting power retrieval function instead of totalSupply() */;

8. In BaseGauge::_applyBoost:

- uint256 veBalance = veToken.balanceOf(account);
- uint256 totalVeSupply = veToken.totalSupply();
+ uint256 veBalance = veRAACToken.getVotingPower(account);
+ uint256 totalVeSupply = /* Use an updated total voting power metric */;

9. In BaseGauge::voteDirection:

- uint256 votingPower = IERC20(IGaugeController(controller).veRAACToken()).balanceOf(msg.sender);
+ uint256 votingPower = veRAACToken.getVotingPower(msg.sender);

10. In BoostController::delegateBoost and _calculateBoost:

Replace any instance of veToken.balanceOf(user) and veToken.totalSupply() with the corresponding updated functions that reflect current, time-weighted voting power.

Additional Check:
For the lock and increase functions, implement a check to enforce the maximum total locked amount cap:

+ if (_lockState.totalLocked + amount > MAX_TOTAL_LOCKED_AMOUNT) {
+ revert MaxTotalLockAmountExceeds();
+ }

Implementing these modifications ensures that all functions throughout the protocol use the correct, time-weighted voting power for all calculations, thereby preserving the integrity of minting/burning, reward allocation, and governance mechanisms.

By addressing this critical issue, the RAAC Protocol can safeguard its tokenomics, ensure fair reward distribution, and maintain robust governance functionality based on accurate, up-to-date voting power metrics.

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.