Core Contracts

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

Broken Access Control Ownership Transfer and RAACToken Parameter update Issues in RAACMinter

Summary

The RAACMinter contract, which is designed to be the owner of the RAACToken, lacks critical functionality for managing token ownership and paramter updates of the RAACToken. Once ownership is transferred to the RAACMinter, there is no mechanism to transfer ownership again, and the contract cannot disable fee collection due to a zero address check. It also doesn't implement the functions setTaxRateIncrementLimit and manageWhitelist which can't be called anymore once the Ownership is transferred.

As stated in the implementation details of the contract the RAACMinter controls RAAC token parameters such as tax rates and fee collector but in order to do this the RAACMinter must be the Owner of the RAACToken, because the RAACToken has the onlyOwner modifier applied to it's update functions.

It's also important to notice that the documentation outligns that there should be a time delay mechanism for ownership transfers which is also missing in the current implementation:

  • Implements a 7-day delay mechanism for transferring RAACToken ownership

  • Provides a 24-hour window after the delay period to complete the ownership transfer

Vulnerability Details

  1. Missing Ownership Transfer Functionality and missing setTaxRateIncrementLimit and manageWhitelist in RAACMinter

  2. While the RAACToken allows setting the fee collector to address(0) to disable fee collection, the RAACMinter prevents this functionality through its zero address check:

    function setFeeCollector(address _feeCollector) external onlyRole(UPDATER_ROLE) {
    if (_feeCollector == address(0)) revert FeeCollectorCannotBeZeroAddress();
    raacToken.setFeeCollector(_feeCollector);
    emit ParameterUpdated("feeCollector", uint256(uint160(_feeCollector)));
    }

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: MIT
pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import {console2} from "forge-std/console2.sol";
import {StabilityPool} from "../../contracts/core/pools/StabilityPool/StabilityPool.sol";
import {crvUSDToken} from "../../contracts/mocks/core/tokens/crvUSDToken.sol";
import {RAACToken} from "../../contracts/core/tokens/RAACToken.sol";
import {RAACHousePrices} from "../../contracts/core/primitives/RAACHousePrices.sol";
import {RAACNFT} from "../../contracts/core/tokens/RAACNFT.sol";
import {RToken} from "../../contracts/core/tokens/RToken.sol";
import {DebtToken} from "../../contracts/core/tokens/DebtToken.sol";
import {DEToken} from "../../contracts/core/tokens/DEToken.sol";
import {LendingPool} from "../../contracts/core/pools/LendingPool/LendingPool.sol";
import {RAACMinter, IRAACMinter} from "../../contracts/core/minters/RAACMinter/RAACMinter.sol";
contract FoundryTest is Test {
StabilityPool public stabilityPool;
LendingPool public lendingPool;
RAACMinter public raacMinter;
crvUSDToken public crvusd;
RToken public rToken;
DEToken public deToken;
RAACToken public raacToken;
RAACNFT public raacNFT;
DebtToken public debtToken;
RAACHousePrices public raacHousePrices;
address public owner;
address public user1;
address public user2;
address public user3;
address public treasury;
uint256 public constant INITIAL_BALANCE = 1000e18;
uint256 public constant INITIAL_PRIME_RATE = 0.1e27;
function setUp() public {
// Setup accounts
owner = address(this);
user1 = makeAddr("user1");
user2 = makeAddr("user2");
user3 = makeAddr("user3");
treasury = makeAddr("treasury");
// Deploy base tokens
crvusd = new crvUSDToken(owner);
crvusd.setMinter(owner);
raacToken = new RAACToken(owner, 100, 50);
// Deploy price oracle and set oracle
raacHousePrices = new RAACHousePrices(owner);
raacHousePrices.setOracle(owner);
// Deploy NFT
raacNFT = new RAACNFT(address(crvusd), address(raacHousePrices), owner);
// Deploy pool tokens
rToken = new RToken("RToken", "RToken", owner, address(crvusd));
debtToken = new DebtToken("DebtToken", "DT", owner);
deToken = new DEToken("DEToken", "DEToken", owner, address(rToken));
// Deploy pools
lendingPool = new LendingPool(
address(crvusd),
address(rToken),
address(debtToken),
address(raacNFT),
address(raacHousePrices),
INITIAL_PRIME_RATE
);
stabilityPool = new StabilityPool(owner);
// this is needed otherwise lastEmissionUpdateTimestamp will underflow in the RAACMinter constructor
vm.warp(block.timestamp + 2 days);
// Deploy RAAC minter
raacMinter = new RAACMinter(address(raacToken), address(stabilityPool), address(lendingPool), owner);
// Setup cross-contract references
lendingPool.setStabilityPool(address(stabilityPool));
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
rToken.transferOwnership(address(lendingPool));
debtToken.transferOwnership(address(lendingPool));
deToken.setStabilityPool(address(stabilityPool));
deToken.transferOwnership(address(stabilityPool));
// Initialize Stability Pool
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(raacMinter),
address(crvusd),
address(lendingPool)
);
// Setup permissions
raacToken.setMinter(address(raacMinter));
raacToken.manageWhitelist(address(stabilityPool), true);
}
function test_raacMinterAdminFunctions() public {
// we also can't set the fee collector if the raacMinter is not the owner of the raacToken
vm.expectRevert();
raacMinter.setFeeCollector(address(0x01));
// so we transfer ownership of raacToken to raacMinter
// there is also no time-delay mechanism for this function
raacToken.transferOwnership(address(raacMinter));
assertEq(raacToken.owner(), address(raacMinter));
// now we can't transfer ownership of raacToken anymore because there is no function in the RAACMinter contract
// we also can't set the fee collector to address(0) as well
vm.expectRevert(IRAACMinter.FeeCollectorCannotBeZeroAddress.selector);
raacMinter.setFeeCollector(address(0));
// this should work now because the raacMinter is the owner of the raacToken
raacMinter.setFeeCollector(address(0x01));
}
}

Impact

  • Once ownership is transferred to the RAACMinter, the RAACToken becomes permanently locked under its control with no mechanism to transfer ownership again.

  • The inability to set the fee collector to address(0) prevents the RAACMinter from disabling fee collection, a feature that is intentionally supported by the RAACToken.

  • Can't manage setTaxRateIncrementLimit and manageWhitelist anymore

Tools Used

  • Manual review

  • Foundry

Recommendations

  1. Implement ownership transfer functionality in the RAACMinter (and time delay mechanism)

  2. Remove the zero address check for fee collector

  3. Add functions to manage setTaxRateIncrementLimit and manageWhitelist

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 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

RAACMinter::setFeeCollector prevents disabling fees by blocking zero address assignment

inallhonesty Lead Judge 4 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

RAACMinter::setFeeCollector prevents disabling fees by blocking zero address assignment

Support

FAQs

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