Root + Impact
Description
-
The buyOrder() function calculates protocol fees using integer division
-
Due to Solidity’s truncating division, orders with very low priceInUSDC (e.g. 99) result in negligible or zero protocol fees. These “dust” orders can still be processed, despite generating minimal fees.
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:
Impact:
Proof of Concept
In the following POC the protocol is getting 2 USDC instead of 2.97 USDC as the remaining got truncated as we have considered 99 USDC as price in USDC.
function test_feePrecisionWithLowPriceOrder() public {
uint256 priceInUSDC = 99;
uint256 expectedFee = (priceInUSDC * 3) / 100;
uint256 expectedSellerReceives = priceInUSDC - expectedFee;
wsol.mint(clara, 1e18);
vm.startPrank(clara);
wsol.approve(address(book), 1e18);
uint256 orderId = book.createSellOrder(address(wsol), 1e18, priceInUSDC, 1 days);
vm.stopPrank();
usdc.mint(dan, priceInUSDC);
vm.startPrank(dan);
usdc.approve(address(book), priceInUSDC);
book.buyOrder(orderId);
vm.stopPrank();
assertEq(wsol.balanceOf(dan), 1e18, "Dan should receive 1 WSOL");
assertEq(usdc.balanceOf(clara), expectedSellerReceives, "Clara should receive 97 USDC");
assertEq(book.totalFees(), expectedFee, "Protocol fee should be 2 USDC");
}
Recommended Mitigation
The mitigation can be done by simply adding check for priceinUSDC to be greater than 1 USDC so that micro USDC shall not pass
function createSellOrder(
address _tokenToSell,
uint256 _amountToSell,
uint256 _priceInUSDC,
uint256 _deadlineDuration
) public returns (uint256) {
if (!allowedSellToken[_tokenToSell]) revert InvalidToken();
if (_amountToSell == 0) revert InvalidAmount();
- if (_priceInUSDC == 0) revert InvalidPrice();
+ if (_priceInUSDC == 0) && (_priceInUSDC < 1e6) revert InvalidPrice();
if (_deadlineDuration == 0 || _deadlineDuration > MAX_DEADLINE_DURATION) revert InvalidDeadline();
uint256 deadlineTimestamp = block.timestamp + _deadlineDuration;
uint256 orderId = _nextOrderId++;
IERC20(_tokenToSell).safeTransferFrom(msg.sender, address(this), _amountToSell);
// Store the order
orders[orderId] = Order({
id: orderId,
seller: msg.sender,
tokenToSell: _tokenToSell,
amountToSell: _amountToSell,
priceInUSDC: _priceInUSDC,
deadlineTimestamp: deadlineTimestamp,
isActive: true
});
emit OrderCreated(orderId, msg.sender, _tokenToSell, _amountToSell, _priceInUSDC, deadlineTimestamp);
return orderId;
}