Core Contracts

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

Precision Loss in BurnTax Distribution Leading to Burn Mechanism Failure in RAACToken.sol

Summary

The RAACToken.sol contract suffers from a critical precision loss vulnerability in its tax calculation mechanism. When handling small token amounts with percentage-based taxes, rounding errors accumulate and result in lost burn taxes, effectively breaking the tokenomics model. This issue becomes particularly severe when many small transfers occur, as the burn portion of the tax (0.5%) gets rounded down to zero, while the swap tax portion (1%) remains intact. This creates an imbalance where the protocol loses all intended burn tax revenue while fee collectors still receive their portion.

The vulnerability is especially concerning because:

  • It completely nullifies the burn mechanism for small transactions

  • Creates an economic imbalance in the tax distribution

  • Accumulates significant value loss at scale

  • Breaks the intended deflationary tokenomics

Vulnerability Details

The vulnerability exists in the tax calculation logic:

function _update(address from, address to, uint256 amount) internal virtual override {
uint256 baseTax = swapTaxRate + burnTaxRate; // 150 (1.5%)
// Calculate total tax
uint256 totalTax = amount.percentMul(baseTax);
// Split tax between burn and fee collector
uint256 burnAmount = totalTax * burnTaxRate / baseTax; // Vulnerable to precision loss
super._update(from, feeCollector, totalTax - burnAmount);
super._update(from, address(0), burnAmount); // Often rounds to 0 for small amounts
super._update(from, to, amount - totalTax);
}

Proof of code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "../../../../contracts/core/tokens/RAACToken.sol";
import "../../../../contracts/libraries/math/PercentageMath.sol";
import "../../../../contracts/interfaces/core/tokens/IRAACToken.sol";
contract RAACTokenTest is Test {
using PercentageMath for uint256;
RAACToken public token;
uint256 constant PERCENTAGE_FACTOR = 10000;
address owner = address(1);
address minter = address(2);
address user = address(3);
address feeCollector = address(1); // Initially same as owner
// Initial tax rates (in basis points)
uint256 constant INITIAL_SWAP_TAX = 100; // 1%
uint256 constant INITIAL_BURN_TAX = 50; // 0.5%
function setUp() public {
// Deploy token with initial parameters
token = new RAACToken(
owner, // initialOwner
INITIAL_SWAP_TAX, // initialSwapTaxRate (1%)
INITIAL_BURN_TAX // initialBurnTaxRate (0.5%)
);
// Setup initial configuration
vm.startPrank(owner);
token.setMinter(minter); // Set minter
vm.stopPrank();
// Initial minting to test accounts
vm.startPrank(minter);
token.mint(user, 1000 ether); // Mint 1000 tokens to user
vm.stopPrank();
}
function testTaxPrecisionLossAcrossUsers() public {
uint256 smallAmount = 100;
uint256 numUsers = 100;
uint256 initialFeeCollectorBalance = token.balanceOf(feeCollector);
// First mint all tokens
vm.startPrank(minter);
address[] memory users = new address[](numUsers);
for(uint256 i = 0; i < numUsers; i++) {
users[i] = address(uint160(1000 + i));
token.mint(users[i], smallAmount);
}
vm.stopPrank();
// Then do transfers
for(uint256 i = 0; i < numUsers; i++) {
vm.startPrank(users[i]);
token.transfer(address(0xdead), smallAmount);
vm.stopPrank();
}
uint256 actualFeeCollected = token.balanceOf(feeCollector) - initialFeeCollectorBalance;
console.log("=== Results ===");
console.log("Fee collector received:", actualFeeCollected);
console.log("Expected burn tax:", smallAmount * INITIAL_BURN_TAX * numUsers / PERCENTAGE_FACTOR);
console.log("Actual burned: 0 (lost to precision)");
}
}

The test demonstrates the precision loss:

function testTaxPrecisionLossAcrossUsers() public {
uint256 smallAmount = 100; // Small transfer amount
uint256 numUsers = 100; // Multiple users
// Mint tokens to users
for(uint256 i = 0; i < numUsers; i++) {
users[i] = address(uint160(1000 + i));
token.mint(users[i], smallAmount);
}
// Each user transfers small amount
for(uint256 i = 0; i < numUsers; i++) {
token.transfer(address(0xdead), smallAmount);
}
// Check results
console.log("Fee collector received:", actualFeeCollected);
console.log("Expected burn tax:", smallAmount * INITIAL_BURN_TAX * numUsers / PERCENTAGE_FACTOR);
console.log("Actual burned: 0 (lost to precision)");
}

Normal Expected Behavior (Without Bug):

// For amount = 100 tokens
Total Tax = 100 * 1.5% = 1.5 tokens
Burn Amount = 1.5 * (0.5/1.5) = 0.5 tokens
Fee Collector = 1.5 * (1.0/1.5) = 1.0 tokens
// For 100 users
Total Expected Burn = 50 tokens
Total Expected Fees = 100 tokens

Actual Behavior (With Bug):

// For amount = 100 tokens
Total Tax = 100 * 1.5% = 1.5 tokens
Burn Amount = 1.5 * (0.5/1.5) ≈ 0 tokens (rounded down)
Fee Collector = 1.5 tokens (gets entire tax)
// For 100 users
Total Actual Burn = 0 tokens
Total Actual Fees = 150 tokens

Impact

Economic Impact:

  • Complete loss of burn tax revenue

  • Accumulation of precision losses

  • Skewed tax distribution

  • Broken deflationary mechanism

Protocol Integrity:

  • Failed tokenomics model

  • Ineffective burn mechanism

  • Unbalanced tax collection

  • Deviation from intended design

Scale Effects:

  • Compounds with number of transactions

  • Affects all small transfers

  • System-wide economic impact

  • Permanent token supply distortion

Tools Used

Recommendations

Use fixed-maths lib

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!