Core Contracts

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

Permanent Token Lock in RAACReleaseOrchestrator Through Past startTime Manipulation

Summary

A critical vulnerability in RAACReleaseOrchestrator.sol allows permanent locking of vested tokens by manipulating the startTime parameter. An admin can accidentally or maliciously set the startTime to a past timestamp, causing the vesting schedule to expire before tokens can be claimed, effectively locking them forever.

Vulnerability Details

The vulnerability exists in createVestingSchedule():

function createVestingSchedule(
address beneficiary,
bytes32 category,
uint256 amount,
uint256 startTime // @audit - No validation for past timestamp
) 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();
// @audit - No check for startTime being in the past
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; // 700 days
schedule.initialized = true;
}

The issue becomes critical when combined with _calculateReleasableAmount():

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) {
// @audit - If startTime + duration is in the past, tokens are locked
return schedule.totalAmount - schedule.releasedAmount;
}
uint256 vestedAmount = (schedule.totalAmount * timeFromStart) / schedule.duration;
return vestedAmount - schedule.releasedAmount;
}

Impact

  • Up to 65% of total token supply could be permanently locked

  • Affects critical stakeholder categories:

    • Team tokens (18%)

    • Advisor tokens (10.3%)

    • Treasury (5%)

    • Private Sale (10%)

    • Public Sale (15%)

    • Liquidity (6.8%)

  • No recovery mechanism exists

  • Emergency functions cannot rescue locked tokens

Proof of Concept

Detailed Exploitation Scenario

To demonstrate this vulnerability, we'll show how tokens can be permanently locked through the following steps:

  1. Initial Setup

    • A team member is allocated 1,000,000 RAAC tokens for vesting

    • The vesting duration is set to 700 days

    • The cliff period is 90 days

  2. Attack Vectors

    • An admin (either maliciously or by mistake) can set the startTime to any past timestamp

    • If startTime + VESTING_DURATION < current time, tokens become locked

    • No validation exists to prevent this scenario

  3. Why Tokens Get Locked

    • When release() is called, _calculateReleasableAmount() detects the schedule has "expired"

    • The vesting cliff is already passed

    • The full duration has elapsed

    • But since no tokens were ever released, they remain locked in the contract

    • The emergency functions cannot rescue these tokens

  4. Key Test Scenarios

    • Create a vesting schedule with start time 800 days in the past

    • Attempt to release tokens immediately

    • Try using emergency functions to recover tokens

    • Compare with a normal vesting schedule using future start time

  5. Real World Impact

    • Team members could lose their allocated tokens

    • Project launches could be severely impacted

    • Loss of investor confidence

    • Potential legal implications

The following POC demonstrates these scenarios using Hardhat testing framework...

const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("RAACReleaseOrchestrator Lock Vulnerability", function() {
let orchestrator, raacToken;
let owner, teamMember;
const AMOUNT = ethers.utils.parseEther("1000000"); // 1M tokens
beforeEach(async function() {
// Get signers
[owner, teamMember] = await ethers.getSigners();
// Deploy RAAC Token
const RAACToken = await ethers.getContractFactory("RAACToken");
raacToken = await RAACToken.deploy(
owner.address,
100, // swapTaxRate
50 // burnTaxRate
);
// Deploy Orchestrator
const RAACReleaseOrchestrator = await ethers.getContractFactory("RAACReleaseOrchestrator");
orchestrator = await RAACReleaseOrchestrator.deploy(raacToken.address);
// Setup roles and initial tokens
await orchestrator.grantRole(await orchestrator.ORCHESTRATOR_ROLE(), owner.address);
await raacToken.mint(orchestrator.address, AMOUNT);
});
it("Should demonstrate permanent token lock through past startTime", async function() {
// 1. Calculate a time in the past that's beyond vesting duration
const VESTING_DURATION = 700 * 24 * 60 * 60; // 700 days in seconds
const PAST_START = (await ethers.provider.getBlock('latest')).timestamp - (VESTING_DURATION + 100);
// 2. Create vesting schedule with past start time
const teamCategory = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("TEAM"));
await orchestrator.createVestingSchedule(
teamMember.address,
teamCategory,
AMOUNT,
PAST_START
);
// 3. Verify schedule was created
const schedule = await orchestrator.getVestingSchedule(teamMember.address);
expect(schedule.totalAmount).to.equal(AMOUNT);
expect(schedule.startTime).to.equal(PAST_START);
// 4. Try to release tokens
await expect(
orchestrator.connect(teamMember).release()
).to.be.revertedWith("NothingToRelease");
// 5. Verify tokens are locked
expect(await raacToken.balanceOf(teamMember.address)).to.equal(0);
// 6. Try emergency revoke
await orchestrator.grantRole(
ethers.utils.keccak256(ethers.utils.toUtf8Bytes("EMERGENCY_ROLE")),
owner.address
);
await orchestrator.emergencyRevoke(teamMember.address);
// 7. Verify tokens are still locked in contract
expect(await raacToken.balanceOf(orchestrator.address)).to.equal(AMOUNT);
});
it("Should allow normal vesting with future startTime", async function() {
// 1. Setup vesting with future start
const FUTURE_START = (await ethers.provider.getBlock('latest')).timestamp + 3600; // 1 hour from now
const teamCategory = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("TEAM"));
await orchestrator.createVestingSchedule(
teamMember.address,
teamCategory,
AMOUNT,
FUTURE_START
);
// 2. Fast forward past cliff period
await ethers.provider.send("evm_increaseTime", [91 * 24 * 60 * 60]); // 91 days
await ethers.provider.send("evm_mine");
// 3. Release should work
await orchestrator.connect(teamMember).release();
expect(await raacToken.balanceOf(teamMember.address)).to.be.gt(0);
});
});

Tools Used

  • Manual code review

  • Hardhat for testing

  • Ethers.js

Recommended Mitigation

  1. Add startTime validation to 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();
// @audit Add validation
if (startTime <= block.timestamp) revert StartTimeInPast();
// Rest of the function...
}
  1. Add new error to interface:

error StartTimeInPast();
  1. Add emergency recovery function that can reset expired vesting schedules.

  2. Consider adding minimum and maximum bounds for startTime to prevent extreme timestamp manipulation.

Risk Breakdown

  • Severity: CRITICAL

  • Likelihood: HIGH (no validation in place)

  • Impact: CRITICAL (affects 65% of token supply)

Recommendation Status

The fix is straightforward and should be implemented before mainnet deployment to prevent potential permanent loss of a significant portion of the token supply.

Updates

Lead Judging Commences

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