Core Contracts

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

Multiple Vesting Schedules Can’t Be Created for Beneficiaries in Different Categories

Summary

The RAACReleaseOrchestrator contract restricts each beneficiary to a single vesting schedule, meaning that users who participate in both the Private Sale and Public Sale cannot create separate schedules for each category. As a result, the funds invested in the second sale (if a vesting schedule has already been created for the first sale) cannot be vested, potentially causing users to lose their invested funds. This limitation is enforced by the contract's current logic that prevents the creation of multiple schedules per beneficiary.

Vulnerability Details

The RAACReleaseOrchestrator contract contains a mechanism that prevents the creation of more than one vesting schedule per beneficiary. This is enforced in the createVestingSchedule function with the following check:

if (vestingSchedules[beneficiary].initialized) revert VestingAlreadyInitialized();

This results in the contract not allowing a beneficiary to create multiple vesting schedules across different categories (e.g., Private Sale and Public Sale). As a result, if a user participates in both sales, they will be able to create only one vesting schedule, and any subsequent investment in the other sale will not be properly vested.

Impact

  • Permanent Loss of Funds: A user who participates in both the Private Sale and Public Sale will only have a vesting schedule created for one of the categories. Their funds in the second sale will not be vested, and the user will lose access to those tokens.

  • Violation of Expected Functionality: The contract was likely intended to allow multiple vesting schedules for a beneficiary to handle various categories, but the current design fails to support this use case. This could confuse users and lead to a poor user experience.

Tools Used

Manual Code Review

Recommendations

1. Updating the vestingSchedules variable:

Instead of using:

mapping(address => VestingSchedule) public vestingSchedules;

We will use:

mapping(address => mapping(bytes32 => VestingSchedule)) public vestingSchedules;

2. Updating the createVestingSchedule function:

We need to modify the check for the existing schedule and change how we assign the vesting schedule:

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][category].initialized) revert VestingAlreadyInitialized();
if (categoryAllocations[category] == 0) revert InvalidCategory();
// Check category allocation limits
uint256 newCategoryTotal = categoryUsed[category] + amount;
if (newCategoryTotal > categoryAllocations[category]) revert CategoryAllocationExceeded();
categoryUsed[category] = newCategoryTotal;
VestingSchedule storage schedule = vestingSchedules[beneficiary][category];
schedule.totalAmount = amount;
schedule.startTime = startTime;
schedule.duration = VESTING_DURATION;
schedule.initialized = true;
emit VestingScheduleCreated(beneficiary, category, amount, startTime);
}

3. Updating the release function:

The release function needs to handle the case where a beneficiary has multiple vesting schedules, one for each category. So, it needs to check the schedules for the caller across all categories.

function release() external nonReentrant whenNotPaused {
address beneficiary = msg.sender;
uint256 totalReleasableAmount = 0;
// Iterate over all possible categories and calculate the releasable amount
for (bytes32 category = TEAM_CATEGORY; category <= LIQUIDITY_CATEGORY; category = bytes32(uint256(category) + 1)) {
VestingSchedule storage schedule = vestingSchedules[beneficiary][category];
if (schedule.initialized) {
uint256 releasableAmount = _calculateReleasableAmount(schedule);
totalReleasableAmount += releasableAmount;
schedule.releasedAmount += releasableAmount;
schedule.lastClaimTime = block.timestamp;
}
}
if (totalReleasableAmount == 0) revert NothingToRelease();
raacToken.transfer(beneficiary, totalReleasableAmount);
emit TokensReleased(beneficiary, totalReleasableAmount);
}

4. Updating the getVestingSchedule function:

This function will need to return the schedule for a specific category, so we will add a parameter to specify which category's vesting schedule the user wants to retrieve.

function getVestingSchedule(address beneficiary, bytes32 category) external view returns (VestingSchedule memory schedule) {
return vestingSchedules[beneficiary][category];
}

5. Updating the emergencyRevoke function:

We need to make sure that when we revoke a vesting schedule, we specify the category. This will allow us to revoke a specific schedule and ensure that multiple schedules across different categories can be managed.

function emergencyRevoke(address beneficiary, bytes32 category) external onlyRole(EMERGENCY_ROLE) {
VestingSchedule storage schedule = vestingSchedules[beneficiary][category];
if (!schedule.initialized) revert NoVestingSchedule();
uint256 unreleasedAmount = schedule.totalAmount - schedule.releasedAmount;
delete vestingSchedules[beneficiary][category];
if (unreleasedAmount > 0) {
raacToken.transfer(address(this), unreleasedAmount);
emit EmergencyWithdraw(beneficiary, unreleasedAmount);
}
emit VestingScheduleRevoked(beneficiary);
}

6. Updating the getCategoryDetails function:

We'll need to update this function to return details for a specific category and user:

function getCategoryDetails(address beneficiary, bytes32 category) external view returns (uint256 allocation, uint256 used) {
return (categoryAllocations[category], categoryUsed[category]);
}

The major change here is that we now use a nested mapping (mapping(address => mapping(bytes32 => VestingSchedule))), allowing each beneficiary to have multiple schedules, one per category. This modification will also require corresponding changes to other functions that interact with the vesting schedules, as shown above.

By doing this, a beneficiary can participate in both Private Sale and Public Sale and have distinct vesting schedules for each, ensuring their tokens are properly vested and released according to the terms of each sale.

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

RAACReleaseOrchestrator restricts beneficiaries to a single vesting schedule across all categories, causing funds from secondary investments to be permanently lost

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

RAACReleaseOrchestrator restricts beneficiaries to a single vesting schedule across all categories, causing funds from secondary investments to be permanently lost

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.