Core Contracts

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

RAAC Tax Conservation failure in Transfer Logic

Summary

RAACToken's tax mechanism has a potential conservation violation in its transfer logic. I expected that the sum of balance changes matches the transfer amount, ensuring no tokens are lost or created during transfers with tax application. The contract RAACToken.sol implements tax logic in its _update function

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);

The tax distribution splits into

  • Regular transfer amount (amount - totalTax)

  • Fee collector portion (totalTax - burnAmount)

  • Burn amount (burnAmount)

This verification ensures the protocol's tax mechanism preserves token supply integrity.

Vulnerability Details

Looking at the implementation in RAACToken.sol, we see the careful orchestration

function _update(address from, address to, uint256 amount) internal virtual override {
// Combined tax rate from swap and burn components
uint256 baseTax = swapTaxRate + burnTaxRate; // e.g., 1.5% + 0.5% = 2%
// Tax Exemption Logic
if (baseTax == 0 || // No tax configured
from == address(0) || // Minting scenario
to == address(0) || // Burning scenario
whitelistAddress[from] || // Whitelisted sender
whitelistAddress[to] || // Whitelisted receiver
feeCollector == address(0)) { // Fee collection disabled
super._update(from, to, amount); // Direct transfer without tax
return;
}
// Tax Calculation and Distribution
// For 1000 RAAC with 2% tax:
uint256 totalTax = amount.percentMul(baseTax); // 1000 * 2% = 20 RAAC total tax
uint256 burnAmount = totalTax * burnTaxRate / baseTax; // 20 * 0.5/2.0 = 5 RAAC to burn
// Three-Way Split (using 1000 RAAC example):
super._update(from, feeCollector, totalTax - burnAmount); // Send 15 RAAC to fee collector
super._update(from, address(0), burnAmount); // Burn 5 RAAC
super._update(from, to, amount - totalTax); // Send 980 RAAC to recipient
// Critical Point: These three operations must sum exactly to the original amount
// 15 + 5 + 980 = 1000 RAAC (Conservation Rule)
}

The core mechanism splits transfers into three streams, direct transfer, fee collection, and token burning. This design supports protocol sustainability while maintaining market stability for real estate assets.

The vulnerability lies in ensuring these three separate updates maintain perfect token conservation across all possible scenarios, especially considering rounding in percentage calculations.

Notice how the tax calculation flows: When Alice transfers 1000 RAAC to Bob, the protocol calculates a base tax (let's say 2%). From this 20 RAAC tax:

  • 15 RAAC might go to the fee collector for protocol operations

  • 5 RAAC gets burned to create deflationary pressure

  • 980 RAAC reaches Bob's wallet

The sum of all balance changes must exactly match the transfer amount. This means if Alice sends 1000 RAAC:

balanceChange = recipientGain + feeCollectorGain + burnAmount
980 + 15 + 5 = 1000

What makes this interesting is how it interacts with the protocol's real estate backing. Each RAAC token represents fractional real estate value, so maintaining precise accounting is essential for the protocol's economic model.

Impact

When Alice transfers RAAC tokens representing her tokenized property share, the protocol automatically handles three distinct flows. Think of it like a real estate transaction where closing costs get distributed to different parties automatically.

The core mistake emerges in the tax distribution logic. The protocol attempts to split each transfer into three streams:

function _update(address from, address to, uint256 amount) internal virtual {
uint256 totalTax = amount.percentMul(baseTax); // Calculate total tax
uint256 burnAmount = totalTax * burnTaxRate / baseTax; // Calculate burn portion
// Distribute the streams
super._update(from, feeCollector, totalTax - burnAmount); // Protocol fees
super._update(from, address(0), burnAmount); // Token burning
super._update(from, to, amount - totalTax); // Final transfer
}

This means that for a 1000 RAAC transfer with a 2% tax rate and 25% burn rate:

  • 15 RAAC goes to protocol operations

  • 5 RAAC gets burned

  • 980 RAAC reaches the recipient

Recommendations

function _update(
address from,
address to,
uint256 amount
) internal virtual override {
// 1: explicit conservation check
uint256 baseTax = swapTaxRate + burnTaxRate;
// Existing exemption logic remains unchanged
if (baseTax == 0 || from == address(0) || to == address(0) ||
whitelistAddress[from] || whitelistAddress[to] ||
feeCollector == address(0)) {
super._update(from, to, amount);
return;
}
// 2: Pre-calculate all amounts for verification
uint256 totalTax = amount.percentMul(baseTax);
uint256 burnAmount = totalTax * burnTaxRate / baseTax;
uint256 feeAmount = totalTax - burnAmount;
uint256 transferAmount = amount - totalTax;
// 3: Verify conservation before updates
require(feeAmount + burnAmount + transferAmount == amount,
"RAAC: Tax conservation violated");
// 4: Add events for tracking
emit TaxDistribution(
from,
to,
feeAmount,
burnAmount,
transferAmount
);
// Execute transfers with verified amounts
super._update(from, feeCollector, feeAmount); // Fee collector portion
super._update(from, address(0), burnAmount); // Burn portion
super._update(from, to, transferAmount); // Recipient portion
}
// 5: View function for tax calculation
function calculateTaxDistribution(uint256 amount) public view returns (
uint256 feeAmount,
uint256 burnAmount,
uint256 transferAmount
) {
uint256 baseTax = swapTaxRate + burnTaxRate;
uint256 totalTax = amount.percentMul(baseTax);
burnAmount = totalTax * burnTaxRate / baseTax;
feeAmount = totalTax - burnAmount;
transferAmount = amount - totalTax;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!