Summary
The RAACReleaseOrchestrator
contract fails to properly manage category allocation accounting when emergency revoking a vesting schedule. When a vesting schedule is revoked, the categoryUsed
amount remains unchanged, effectively reducing the available allocation for future vesting schedules in that category.
Vulnerability Details
[](https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/minters/RAACReleaseOrchestrator/RAACReleaseOrchestrator.sol#L126-L131)
[](https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/minters/RAACReleaseOrchestrator/RAACReleaseOrchestrator.sol#L28-L29)
In the emergencyRevoke function, the contract deletes the vesting schedule but does not update the categoryUsed mapping to reflect this change. This creates a discrepancy between the actual tokens allocated and the tracked amounts in categoryUsed.
The issue manifests in these steps:
A vesting schedule is created, incrementing categoryUsed
The schedule is emergency revoked via emergencyRevoke
The vestingSchedules
mapping entry is deleted
The categoryUsed
amount remains unchanged
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);
}
emit VestingScheduleRevoked(beneficiary);
}
Impact
Categories permanently lose allocatable capacity after revokes
Contract cannot utilize its full intended allocation for each category
May prevent new legitimate vesting schedules from being created
Tools Used
Manual code review
Recommendations
Store the category information in the VestingSchedule struct:
struct VestingSchedule {
uint256 totalAmount;
uint256 releasedAmount;
uint256 startTime;
uint256 duration;
uint256 lastClaimTime;
bool initialized;
+ bytes32 category; // Add category field
}
2. Store the category when creating a vesting schedule:
```diff
function createVestingSchedule(
address beneficiary,
bytes32 category,
uint256 amount,
uint256 startTime // @audit - make sure startTime is in future
) external onlyRole(ORCHESTRATOR_ROLE) whenNotPaused {
...
VestingSchedule storage schedule = vestingSchedules[beneficiary];
schedule.totalAmount = amount;
schedule.startTime = startTime;
schedule.duration = VESTING_DURATION;
schedule.initialized = true;
+ schedule.category = category;
...
Update the emergencyRevoke
function to decrement categoryUsed:
function emergencyRevoke(address beneficiary) external onlyRole(EMERGENCY_ROLE) {
VestingSchedule storage schedule = vestingSchedules[beneficiary];
if (!schedule.initialized) revert NoVestingSchedule();
uint256 unreleasedAmount = schedule.totalAmount - schedule.releasedAmount;
bytes32 category = schedule.category;
+ // Decrement category used
+ categoryUsed[category] -= schedule.totalAmount;
delete vestingSchedules[beneficiary];
emit VestingScheduleRevoked(beneficiary);
emit EmergencyWithdraw(beneficiary, unreleasedAmount);
}