Core Contracts

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

Improper Ownership Transfer of RAACToken to RAACMinter Causes Configuration Lockout

Summary

The RAACMinter contract manages the minting and distribution of RAAC tokens based on a dynamic emissions schedule. To operate correctly, RAACMinter must call several functions on the RAACToken contract that are restricted by onlyOwner or onlyMinter modifiers. These include:

  • mint(address to, uint256 amount) – can only be called by an authorized minter.

  • setFeeCollector(address _feeCollector), setSwapTaxRate(uint256 rate), and setBurnTaxRate(uint256 rate) – restricted to the owner.

  • Additionally, functions such as setTaxRateIncrementLimit, manageWhitelist, and setMinter (which update critical configuration parameters) are also restricted by onlyOwner.

The intended design is for the RAACMinter contract to have ownership of the RAACToken so that it can perform minting and fee operations. However, if full ownership is transferred to RAACMinter, these additional configuration functions become inaccessible because RAACMinter does not implement any logic to call them. Consequently, no one can update key parameters such as the minter setting, whitelist management, or tax rate increment limits—resulting in a configuration lockout that could lead to a denial of service for future administrative adjustments. Although this might be considered a low issue by design, if not managed properly, it represents a critical vulnerability.

Vulnerability Details

How It Begins

  1. RAACToken Restricted Functions:

    The RAACToken contract contains several functions with access restrictions:

    function mint(address to, uint256 amount) external onlyMinter {
    if (to == address(0)) revert InvalidAddress();
    _mint(to, amount);
    }
    function setFeeCollector(address _feeCollector) external onlyOwner {
    if (feeCollector == address(0) && _feeCollector != address(0)) {
    emit FeeCollectionEnabled(_feeCollector);
    }
    if (_feeCollector == address(0)) {
    emit FeeCollectionDisabled();
    }
    feeCollector = _feeCollector;
    emit FeeCollectorSet(_feeCollector);
    }
    function setSwapTaxRate(uint256 rate) external onlyOwner {
    _setTaxRate(rate, true);
    }
    function setBurnTaxRate(uint256 rate) external onlyOwner {
    _setTaxRate(rate, false);
    }

    In addition, the following configuration functions are also restricted by onlyOwner:

    function setTaxRateIncrementLimit(uint256 limit) external onlyOwner {
    if (limit > BASE_INCREMENT_LIMIT) revert IncrementLimitExceedsBaseLimit();
    taxRateIncrementLimit = limit;
    emit TaxRateIncrementLimitUpdated(limit);
    }
    function manageWhitelist(address account, bool add) external onlyOwner {
    if (add) {
    if (account == address(0)) revert CannotWhitelistZeroAddress();
    if (whitelistAddress[account]) revert AddressAlreadyWhitelisted();
    emit AddressWhitelisted(account);
    } else {
    if (account == address(0)) revert CannotRemoveZeroAddressFromWhitelist();
    if (!whitelistAddress[account]) revert AddressNotWhitelisted();
    emit AddressRemovedFromWhitelist(account);
    }
    whitelistAddress[account] = add;
    }
    function setMinter(address _minter) external onlyOwner {
    if (_minter == address(0)) revert InvalidAddress();
    minter = _minter;
    emit MinterSet(_minter);
    }
  2. RAACMinter Function Calls:

    The RAACMinter contract calls some of these restricted functions:

    function mintRewards(address to, uint256 amount) external nonReentrant whenNotPaused {
    if (msg.sender != address(stabilityPool)) revert OnlyStabilityPool();
    uint256 toMint = excessTokens >= amount ? 0 : amount - excessTokens;
    excessTokens = excessTokens >= amount ? excessTokens - amount : 0;
    if (toMint > 0) {
    raacToken.mint(address(this), toMint);
    }
    raacToken.safeTransfer(to, amount);
    emit RAACMinted(amount);
    }
    function setFeeCollector(address _feeCollector) external onlyRole(UPDATER_ROLE) {
    if (_feeCollector == address(0)) revert FeeCollectorCannotBeZeroAddress();
    raacToken.setFeeCollector(_feeCollector);
    emit ParameterUpdated("feeCollector", uint256(uint160(_feeCollector)));
    }
    function setSwapTaxRate(uint256 _swapTaxRate) external onlyRole(UPDATER_ROLE) {
    if (_swapTaxRate > 1000) revert SwapTaxRateExceedsLimit();
    raacToken.setSwapTaxRate(_swapTaxRate);
    emit ParameterUpdated("swapTaxRate", _swapTaxRate);
    }
    function setBurnTaxRate(uint256 _burnTaxRate) external onlyRole(UPDATER_ROLE) {
    if (_burnTaxRate > 1000) revert BurnTaxRateExceedsLimit();
    raacToken.setBurnTaxRate(_burnTaxRate);
    emit ParameterUpdated("burnTaxRate", _burnTaxRate);
    }
  3. Ownership Transfer Issue:

    For RAACMinter to call mint and fee-related functions, the deployer must transfer the ownership of RAACToken to RAACMinter and set RAACMinter as the minter. However, after transferring full ownership, the following functions become inaccessible:

    • setTaxRateIncrementLimit

    • manageWhitelist

    • setMinter

    These functions are crucial for adjusting token configuration parameters. If they become locked out due to ownership transfer, the protocol loses its ability to update critical parameters, leading to a complete denial of service for administrative adjustments.

How the Issue Arises (Using Code Excerpts)

  • Ownership Transfer Flow:

    vm.startPrank(RAAC_OWNER);
    raacToken.setMinter(address(raacMinter));
    raacToken.transferOwnership(address(raacMinter));
    vm.stopPrank();
  • Configuration Function Call Attempts Post-Transfer:

    vm.startPrank(RAAC_OWNER);
    vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", address(RAAC_OWNER)));
    raacToken.setMinter(address(raacMinter));
    vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", address(RAAC_OWNER)));
    raacToken.manageWhitelist(ALICE, true);
    vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", address(RAAC_OWNER)));
    raacToken.setTaxRateIncrementLimit(900);
    vm.stopPrank();

    The above calls revert, confirming that after transferring ownership, the original owner (or any other account) cannot update critical configuration parameters.

Proof of Concept

Test Suite Walkthrough

The provided test suite includes two tests:

  • testRaacMinterInterfaceDosOnCallingRaacTokenFunctionDueToOwnershipMismatch:
    This test verifies that RAACMinter cannot call configuration functions because it is not recognized as the owner by RAACToken.

  • testRAACTokenFunctionNotAccessibleAfterOwnershipTransferToRAACMinter:
    This test shows that after transferring ownership of RAACToken to RAACMinter, attempts to call configuration functions revert, confirming the lockout.

Full Test Suite

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {RAACMinter} from "../src/core/minters/RAACMinter/RAACMinter.sol";
import {RAACToken} from "../src/core/tokens/RAACToken.sol";
contract RAACMinterTest is Test {
RAACToken raacToken;
RAACMinter raacMinter;
address RAAC_OWNER = makeAddr("RAAC_OWNER");
address RAAC_MINTER_OWNER = makeAddr("RAAC_MINTER_OWNER");
uint256 initialRaacSwapTaxRateInBps = 200; // 2%, 10000 - 100%
uint256 initialRaacBurnTaxRateInBps = 150; // 1.5%, 10000 - 100%
address ALICE = makeAddr("ALICE");
address BOB = makeAddr("BOB");
address CHARLIE = makeAddr("CHARLIE");
address DEVIL = makeAddr("DEVIL");
address stabilityPool = makeAddr("STABILITY_POOL");
address lendingPool = makeAddr("LENDING_POOL");
function setUp() public {
raacToken = new RAACToken(RAAC_OWNER, initialRaacSwapTaxRateInBps, initialRaacBurnTaxRateInBps);
vm.warp(block.timestamp + 1 days);
raacMinter = new RAACMinter(address(raacToken), stabilityPool, lendingPool, RAAC_MINTER_OWNER);
}
function testRaacMinterFaceDosOnCallingRaacTokenFunctionDueToOwnershipMismatch() public {
vm.startPrank(RAAC_MINTER_OWNER);
vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", address(raacMinter)));
raacMinter.setSwapTaxRate(initialRaacSwapTaxRateInBps);
vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", address(raacMinter)));
raacMinter.setBurnTaxRate(initialRaacBurnTaxRateInBps);
vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", address(raacMinter)));
raacMinter.setFeeCollector(address(1));
vm.stopPrank();
vm.startPrank(stabilityPool);
vm.expectRevert(bytes4(keccak256("OnlyMinterCanMint()")));
raacMinter.mintRewards(ALICE, 1e18);
vm.stopPrank();
}
function testRAACTokenFunctionNotAccessibleAfterOwnershipTransferToRAACMinter() public {
vm.startPrank(RAAC_OWNER);
raacToken.setMinter(address(raacMinter));
raacToken.transferOwnership(address(raacMinter));
vm.stopPrank();
vm.startPrank(RAAC_MINTER_OWNER);
raacMinter.setSwapTaxRate(initialRaacSwapTaxRateInBps);
raacMinter.setBurnTaxRate(initialRaacBurnTaxRateInBps);
raacMinter.setFeeCollector(address(1));
vm.stopPrank();
vm.startPrank(stabilityPool);
raacMinter.mintRewards(ALICE, 1e18);
vm.stopPrank();
vm.startPrank(RAAC_OWNER);
vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", address(RAAC_OWNER)));
raacToken.setMinter(address(raacMinter));
vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", address(RAAC_OWNER)));
raacToken.manageWhitelist(ALICE, true);
vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", address(RAAC_OWNER)));
raacToken.setTaxRateIncrementLimit(900);
vm.stopPrank();
}
}

How to Run the Test

  1. Create a Foundry Project:

    forge init my-foundry-project
  2. Place Contract Files:
    Ensure that RAACToken.sol, RAACMinter.sol, and related contracts are in the src directory.

  3. Create Test Directory:
    Create a test directory adjacent to src and add the test file (e.g., RAACMinterTest.t.sol).

  4. Run the Test:

    forge test --mt testRaacMinterFaceDosOnCallingRaacTokenFunctionDueToOwnershipMismatch -vv
    forge test --mt testRAACTokenFunctionNotAccessibleAfterOwnershipTransferToRAACMinter -vv
  5. Expected Outcome:

    • testRaacMinterFaceDosOnCallingRaacTokenFunctionDueToOwnershipMismatch: This test should indicate that RAACMinter isn't able to call (setSwapTaxRate, setBurnTaxRate, and mintRewards) because of ownership mismatch and requires RAACToken ownership transfer to RAACMinter contract.

    • testRAACTokenFunctionNotAccessibleAfterOwnershipTransferToRAACMinter: This test should indicate that after RAACToken ownership transfer and setting RAACMinter contract as a authorized raac tokens minter, the RAACMinter is able to call (setSwapTaxRate, setBurnTaxRate, and mintRewards), however a lockout happened to other configuration functions i.e., setMinter, manageWhitelist, and setTaxRateIncrementLimit and become inaccessible.

Impact

  • Critical Configuration Lockout:
    Once RAACToken’s ownership is transferred to RAACMinter, key administrative functions become inaccessible. This prevents any future updates to minter settings, whitelist management, and tax rate increment limits, effectively locking the token’s configuration.

  • Operational Rigidity:
    Inability to update configuration parameters means that the protocol cannot adapt to changes in market conditions or security requirements, potentially leading to long-term economic and operational issues.

  • Denial of Service for Administration:
    The complete lockout of administrative functions constitutes a denial of service for token configuration management, impairing the protocol’s maintainability and evolution.

  • Erosion of Trust:
    Stakeholders may lose confidence in the protocol if critical parameters cannot be updated, leading to decreased participation and potential migration to alternative platforms.

Tools Used

  • Manual Review

  • Foundry

Recommendations

To resolve this vulnerability, consider the following approaches:

1. Maintain a Dual-Authority Model

  • Separate Administrative Role:
    Instead of transferring full ownership of RAACToken to RAACMinter, implement role-based access control (using OpenZeppelin's AccessControl). Assign RAACMinter the minter role while retaining an independent administrative role for configuration functions:

    // Example: Using AccessControl in RAACToken
    bytes32 public constant CONFIG_ADMIN_ROLE = keccak256("CONFIG_ADMIN_ROLE");
    // Then, protect configuration functions with:
    function setTaxRateIncrementLimit(uint256 limit) external onlyRole(CONFIG_ADMIN_ROLE) { ... }
  • Benefits:

    • RAACMinter can mint tokens and manage fee-related operations.

    • Critical configuration functions remain accessible to the designated admin, preserving protocol flexibility.

2. Deploy RAACToken Within the RAACMinter Contract

  • Internal Deployment:
    Modify the architecture so that RAACMinter deploys RAACToken internally. This ensures that RAACMinter is the owner from inception while still allowing a separate administrative role (or proxy) for configuration updates.

3. Update Deployment Procedures and Documentation

  • Automated Ownership Transfer with Proxy Admin:
    If full ownership transfer is chosen, update deployment scripts to include a mechanism (e.g., a multisig or proxy) that retains administrative control over configuration functions even after ownership is transferred to RAACMinter.

  • Documentation:
    Update the NatSpec and developer documentation to clearly outline the ownership model, the separation of minting and configuration roles, and the necessary steps to retain administrative access.

By implementing these recommendations, the protocol can ensure that RAACMinter is empowered to mint RAAC tokens while critical configuration functions remain accessible for future updates. This balanced approach will prevent a complete administrative lockout, maintain operational flexibility, and preserve stakeholder trust and protocol stability.

Updates

Lead Judging Commences

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

Give us feedback!