Summary
The emergencyRevoke()
function fails to update categoryUsed
counters, allowing revoked tokens to remain counted towards category limits.
Vulnerability Details
When creatingSchedule()
we account for the amount allowed per category. The issue is that emergencyRevoke()
do not remove the amount added to a category :
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);
}
Impact
Wrong accounting of how much has been spent per category.
A category that had several schedule canceled can end up with no more schedule as it will be seen as they spent everything, where in reality they did not.
Tools Used
Manual
Recommendations
Store the category in VestingSchedule at creation and delete the unreleased amount from categoryUsed when using emergencyRevoke :
struct VestingSchedule {
uint256 totalAmount;
uint256 startTime;
uint256 duration;
uint256 releasedAmount;
bool initialized;
+ bytes32 category; // Add category tracking
}
function createVestingSchedule(...) {
//..
schedule.category = category;
categoryUsed[category] += amount;
//..
}
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];
+ categoryUsed[schedule.category] -= unreleasedAmount;
if (unreleasedAmount > 0) {
raacToken.transfer(address(this), unreleasedAmount);
emit EmergencyWithdraw(beneficiary, unreleasedAmount);
}
emit VestingScheduleRevoked(beneficiary);
}