FeeCollector may distribute less fee amount to stakeholders, due to unncessarily precision loss caused by division before multiplication.
When calculates fee distribution shares for different stakeholders, protocol firstly calculates weight and uses it to compute each fee type's shares.
As can be seen, there is division before multiplication, this can leads to unnecessary precision loss. The precision loss is in basis point, meaning for every 10000 tokens, the loss is 1 token and is not trivial.
Unecessary precision loss leads to non-trivial token loss, and stakeholders receive less distribution that expected.
pragma solidity ^0.8.19;
import {Test, console, stdError} from "forge-std/Test.sol";
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "../contracts/libraries/math/WadRayMath.sol";
import "../contracts/core/pools/LendingPool/LendingPool.sol";
import "../contracts/core/pools/StabilityPool/StabilityPool.sol";
import "../contracts/mocks/core/tokens/crvUSDToken.sol";
import "../contracts/core/tokens/RToken.sol";
import "../contracts/core/tokens/DebtToken.sol";
import "../contracts/core/tokens/DeToken.sol";
import "../contracts/core/tokens/RAACToken.sol";
import "../contracts/core/tokens/RAACNFT.sol";
import "../contracts/core/tokens/veRAACToken.sol";
import "../contracts/core/primitives/RAACHousePrices.sol";
import "../contracts/core/minters/RAACMinter/RAACMinter.sol";
import "../contracts/core/collectors/FeeCollector.sol";
import "../contracts/core/collectors/Treasury.sol";
contract Audit is Test {
using WadRayMath for uint256;
using SafeCast for uint256;
address owner = makeAddr("Owner");
address repairFund = makeAddr("RepairFund");
LendingPool lendingPool;
StabilityPool stabilityPool;
RAACHousePrices raacHousePrices;
crvUSDToken crvUSD;
RToken rToken;
DebtToken debtToken;
DEToken deToken;
RAACToken raacToken;
RAACNFT raacNft;
veRAACToken veRaacToken;
RAACMinter raacMinter;
FeeCollector feeCollector;
Treasury treasury;
function setUp() public {
vm.warp(1 days);
raacHousePrices = new RAACHousePrices(owner);
raacToken = new RAACToken(owner, 100, 50);
veRaacToken = new veRAACToken(address(raacToken));
crvUSD = new crvUSDToken(owner);
rToken = new RToken("RToken", "RToken", owner, address(crvUSD));
debtToken = new DebtToken("DebtToken", "DT", owner);
raacNft = new RAACNFT(address(crvUSD), address(raacHousePrices), owner);
deToken = new DEToken("DEToken", "DEToken", owner, address(rToken));
treasury = new Treasury(owner);
feeCollector = new FeeCollector(
address(raacToken),
address(veRaacToken),
address(treasury),
repairFund,
owner
);
lendingPool = new LendingPool(
address(crvUSD),
address(rToken),
address(debtToken),
address(raacNft),
address(raacHousePrices),
0.1e27
);
lendingPool.transferOwnership(owner);
bytes memory data = abi.encodeWithSelector(
StabilityPool.initialize.selector,
address(rToken),
address(deToken),
address(raacToken),
address(owner),
address(crvUSD),
address(lendingPool)
);
address stabilityPoolProxy = address(
new TransparentUpgradeableProxy(
address(new StabilityPool(owner)),
owner,
data
)
);
stabilityPool = StabilityPool(stabilityPoolProxy);
raacMinter = new RAACMinter(
address(raacToken),
address(stabilityPool),
address(lendingPool),
owner
);
vm.startPrank(owner);
raacHousePrices.setOracle(owner);
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
deToken.setStabilityPool(address(stabilityPool));
stabilityPool.setRAACMinter(address(raacMinter));
lendingPool.setStabilityPool(address(stabilityPool));
raacToken.setMinter(address(raacMinter));
raacToken.setFeeCollector(address(feeCollector));
raacToken.manageWhitelist(address(feeCollector), true);
raacToken.manageWhitelist(address(raacMinter), true);
raacToken.manageWhitelist(address(stabilityPool), true);
vm.stopPrank();
vm.label(address(crvUSD), "crvUSD");
vm.label(address(rToken), "RToken");
vm.label(address(debtToken), "DebtToken");
vm.label(address(deToken), "DEToken");
vm.label(address(raacToken), "RAACToken");
vm.label(address(raacNft), "RAAC NFT");
vm.label(address(lendingPool), "LendingPool");
vm.label(address(stabilityPool), "StabilityPool");
vm.label(address(raacMinter), "RAACMinter");
vm.label(address(veRaacToken), "veRAAC");
vm.label(address(feeCollector), "FeeCollector");
vm.label(address(treasury), "Treasury");
vm.label(repairFund, "RepairFund");
}
function testAudit_DistributeFeePrecisionLoss() public {
address alice = makeAddr("Alice");
deal(address(raacToken), alice, 100e18);
vm.startPrank(alice);
raacToken.approve(address(veRaacToken), 100e18);
veRaacToken.lock(100e18, 365 days);
vm.stopPrank();
uint256 distributeAmount = 60000e18;
uint256 feeTypeAmount = 3;
address feeDistributor = makeAddr("FeeDistributor");
deal(address(raacToken), feeDistributor, distributeAmount);
vm.startPrank(owner);
feeCollector.grantRole(feeCollector.DISTRIBUTOR_ROLE(), feeDistributor);
raacToken.manageWhitelist(feeDistributor, true);
vm.stopPrank();
vm.startPrank(feeDistributor);
raacToken.approve(address(feeCollector), distributeAmount);
feeCollector.collectFee(10000e18, 0);
feeCollector.collectFee(20000e18, 1);
feeCollector.collectFee(30000e18, 2);
feeCollector.distributeCollectedFees();
vm.stopPrank();
feeCollector.claimRewards(alice);
assertEq(raacToken.balanceOf(alice), 39990e18);
}
}
Multiply before dividing to avoid unnecessary precision loss.
for (uint8 i = 0; i < 8; i++) {
uint256 feeAmount = _getFeeAmountByType(i);
if (feeAmount == 0) continue;
FeeType memory feeType = feeTypes[i];
totalCollected += feeAmount;
- uint256 weight = (feeAmount * BASIS_POINTS) / totalFees;
- shares[0] += (weight * feeType.veRAACShare) / BASIS_POINTS;
- shares[1] += (weight * feeType.burnShare) / BASIS_POINTS;
- shares[2] += (weight * feeType.repairShare) / BASIS_POINTS;
- shares[3] += (weight * feeType.treasuryShare) / BASIS_POINTS;
+ shares[0] += totalFees * feeAmount * feeType.veRAACShare / totalFees / BASIS_POINTS;
+ shares[1] += totalFees * feeAmount * feeType.burnShare / totalFees / BASIS_POINTS;
+ shares[2] += totalFees * feeAmount * feeType.repairShare / totalFees / BASIS_POINTS;
+ shares[3] += totalFees * feeAmount * feeType.treasuryShare / totalFees / BASIS_POINTS;
}
if (totalCollected != totalFees) revert InvalidFeeAmount();
- shares[0] = (totalFees * shares[0]) / BASIS_POINTS;
- shares[1] = (totalFees * shares[1]) / BASIS_POINTS;
- shares[2] = (totalFees * shares[2]) / BASIS_POINTS;
- shares[3] = (totalFees * shares[3]) / BASIS_POINTS;