The issue occurs because while the function handles the token transfer and schedule deletion, it doesn't properly update the category accounting:
function emergencyRevoke(address beneficiary) external onlyRole(EMERGENCY_ROLE) {
VestingSchedule storage schedule = vestingSchedules[beneficiary];
if (!schedule.initialized) revert NoVestingSchedule();
uint256 unreleasedAmount = schedule.totalAmount - schedule.releasedAmount;
delete vestingSchedules[beneficiary];
if (unreleasedAmount > 0) {
@>
raacToken.transfer(address(this), unreleasedAmount);
emit EmergencyWithdraw(beneficiary, unreleasedAmount);
}
emit VestingScheduleRevoked(beneficiary);
}
import { expect } from "chai";
import hre from "hardhat";
const { ethers } = hre;
import { time } from "@nomicfoundation/hardhat-network-helpers";
describe("RAACReleaseOrchestrator", function () {
it('should demonstrate categoryUsed accounting issue', async function () {
const [owner, user1, user2] = await ethers.getSigners();
const RAACTokenFactory = await ethers.getContractFactory("RAACToken");
const raacToken = await RAACTokenFactory.deploy(owner.address, 0, 0);
await raacToken.connect(owner).setMinter(owner.address);
const RAACReleaseOrchestratorFactory = await ethers.getContractFactory("RAACReleaseOrchestrator");
const raacReleaseOrchestrator = await RAACReleaseOrchestratorFactory.deploy(await raacToken.getAddress());
await raacToken.connect(owner).manageWhitelist(raacReleaseOrchestrator.target, true);
const TEAM_CATEGORY = await raacReleaseOrchestrator.TEAM_CATEGORY();
const initialCategoryAllocations = await raacReleaseOrchestrator.categoryAllocations(TEAM_CATEGORY);
const initialCategoryUsed = await raacReleaseOrchestrator.categoryUsed(TEAM_CATEGORY);
expect(initialCategoryAllocations).to.equal(ethers.parseEther("18000000"));
expect(initialCategoryUsed).to.equal(0);
const startTime = await time.latest();
await raacReleaseOrchestrator.connect(owner).createVestingSchedule(user1.address, TEAM_CATEGORY, initialCategoryAllocations, startTime);
await raacToken.connect(owner).mint(raacReleaseOrchestrator.target, initialCategoryAllocations);
const categoryUsedAfterAllocation = await raacReleaseOrchestrator.categoryUsed(TEAM_CATEGORY);
expect(categoryUsedAfterAllocation).to.equal(initialCategoryAllocations);
const VESTING_DURATION = await raacReleaseOrchestrator.VESTING_DURATION();
await time.increase(VESTING_DURATION / 2n);
await raacReleaseOrchestrator.connect(owner).emergencyRevoke(user1.address);
const user1Balance = await raacToken.balanceOf(user1.address);
expect(user1Balance).to.equal(0);
const categoryUsedAfterEmergencyRevoke = await raacReleaseOrchestrator.categoryUsed(TEAM_CATEGORY);
expect(categoryUsedAfterEmergencyRevoke).to.equal(initialCategoryAllocations);
const orchestratorBalance = await raacToken.balanceOf(raacReleaseOrchestrator.target);
expect(orchestratorBalance).to.equal(initialCategoryAllocations);
await expect(raacReleaseOrchestrator.connect(owner).createVestingSchedule(user2.address, TEAM_CATEGORY, orchestratorBalance, startTime)).to.be.revertedWithCustomError(raacReleaseOrchestrator, "CategoryAllocationExceeded");
await raacReleaseOrchestrator.connect(owner).updateCategoryAllocation(TEAM_CATEGORY, initialCategoryAllocations + orchestratorBalance);
await raacReleaseOrchestrator.connect(owner).createVestingSchedule(user2.address, TEAM_CATEGORY, orchestratorBalance, startTime);
const categoryUsedAfterUpdate = await raacReleaseOrchestrator.categoryUsed(TEAM_CATEGORY);
expect(categoryUsedAfterUpdate).to.equal(initialCategoryAllocations + orchestratorBalance);
});
});
function emergencyRevoke(address beneficiary) external onlyRole(EMERGENCY_ROLE) {
VestingSchedule storage schedule = vestingSchedules[beneficiary];
if (!schedule.initialized) revert NoVestingSchedule();
uint256 unreleasedAmount = schedule.totalAmount - schedule.releasedAmount;
delete vestingSchedules[beneficiary];
if (unreleasedAmount > 0) {
+ categoryUsed[category] -= unreleasedAmount;
raacToken.transfer(address(this), unreleasedAmount);
emit EmergencyWithdraw(beneficiary, unreleasedAmount);
}
emit VestingScheduleRevoked(beneficiary);
}