The FeeDistributionBranch contract suffers from severe precision loss when converting low decimal tokens (e.g., USDC with 6 decimals) to WETH (18 decimals) during fee distribution. This results in approximately 99.95% fee loss for small trades, effectively reducing protocol and LP revenue to near zero for these transactions.
The issue occurs during fee distribution when converting collected fees from low decimal tokens to WETH. The vulnerability stems from two key components:
pragma solidity 0.8.25;
import { Base_Test } from "test/Base.t.sol";
import { FeeDistributionBranch } from "@zaros/market-making/branches/FeeDistributionBranch.sol";
import { Market } from "src/market-making/leaves/Market.sol";
import { Collateral } from "@zaros/market-making/leaves/Collateral.sol";
import { DexSwapStrategy } from "@zaros/market-making/leaves/DexSwapStrategy.sol";
import { IERC20 } from "@openzeppelin/token/ERC20/ERC20.sol";
import { ud60x18, UD60x18 } from "@prb-math/UD60x18.sol";
import { console } from "forge-std/console.sol";
import { MockUniswapV3SwapStrategyRouter } from "test/mocks/MockUniswapV3SwapStrategyRouter.sol";
import { UniswapV3Adapter } from "@zaros/utils/dex-adapters/UniswapV3Adapter.sol";
contract FeeDistributionBranchFeePrecisionLoss_Test is Base_Test {
using Market for Market.Data;
using Collateral for Collateral.Data;
using DexSwapStrategy for DexSwapStrategy.Data;
uint256 public cumulativeTheoreticalFees;
uint256 public cumulativeActualFees;
uint256 public totalFeesLost;
DexSwapStrategy.Data public dexSwapStrategy;
function setUp() public virtual override {
Base_Test.setUp();
vm.stopPrank();
vm.startPrank(users.owner.account);
configureSystemParameters();
createVaults(marketMakingEngine, INITIAL_VAULT_ID, FINAL_VAULT_ID, true, address(perpsEngine));
configureMarkets();
marketMakingEngine.configureEngine(address(marketMakingEngine), address(wEth), true);
marketMakingEngine.configureEngine(address(perpsEngine), address(wEth), true);
marketMakingEngine.configureEngine(address(perpsEngine), address(usdc), true);
marketMakingEngine.configureMarket(
address(perpsEngine),
1,
1e18,
5e17,
2e18
);
MockUniswapV3SwapStrategyRouter mockRouter = new MockUniswapV3SwapStrategyRouter();
uniswapV3Adapter.setUniswapV3SwapStrategyRouter(address(mockRouter));
marketMakingEngine.configureDexSwapStrategy(
1,
address(uniswapV3Adapter)
);
vm.startPrank(address(marketMakingEngine));
usdc.approve(address(uniswapV3Adapter), type(uint256).max);
wEth.approve(address(uniswapV3Adapter), type(uint256).max);
vm.stopPrank();
deal({
token: address(wEth),
to: address(marketMakingEngine),
give: 1000e18
});
deal({
token: address(usdc),
to: address(marketMakingEngine),
give: 1000000e6
});
deal({
token: address(usdc),
to: address(mockRouter),
give: 1000000e6
});
deal({
token: address(wEth),
to: address(mockRouter),
give: 1000e18
});
vm.stopPrank();
}
function testFeeDistributionPrecisionLoss() public {
address lowDecimalToken = address(usdc);
uint128 marketId = 1;
console.log("\n=== Starting test with USDC ===");
console.log("USDC address:", lowDecimalToken);
console.log("Market ID:", marketId);
uint256[] memory tradeSizes = new uint256[]();
tradeSizes[0] = 1;
tradeSizes[1] = 10;
tradeSizes[2] = 100;
tradeSizes[3] = 1000;
tradeSizes[4] = 10000;
for(uint256 i = 0; i < tradeSizes.length; i++) {
console.log("\n--- Trade Size:", tradeSizes[i], "wei ---");
console.log("Funding perpsEngine with USDC");
deal({
token: address(usdc),
to: address(perpsEngine),
give: tradeSizes[i]
});
console.log("Getting initial state");
(uint128 protocolBefore, uint128 vaultShareBefore) = marketMakingEngine.getWethRewardDataRaw(marketId);
console.log("Protocol before:", protocolBefore);
console.log("Vault share before:", vaultShareBefore);
console.log("Calculating theoretical distribution");
Collateral.Data storage collateral = Collateral.load(lowDecimalToken);
UD60x18 amountX18 = collateral.convertTokenAmountToUd60x18(tradeSizes[i]);
uint256 theoreticalFee = amountX18.intoUint256();
cumulativeTheoreticalFees += theoreticalFee;
console.log("Executing receiveMarketFee");
changePrank(address(perpsEngine));
marketMakingEngine.receiveMarketFee(marketId, lowDecimalToken, tradeSizes[i]);
console.log("Executing convertAccumulatedFeesToWeth");
marketMakingEngine.convertAccumulatedFeesToWeth(marketId, lowDecimalToken, 1, "");
(uint128 protocolAfter, uint128 vaultShareAfter) = marketMakingEngine.getWethRewardDataRaw(marketId);
uint256 protocolShare = protocolAfter - protocolBefore;
uint256 vaultShare = vaultShareAfter - vaultShareBefore;
uint256 actualFee = protocolShare + vaultShare;
cumulativeActualFees += actualFee;
console.log("Theoretical Fee:", theoreticalFee);
console.log("Actual Fee:", actualFee);
console.log("Running Difference:", cumulativeTheoreticalFees - cumulativeActualFees);
if(theoreticalFee > actualFee) {
totalFeesLost += (theoreticalFee - actualFee);
}
}
console.log("\nFinal Results:");
console.log("Total Theoretical Fees:", cumulativeTheoreticalFees);
console.log("Total Actual Fees:", cumulativeActualFees);
console.log("Total Fees Lost:", totalFeesLost);
assertLt(
cumulativeActualFees,
cumulativeTheoreticalFees,
"Should have precision loss in fee distribution"
);
}
}
=== Starting test with USDC ===
USDC address: 0x8392F9cC30c5e7b7E9095c746784573CAFD68432
Market ID: 1
--- Trade Size: 1 wei ---
Funding perpsEngine with USDC
Getting initial state
Protocol before: 0
Vault share before: 0
Calculating theoretical distribution
Executing receiveMarketFee
changePrank is deprecated. Please use vm.startPrank instead.
Executing convertAccumulatedFeesToWeth
Theoretical Fee: 1000000000000000000
Actual Fee: 495000000
Running Difference: 999999999505000000
--- Trade Size: 10 wei ---
Funding perpsEngine with USDC
Getting initial state
Protocol before: 49500000
Vault share before: 445500000
Calculating theoretical distribution
Executing receiveMarketFee
changePrank is deprecated. Please use vm.startPrank instead.
Executing convertAccumulatedFeesToWeth
Theoretical Fee: 10000000000000000000
Actual Fee: 4950000000
Running Difference: 10999999994555000000
--- Trade Size: 100 wei ---
Funding perpsEngine with USDC
Getting initial state
Protocol before: 544500000
Vault share before: 4900500000
Calculating theoretical distribution
Executing receiveMarketFee
changePrank is deprecated. Please use vm.startPrank instead.
Executing convertAccumulatedFeesToWeth
Theoretical Fee: 100000000000000000000
Actual Fee: 49500000000
Running Difference: 110999999945055000000
--- Trade Size: 1000 wei ---
Funding perpsEngine with USDC
Getting initial state
Protocol before: 5494500000
Vault share before: 49450500000
Calculating theoretical distribution
Executing receiveMarketFee
changePrank is deprecated. Please use vm.startPrank instead.
Executing convertAccumulatedFeesToWeth
Theoretical Fee: 1000000000000000000000
Actual Fee: 495000000000
Running Difference: 1110999999450055000000
--- Trade Size: 10000 wei ---
Funding perpsEngine with USDC
Getting initial state
Protocol before: 54994500000
Vault share before: 494950500000
Calculating theoretical distribution
Executing receiveMarketFee
changePrank is deprecated. Please use vm.startPrank instead.
Executing convertAccumulatedFeesToWeth
Theoretical Fee: 10000000000000000000000
Actual Fee: 4950000000000
Running Difference: 11110999994500055000000
Final Results:
Total Theoretical Fees: 11111000000000000000000
Total Actual Fees: 5499945000000
Total Fees Lost: 11110999994500055000000