Core Contracts

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

RaacMinter unable to manage RAACToken's whitelist if a new FeeCollector is set

Summary

The RAACMinter is designed to be the owner of RAACToken after deployment. It is able to change the taxRate and burnRate, which are annotated with the onlyOwner modifier.

However, it does not have any function to control over adding a whitelistedAddress. Should a new FeeCollector be set or a new whiteListed address needs to be added, it cannot do so.

Vulnerability Details

  1. Missing function in the RAACMinter contract to manage the whitelist in the RAACToken contract.

  2. Ownership of the RAACToken contract is transferred to RAACMinter, so the manageWhitelist() function can never be called again.

Impact

RaacMinter is unable to set a whitelist for a new FeeCollector or any other whitelisted address.

Tools Used

Manual Review

PoC

Please use below foundry test:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import {IERC20, ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
import "../../contracts/libraries/math/PercentageMath.sol";
import "../../contracts/libraries/math/WadRayMath.sol";
import {BaseSetup} from "./BaseSetup.t.sol";
import "../../contracts/interfaces/core/pools/LendingPool/ILendingPool.sol";
import "../../contracts/core/collectors/FeeCollector.sol";
contract FeeCollectorTest is BaseSetup {
using WadRayMath for uint256;
using PercentageMath for uint256;
using SafeCast for uint256;
using SafeERC20 for IERC20;
IERC20 mainnetUSDC = IERC20(address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48));
address mainnetUsdcCurveUSDVault = address(0x7cA00559B978CFde81297849be6151d3ccB408A9);
address curveUSDWhale = address(0x4e59541306910aD6dC1daC0AC9dFB29bD9F15c67);
address newUser = makeAddr("new user");
address newUser2 = makeAddr("new user2");
address newUser3 = makeAddr("new user3");
address newUser4 = makeAddr("new user4");
function setUp() public override {
string memory MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL");
uint256 mainnetFork = vm.createFork(MAINNET_RPC_URL, 19614507);
vm.selectFork(mainnetFork);
super.setUp();
vm.startPrank(owner);
lendingPool.setParameter(ILendingPool.OwnerParameter.LiquidationThreshold, 8000); // 80%
lendingPool.setParameter(ILendingPool.OwnerParameter.HealthFactorLiquidationThreshold, 1e18); // 1.0e18
lendingPool.setParameter(ILendingPool.OwnerParameter.LiquidationGracePeriod, 3 days);
lendingPool.setParameter(ILendingPool.OwnerParameter.LiquidityBufferRatio, 2000); // 20%
lendingPool.setParameter(ILendingPool.OwnerParameter.WithdrawalStatus, 0); // Allow withdrawals
lendingPool.setParameter(ILendingPool.OwnerParameter.CanPaybackDebt, 1); // Enable payback
// house is 1000 with 18 decimals, same as crvUSD
uint256 housePrice = 1_000_000e18;
housePrices.setHousePrice(1, housePrice);
housePrices.setHousePrice(2, housePrice);
housePrices.setHousePrice(3, housePrice);
housePrices.setHousePrice(4, housePrice);
housePrices.setHousePrice(5, housePrice);
housePrices.setHousePrice(6, housePrice);
housePrices.setHousePrice(7, housePrice);
housePrices.setHousePrice(8, housePrice);
vm.stopPrank();
vm.startPrank(curveUSDWhale);
crvUSD.transfer(newUser, 15_000_000e18);
crvUSD.transfer(newUser2, 1_000_000e18); // newUser2 has 1M crvUSD
crvUSD.transfer(newUser3, 5_000_000e18);
crvUSD.transfer(newUser4, 5_000_000e18);
crvUSD.transfer(address(stabilityPool), 10_000_000e18);
vm.stopPrank();
}
function test_CannotSetWhiteListForNewFeeCollector() public {
assertEq(raacToken.owner(), address(minter)); // ok raacOwner is minter
assertEq(raacToken.minter(), address(minter));
vm.startPrank(owner);
FeeCollector newFeeCollector =
new FeeCollector(address(raacToken), address(veRaacToken), address(treasury), address(repairFund), owner);
minter.setFeeCollector(address(newFeeCollector));
assertTrue(raacToken.feeCollector() == address(newFeeCollector));
assertFalse(raacToken.isWhitelisted(address(newFeeCollector)));
}
}

With this BaseSetup

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
// OpenZeppelin Imports
// import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
// import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
// Primitives Import
import {RAACHousePrices} from "../../contracts/core/primitives/RAACHousePrices.sol";
// Token Imports
import {DebtToken} from "../../contracts/core/tokens/DebtToken.sol";
import {DEToken} from "../../contracts/core/tokens/DEToken.sol";
import {IndexToken} from "../../contracts/core/tokens/IndexToken.sol";
import {LPToken} from "../../contracts/core/tokens/LPToken.sol";
import {RAACNFT} from "../../contracts/core/tokens/RAACNFT.sol";
import {RAACToken} from "../../contracts/core/tokens/RAACToken.sol";
import {RToken} from "../../contracts/core/tokens/RToken.sol";
import {veRAACToken} from "../../contracts/core/tokens/veRAACToken.sol";
// Collector Imports
import {Treasury} from "../../contracts/core/collectors/Treasury.sol";
import {FeeCollector} from "../../contracts/core/collectors/FeeCollector.sol";
// Governance Imports
import {Governance} from "../../contracts/core/governance/proposals/Governance.sol";
import {TimelockController} from "../../contracts/core/governance/proposals/TimelockController.sol";
// Gauge Imports
import {BaseGauge} from "../../contracts/core/governance/gauges/BaseGauge.sol";
import {GaugeController} from "../../contracts/core/governance/gauges/GaugeController.sol";
import {IGaugeController} from "../../contracts/interfaces/core/governance/gauges/IGaugeController.sol";
import {RAACGauge} from "../../contracts/core/governance/gauges/RAACGauge.sol";
import {RWAGauge} from "../../contracts/core/governance/gauges/RWAGauge.sol";
// Boost Import
import {BoostController} from "../../contracts/core/governance/boost/BoostController.sol";
// Minter Imports
import {RAACMinter} from "../../contracts/core/minters/RAACMinter/RAACMinter.sol";
import {RAACReleaseOrchestrator} from "../../contracts/core/minters/RAACReleaseOrchestrator/RAACReleaseOrchestrator.sol";
// Oracle Imports
import {BaseChainlinkFunctionsOracle} from "../../contracts/core/oracles/BaseChainlinkFunctionsOracle.sol";
import {RAACHousePriceOracle} from "../../contracts/core/oracles/RAACHousePriceOracle.sol";
import {RAACPrimeRateOracle} from "../../contracts/core/oracles/RAACPrimeRateOracle.sol";
// Pool Imports
import {LendingPool} from "../../contracts/core/pools/LendingPool/LendingPool.sol";
import {StabilityPool} from "../../contracts/core/pools/StabilityPool/StabilityPool.sol";
import {MarketCreator} from "../../contracts/core/pools/StabilityPool/MarketCreator.sol";
import {NFTLiquidator} from "../../contracts/core/pools/StabilityPool/NFTLiquidator.sol";
contract StakingToken is ERC20 {
constructor() ERC20("ST","ST"){
}
function mint(address account, uint256 value) public {
_mint(account, value);
}
function burn(address account, uint256 value) public {
_burn(account, value);
}
}
contract BaseSetup is Test {
IERC20 baseUSDC = IERC20(address(0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913));
address baseUSDCWhale = address(0x20FE51A9229EEf2cF8Ad9E89d91CAb9312cF3b7A);
// Collectors
Treasury treasury;
Treasury repairFund;
FeeCollector feeCollector;
// Tokens
IERC20 crvUSD = IERC20(address(0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E)); // main
IERC20 basecrvUSD = IERC20(address(0x417Ac0e078398C154EdFadD9Ef675d30Be60Af93)); // base
RAACToken raacToken;
veRAACToken veRaacToken;
RAACNFT raacNFT;
RToken rToken;
DebtToken debtToken;
DEToken deToken;
address mainnetChainlinkRouter = address(0x65Dcc24F8ff9e51F10DCc7Ed1e4e2A61e6E14bd6);
//m Orchestatro
RAACReleaseOrchestrator raacReleaseOrchestrator;
RAACHousePrices housePrices;
LendingPool lendingPool;
StabilityPool stabilityPool;
RAACMinter minter;
// Governance
Governance public governance;
TimelockController public timelockController;
BoostController public boostController;
GaugeController public gaugeController;
RAACGauge public raacGauge;
RWAGauge public rwaGauge;
address owner;
address user1;
address user2;
address user3;
StakingToken stakingToken;
function setUp() public virtual {
stakingToken = new StakingToken();
owner = makeAddr("owner");
user1 = makeAddr("user1");
user2 = makeAddr("user2");
user3 = makeAddr("user3");
vm.startPrank(owner);
// collectors
//
// Setup Tokens
raacToken = new RAACToken(owner, 100, 50);
veRaacToken = new veRAACToken(address(raacToken));
// veRAACToken
// RAACNFT
// DeToken
// Setup contracts
raacReleaseOrchestrator = new RAACReleaseOrchestrator(address(raacToken));
housePrices = new RAACHousePrices(owner);
housePrices.setOracle(owner);
raacNFT = new RAACNFT(address(crvUSD), address(housePrices), owner);
rToken = new RToken("RToken", "RT", owner, address(crvUSD));
debtToken = new DebtToken("DebtToken", "DT", owner);
lendingPool = new LendingPool(
address(crvUSD), address(rToken), address(debtToken), address(raacNFT), address(housePrices), 0.1e27
);
console.log("lendingPool:", address(lendingPool));
treasury = new Treasury(owner);
repairFund = new Treasury(owner);
deToken = new DEToken("DEToken", "DEToken", owner, address(rToken));
stabilityPool = new StabilityPool(owner);
minter = new RAACMinter(address(raacToken), address(stabilityPool), address(lendingPool), owner); // should be owner
feeCollector =
new FeeCollector(address(raacToken), address(veRaacToken), address(treasury), address(repairFund), owner);
raacToken.setFeeCollector(address(feeCollector));
raacToken.manageWhitelist(address(feeCollector), true);
raacToken.manageWhitelist(address(veRaacToken), true);
raacToken.manageWhitelist(owner, true);
raacToken.setMinter(address(owner));
raacToken.mint(user2, 1000e18);
raacToken.mint(user3, 1000e18);
feeCollector.grantRole(feeCollector.FEE_MANAGER_ROLE(), owner);
feeCollector.grantRole(feeCollector.EMERGENCY_ROLE(), owner);
feeCollector.grantRole(feeCollector.DISTRIBUTOR_ROLE(), owner);
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
deToken.setStabilityPool(address(stabilityPool));
//
raacToken.setMinter(address(minter));
raacToken.transferOwnership(address(minter));
rToken.transferOwnership(address(lendingPool));
debtToken.transferOwnership(address(lendingPool));
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(minter),
address(crvUSD),
address(lendingPool)
);
lendingPool.setStabilityPool(address(stabilityPool));
// Governance Components
// Timelock controller
address[] memory proposers = new address[](1);
proposers[0] = owner;
address[] memory executors = new address[](1);
executors[0] = owner;
timelockController = new TimelockController(2 days, proposers, executors, owner);
// Governance
governance = new Governance(address(veRaacToken), address(timelockController));
timelockController.grantRole(timelockController.PROPOSER_ROLE(), address(governance));
timelockController.grantRole(timelockController.EXECUTOR_ROLE(), address(governance));
timelockController.grantRole(timelockController.CANCELLER_ROLE(), address(governance));
//Gauge
gaugeController = new GaugeController(address(veRaacToken));
boostController = new BoostController(address(veRaacToken));
rwaGauge = new RWAGauge(address(raacToken), address(stakingToken), address(gaugeController));
raacGauge = new RAACGauge(address(raacToken), address(stakingToken), address(gaugeController));
gaugeController.addGauge(address(rwaGauge),IGaugeController.GaugeType.RWA , 0);
gaugeController.addGauge(address(raacGauge),IGaugeController.GaugeType.RAAC , 0);
rwaGauge.grantRole(rwaGauge.CONTROLLER_ROLE(),owner);
raacGauge.grantRole(rwaGauge.CONTROLLER_ROLE(),owner);
vm.stopPrank();
}
}

Recommendations

contract RAACMinter is IRAACMinter, Ownable, ReentrancyGuard, Pausable, AccessControl {
// ...omitted for brevity
+ function manageWhiteList(address account, bool add) external onlyRole(UPDATER_ROLE) {
+ if (account == address(0)) revert InvalidWhiteListAddress();
+ raacToken.manageWhitelist(account, add);
+ emit WhitelistUpdated(account,add);
+ }
// ...omitted for brevity
}
Updates

Lead Judging Commences

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

RAACMinter lacks critical ownership transfer functionality and parameter management after receiving RAACToken ownership, causing permanent protocol rigidity

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

RAACMinter lacks critical ownership transfer functionality and parameter management after receiving RAACToken ownership, causing permanent protocol rigidity

Support

FAQs

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