Core Contracts

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

Incorrect token transfer recipient in emergencyRevoke leads to token loss

Summary

The emergencyRevoke() function in RAACReleaseOrchestrator incorrectly transfers unreleased tokens to the contract itself instead of the beneficiary, resulting in permanent token loss.

Vulnerability Details

In the emergencyRevoke function, when revoking a vesting schedule, any unreleased tokens are meant to be returned to the beneficiary. However, the function incorrectly uses address(this) as the transfer recipient instead of the beneficiary address:

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); // Incorrect recipient
emit EmergencyWithdraw(beneficiary, unreleasedAmount);
}
emit VestingScheduleRevoked(beneficiary);
}

Proof of Concept

  1. A vesting schedule is created for Alice with 1000 RAAC tokens

  2. After 100 days, Alice has claimed 200 tokens, leaving 800 unreleased

  3. Emergency role calls emergencyRevoke for Alice's address

  4. Instead of sending the 800 tokens to Alice, they are transferred to the contract itself

  5. The tokens become permanently locked in the contract as there is no function to withdraw them

Impact

  • Beneficiaries lose their unreleased tokens when their vesting schedule is revoked

  • Tokens become permanently locked in the contract

  • The emergency revoke feature fails its intended purpose of returning tokens to beneficiaries

Recommendations

Option 1: Fix transfer recipient

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);
+ raacToken.transfer(beneficiary, unreleasedAmount);
emit EmergencyWithdraw(beneficiary, unreleasedAmount);
}
emit VestingScheduleRevoked(beneficiary);
}

Option 2: Remove emergency revoke

Consider removing the emergency revoke functionality entirely if it's not strictly necessary, as it introduces additional risk and complexity. The vesting schedule could be made immutable after creation.

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!