The RAACReleaseOrchestrator contract incorrectly calculates vested amounts by including the cliff period in vesting progress, allowing beneficiaries to receive tokens earlier than intended.
In RAACReleaseOrchestrator.sol, the _calculateReleasableAmount function calculates vesting progress from startTime without properly accounting for the cliff period:
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();
const ERC20Mock = await ethers.getContractFactory("ERC20Mock");
token = await ERC20Mock.deploy("RAAC Token", "RAAC", admin.address, ethers.utils.parseEther("100000000"));
await token.deployed();
const RAACReleaseOrchestrator = await ethers.getContractFactory("RAACReleaseOrchestrator");
orchestrator = await RAACReleaseOrchestrator.deploy(token.address, admin.address);
await orchestrator.deployed();
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;
await orchestrator.connect(admin).createVestingSchedule(
beneficiary.address,
ethers.utils.formatBytes32String("TEAM"),
ethers.utils.parseEther("1000"),
currentTime
);
const cliffTime = 90 * 24 * 60 * 60;
await ethers.provider.send("evm_increaseTime", [cliffTime]);
await ethers.provider.send("evm_mine");
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);
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.
allowed beneficiaries to receive tokens earlier than intended since vesting progress was calculated from the start time rather than after the cliff period
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;
}