Core Contracts

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

Inaccurate Event Emissions in RAACReleaseOrchestrator Due to Token Transfer Taxes

Summary

The RAACReleaseOrchestrator contract manages vesting schedules for RAAC tokens, allowing beneficiaries to release their vested tokens via the release function and enabling administrators to revoke vesting via the emergencyRevoke function. Both functions call the RAACToken’s transfer method to move tokens. However, since the RAACToken contract applies transfer taxes (as seen in its internal _update function), the actual amount received by the beneficiary (or the contract in the case of a revocation) is lower than the nominal amount passed to transfer. Despite this, the events emitted by release and emergencyRevoke report the full nominal amounts. This discrepancy results in off-chain event logs that do not accurately reflect the net tokens transferred, potentially leading to misreporting in indexing, auditing, or accounting systems.

Vulnerability Details

How It Arises

  • Token Transfer Taxation:
    In the RAACToken contract, the internal _update function deducts taxes from transfers:

    uint256 totalTax = amount.percentMul(baseTax);
    uint256 burnAmount = totalTax * burnTaxRate / baseTax;
    super._update(from, feeCollector, totalTax - burnAmount);
    super._update(from, address(0), burnAmount);
    super._update(from, to, amount - totalTax);

    Therefore, when transfer is called, the recipient receives only amount - totalTax.

  • Event Emission Mismatch:
    In both the release and emergencyRevoke functions, the contract emits events using the nominal amount specified in the function call:

    // In release:
    raacToken.transfer(beneficiary, releasableAmount);
    emit TokensReleased(beneficiary, releasableAmount);
    // In emergencyRevoke:
    raacToken.transfer(address(this), unreleasedAmount);
    emit EmergencyWithdraw(beneficiary, unreleasedAmount);

    Since the actual transferred amount is lower due to tax deductions, the emitted events overstate the amount of tokens moved. This misrepresentation can lead to inconsistencies in off-chain systems that rely on these events for tracking token flows.

Potential Consequences

  • Off-Chain Accounting Errors:
    Systems that index events to monitor vesting releases or emergency revocations may record incorrect token flows, leading to erroneous financial reports and audit discrepancies.

  • Misleading Analytics:
    Investors and stakeholders relying on event logs for transparency may be misled about the actual token distribution, affecting trust and decision-making.

  • Audit Inconsistencies:
    Discrepancies between on-chain token balances and event-reported amounts could trigger audit issues and regulatory concerns.

Proof of Concept

Test Suite Overview

The provided test suite demonstrates the vulnerability by:

  1. Creating a vesting schedule for a beneficiary (ALICE).

  2. Fast-forwarding time to allow the full vesting amount to become releasable.

  3. Calling the release function and then comparing the emitted event amount with the actual net amount received (which is lower due to token transfer taxes).

Below is the PoC test suite:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {RAACToken} from "../src/core/tokens/RAACToken.sol";
import {RAACReleaseOrchestrator} from "../src/core/minters/RAACReleaseOrchestrator/RAACReleaseOrchestrator.sol";
contract RAACReleaseOrchestratorTest is Test {
RAACToken raacToken;
RAACReleaseOrchestrator raacReleaseOrchestrator;
address RAAC_OWNER = makeAddr("RAAC_OWNER");
address RAAC_MINTER = makeAddr("RAAC_MINTER");
address RAAC_ORCHESTRATOR_OWNER = makeAddr("RAAC_ORCHESTRATOR_OWNER");
uint256 initialRaacSwapTaxRateInBps = 200; // 2%
uint256 initialRaacBurnTaxRateInBps = 150; // 1.5%
address ALICE = makeAddr("ALICE");
address BOB = makeAddr("BOB");
address CHARLIE = makeAddr("CHARLIE");
address DEVIL = makeAddr("DEVIL");
event TokensReleased(address indexed beneficiary, uint256 amount);
function setUp() public {
raacToken = new RAACToken(RAAC_OWNER, initialRaacSwapTaxRateInBps, initialRaacBurnTaxRateInBps);
vm.startPrank(RAAC_ORCHESTRATOR_OWNER);
raacReleaseOrchestrator = new RAACReleaseOrchestrator(address(raacToken));
vm.stopPrank();
}
function testWrongAmountParamentersInEventsEmissions() public {
bytes32 category = raacReleaseOrchestrator.TREASURY_CATEGORY();
uint256 categoryAllocation = raacReleaseOrchestrator.categoryAllocations(category);
vm.startPrank(RAAC_OWNER);
raacToken.setMinter(RAAC_MINTER);
vm.stopPrank();
vm.startPrank(RAAC_MINTER);
raacToken.mint(address(raacReleaseOrchestrator), categoryAllocation);
vm.stopPrank();
uint256 vestingDuration = raacReleaseOrchestrator.VESTING_DURATION();
vm.startPrank(RAAC_ORCHESTRATOR_OWNER);
raacReleaseOrchestrator.createVestingSchedule(ALICE, category, categoryAllocation, block.timestamp);
vm.stopPrank();
(
uint256 totalAmount,
uint256 releasedAmount,
uint256 startTime,
uint256 duration,
uint256 lastClaimTime,
bool initialized
) = raacReleaseOrchestrator.vestingSchedules(ALICE);
console.log("Alice's information before release...");
console.log("raac balance : ", raacToken.balanceOf(ALICE));
console.log("current timestamp: ", block.timestamp);
console.log("totalAmount : ", totalAmount);
console.log("releasedAmount : ", releasedAmount);
console.log("startTime : ", startTime);
console.log("duration : ", duration);
console.log("lastClaimTime : ", lastClaimTime);
console.log("initialized : ", initialized);
assertEq(raacToken.balanceOf(ALICE), 0);
assertEq(totalAmount, categoryAllocation);
assertEq(releasedAmount, 0);
assertEq(startTime, block.timestamp);
assertEq(duration, vestingDuration);
assertEq(lastClaimTime, 0);
assertEq(initialized, true);
vm.warp(block.timestamp + vestingDuration + 1);
vm.startPrank(ALICE);
vm.expectEmit(); // Expect event emission with net amount after tax
emit TokensReleased(ALICE, totalAmount); // This event shows nominal amount, not net amount.
raacReleaseOrchestrator.release();
vm.stopPrank();
(totalAmount, releasedAmount, startTime, duration, lastClaimTime, initialized) =
raacReleaseOrchestrator.vestingSchedules(ALICE);
console.log("Alice's information after release...");
console.log("raac balance : ", raacToken.balanceOf(ALICE));
console.log("totalAmount : ", totalAmount);
console.log("releasedAmount : ", releasedAmount);
console.log("lastClaimTime : ", lastClaimTime);
// The test asserts that the releasedAmount in the schedule equals totalAmount,
// but the event and the actual balance differ because of tax deductions.
assertEq(releasedAmount, totalAmount);
assertNotEq(releasedAmount, raacToken.balanceOf(ALICE));
assertEq(lastClaimTime, block.timestamp);
}
}

How to Run the Test

  1. Initialize a Foundry Project:

    forge init my-foundry-project
  2. Place Contract Files:
    Ensure that RAACToken.sol and RAACReleaseOrchestrator.sol are in their respective directories under src/core/tokens and src/core/minters/RAACReleaseOrchestrator.

  3. Create Test Directory:
    Create a test directory adjacent to src and add the above test file (e.g., RAACReleaseOrchestratorTest.t.sol).

  4. Run the Test:

    forge test --mt testWrongAmountParamentersInEventsEmissions -vv
  5. Expected Outcome:
    The logs will indicate that the event emitted in release reports the nominal amount (totalAmount) while the actual token balance received by ALICE is lower due to tax deductions, highlighting the inconsistency.

Impact

  • Inaccurate Off-Chain Reporting:
    Event logs are used by off-chain indexing and auditing systems. Emitting the nominal amount rather than the net transferred amount (after tax deductions) may lead to incorrect calculations and misleading analytics.

  • Potential Misinterpretation:
    Stakeholders relying on event data to assess token vesting and distribution may be misled about the actual amount of tokens released, undermining trust in the protocol’s transparency.

  • Audit and Compliance Issues:
    Discrepancies between on-chain balances and event logs could trigger audit flags or regulatory scrutiny, as financial data would not accurately reflect true token flows.

Tools Used

  • Manual Review

  • Foundry

Recommendations

To address this vulnerability, the release and emergencyRevoke functions should be updated to capture the net amount transferred (i.e., after token transfer taxes) and emit that value in the corresponding events.

Proposed Diff for release Function

function release() external nonReentrant whenNotPaused {
address beneficiary = msg.sender;
VestingSchedule storage schedule = vestingSchedules[beneficiary];
if (!schedule.initialized) revert NoVestingSchedule();
uint256 releasableAmount = _calculateReleasableAmount(schedule);
if (releasableAmount == 0) revert NothingToRelease();
schedule.releasedAmount += releasableAmount;
schedule.lastClaimTime = block.timestamp;
- raacToken.transfer(beneficiary, releasableAmount);
- emit TokensReleased(beneficiary, releasableAmount);
+ uint256 balanceBefore = raacToken.balanceOf(beneficiary);
+ raacToken.transfer(beneficiary, releasableAmount);
+ uint256 balanceAfter = raacToken.balanceOf(beneficiary);
+ uint256 netReleased = balanceAfter - balanceBefore;
+ emit TokensReleased(beneficiary, netReleased);
}

Proposed Diff for emergencyRevoke Function

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);
+ uint256 balanceBefore = raacToken.balanceOf(address(this));
+ raacToken.transfer(address(this), unreleasedAmount);
+ uint256 balanceAfter = raacToken.balanceOf(address(this));
+ uint256 netRevoked = balanceAfter - balanceBefore;
+ emit EmergencyWithdraw(beneficiary, netRevoked);
}
emit VestingScheduleRevoked(beneficiary);
}
Updates

Lead Judging Commences

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

Give us feedback!