Core Contracts

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

Incorrect Vesting Schedule Calculation Leads to Early Token Release

Summary

The RAACReleaseOrchestrator contract incorrectly calculates vested amounts by including the cliff period in vesting progress, allowing beneficiaries to receive tokens earlier than intended.

Vulnerability Details

In RAACReleaseOrchestrator.sol, the _calculateReleasableAmount function calculates vesting progress from startTime without properly accounting for the cliff period:

https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/minters/RAACReleaseOrchestrator/RAACReleaseOrchestrator.sol#L192

While the function checks for cliff expiry:

if (block.timestamp < schedule.startTime + VESTING_CLIFF) return 0;

Proof of concept

const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Vesting Cliff Exploit PoC", function () {
let token, orchestrator, admin, beneficiary;
before(async function () {
[admin, beneficiary] = await ethers.getSigners();
// Deploy dummy ERC20 token (ERC20Mock)
const ERC20Mock = await ethers.getContractFactory("ERC20Mock");
token = await ERC20Mock.deploy("RAAC Token", "RAAC", admin.address, ethers.utils.parseEther("100000000"));
await token.deployed();
// Deploy RAACReleaseOrchestrator
const RAACReleaseOrchestrator = await ethers.getContractFactory("RAACReleaseOrchestrator");
orchestrator = await RAACReleaseOrchestrator.deploy(token.address, admin.address);
await orchestrator.deployed();
// Fund the orchestrator with tokens
await token.connect(admin).transfer(orchestrator.address, ethers.utils.parseEther("5000000"));
});
it("should allow beneficiary to release tokens at cliff time due to vulnerability", async function () {
const currentTime = (await ethers.provider.getBlock("latest")).timestamp;
// Create vesting schedule for beneficiary with totalAmount = 1000 tokens
await orchestrator.connect(admin).createVestingSchedule(
beneficiary.address,
ethers.utils.formatBytes32String("TEAM"),
ethers.utils.parseEther("1000"),
currentTime
);
// Increase time by VESTING_CLIFF (90 days)
const cliffTime = 90 * 24 * 60 * 60;
await ethers.provider.send("evm_increaseTime", [cliffTime]);
await ethers.provider.send("evm_mine");
// Beneficiary calls release
const balanceBefore = await token.balanceOf(beneficiary.address);
await orchestrator.connect(beneficiary).release();
const balanceAfter = await token.balanceOf(beneficiary.address);
expect(balanceAfter).to.be.gt(balanceBefore);
// Expected amount ≈ 1000 * (90 / 700) tokens in the vulnerable contract
const released = balanceAfter.sub(balanceBefore);
expect(released).to.be.closeTo(ethers.utils.parseEther("128.57"), ethers.utils.parseEther("1"));
});
});

It still calculates vesting progress including the cliff period, effectively starting linear vesting from start time rather than after the cliff period.

Impact

allowed beneficiaries to receive tokens earlier than intended since vesting progress was calculated from the start time rather than after the cliff period

Recommendations

function _calculateReleasableAmount(
VestingSchedule memory schedule
) internal view returns (uint256) {
if (block.timestamp < schedule.startTime + VESTING_CLIFF) return 0;
uint256 elapsedAfterCliff = block.timestamp - (schedule.startTime + VESTING_CLIFF);
uint256 effectiveDuration = schedule.duration - VESTING_CLIFF;
if (elapsedAfterCliff >= effectiveDuration) {
return schedule.totalAmount - schedule.releasedAmount;
}
uint256 vestedAmount = (schedule.totalAmount * elapsedAfterCliff) / effectiveDuration;
return vestedAmount - schedule.releasedAmount;
}
Updates

Lead Judging Commences

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

RAACReleaseOrchestrator vesting calculation includes cliff period in duration, doubling token release rate after cliff ends

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

RAACReleaseOrchestrator vesting calculation includes cliff period in duration, doubling token release rate after cliff ends

Appeal created

inallhonesty Lead Judge
3 months ago
inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

RAACReleaseOrchestrator vesting calculation includes cliff period in duration, doubling token release rate after cliff ends

Support

FAQs

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