Inside RAACToken.sol
, the overridden _update() function is called after each burn()
and transfer()
call. While no additional calculation happens in case of burn()
as to == address(0)
, additional tax calculation does happen in transfer()
:
This causes reduced amount of fee to be collected each time. Let's consider the burn() call:
Imagine:
amount = 1_000_000
and burnTaxRate = 0.5%
Thus taxAmount = 0.5% * 1_000_000 = 5_000
L82 burns 1_000_000 - 5_000 = 995_000
L84 calls ERC20's _transfer(msg.sender, feeCollector, 5000)
which internally calls _update()
and control goes to L185, our overridden _update()
implementation
amount
here is 5000
. Also, baseTax = swapTaxRate + burnTaxRate = 1% + 0.5% = 1.5%
On L198 totalTax = 1.5% of 5000 = 75
On L199 burnAmount = 75 * 0.5% / 1.5% = 25
On L201 feeCollector
gets totalTax - burnAmount = 75 - 25 = 50
On L202 address(0)
gets burnAmount = 25
i.e. it is burned!
On L203 feeCollector
(same as to
) gets amount - totalTax = 5000 - 75 = 4925
As we can see the burnTaxRate
was erroneously applied on the initial 1_000_000
twice. Instead of burning 995_000
RAAC tokens and passing on the rest as fees, the code burns 995_000 + 25 = 995_025
tokens. And the feeCollector
collects a reduced figure of 4975
as tax instead of 5000
.
Note that to fix this, we can NOT change L202 to:
because changing this will mess up the normal transfer()
calls and the current mechanics is supposed to burn the burnAmount
(calculated based on burnTaxRate
) by sending it to address(0)
.
As a result of this:
The actual circulating supply will be lower than intended
This amplifies the deflationary effect intended by the protocol's tokenomics
The feeCollector
receives less fee each time.
Note that burn()
is often called by the protocol. Inside FeeCollector.sol
, the execution flow is distributeCollectedFees() --> _processDistributions() and _processDistributions()
has:
Add this as a file named RAACburn.test.js
under test/unit/core/pools/LendingPool/
and run with npx hardhat test test/unit/core/pools/LendingPool/RAACburn.test.js
:
Output:
One way would be to temporarily whitelist the feeCollector
address inside burn()
so that the custom calculation inside _update()
is entirely skipped:
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
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
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
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.