Core Contracts

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

Release orchestrator can create vesting schedules that bypass vesting cliff

Summary

The RAACReleaseOrchestrator contract comes with a createVestingSchedule() function that allows for creating vesting schedules. The creation of such schedules performs no validity checks on the vesting schedule's startTime, allowing for creating vesting schedules that started in the past, which can effectively bypass the vesting cliff of 90 days.

Vulnerability Details

Taking a look at RAACReleaseOrchestrator#createVestingSchedule, we see that it expects a startTime for the vesting schedule to be created:

function createVestingSchedule(
address beneficiary,
bytes32 category,
uint256 amount,
uint256 startTime // <--
) external onlyRole(ORCHESTRATOR_ROLE) whenNotPaused {
...
}

This startTime is set to determine when the vesting schedule has started, or will start. Later, when accounts try to release their RAACToken from the allocation, this value is used to check whether or not the vesting cliff of 90 days has passed.

Because there's no check on the startTime's value, the account that creates the vesting schedule can easily create schedules that "started" far enough in the past, such that the vest cliff has already elapsed, allowing the beneficiary to unlock their funds earlier than planned.

Impact

Beneficiaries essentially can access to their vested funds earlier than planned, which also allows them to dump their allocations on the market sooner than expected. Given that the idea of vesting schedules is that we want to prevent users from releasing funds to the market too soon, this is a potential risk that can unfairly devalue the allocation of other beneficiaries.

Tools Used

Manual review.

Recommendations

At the very least, require that startTime has to be block.timestamp or greater, so that no vesting schedule can be created "retroactively".
One could also protect against start times that live way too far in the future:

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 || startTime >= block.timestamp + MAX_START_TIME_DELAY) revert InvalidStartTime();
// Check category allocation limits
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);
}

Relevant links

Updates

Lead Judging Commences

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