Core Contracts

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

Fee Collector Overpaid During Token Burn

Summary

The RAACToken contract has an issue in its burn function where the fee collector receives an excessive amount of fees being paid almost twice. This occurs due to the way the _burn function interacts with the _update function, causing fees to be deducted and transferred multiple times.

Vulnerability Details

Source

In the burn function, the fee is calculated and sent to the fee collector using the _transfer function:

function burn(uint256 amount) external {
uint256 taxAmount = amount.percentMul(burnTaxRate);
_burn(msg.sender, amount - taxAmount);
if (taxAmount > 0 && feeCollector != address(0)) {
@>> _transfer(msg.sender, feeCollector, taxAmount);
}
}

However, when _burn(msg.sender, amount - taxAmount); is called, it triggers the overridden _update function:

function _update(address from, address to, uint256 amount) internal virtual override {
uint256 baseTax = swapTaxRate + burnTaxRate;
if (
baseTax == 0 || from == address(0) || to == address(0) || whitelistAddress[from] || whitelistAddress[to]
|| feeCollector == address(0)
) {
super._update(from, to, amount);
return;
}
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);
}

Since _burn internally calls _update, and _update also applies fees again, the fee collector receives additional tokens, resulting in an overpayment.

Proof of Concept

Set up foundry in hardhat

Foundry Test

import "forge-std/Test.sol";
import {RAACToken} from "../contracts/core/tokens/RAACToken.sol";
contract RAACTokenPoc is Test {
RAACToken raacToken;
address owner;
address user1;
address minter;
address feeCollector;
function setUp() public {
owner = address(this);
user1 = makeAddr("user1");
minter = makeAddr("minter");
feeCollector = makeAddr("feeCollector");
raacToken = new RAACToken(owner, 0, 0);
raacToken.setMinter(minter);
raacToken.setFeeCollector(feeCollector);
}
function test_feeCollector_paid_twice_poc() public {
vm.prank(minter);
raacToken.mint(user1, 1000 ether);
vm.prank(user1);
raacToken.burn(1000 ether);
// Since default burn rate is 0.5%
// this assertion will faill
assert(raacToken.balanceOf(feeCollector) == 5 ether);
}
}

Shell Output

[111728] RAACTokenPoc::test_feeCollector_paid_twice_poc()
├─ [0] VM::prank(minter: [0x030F6a4C5Baa7350405fA8122cF458070Abd1B59])
│ └─ ← [Return]
├─ [53510] RAACToken::mint(user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], 1000000000000000000000 [1e21])
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], value: 1000000000000000000000 [1e21])
│ └─ ← [Stop]
├─ [0] VM::prank(user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF])
│ └─ ← [Return]
├─ [41762] RAACToken::burn(1000000000000000000000 [1e21])
│ ├─ emit Transfer(from: user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], to: 0x0000000000000000000000000000000000000000, value: 995000000000000000000 [9.95e20])
│ ├─ emit Transfer(from: user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], to: feeCollector: [0x0ae039EbCFe635f3Ede0F574ea8a6A9aaC3254e7], value: 50000000000000000 [5e16]) // first 5% of the 100%
│ ├─ emit Transfer(from: user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], to: 0x0000000000000000000000000000000000000000, value: 25000000000000000 [2.5e16])
│ ├─ emit Transfer(from: user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], to: feeCollector: [0x0ae039EbCFe635f3Ede0F574ea8a6A9aaC3254e7], value: 4925000000000000000 [4.925e18]) // 5% of the 95%
│ └─ ← [Stop]
├─ [627] RAACToken::balanceOf(feeCollector: [0x0ae039EbCFe635f3Ede0F574ea8a6A9aaC3254e7]) [staticcall]
│ └─ ← [Return] 4975000000000000000 [4.975e18]
└─ ← [Revert] panic: assertion failed (0x01)
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 3.14ms (688.78µs CPU time)
Ran 1 test suite in 5.12s (3.14ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in poc/RaaCTokenPoc.sol:RAACTokenPoc
[FAIL: panic: assertion failed (0x01)] test_feeCollector_paid_twice_poc() (gas: 111728)

Impact

  • The fee collector receives almost twice the intended amount in fees.

  • Users burning tokens experience excessive taxation.

  • Incorrect token accounting, leading to financial discrepancies.

Tools Used

Recommendations

To prevent duplicate fee deductions, replace the burn function with the following implementation:

- function burn(uint256 amount) external {
- uint256 taxAmount = amount.percentMul(burnTaxRate);
- _burn(msg.sender, amount - taxAmount);
- if (taxAmount > 0 && feeCollector != address(0)) {
- _transfer(msg.sender, feeCollector, taxAmount);
- }
- }
+function burn(uint256 amount) external {
+ _burn(msg.sender);
+ }

This ensures that _update does not apply the fee calculation a second time, eliminating the overpayment issue.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

RAACToken::burn applies burn tax twice when transferring to feeCollector, causing excess tokens to be burned and reduced fees to be collected

This is by design, sponsor's words: Yes, burnt amount, done by whitelisted contract or not always occur the tax. The feeCollector is intended to always be whitelisted and the address(0) is included in the _transfer as a bypass of the tax amount, so upon burn->_burn->_update it would have not applied (and would also do another burn...). For this reason, to always apply such tax, the burn function include the calculation (the 2 lines that applies) and a direct transfer to feeCollector a little bit later. This is done purposefully

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

RAACToken::burn applies burn tax twice when transferring to feeCollector, causing excess tokens to be burned and reduced fees to be collected

This is by design, sponsor's words: Yes, burnt amount, done by whitelisted contract or not always occur the tax. The feeCollector is intended to always be whitelisted and the address(0) is included in the _transfer as a bypass of the tax amount, so upon burn->_burn->_update it would have not applied (and would also do another burn...). For this reason, to always apply such tax, the burn function include the calculation (the 2 lines that applies) and a direct transfer to feeCollector a little bit later. This is done purposefully

Appeal created

inallhonesty Lead Judge 28 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

RAACToken::burn applies burn tax twice when transferring to feeCollector, causing excess tokens to be burned and reduced fees to be collected

This is by design, sponsor's words: Yes, burnt amount, done by whitelisted contract or not always occur the tax. The feeCollector is intended to always be whitelisted and the address(0) is included in the _transfer as a bypass of the tax amount, so upon burn->_burn->_update it would have not applied (and would also do another burn...). For this reason, to always apply such tax, the burn function include the calculation (the 2 lines that applies) and a direct transfer to feeCollector a little bit later. This is done purposefully

Support

FAQs

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