Summary
The RAACReleaseOrchestrator::createVestingSchedule function does not validate whether the startTime is greater than the current timestamp (block.timestamp). If a vesting schedule is created with a past start time, it will lead to incorrect calculations when determining the releasable amount, potentially allowing premature or excessive token releases.
Vulnerability Details
The function allows setting startTime to any value, including timestamps in the past. This can result in incorrect calculations when determining how many tokens should be released, as the RAACReleaseOrchestrator::_calculateReleasableAmount function assumes that startTime is always in the future or the present.
Affected Code in RAACReleaseOrchestrator::createVestingSchedule
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);
}
The issue might lead to incorrect releasable amount calculation
The RAACReleaseOrchestrator::_calculateReleasableAmount function assumes startTime is valid. However, if startTime is in the past, the function may incorrectly calculate timeFromStart, leading to an unintended immediate full release of the vesting schedule.
function _calculateReleasableAmount(
VestingSchedule memory schedule
) internal view returns (uint256) {
if (block.timestamp < schedule.startTime + VESTING_CLIFF) return 0;
if (block.timestamp < schedule.lastClaimTime + MIN_RELEASE_INTERVAL) return 0;
uint256 timeFromStart = block.timestamp - schedule.startTime;
if (timeFromStart >= schedule.duration) {
return schedule.totalAmount - schedule.releasedAmount;
}
uint256 vestedAmount = (schedule.totalAmount * timeFromStart) / schedule.duration;
return vestedAmount - schedule.releasedAmount;
}
Steps to Reproduce
Accidentally call createVestingSchedule with startTime set to a past timestamp.
Accidentally call _calculateReleasableAmount and observe that the function treats the schedule as fully vested, allowing immediate full release.
Beneficiary can withdraw the entire vested amount earlier than expected.
Impact
Immediate Full Vesting: Setting startTime in the past could cause the entire vesting schedule to be considered fully vested immediately, bypassing intended vesting restrictions.
Unexpected Fund Unlocking: Funds could be released earlier than designed, which could lead to liquidity issues or manipulation of vesting mechanics.
Tools Used
Manual Review
Recommendations
Modify createVestingSchedule to ensure that startTime is greater than or equal to block.timestamp
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();
+ if (startTime < block.timestamp) revert InvalidStartTime(); // @audit-fix Ensure valid start time
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);
}