Summary
In FeeCollector::_calculateDistribution
function, the code performs division before multiplication when calculating weights
and shares
. This order of operations can lead to truncation and precision loss due to Solidity's integer division behavior that results in wrong fee distribution calculations and unfair distribution of fees.
Vulnerability Details
function _calculateDistribution(uint256 totalFees) internal view returns (uint256[4] memory shares) {
uint256 totalCollected;
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;
}
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;
uint256 remainder = totalFees - (shares[0] + shares[1] + shares[2] + shares[3]);
if (remainder > 0) shares[3] += remainder;
}
Impact
Add Foundry to the project using this procedure
Create a file named FeeCollector.t.sol and copy / paste this:
pragma solidity ^0.8.0;
import {Test, console2} from "forge-std/Test.sol";
import {FeeCollector} from "../contracts/core/collectors/FeeCollector.sol";
import {RAACToken} from "../contracts/core/tokens/RAACToken.sol";
import {veRAACToken} from "../contracts/core/tokens/veRAACToken.sol";
contract FeeCollectorTest is Test {
FeeCollector public feeCollector;
RAACToken public raacToken;
veRAACToken public veRaacToken;
address owner = makeAddr("owner");
address treasury = makeAddr("treasury");
address repairFund = makeAddr("repairFund");
uint256 SWAP_TAX_RATE = 100;
uint256 BURN_TAX_RATE = 50;
function setUp() public {
raacToken = new RAACToken(owner, SWAP_TAX_RATE, BURN_TAX_RATE);
veRaacToken = new veRAACToken(address(raacToken));
feeCollector = new FeeCollector(address(raacToken), address(veRaacToken), treasury, repairFund, owner);
console2.log("raacToken: ", address(raacToken));
console2.log("veRaacToken: ", address(veRaacToken));
console2.log("feeCollector: ", address(feeCollector));
}
function testFuzz_precisionLossInDistribution(uint256 largeTotalFees) public {
largeTotalFees = bound(largeTotalFees, 1000 * 1e18, 10000 * 1e18);
uint256 smallFeeAmount = 100 * 1e18;
uint256 BASIS_POINTS = 10000;
uint256 veRAACShare = 8000;
vm.startPrank(owner);
uint256 currentWeight = (smallFeeAmount * BASIS_POINTS) / largeTotalFees;
uint256 currentVeRAACShare = (currentWeight * veRAACShare) / BASIS_POINTS;
uint256 correctVeRAACShare = ((smallFeeAmount * BASIS_POINTS * veRAACShare) / largeTotalFees) / BASIS_POINTS;
assertEq(currentVeRAACShare, correctVeRAACShare, "Precision loss should be demonstrated");
console2.log("Current implementation result:", currentVeRAACShare);
console2.log("Correct calculation result:", correctVeRAACShare);
vm.stopPrank();
}
function test_precisionLossInDistributionCheck() public {
uint256 smallFeeAmount = 1e20;
uint256 largeTotalFees = 7606554941953871744457;
uint256 BASIS_POINTS = 10000;
uint256 veRAACShare = 8000;
vm.startPrank(owner);
uint256 currentWeight = (smallFeeAmount * BASIS_POINTS) / largeTotalFees;
uint256 currentVeRAACShare = (currentWeight * veRAACShare) / BASIS_POINTS;
uint256 correctVeRAACShare = ((smallFeeAmount * BASIS_POINTS * veRAACShare) / largeTotalFees) / BASIS_POINTS;
assertNotEq(currentVeRAACShare, correctVeRAACShare, "Precision loss should be demonstrated");
console2.log("Small fee amount:", smallFeeAmount);
console2.log("Large fee amount:", largeTotalFees);
console2.log("Current implementation result:", currentVeRAACShare);
console2.log("Correct calculation result:", correctVeRAACShare);
vm.stopPrank();
}
}
Run forge test --match-test testFuzz_precisionLossInDistribution -vv
Logs:
raacToken: 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
veRaacToken: 0x2e234DAe75C793f67A35089C9d99245E1C58470b
feeCollector: 0xF62849F9A0B5Bf2913b396098F7c7019b51A820a
Bound result 7606554941953871744457
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 16.44ms (6.96ms CPU time)
Ran 1 test suite in 189.88ms (16.44ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/FeeCollector.t.sol:FeeCollectorTest
[FAIL: Precision loss should be demonstrated: 104 != 105; counterexample: calldata=0x2f8273cb0000000000000001ca1f27d1773513b94171aea0745adeb2bf83183ae9a86460 args=[11233111431342404087002701263833437960838443992302798529632 [1.123e58]]] testFuzz_precisionLossInDistribution(uint256) (runs: 4, μ: 12241, ~: 12241)
The test fails. For largeTotalFees = 7606554941953871744457
the calculated shares are different.
We can prove this running the other test: forge test --match-test test_precisionLossInDistributionCheck -vv
Logs:
raacToken: 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
veRaacToken: 0x2e234DAe75C793f67A35089C9d99245E1C58470b
feeCollector: 0xF62849F9A0B5Bf2913b396098F7c7019b51A820a
Small fee amount: 100000000000000000000
Large fee amount: 7606554941953871744457
Current implementation result: 104
Correct calculation result: 105
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.60ms (1.20ms CPU time)
The test shows a precision loss for the current implementation that performs a division before a multiplication.
Tools Used
Manual review
Recommendations
Rewrite the calculations to perform multiplication before division.
function _calculateDistribution(uint256 totalFees) internal view returns (uint256[4] memory shares) {
uint256 totalCollected;
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] += (feeAmount * feeType.veRAACShare) / totalFees;
+ shares[1] += (feeAmount * feeType.veRAACShare) / totalFees;
+ shares[2] += (feeAmount * feeType.veRAACShare) / totalFees;
+ shares[3] +=(feeAmount * feeType.veRAACShare) / totalFees;
}
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;
uint256 remainder = totalFees - (shares[0] + shares[1] + shares[2] + shares[3]);
if (remainder > 0) shares[3] += remainder;
}