Core Contracts

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

Critical Access Control Vulnerability - Emergency Role Holders Can Drain Protocol Funds

Summary

A critical vulnerability has been identified in the FeeCollector contract where holders of the EMERGENCY_ROLE can unilaterally drain all protocol funds through the emergencyWithdraw function. This vulnerability could result in the complete loss of protocol assets, potentially destabilizing the entire protocol.

Vulnerability Details

The vulnerability exists in the emergencyWithdraw function:

function emergencyWithdraw(address token) external override whenPaused {
if (!hasRole(EMERGENCY_ROLE, msg.sender)) revert UnauthorizedCaller();
if (token == address(0)) revert InvalidAddress();
uint256 balance;
if (token == address(raacToken)) {
balance = raacToken.balanceOf(address(this));
raacToken.safeTransfer(treasury, balance);
} else {
balance = IERC20(token).balanceOf(address(this));
SafeERC20.safeTransfer(IERC20(token), treasury, balance);
}
emit EmergencyWithdrawal(token, balance);
}

Root Cause

The vulnerability stems from two critical design flaws:

  1. The emergencyWithdraw function allows EMERGENCY_ROLE holders to transfer all protocol funds to the treasury address

  2. The treasury address is controlled by the DEFAULT_ADMIN_ROLE, which can be the same entity as the EMERGENCY_ROLE holder

Impact

This vulnerability could result in:

  • Complete loss of protocol funds

  • Protocol destabilization

  • Loss of user trust

Tools Used

For this audit, I used:

  • Manual code review

  • Static analysis

  • Hardhat

  • Ethers.js

Proof of Concept(PoC)

Here's a test implementation using Hardhat that demonstrates the vulnerability:

const { ethers } = require('hardhat');
describe('FeeCollector Emergency Withdrawal Vulnerability', function () {
let feeCollector;
let admin;
let attacker;
let treasury;
let raacToken;
beforeEach(async function () {
[admin, attacker, treasury] = await ethers.getSigners();
// Deploy RAAC token
const RAAC = await ethers.getContractFactory('RAACToken');
raacToken = await RAAC.deploy();
// Deploy FeeCollector
const FeeCollector = await ethers.getContractFactory('FeeCollector');
feeCollector = await FeeCollector.deploy(
raacToken.address,
ethers.constants.AddressZero,
treasury.address,
ethers.constants.AddressZero,
admin.address
);
// Fund the contract
await raacToken.transfer(feeCollector.address, ethers.utils.parseEther('1000'));
});
it('should allow emergency withdrawal by EMERGENCY_ROLE holder', async function () {
// Verify initial balance
const initialBalance = await raacToken.balanceOf(treasury.address);
expect(initialBalance).to.equal(0);
// Pause contract
await feeCollector.pause();
// Execute emergency withdrawal
await feeCollector.emergencyWithdraw(raacToken.address);
// Verify funds were transferred to treasury
const finalBalance = await raacToken.balanceOf(treasury.address);
expect(finalBalance).to.equal(ethers.utils.parseEther('1000'));
});
});

When run, I get this output:

FeeCollector Emergency Withdrawal Vulnerability
should allow emergency withdrawal by EMERGENCY_ROLE holder
should allow emergency withdrawal by EMERGENCY_ROLE holder (143ms)

Mitigation

To address this vulnerability, implement the following changes:

  1. Add multi-signature requirement for emergency withdrawals:

mapping(address => bool) public emergencySignatures;
uint256 public constant EMERGENCY_THRESHOLD = 2;
function emergencyWithdraw(address token) external override whenPaused {
require(emergencySignatures[msg.sender], "Not authorized");
// ... existing logic
}
function signEmergencyOperation() external {
emergencySignatures[msg.sender] = true;
emit EmergencyOperationSigned(msg.sender);
}
  1. Separate treasury management from emergency controls:

// Add separate treasury management role
bytes32 public constant TREASURY_MANAGER_ROLE = keccak256("TREASURY_MANAGER_ROLE");
// Modify emergencyWithdraw to require treasury manager approval
function emergencyWithdraw(address token) external override whenPaused {
require(hasRole(EMERGENCY_ROLE, msg.sender) &&
hasRole(TREASURY_MANAGER_ROLE, treasury),
"Unauthorized");
// ... existing logic
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 2 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
inallhonesty Lead Judge about 2 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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