Precision Loss in Fee Calculation Leads to Zero-Fee Trades
Description
-
The buyOrder
function is intended to collect a 3% fee on the USDC value of every filled order.
-
Due to precision loss from integer division, if an order's priceInUSDC
is less than 34 (the smallest integer that yields a non-zero fee), the calculated protocolFee
rounds down to zero.
function buyOrder(uint256 _orderId) public {
Order storage order = orders[_orderId];
if (order.seller == address(0)) revert OrderNotFound();
if (!order.isActive) revert OrderNotActive();
if (block.timestamp >= order.deadlineTimestamp) revert OrderExpired();
order.isActive = false;
@> uint256 protocolFee = (order.priceInUSDC * FEE) / PRECISION;
uint256 sellerReceives = order.priceInUSDC - protocolFee;
iUSDC.safeTransferFrom(msg.sender, address(this), protocolFee);
iUSDC.safeTransferFrom(msg.sender, order.seller, sellerReceives);
IERC20(order.tokenToSell).safeTransfer(msg.sender, order.amountToSell);
totalFees += protocolFee;
emit OrderFilled(_orderId, msg.sender, order.seller);
}
Risk
Likelihood:
A seller creates an order with a priceInUSDC
value of 33 or less, and a buyer subsequently fills it.
Impact:
The protocol earns no fee from these small-value trades, resulting in a minor loss of revenue.
Proof of Concept
Below scenario shows that the zero-fee trade is executed with 33 USDC.
function test_audit_feePrecisionLoss() public {
uint256 sellingWSOLAmount = 2e11;
uint256 priceUSDC = 33;
vm.startPrank(clara);
wsol.approve(address(book), sellingWSOLAmount);
uint256 claraId = book.createSellOrder(address(wsol), sellingWSOLAmount, priceUSDC, 2 days);
vm.stopPrank();
vm.startPrank(dan);
usdc.approve(address(book), priceUSDC);
book.buyOrder(claraId);
vm.stopPrank();
assertEq(book.totalFees(), 0);
}
Recommended Mitigation
Enforce a minimum priceInUSDC
in the createSellOrder
and amendSellOrder
functions to ensure the calculated fee is always non-zero.
+ uint256 public constant MIN_PRICE_IN_USDC = 34;
- if (_priceInUSDC == 0) revert InvalidPrice();
+ if (_priceInUSDC < MIN_PRICE_IN_USDC) revert InvalidPrice();