Summary
Incorrect acounting of categoryUsed in RAACReleaseOrchestrator will cause revoked RAACToken to be stuck in orchestrator.
Vulnerability Details
When creating a vesting schedule, RAACReleaseOrchestrator accounts total allocated RAACToken amount in categoryUsed state variable:
uint256 newCategoryTotal = categoryUsed[category] + amount;
if (newCategoryTotal > categoryAllocations[category]) revert CategoryAllocationExceeded();
categoryUsed[category] = newCategoryTotal;
However, when a vesting is emergency revoked, revoked amount is not deducted from categoryUsed.
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);
}
This will prevent rellocation of those unreleased RAACToken and those cannot be reallocated to other categories either. Because other categories have their own max allocations.
POC
First, integrate foundry and run the following test with forge.
pragma solidity ^0.8.19;
import "../lib/forge-std/src/Test.sol";
import {RAACToken} from "../contracts/core/tokens/RAACToken.sol";
import {RAACReleaseOrchestrator} from "../contracts/core/minters/RAACReleaseOrchestrator/RAACReleaseOrchestrator.sol";
contract RAACReleaseOrchestratorTest is Test {
RAACToken raacToken;
RAACReleaseOrchestrator orchestrator;
function setUp() external {
raacToken = new RAACToken(address(this), 0, 0);
orchestrator = new RAACReleaseOrchestrator(address(raacToken));
deal(address(raacToken), address(orchestrator), orchestrator.getTotalAllocation());
}
function testRevertEmergencyRevoke() external {
uint256 startTime = block.timestamp;
address team = makeAddr("team");
bytes32 category = orchestrator.TEAM_CATEGORY();
orchestrator.createVestingSchedule(team, category, 18_000_000 ether, startTime);
orchestrator.emergencyRevoke(team);
vm.expectRevert(abi.encodeWithSignature("CategoryAllocationExceeded()"));
orchestrator.createVestingSchedule(team, category, 1 ether, startTime);
}
}
Impact
Revoked RAACTokens will be stuck at orchestrator
Tools Used
Manual Review, Foundry
Recommendation
Deducted unreleased amount from categoryUsed on emergency revoke