Summary
The RAACReleaseOrchestrator#createVestingSchedule()
allows a beneficiary to set startTime
in the past, bypassing the VESTING_CLIFF
check. This enables the immediate release of all vested tokens instead of waiting for the cliff and linear vesting schedule.
Vulnerability Details
There's no validation for startTime. If the function does not enforce startTime >= block.timestamp
, allowing an attacker to set startTime
far in the past.
This skips the vesting cliff and allows instant release of tokens.
function createVestingSchedule(
address beneficiary,
bytes32 category,
uint256 amount,
uint256 startTime
) external onlyRole(ORCHESTRATOR_ROLE) whenNotPaused {
if (beneficiary == address(0)) revert InvalidAddress();
if (amount == 0) revert InvalidAmount();
if (vestingSchedules[beneficiary].initialized) revert VestingAlreadyInitialized();
if (categoryAllocations[category] == 0) revert InvalidCategory();
uint256 newCategoryTotal = categoryUsed[category] + amount;
if (newCategoryTotal > categoryAllocations[category]) revert CategoryAllocationExceeded();
categoryUsed[category] = newCategoryTotal;
VestingSchedule storage schedule = vestingSchedules[beneficiary];
schedule.totalAmount = amount;
@> schedule.startTime = startTime;
schedule.duration = VESTING_DURATION;
schedule.initialized = true;
emit VestingScheduleCreated(beneficiary, category, amount, startTime);
}
Impact
No waiting period; funds are released instantly by beneficiary.
If exploited, an attacker can withdraw all of tokens immediately without duration periods.
Tools Used
manual
Recommendations
Modify createVestingSchedule()
to reject past timestamps:
function createVestingSchedule(
address beneficiary,
bytes32 category,
uint256 amount,
uint256 startTime
) external onlyRole(ORCHESTRATOR_ROLE) whenNotPaused {
...
if (categoryAllocations[category] == 0) revert InvalidCategory();
+ if (startTime < block.timestamp) revert InvalidStartTime();
...
}