Core Contracts

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

Malleable Allocations

Summary

The RAACReleaseOrchestrator contract allows administrators to retroactively reduce the token allocation for a vesting category (e.g., Team, Advisors) even after vesting schedules have been created. This enables admins to break promises to beneficiaries by reducing their total entitlements, violating the immutability expected in vesting contracts.

Code Snippet

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

Vulnerability Details

The contract’s vesting mechanism does not reserve tokens for individual schedules. Instead, it checks if the category’s total allocation has enough tokens to cover claims.

Exploit Scenario :

  • Create Vesting Schedules: Admins allocate tokens to users under a category (e.g., TEAM_CATEGORY = 18,000,000 RAAC).

  • Retroactively Reduce Allocation: Admins call updateCategoryAllocation to lower the category’s total allocation below the amount already distributed.

  • Break Vesting Schedules: Users can no longer claim their full vested amounts, as the category’s allocation is insufficient.

Code Proof:

In RAACReleaseOrchestrator.sol, allocations can be reduced without safeguarding vested schedules:

function updateCategoryAllocation(bytes32 category, uint256 newAllocation)
external
onlyRole(DEFAULT_ADMIN_ROLE)
{
// No check to prevent reducing below already used allocations
require(categoryAllocations[category] != 0, "Invalid category");
require(newAllocation >= categoryUsed[category], "Invalid amount"); // MISSING CHECK
categoryAllocations[category] = newAllocation;
}

Attack Simulation:

Setup

Category: TEAM_CATEGORY has 18,000,000 RAAC allocated.

Beneficiary: Alice is granted a vesting schedule for 18,000,000 RAAC (the entire category).

// Admin creates Alice's vesting schedule
createVestingSchedule(
aliceAddress,
TEAM_CATEGORY,
18_000_000e18, // 18M RAAC
startTime
);

Malicious Admin Action
The admin reduces the TEAM_CATEGORY allocation to 10,000,000 RAAC (below Alice’s vested amount).

// Admin reduces allocation (original vulnerable code)
updateCategoryAllocation(TEAM_CATEGORY, 10_000_000e18); // 10M RAAC

Alice Tries to Claim
After the vesting period, Alice attempts to claim her tokens.

// Alice calls release()
release();

Result:
Expected: Alice claims 18,000,000 RAAC.

Actual:
Category allocation: 10,000,000 RAAC (reduced by admin).
Category used: 18,000,000 RAAC (Alice’s full entitlement).

Error: CategoryAllocationExceeded – Alice cannot claim any tokens.

Impact

Users unable to claim full vested amounts

Tools Used

Manual review, static analysis

Recommendations

Enhance the code to prevent admins from reducing category allocations below the total tokens reserved for active vesting schedules.

  1. Add categoryReserved to Track Reserved Tokens

// Add this line under existing mappings
mapping(bytes32 => uint256) public categoryReserved;
  1. Modify createVestingSchedule to Track Reserved Tokens

function createVestingSchedule(
address beneficiary,
bytes32 category,
uint256 amount,
uint256 startTime
) external onlyRole(ORCHESTRATOR_ROLE) whenNotPaused {
if (beneficiary == address(0)) revert InvalidAddress();
if (amount == 0) revert InvalidAmount();
if (vestingSchedules[beneficiary].initialized) revert VestingAlreadyInitialized();
if (categoryAllocations[category] == 0) revert InvalidCategory();
// Track reserved tokens upfront
uint256 newReserved = categoryReserved[category] + amount;
if (newReserved > categoryAllocations[category]) revert CategoryAllocationExceeded();
categoryReserved[category] = newReserved;
// Existing logic to create the schedule
VestingSchedule storage schedule = vestingSchedules[beneficiary];
schedule.totalAmount = amount;
schedule.startTime = startTime;
schedule.duration = VESTING_DURATION;
schedule.initialized = true;
emit VestingScheduleCreated(beneficiary, category, amount, startTime);
}
  1. Update updateCategoryAllocation to Check Reserved Tokens

function updateCategoryAllocation(
bytes32 category,
uint256 newAllocation
) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (categoryAllocations[category] == 0) revert InvalidCategory();
// Prevent reductions below reserved tokens
if (newAllocation < categoryReserved[category]) revert InvalidAmount();
categoryAllocations[category] = newAllocation;
emit CategoryAllocationUpdated(category, newAllocation);
}
  1. Add a Lock Mechanism for Categories

// Add this mapping under existing state variables
mapping(bytes32 => bool) public allocationLocked;
// Add this function to lock categories
function lockCategoryAllocation(bytes32 category) external onlyRole(DEFAULT_ADMIN_ROLE) {
allocationLocked[category] = true;
emit CategoryLocked(category);
}
// Update updateCategoryAllocation to check locks
function updateCategoryAllocation(
bytes32 category,
uint256 newAllocation
) external onlyRole(DEFAULT_ADMIN_ROLE) {
require(!allocationLocked[category], "Category locked");
if (categoryAllocations[category] == 0) revert InvalidCategory();
if (newAllocation < categoryReserved[category]) revert InvalidAmount();
categoryAllocations[category] = newAllocation;
emit CategoryAllocationUpdated(category, newAllocation);
}

Why This Works

  • Reserved Tokens: Admins cannot reduce allocations below the total reserved for vesting schedules, protecting users' entitlements.

  • Immutability: Locked categories prevent retroactive changes, ensuring vesting promises are honored.

  • Transparency: Clear checks and events make allocation changes auditable.

Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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