Core Contracts

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

Inconsistent Boost Calculations in BoostController Between External and Internal Functions

Summary

The BoostController contract exhibits inconsistent behavior between its public calculateBoost() and internal _calculateBoost() functions, leading to different boost amounts being calculated for the same inputs.

Vulnerability Details

The contract uses two different methods to calculate boosts:

// Internal calculation using token balances
function _calculateBoost(address user, address pool, uint256 amount) internal view returns (uint256) {
// ...
uint256 userBalance = IERC20(address(veToken)).balanceOf(user);
uint256 totalSupply = IERC20(address(veToken)).totalSupply();
// Uses token balances for calculation
return BoostCalculator.calculateTimeWeightedBoost(
params,
userBalance, // Uses token balance
totalSupply, // Uses total supply
amount
);
}
// External calculation using voting power
function calculateBoost(address user, address pool, uint256 amount)
external view override returns (uint256 boostBasisPoints, uint256 boostedAmount) {
// ...
uint256 userVotingPower = veToken.getVotingPower(user, block.timestamp);
// Uses voting power for calculation
return BoostCalculator.calculateTimeWeightedBoost(
params,
userVotingPower, // Uses voting power
totalVotingPower, // Uses total voting power
amount
);
}

The issue arises because:

  1. _calculateBoost() uses raw token balances (used by updateUserBoost()

  2. calculateBoost() uses voting power calculations

  3. These different calculation methods result in inconsistent boost amounts

  4. Users cannot accurately predict their boost amount using the public view function

Test Results:

amount calculated by _calculateBoost(): 25000

amount calculated by calculateBoost(): 24589

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";
contract FoundryTest 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_calculateBoost() public {
uint256 lockAmount = 100e18;
vm.startPrank(alice);
veToken.lock(lockAmount, YEAR);
vm.warp(block.timestamp + 10 days);
controller.updateUserBoost(alice, pool);
(, uint256 calculatedBoostedAmount) = controller.calculateBoost(alice, pool, 10000);
(uint256 amount, , , ) = controller.getUserBoost(alice, pool);
(uint256 totalBoost, uint256 workingSupply, uint256 baseSupply, ) = controller.getPoolBoost(pool);
vm.stopPrank();
console2.log("\nTest Results:");
console2.log("amount calculated by _calculateBoost():", amount);
console2.log("totalBoost", totalBoost);
console2.log("workingSupply", workingSupply);
console2.log("baseSupply", baseSupply);
console2.log("amount calculated by calculateBoost():", calculatedBoostedAmount);
}
}

Impact

  • Users receive different boost amounts than what the public calculation function predicts

  • Difficulty in accurately forecasting boost amounts

  • Possible exploitation of the difference between calculation methods => especially when other parts of the protocol rely on this information

Tools Used

  • Foundry

  • Manual Review

Recommendations

Standardize the calculation method by using voting power or the token balance in both functions and add documentation for explaining the calculation method.

Updates

Lead Judging Commences

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