Core Contracts

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

Arithmetic Underflow in BoostController::updateUserBoost Due to Voting Power Decay and veToken Balance Changes

Summary

The BoostController contract is vulnerable to arithmetic underflow in the updateUserBoost() function when recalculating pool's totalBoost. The root cause lies in using decaying voting power or fluctuating veToken balances without adequate validation or boundary checks. As a result, this can lead to a revert due to underflow, causing inaccurate poolBoost calculations, which in turn impacts reward distribution, governance, and financial stability.

Vulnerability Details

Arithmetic Underflow in updateUserBoost()

The updateUserBoost() function updates a user’s boost by calculating the difference between the new and old boost values, then adjusting the pool’s totalBoost accordingly. However, if the new boost is less than the old boost due to voting power decay or veToken balance changes, an arithmetic underflow can occur:

if (newBoost >= oldBoost) {
poolBoost.totalBoost = poolBoost.totalBoost + (newBoost - oldBoost);
} else {
poolBoost.totalBoost = poolBoost.totalBoost - (oldBoost - newBoost); // Potential underflow
}

The issue arises because oldBoost - newBoost can exceed poolBoost.totalBoost, causing an underflow and a revert in Solidity ^0.8.19.

Proof of Concept

Scenario 1: Voting Power Decay

  1. Alice delegates her boost to a pool with a calculated amount based on her voting power. The boost is calculated using BoostController::_calculateBoost, which utilizes the user's voting power obtained from veToken.getVotingPower(user, block.timestamp);.

  2. As time progresses, Alice’s voting power decays due to the time-weighted nature of veTokens.

  3. A call to updateUserBoost() is made by either Alice or another actor, as this function has no access control restrictions.

  4. The function recalculates Alice's new boost using _calculateBoost(), which now returns a lower value due to the decayed voting power.

  5. In the updateUserBoost() function, the pool's totalBoost is updated as follows:

if (newBoost >= oldBoost) {
poolBoost.totalBoost = poolBoost.totalBoost + (newBoost - oldBoost);
} else {
poolBoost.totalBoost = poolBoost.totalBoost - (oldBoost - newBoost); // Potential underflow
}
  1. Since newBoost is smaller than oldBoost, the calculation attempts to subtract a larger value from a smaller one, leading to an arithmetic underflow and a revert due to Solidity ^0.8.19's checked arithmetic.

Code Mutation

In this scenario, we experimented by mutating the _calculateBoost() function to simulate decayed voting power using:

uint256 userBalance = veToken.getVotingPower(user, block.timestamp);

This provided an accurate simulation of real-world decay, validating the underflow issue.

Code PoC (to above scenario 1)

Note: Remember to mutate the above mentioned LoC in BoostController::_calculateBoost().

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 testArithmeticOverUnderflowInUpdateUserBoostCouldLeadToPermanentDoSMutationalTesting() public {
vm.warp(block.timestamp + 365 days / 2);
address poolAddress = makeAddr("DUMMY_POOL");
vm.startPrank(BOOST_CONTROLLER_OWNER);
boostController.modifySupportedPool(poolAddress, true);
vm.stopPrank();
(uint256 amount, uint256 expiry, address delegatee, uint256 userLastUpdateTime) =
boostController.getUserBoost(ALICE, poolAddress);
console.log("alice's boost info before delegation...");
console.log("amount : ", amount);
console.log("expiry : ", expiry);
console.log("delegatee : ", delegatee);
console.log("userLastUpdateTime: ", userLastUpdateTime);
(uint256 totalBoost, uint256 workingSupply, uint256 baseSupply, uint256 lastUpdateTime) =
boostController.getPoolBoost(poolAddress);
console.log("\npool's boost before alice's delegation...");
console.log("totalBoost : ", totalBoost);
console.log("workingSupply : ", workingSupply);
console.log("baseSupply : ", baseSupply);
console.log("lastUpdateTime: ", lastUpdateTime);
(, uint256 calculatedAliceBoostAmount) = boostController.calculateBoost(ALICE, poolAddress, 10_000);
console.log("calculatedAliceBoostAmount: ", calculatedAliceBoostAmount);
vm.startPrank(ALICE);
boostController.delegateBoost(poolAddress, 10_000, 7 days);
boostController.updateUserBoost(ALICE, poolAddress);
vm.stopPrank();
(amount, expiry, delegatee, userLastUpdateTime) = boostController.getUserBoost(ALICE, poolAddress);
console.log("\nalice's boost info after delegation...");
console.log("amount : ", amount);
console.log("expiry : ", expiry);
console.log("delegatee : ", delegatee);
console.log("userLastUpdateTime: ", userLastUpdateTime);
(totalBoost, workingSupply, baseSupply, lastUpdateTime) = boostController.getPoolBoost(poolAddress);
console.log("\npool's boost after alice's delegation...");
console.log("totalBoost : ", totalBoost);
console.log("workingSupply : ", workingSupply);
console.log("baseSupply : ", baseSupply);
console.log("lastUpdateTime: ", lastUpdateTime);
address DIANA = makeAddr("DIANA");
vm.startPrank(RAAC_MINTER);
raacToken.mint(DIANA, 10_000_000e18);
vm.stopPrank();
vm.startPrank(DIANA);
raacToken.approve(address(veRaacToken), 10_000_000e18);
veRaacToken.lock(10_000_000e18, 365 days);
vm.stopPrank();
(, uint256 calculatedDianaBoost) = boostController.calculateBoost(DIANA, poolAddress, 10_000);
console.log("\ncalculatedDianaBoost: ", calculatedDianaBoost);
vm.startPrank(DIANA);
boostController.delegateBoost(poolAddress, calculatedDianaBoost, 365 days);
vm.stopPrank();
vm.warp(block.timestamp + 364 days);
(amount, expiry, delegatee, userLastUpdateTime) = boostController.getUserBoost(DIANA, poolAddress);
console.log("\ndiana's boost info after diana delegation...");
console.log("amount : ", amount);
console.log("expiry : ", expiry);
console.log("delegatee : ", delegatee);
console.log("userLastUpdateTime: ", userLastUpdateTime);
// here old boost > new boost
// and pool totalBoost is smaller than difference
// pool's totalBoost = 625
// user's oldBoost = 16818
// user's newBoost = 10018
// 16818 - 10018 = 6800
// 625 - 6800 = -6175 (arithmetic underflow actually)
// it will revert: arithmetic over/underflow
vm.expectRevert();
boostController.updateUserBoost(DIANA, poolAddress);
}
  1. Step 6: To run the test, execute the following commands in your terminal: (be sure you mutated the code)

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

[⠒] Compiling...
No files changed, compilation skipped
Ran 1 test for test/GovernanceTest.t.sol:GovernanceTest
[PASS] testArithmeticOverUnderflowInUpdateUserBoostCouldLeadToPermanentDoSMutationalTesting() (gas: 755432)
Logs:
alice's boost info before delegation...
amount : 0
expiry : 0
delegatee : 0x0000000000000000000000000000000000000000
userLastUpdateTime: 0
pool's boost before alice's delegation...
totalBoost : 0
workingSupply : 0
baseSupply : 0
lastUpdateTime: 0
calculatedAliceBoostAmount: 10625
alice's boost info after delegation...
amount : 10625
expiry : 16372801
delegatee : 0x61f66E9ad8A6E4Ef9A663B23F497189574b95D5C
userLastUpdateTime: 15768001
pool's boost after alice's delegation...
totalBoost : 625
workingSupply : 10625
baseSupply : 0
lastUpdateTime: 15768001
calculatedDianaBoost: 16818
diana's boost info after diana delegation...
amount : 16818
expiry : 47304001
delegatee : 0x61f66E9ad8A6E4Ef9A663B23F497189574b95D5C
userLastUpdateTime: 15768001
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 9.47ms (1.02ms CPU time)
Ran 1 test suite in 14.39ms (9.47ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

As demonstrated, the test confirms that there exist an Arithmetic Over/Underflow (Actually Underflow) Error in the BoostController::updateUserBoost.

Scenario 2: veToken Balance Change

  1. Diana locks RAAC tokens into the veRAACToken contract for 2 years, acquiring voting power and delegating her boost to a pool. Her boost is calculated using _calculateBoost() based on her veToken balance obtained from:

uint256 userBalance = IERC20(address(veToken)).balanceOf(user);
  1. Almost 2 years pass, and Diana decides to extend her veToken lock duration by an additional 10 days using veRAACToken::extend().

  2. The extend() function adjusts her veToken balance by burning or minting tokens according to the decay slope rate. Consequently, her balance decreases due to decay adjustments.

  3. updateUserBoost() is triggered, which calls _calculateBoost() again. Due to the reduced veToken balance, the new boost value is lower than her old boost.

  4. The pool's totalBoost is updated as follows:

if (newBoost >= oldBoost) {
poolBoost.totalBoost = poolBoost.totalBoost + (newBoost - oldBoost);
} else {
poolBoost.totalBoost = poolBoost.totalBoost - (oldBoost - newBoost); // Potential underflow
}
  1. Since newBoost is smaller than oldBoost, the subtraction in the else block causes an arithmetic underflow, leading to a revert.

Code Mutation

To simulate this scenario, the _calculateBoost() function was mutated back to its original form to use stale voting power as follows:

uint256 userBalance = IERC20(address(veToken)).balanceOf(user);

This LoC is already present in the function which is a buggy already and causes problems but help validate the underflow by accounting for veToken balance changes caused by the extend() function.

Code PoC (to above scenario 2)

Note: Remember do not mutate any code in the BoostController::_calculateBoost(). Let the function in its original form, If you've mutated this function to test above scenario's PoC Test then undo all changes you've made in BoostController::_calculateBoost().

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 testArithmeticOverUnderflowInUpdateUserBoostCouldLeadToPermanentDoSNoMutation() public {
vm.warp(block.timestamp + 365 days / 2);
address poolAddress = makeAddr("DUMMY_POOL");
vm.startPrank(BOOST_CONTROLLER_OWNER);
boostController.modifySupportedPool(poolAddress, true);
vm.stopPrank();
address DIANA = makeAddr("DIANA");
vm.startPrank(RAAC_MINTER);
raacToken.mint(DIANA, 10_000_000e18);
vm.stopPrank();
vm.startPrank(DIANA);
raacToken.approve(address(veRaacToken), 10_000_000e18);
veRaacToken.lock(100_000e18, 365 days * 2);
vm.stopPrank();
(uint256 amount, uint256 expiry, address delegatee, uint256 userLastUpdateTime) =
boostController.getUserBoost(DIANA, poolAddress);
console.log("diana's boost info before delegation...");
console.log("amount : ", amount);
console.log("expiry : ", expiry);
console.log("delegatee : ", delegatee);
console.log("userLastUpdateTime: ", userLastUpdateTime);
(uint256 totalBoost, uint256 workingSupply, uint256 baseSupply, uint256 lastUpdateTime) =
boostController.getPoolBoost(poolAddress);
console.log("\npool's boost before alice's delegation...");
console.log("totalBoost : ", totalBoost);
console.log("workingSupply : ", workingSupply);
console.log("baseSupply : ", baseSupply);
console.log("lastUpdateTime: ", lastUpdateTime);
(, uint256 calculatedDianaBoost) = boostController.calculateBoost(DIANA, poolAddress, 10_000);
console.log("\ncalculatedDianaBoost: ", calculatedDianaBoost);
vm.startPrank(DIANA);
boostController.delegateBoost(poolAddress, calculatedDianaBoost, 365 days);
vm.stopPrank();
(amount, expiry, delegatee, userLastUpdateTime) = boostController.getUserBoost(DIANA, poolAddress);
console.log("\ndiana's boost info after delegation...");
console.log("amount : ", amount);
console.log("expiry : ", expiry);
console.log("delegatee : ", delegatee);
console.log("userLastUpdateTime: ", userLastUpdateTime);
(totalBoost, workingSupply, baseSupply, lastUpdateTime) = boostController.getPoolBoost(poolAddress);
console.log("\npool's boost before diana's delegation...");
console.log("totalBoost : ", totalBoost);
console.log("workingSupply : ", workingSupply);
console.log("baseSupply : ", baseSupply);
console.log("lastUpdateTime: ", lastUpdateTime);
vm.warp(block.timestamp + 365 days * 2 - 10);
// diana extends her locked amount duration
vm.startPrank(DIANA);
veRaacToken.extend(10 days);
vm.stopPrank();
(, calculatedDianaBoost) = boostController.calculateBoost(DIANA, poolAddress, 10_000);
console.log("\n after increasing locked amount...");
console.log("calculatedDianaBoost: ", calculatedDianaBoost);
// it will revert because
// oldBoost is 10245
// newBoost is 10003
// totalBoost is 0
// revert reason: arithmetic underflow
vm.expectRevert();
boostController.updateUserBoost(DIANA, poolAddress);
}
  1. Step 6: To run the test, execute the following commands in your terminal:

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

[⠒] Compiling...
No files changed, compilation skipped
Ran 1 test for test/GovernanceTest.t.sol:GovernanceTest
[PASS] testArithmeticOverUnderflowInUpdateUserBoostCouldLeadToPermanentDoSNoMutation() (gas: 669853)
Logs:
diana's boost info before delegation...
amount : 0
expiry : 0
delegatee : 0x0000000000000000000000000000000000000000
userLastUpdateTime: 0
pool's boost before alice's delegation...
totalBoost : 0
workingSupply : 0
baseSupply : 0
lastUpdateTime: 0
calculatedDianaBoost: 10245
diana's boost info after delegation...
amount : 10245
expiry : 47304001
delegatee : 0x61f66E9ad8A6E4Ef9A663B23F497189574b95D5C
userLastUpdateTime: 15768001
pool's boost before diana's delegation...
totalBoost : 0
workingSupply : 0
baseSupply : 0
lastUpdateTime: 0
after increasing locked amount...
calculatedDianaBoost: 10003
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.05ms (840.80µs CPU time)
Ran 1 test suite in 10.74ms (7.05ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

As demonstrated, the test confirms that there exist an Arithmetic Over/Underflow (Actually Underflow) Error in the BoostController::updateUserBoost even without any mutation.

Impact

  • Revert due to arithmetic underflow in updateUserBoost().

  • Inaccurate poolBoost calculations leading to unstable reward distribution.

  • Potential governance manipulation or financial instability.

Tools Used

  • Manual Review

  • Foundry

Recommendations

Fix for updateUserBoost()

- if (newBoost >= oldBoost) {
- poolBoost.totalBoost = poolBoost.totalBoost + (newBoost - oldBoost);
- } else {
- poolBoost.totalBoost = poolBoost.totalBoost - (oldBoost - newBoost);
- }
+ if (newBoost >= oldBoost) {
+ poolBoost.totalBoost = poolBoost.totalBoost + (newBoost - oldBoost);
+ } else if (poolBoost.totalBoost >= (oldBoost - newBoost)) {
+ poolBoost.totalBoost = poolBoost.totalBoost - (oldBoost - newBoost);
+ } else {
+ poolBoost.totalBoost = 0;
+ }

This fix ensures accurate boost calculations while preventing underflow by adding a boundary check before performing the subtraction. Implementing this change will enhance protocol stability and security.

Updates

Lead Judging Commences

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

BoostController removes pool boost on delegation removal without adding it on delegation creation, leading to accounting inconsistencies and potential underflows

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

BoostController removes pool boost on delegation removal without adding it on delegation creation, leading to accounting inconsistencies and potential underflows

Support

FAQs

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