Summary
The RAACReleaseOrchestrator contract fails to properly handle vesting schedule revocations that occur before the start time, leading to inflated category allocations and unnecessary token transfers.
Vulnerability Details
In the emergencyRevoke function:
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);
emit EmergencyWithdraw(beneficiary, unreleasedAmount);
}
}
The Issue:
When a vesting schedule is created, it increases categoryUsed[category]
When emergency revoking before vesting starts, the categoryUsed amount is not decreased
This means the category allocation remains inflated
Future legitimate vestings might be rejected due to CategoryAllocationExceeded()
Example Attack Scenario:
categoryAllocations[TEAM_CATEGORY] = 18_000_000 ether;
categoryUsed[TEAM_CATEGORY] = 0;
createVestingSchedule(alice, TEAM_CATEGORY, 10_000_000 ether, futureTime);
emergencyRevoke(alice);
createVestingSchedule(bob, TEAM_CATEGORY, 10_000_000 ether, futureTime);
Impact
Category allocations remain inflated after pre-start revocations
Future legitimate vestings might be rejected due to incorrect allocation tracking
Could prevent legitimate users from receiving their vesting allocations
Affects core token distribution mechanism
Tools Used
Recommendations
Implement proper pre-start revocation handling:
address public recoveryAddress;
function emergencyRevoke(address beneficiary) external onlyRole(EMERGENCY_ROLE) {
VestingSchedule storage schedule = vestingSchedules[beneficiary];
if (!schedule.initialized) revert NoVestingSchedule();
uint256 unreleasedAmount;
bytes32 category = ;
if (block.timestamp < schedule.startTime) {
unreleasedAmount = schedule.totalAmount;
categoryUsed[category] -= schedule.totalAmount;
} else {
unreleasedAmount = schedule.totalAmount - schedule.releasedAmount;
}
delete vestingSchedules[beneficiary];
if (unreleasedAmount > 0) {
raacToken.safeTransfer(recoveryAddress, unreleasedAmount);
emit EmergencyWithdraw(beneficiary, unreleasedAmount, recoveryAddress);
}
emit VestingScheduleRevoked(beneficiary, block.timestamp < schedule.startTime, category);
}