Core Contracts

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

Self-Transfer in EmergencyRevoke Causes Unintended Token Loss via Tax

Relevant GitHub Links

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

https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/tokens/RAACToken.sol#L198-L199

Summary

RAACReleaseOrchestrator's emergencyRevoke function transfers tokens to itself, triggering tax mechanism and causing unrecoverable token loss.

Vulnerability Details

// RAACReleaseOrchestrator.sol
function emergencyRevoke(address beneficiary) external onlyRole(EMERGENCY_ROLE) {
// ...
if (unreleasedAmount > 0) {
raacToken.transfer(address(this), unreleasedAmount); // @audit triggers tax
emit EmergencyWithdraw(beneficiary, unreleasedAmount);
}
}
// RAACToken.sol
function _update(address from, address to, uint256 amount) internal virtual override {
uint256 baseTax = swapTaxRate + burnTaxRate;
// Skip tax only if:
if (baseTax == 0 || from == address(0) || to == address(0) ||
whitelistAddress[from] || whitelistAddress[to] ||
feeCollector == address(0)) {
super._update(from, to, amount);
return;
}
// Otherwise apply tax
uint256 totalTax = amount.percentMul(baseTax);
uint256 burnAmount = totalTax * burnTaxRate / baseTax;
// Send to feeCollector and burn
super._update(from, feeCollector, totalTax - burnAmount);
super._update(from, address(0), burnAmount);
super._update(from, to, amount - totalTax);
}

Impact

  • Permanent loss of tokens via tax and burn mechanisms

  • Reduces recoverable amount in emergency situations

Tools Used

Manual Review

Recommendations

Remove the redundant transfer since tokens are already owned by contract:

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) {
emit EmergencyWithdraw(beneficiary, unreleasedAmount);
}
emit VestingScheduleRevoked(beneficiary);
}
Updates

Lead Judging Commences

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

Give us feedback!