Core Contracts

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

Unsafe ERC20 Transfer Usage in Vesting Release Functions

Root Cause

The RAACReleaseOrchestrator contract imports SafeERC20 but does not use its safe wrappers when releasing vested tokens or during emergency revocation. In the functions:

  • release()
    Tokens are transferred via:

    raacToken.transfer(beneficiary, releasableAmount);
  • emergencyRevoke()
    Unreleased tokens are transferred via:

    raacToken.transfer(address(this), unreleasedAmount);

Because the return values of these calls are not checked and the safe wrappers are not used, if the RAAC token implementation deviates from the ERC20 standard (for example, returning false on failure rather than reverting), the transfer may silently fail. This causes the contract’s internal state (which is updated before the transfer) to be inconsistent with the actual token balances—beneficiaries may have their vesting state advanced while not receiving tokens.


Attack/Issue Path


An attacker (or a faulty token implementation) provides a RAAC token that returns false on a transfer instead of reverting.

  1. Beneficiary Releases Vested Tokens:

    • The beneficiary calls the release() function.

    • The contract calculates a positive releasable amount, updates the vesting schedule (increasing releasedAmount and updating lastClaimTime), and then calls raacToken.transfer(beneficiary, releasableAmount).

    • Because the token’s transfer returns false (without reverting), the transfer fails silently.

  2. Resulting Impact:
    The beneficiary’s vesting schedule is updated as if tokens were delivered, but their token balance remains unchanged. This “lost transfer” can lead to locked or misallocated funds.


Foundry PoC

Below is a professional Foundry test that demonstrates the issue:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "openzeppelin-contracts/token/ERC20/ERC20.sol";
import "../../../contracts/core/tokens/IRAACToken.sol";
import "../../../contracts/core/minters/RAACReleaseOrchestrator/RAACReleaseOrchestrator.sol";
// A malicious RAAC token that does NOT revert on transfer failure.
contract MaliciousRAACToken is ERC20, IRAACToken {
constructor() ERC20("MaliciousRAAC", "MRAAC") {}
// Override transfer to simulate a failure (always return false)
function transfer(address, uint256) public pure override returns (bool) {
return false;
}
// Provide dummy implementations for the remaining interface methods.
function transferFrom(address, address, uint256) public pure override returns (bool) {
return false;
}
function mint(address, uint256) external override {}
function setSwapTaxRate(uint256) external override {}
function setBurnTaxRate(uint256) external override {}
function setFeeCollector(address) external override {}
function totalSupply() public view override(ERC20, IRAACToken) returns (uint256) {
return super.totalSupply();
}
}
// Foundry test contract
contract RAACReleaseOrchestratorTest is Test {
MaliciousRAACToken token;
RAACReleaseOrchestrator orchestrator;
address beneficiary = address(0xBEEF);
function setUp() public {
// Deploy the malicious token and the release orchestrator.
token = new MaliciousRAACToken();
orchestrator = new RAACReleaseOrchestrator(address(token));
// For testing, mint tokens to the orchestrator (if needed) – in our PoC the transfers will fail.
// Create a vesting schedule for the beneficiary with a start time well in the past.
uint256 pastStartTime = block.timestamp - 100 days;
vm.prank(orchestrator.getRoleMember(orchestrator.ORCHESTRATOR_ROLE(), 0));
orchestrator.createVestingSchedule(
beneficiary,
orchestrator.TEAM_CATEGORY(),
1_000 * 1e18,
pastStartTime
);
}
function testReleaseTransfersFailSilently() public {
// Before release, beneficiary balance is zero.
uint256 initialBalance = token.balanceOf(beneficiary);
assertEq(initialBalance, 0);
// Simulate beneficiary calling release.
vm.prank(beneficiary);
// Since transfer returns false, the call will not revert.
orchestrator.release();
// The vesting schedule internal state is updated.
( , uint256 released, , ) = orchestrator.getVestingSchedule(beneficiary);
assertGt(released, 0);
// However, the beneficiary's token balance remains unchanged.
uint256 postBalance = token.balanceOf(beneficiary);
assertEq(postBalance, 0);
}
}


The MaliciousRAACToken contract overrides the standard transfer function to always return false, simulating a non-compliant token that fails silently.

  • Test Flow:

    • A vesting schedule is created for a beneficiary with a start time long enough to overcome the vesting cliff.

    • The beneficiary calls release(). Although the contract’s internal state (i.e. releasedAmount) is updated, the transfer call fails silently.

    • The test confirms that the beneficiary’s balance remains zero even though the vesting schedule shows tokens as “released.”


Fix:
Replace all instances of:

raacToken.transfer(...);

with:

raacToken.safeTransfer(...);

This change leverages SafeERC20’s wrappers to ensure that token transfers revert on failure, preventing the internal state from diverging from the actual token balances.

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Known issue
Assigned finding tags:

[INVALID] SafeERC20 not used

LightChaser Low-60

Support

FAQs

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

Give us feedback!