Core Contracts

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

Irrecoverable Tokens After Emergency Revocation in RAACReleaseOrchestrator

Overview

The RAACReleaseOrchestrator contract is designed to manage vesting schedules for token distributions. In its emergency revocation flow, the contract “withdraws” unreleased tokens from a beneficiary’s vesting schedule via the emergencyRevoke function. Specifically, when an emergency revocation is triggered, the unreleased tokens are transferred to the contract itself. The event name EmergencyWithdraw and the transfer call clearly indicate that these tokens are expected to be recovered for future use or reallocation. However, the contract does not implement any function that allows an administrator (or an authorized party) to withdraw these tokens from its balance. Consequently, tokens recovered through emergency revocation become permanently locked within the contract.

Root Cause & Impact

Root Cause:
Within the emergencyRevoke function, unreleased tokens from a beneficiary’s vesting schedule are transferred to the RAACReleaseOrchestrator contract’s address via:

if (unreleasedAmount > 0) {
raacToken.transfer(address(this), unreleasedAmount);
emit EmergencyWithdraw(beneficiary, unreleasedAmount);
}

This design clearly implies that the tokens are “withdrawn” from the vesting schedule and should later be available for recovery or reallocation. However, the contract does not offer any mechanism to withdraw or “rescue” these tokens from its own balance.

Impact:

  • Locked Funds: Tokens transferred to the contract via emergency revocation remain permanently locked. This contradicts the likely intent behind the EmergencyWithdraw event and the withdrawal logic, where the admin should later be able to recover and reassign these tokens.

  • Misallocation Risk: Should an emergency event occur, the inability to reclaim these tokens may result in capital inefficiencies or even unintentional token supply reductions that could affect downstream mechanisms.

  • Operational Overhead: The absence of a withdrawal function means that, if such a situation arises, the team cannot recover funds for treasury re-allocation or further distribution without a contract upgrade, which is both disruptive and costly.

Attack / Issue Path

  1. Emergency Revocation Triggered:
    An authorized emergency call is made to revoke a beneficiary’s vesting schedule. The unreleased tokens are calculated and transferred from the vesting allocation into the RAACReleaseOrchestrator’s balance.

  2. Funds Locked in Contract:
    As designed, these tokens are no longer held against any beneficiary’s allocation—they now reside within the contract. However, because no withdrawal or rescue function exists, these tokens are effectively “stuck.”

  3. Long-Term Consequences:
    In a scenario where multiple emergency revocations occur, significant token amounts may accumulate in the contract. Without a recovery mechanism, these tokens remain inaccessible for any future reallocation, treasury management, or redistribution, contrary to the system’s design intent.

Foundry PoC Demonstration

The following Foundry PoC demonstrates the issue clearly. It sets up a vesting schedule, triggers an emergency revocation, and then verifies that the unreleased tokens have been locked in the contract—with no subsequent recovery possible.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "openzeppelin-contracts/token/ERC20/ERC20.sol";
import "../../../contracts/core/minters/RAACReleaseOrchestrator/RAACReleaseOrchestrator.sol";
// A minimal ERC20 token to simulate RAAC functionality.
contract TestToken is ERC20 {
constructor() ERC20("TestToken", "TT") {
_mint(msg.sender, 1_000_000e18);
}
// Minimal implementations to satisfy the RAAC token interface.
function mint(address, uint256) external {}
function setSwapTaxRate(uint256) external {}
function setBurnTaxRate(uint256) external {}
function setFeeCollector(address) external {}
}
contract RAACReleaseOrchestratorLockedFundsTest is Test {
TestToken token;
RAACReleaseOrchestrator orchestrator;
address admin = address(1);
address beneficiary = address(0xBEEF);
function setUp() public {
vm.startPrank(admin);
token = new TestToken();
orchestrator = new RAACReleaseOrchestrator(address(token));
vm.stopPrank();
// Create a vesting schedule for the beneficiary with a start time sufficiently in the past.
uint256 pastStartTime = block.timestamp - 100 days;
vm.prank(admin);
orchestrator.createVestingSchedule(
beneficiary,
orchestrator.TEAM_CATEGORY(),
1_000e18,
pastStartTime
);
}
function testLockedFundsAfterEmergencyRevoke() public {
// Assume the full vesting amount is unreleased.
uint256 unreleasedBefore = 1_000e18;
// Perform an emergency revocation.
vm.prank(admin);
orchestrator.emergencyRevoke(beneficiary);
// Unreleased tokens are now transferred to the orchestrator contract.
uint256 lockedBalance = token.balanceOf(address(orchestrator));
assertEq(lockedBalance, unreleasedBefore, "Unreleased tokens should be locked in the contract");
// There is no function to recover these tokens; they remain permanently locked.
}
}

Recommended Mitigation

Mitigation:
Add a withdrawal (or rescue) function to the RAACReleaseOrchestrator contract. This function should allow an authorized role (e.g., DEFAULT_ADMIN_ROLE) to recover any ERC20 tokens held by the contract. A concise implementation would be:

function rescueTokens(address tokenAddress, uint256 amount) external onlyRole(DEFAULT_ADMIN_ROLE) {
IERC20(tokenAddress).safeTransfer(msg.sender, amount);
emit TokensRescued(tokenAddress, msg.sender, amount);
}

Benefits:

  • Recoverability: Enables the recovery of tokens inadvertently locked in the contract following an emergency revocation.

  • Operational Flexibility: Allows the project to reallocate or redistribute recovered tokens as necessary, preserving capital efficiency.

  • Alignment with Design Intent: Matches the implied design of the EmergencyWithdraw flow, ensuring that recovered tokens are not permanently stranded.

Updates

Lead Judging Commences

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

RAACReleaseOrchestrator::emergencyRevoke sends revoked tokens to contract address with no withdrawal mechanism, permanently locking funds

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

RAACReleaseOrchestrator::emergencyRevoke sends revoked tokens to contract address with no withdrawal mechanism, permanently locking funds

Support

FAQs

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

Give us feedback!