Core Contracts

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

`RAACReleaseOrchestrator::createVestingSchedule` cant create vesting schedule due the missing `RAACToken` transfer

Summary

RAACReleaseOrchestrator::createVestingSchedule creates vesting schedules for distributing RAAC tokens to various stakeholders, but fails to transfer the actual tokens (RAACToken) during schedule creation. When beneficiary later attempts to claim their vested tokens via the RAACReleaseOrchestrator::release function, the transfers will fail if the contract doesn't hold the necessary tokens.

Vulnerability Details

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();
// Check category allocation limits
uint256 newCategoryTotal = categoryUsed[category] + amount;
if (newCategoryTotal > categoryAllocations[category]) revert CategoryAllocationExceeded();
categoryUsed[category] = newCategoryTotal;
@>
VestingSchedule storage schedule = vestingSchedules[beneficiary];
schedule.totalAmount = amount;
schedule.startTime = startTime;
schedule.duration = VESTING_DURATION;
schedule.initialized = true;
emit VestingScheduleCreated(beneficiary, category, amount, startTime);
}

Impact

The function records the vesting commitment but fails to transfer the RAACToken to the contract. The RAACReleaseOrchestrator::createVestingSchedule will create unbackable vesting commitments. Subsequently, all RAACReleaseOrchestrator::release calls will revert due to insufficient balance. The vesting process doesn't work as expected.

Add Foundry to the project following this procedure

Create a file named RAACReleaseOrchestrator.t.sol and copy/paste this:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import {Test, console2} from "forge-std/Test.sol";
import {RAACToken} from "../contracts/core/tokens/RAACToken.sol";
import {RAACReleaseOrchestrator} from "../contracts/core/minters/RAACReleaseOrchestrator/RAACReleaseOrchestrator.sol";
contract RAACReleaseOrchestratorTest is Test {
RAACToken public raacToken;
RAACReleaseOrchestrator public raactReleaseOrchestrator;
address raacTokenOwner = makeAddr("raacTokenOwner");
address beneficiary = makeAddr("beneficiary");
uint256 SWAP_TAX_RATE = 100; // 1%
uint256 BURN_TAX_RATE = 50; // 0.5%
function setUp() public {
raacToken = new RAACToken(raacTokenOwner, SWAP_TAX_RATE, BURN_TAX_RATE);
raactReleaseOrchestrator = new RAACReleaseOrchestrator(address(raacToken));
console2.log("raacToken: ", address(raacToken));
console2.log("raactReleaseOrchestrator", address(raactReleaseOrchestrator));
vm.prank(raacTokenOwner);
raacToken.setMinter(address(this));
raacToken.mint(address(raactReleaseOrchestrator), 1000 ether);
raacToken.mint(address(beneficiary), 1000 ether);
}
function test_createVestingDoesntTransferToken() public {
assertEq(raacToken.balanceOf(beneficiary), 1000 ether);
assertEq(raacToken.balanceOf(address(raactReleaseOrchestrator)), 1000 ether);
vm.prank(beneficiary);
raacToken.approve(address(raactReleaseOrchestrator), type(uint256).max);
raactReleaseOrchestrator.createVestingSchedule(
beneficiary, raactReleaseOrchestrator.TEAM_CATEGORY(), 1000 ether, block.timestamp
);
assertEq(raacToken.balanceOf(beneficiary), 1000 ether);
assertEq(raacToken.balanceOf(address(raactReleaseOrchestrator)), 1000 ether);
}
}

Run forge test --match-test test_createVestingDoesntTransferToken -vv

Logs:
raacToken: 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
raactReleaseOrchestrator 0x2e234DAe75C793f67A35089C9d99245E1C58470b
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.64ms (511.67µs CPU time)

The test shows that no RAACToken are transferred from beneficiary to the contract.

Tools Used

Manual review

Recommendations

Add the safeTransferFrom in the RAACReleaseOrchestrator::createVestingSchedule function.

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();
// Check category allocation limits
uint256 newCategoryTotal = categoryUsed[category] + amount;
if (newCategoryTotal > categoryAllocations[category]) revert CategoryAllocationExceeded();
categoryUsed[category] = newCategoryTotal;
+ raacToken.safeTransferFrom(beneficiary, address(this), amount);
VestingSchedule storage schedule = vestingSchedules[beneficiary];
schedule.totalAmount = amount;
schedule.startTime = startTime;
schedule.duration = VESTING_DURATION;
schedule.initialized = true;
emit VestingScheduleCreated(beneficiary, category, amount, startTime);
}

Now copy this test

function test_createVestingDoesntTransferTokenMitigation() public {
assertEq(raacToken.balanceOf(beneficiary), 1000 ether);
assertEq(raacToken.balanceOf(address(raactReleaseOrchestrator)), 1000 ether);
vm.prank(beneficiary);
raacToken.approve(address(raactReleaseOrchestrator), type(uint256).max);
raactReleaseOrchestrator.createVestingSchedule(
beneficiary, raactReleaseOrchestrator.TEAM_CATEGORY(), 1000 ether, block.timestamp
);
assertEq(raacToken.balanceOf(beneficiary), 0 ether);
assertEq(raacToken.balanceOf(address(raactReleaseOrchestrator)), 1985 ether);
}

and run forge test --match-test test_createVestingDoesntTransferTokenMitigation -vv

Logs:
raacToken: 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
raactReleaseOrchestrator 0x2e234DAe75C793f67A35089C9d99245E1C58470b
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.96ms (349.75µs CPU time)

All works fine.

Updates

Lead Judging Commences

inallhonesty Lead Judge 6 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.