Core Contracts

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

Revoked Vesting Tokens Permanently Locked in RAACReleaseOrchestrator

Link to Affected Code:

https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/minters/RAACReleaseOrchestrator/RAACReleaseOrchestrator.sol#L126-L139

function emergencyRevoke(address beneficiary) external onlyRole(EMERGENCY_ROLE) {
VestingSchedule storage schedule = vestingSchedules[beneficiary];
if (!schedule.initialized) revert NoVestingSchedule();
uint256 unreleasedAmount = schedule.totalAmount - schedule.releasedAmount;
delete vestingSchedules[beneficiary];
if (unreleasedAmount > 0) {
raacToken.transfer(address(this), unreleasedAmount); // @audit transfers to self
emit EmergencyWithdraw(beneficiary, unreleasedAmount);
}
emit VestingScheduleRevoked(beneficiary);
}

Description:
The emergencyRevoke function in RAACReleaseOrchestrator contains a critical error where revoked tokens are transferred to the contract itself (address(this)). The contract lacks any function to withdraw or recover these tokens, resulting in them being permanently locked. This affects all categories including TEAM, ADVISOR, TREASURY, and SALE allocations which total 65.1% of token supply.

Impact: The vulnerability causes:

  1. Permanent loss of revoked tokens with no recovery mechanism

  2. Each emergency revocation increases locked tokens

Proof of Concept:

// 1. Create vesting schedule
createVestingSchedule(
beneficiary,
TEAM_CATEGORY,
1_000_000 ether, // 1M tokens
block.timestamp
);
// 2. Emergency revoke after 100 days
// Assuming 200k tokens already released
emergencyRevoke(beneficiary);
// Results in:
unreleasedAmount = 1_000_000 - 200_000 = 800_000 tokens
raacToken.transfer(address(this), 800_000); // Tokens sent to contract
vestingSchedules[beneficiary] deleted // Record removed
// 800k tokens now permanently locked
// 3. No way to recover:
- No withdrawal functions
- No admin recovery methods
- Even EMERGENCY_ROLE cannot access

Recommended Mitigation:

  1. Add recovery function:

function recoverLockedTokens(
address to
) external onlyRole(DEFAULT_ADMIN_ROLE) {
uint256 balance = raacToken.balanceOf(address(this));
raacToken.transfer(to, balance);
emit TokensRecovered(to, balance);
}
  1. Or modify emergencyRevoke to transfer to a recovery address:

function emergencyRevoke(
address beneficiary,
address recoveryAddress
) external onlyRole(EMERGENCY_ROLE) {
require(recoveryAddress != address(0), "Invalid recovery address");
// ... existing code ...
if (unreleasedAmount > 0) {
raacToken.transfer(recoveryAddress, unreleasedAmount);
emit EmergencyWithdraw(beneficiary, unreleasedAmount);
}
}
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!