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;
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);
}
Proof of code:
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);
uint256 constant INITIAL_SWAP_TAX = 100;
uint256 constant INITIAL_BURN_TAX = 50;
function setUp() public {
token = new RAACToken(
owner,
INITIAL_SWAP_TAX,
INITIAL_BURN_TAX
);
vm.startPrank(owner);
token.setMinter(minter);
vm.stopPrank();
vm.startPrank(minter);
token.mint(user, 1000 ether);
vm.stopPrank();
}
function testTaxPrecisionLossAcrossUsers() public {
uint256 smallAmount = 100;
uint256 numUsers = 100;
uint256 initialFeeCollectorBalance = token.balanceOf(feeCollector);
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();
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;
uint256 numUsers = 100;
for(uint256 i = 0; i < numUsers; i++) {
users[i] = address(uint160(1000 + i));
token.mint(users[i], smallAmount);
}
for(uint256 i = 0; i < numUsers; i++) {
token.transfer(address(0xdead), smallAmount);
}
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):
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
Total Expected Burn = 50 tokens
Total Expected Fees = 100 tokens
Actual Behavior (With Bug):
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)
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