OrderBook

First Flight #43
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: low
Valid

Insufficient Fee Precision Allows Dust Orders to Bypass Protocol Fee Model

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];
// Validation checks
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:

  • Creating dust orders is cheap and repeatable.

Impact:

  • Allows fee avoidance through low-value orders.

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 {
// Step 1: Clara creates a sell order for 1 WSOL at 99 USDC
uint256 priceInUSDC = 99;
uint256 expectedFee = (priceInUSDC * 3) / 100; // = 2
uint256 expectedSellerReceives = priceInUSDC - expectedFee; // = 97
wsol.mint(clara, 1e18); // 1 WSOL (18 decimals)
vm.startPrank(clara);
wsol.approve(address(book), 1e18);
uint256 orderId = book.createSellOrder(address(wsol), 1e18, priceInUSDC, 1 days);
vm.stopPrank();
// Step 2: Dan buys the order with 99 USDC
usdc.mint(dan, priceInUSDC);
vm.startPrank(dan);
usdc.approve(address(book), priceInUSDC);
book.buyOrder(orderId);
vm.stopPrank();
// Step 3: Verify fee, balances, and receipt
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;
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge
about 1 month ago
yeahchibyke Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

Fee can be bypassed

Protocol Suffers Potential Revenue Leakage due to Precision Loss in Fee Calculation

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.