Core Contracts

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

Missing Start Time Validation in createVestingSchedule Allows Immediate Vesting

Summary

The RAACReleaseOrchestrator contract manages vesting schedules for RAAC tokens, allowing beneficiaries to receive tokens over time via a release function. However, the createVestingSchedule function fails to validate its startTime parameter. Without a check to ensure that startTime is in the future (or at least not in the past), an orchestrator can mistakenly (or maliciously) set a vesting schedule with a past start time. As a result, beneficiaries can immediately claim their entire vesting allocation by calling the release function—bypassing any intended vesting cliff or delay. This vulnerability can lead to a severe denial of service regarding the vesting mechanism, undermining the protocol’s intended token distribution schedule.

Vulnerability Details

Function Behavior

The createVestingSchedule function is defined as follows:

/**
* @notice Creates a vesting schedule for a beneficiary
* @param beneficiary Address of the beneficiary
* @param category Category of the vesting schedule
* @param amount Amount of tokens to vest
* @param startTime Start time of vesting
*/
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();
// @info: missing startTime validation check
// @danger: startTime could be a past timestamp which would cause the beneficiary to immediately have vesting power,
// bypassing the vesting cliff and intended vesting duration.
// 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);
}

Key Issue:

  • Missing Start Time Check:
    The function does not verify that the provided startTime is valid—that is, it does not ensure that startTime is not in the past relative to the current block timestamp. As a consequence, an orchestrator could set startTime to a value that has already passed, causing the entire vesting schedule to effectively become immediately active.

How It Leads to Abuse

  • Immediate Vesting:
    If the vesting schedule's startTime is set in the past, beneficiaries can call the release function immediately after schedule creation and claim all tokens meant to vest over time.

  • Bypassing Vesting Cliff:
    Even if the protocol includes a vesting cliff elsewhere, a past startTime could negate its effect, allowing beneficiaries to bypass the intended delay and gain full access to vested tokens prematurely.

Proof of Concept

Scenario Walkthrough

  1. Setup:
    An orchestrator creates a vesting schedule for beneficiary ALICE using a startTime that is set to block.timestamp - VESTING_DURATION, effectively putting the vesting schedule entirely in the past.

  2. Immediate Release:
    Since the vesting schedule is immediately active, when ALICE calls release(), the _calculateReleasableAmount function calculates that all tokens are available for release, and ALICE receives the full allocation at once.

Test Suite PoC

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {RAACToken} from "../src/core/tokens/RAACToken.sol";
import {RAACReleaseOrchestrator} from "../src/core/minters/RAACReleaseOrchestrator/RAACReleaseOrchestrator.sol";
contract RAACReleaseOrchestratorTest is Test {
RAACToken raacToken;
RAACReleaseOrchestrator raacReleaseOrchestrator;
address RAAC_OWNER = makeAddr("RAAC_OWNER");
address RAAC_MINTER = makeAddr("RAAC_MINTER");
address RAAC_ORCHESTRATOR_OWNER = makeAddr("RAAC_ORCHESTRATOR_OWNER");
uint256 initialRaacSwapTaxRateInBps = 200;
uint256 initialRaacBurnTaxRateInBps = 150;
address ALICE = makeAddr("ALICE");
address BOB = makeAddr("BOB");
address CHARLIE = makeAddr("CHARLIE");
address DEVIL = makeAddr("DEVIL");
function setUp() public {
raacToken = new RAACToken(RAAC_OWNER, initialRaacSwapTaxRateInBps, initialRaacBurnTaxRateInBps);
vm.startPrank(RAAC_ORCHESTRATOR_OWNER);
raacReleaseOrchestrator = new RAACReleaseOrchestrator(address(raacToken));
vm.stopPrank();
}
function testVestingPowerAbuseViaStartTimeBug() public {
bytes32 category = raacReleaseOrchestrator.TREASURY_CATEGORY();
uint256 categoryAllocation = raacReleaseOrchestrator.categoryAllocations(category);
vm.startPrank(RAAC_OWNER);
raacToken.setMinter(RAAC_MINTER);
vm.stopPrank();
vm.startPrank(RAAC_MINTER);
raacToken.mint(address(raacReleaseOrchestrator), categoryAllocation);
vm.stopPrank();
uint256 vestingDuration = raacReleaseOrchestrator.VESTING_DURATION();
// Warp time to simulate vesting period passage.
vm.warp(vestingDuration + 1);
vm.startPrank(RAAC_ORCHESTRATOR_OWNER);
// Setting startTime to block.timestamp - vestingDuration (i.e., in the past)
raacReleaseOrchestrator.createVestingSchedule(
ALICE, category, categoryAllocation, block.timestamp - vestingDuration
);
vm.stopPrank();
(
uint256 totalAmount,
uint256 releasedAmount,
uint256 startTime,
uint256 duration,
uint256 lastClaimTime,
bool initialized
) = raacReleaseOrchestrator.vestingSchedules(ALICE);
console.log("Alice's vesting information before release:");
console.log("totalAmount : ", totalAmount);
console.log("releasedAmount : ", releasedAmount);
console.log("startTime : ", startTime);
console.log("duration : ", duration);
console.log("lastClaimTime : ", lastClaimTime);
console.log("initialized : ", initialized);
// Before release, no tokens have been claimed.
assertEq(raacToken.balanceOf(ALICE), 0);
assertEq(totalAmount, categoryAllocation);
assertEq(releasedAmount, 0);
assertEq(initialized, true);
// ALICE calls release; due to the past startTime, all tokens vest immediately.
vm.startPrank(ALICE);
raacReleaseOrchestrator.release();
vm.stopPrank();
(totalAmount, releasedAmount, startTime, duration, lastClaimTime, initialized) =
raacReleaseOrchestrator.vestingSchedules(ALICE);
console.log("Alice's vesting information after release:");
console.log("totalAmount : ", totalAmount);
console.log("releasedAmount : ", releasedAmount);
console.log("lastClaimTime : ", lastClaimTime);
// Expect full vesting: all tokens are released immediately.
assertEq(releasedAmount, totalAmount);
assertEq(lastClaimTime, block.timestamp);
}
}

How to Run the Test

  1. Initialize a Foundry Project:

    forge init my-foundry-project
  2. Place Contract Files:
    Ensure that RAACToken.sol and RAACReleaseOrchestrator.sol are in the src/core/tokens and src/core/minters/RAACReleaseOrchestrator directories respectively.

  3. Create Test Directory:
    Create a test directory adjacent to src and add the test file (e.g., RAACReleaseOrchestratorTest.t.sol).

  4. Run the Test:

    forge test --mt testVestingPowerAbuseViaStartTimeBug -vv
  5. Expected Outcome:
    The test should show that ALICE is able to release the full vesting amount immediately due to the startTime being set in the past, thereby demonstrating the vulnerability.

Impact

  • Immediate Vesting Abuse:
    Beneficiaries can bypass the intended vesting cliff or delay by creating a vesting schedule with a past start time, enabling them to claim all tokens immediately.

  • Denial of Vesting Control:
    The protocol loses control over the vesting schedule, potentially leading to a rapid, unplanned release of tokens that could disrupt market stability.

  • Economic Exploitation:
    Immediate vesting can be exploited by beneficiaries, undermining the vesting mechanism designed to align incentives over time.

  • Loss of Protocol Integrity:
    Bypassing vesting rules may lead to unintended token supply increases, which can harm the economic balance and long-term trust in the protocol.

Tools Used

  • Manual Review

  • Foundry

Recommendations

To prevent beneficiaries from abusing the vesting schedule via an incorrect start time, update the createVestingSchedule function to include a validation check that ensures the startTime is not in the past. Additionally, update the beneficiary's vesting schedule to record the last claim time upon creation.

Proposed Diff for 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();
+ // Enforce that startTime is not in the past
+ if (startTime < block.timestamp) revert StartTimeInPast();
// 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;
+ // Optionally, initialize lastClaimTime to the startTime to enforce the vesting cliff.
+ schedule.lastClaimTime = startTime;
emit VestingScheduleCreated(beneficiary, category, amount, startTime);
}
Updates

Lead Judging Commences

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

Give us feedback!