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;
}