No minimal price limit that might leads protocol no fee received
Description
-
The protocol is designed to receive a 3% fee in USDC for each trade.
-
However, the createSellOrder()
function does not enforce a minimum _priceInUSDC
. If a user sets a price lower than 34, the fee calculation will round down to zero due to integer division, and the protocol will not receive any fee when the order is filled.
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: High
Impact: Medium
-
The protocol does not collect its intended fee, leading to a direct loss of revenue.
-
If exploited systematically, this could undermine the platform's economic model, as fee collection is not guaranteed.
Proof of Concept
The following test demonstrates that when an order is created with a price of 33 USDC, the resulting protocol fee is zero.
function test_sellOrderWithLittlePrice() public {
vm.startPrank(alice);
wbtc.approve(address(book), 2e8);
uint256 aliceId = book.createSellOrder(address(wbtc), 2e8, 33, 2 days);
vm.stopPrank();
uint256 price = book.getOrder(aliceId).priceInUSDC;
uint256 fee = price * book.FEE() / book.PRECISION();
assertEq(fee, 0, "Fee should be zero for a price of 33");
vm.startPrank(dan);
usdc.approve(address(book), 200_000e6);
book.buyOrder(aliceId);
vm.stopPrank();
assert(wbtc.balanceOf(dan) == 2e8);
uint256 priceReceived = price - fee;
assert(usdc.balanceOf(alice) == priceReceived);
assert(book.totalFees() == 0);
}
Recommended Mitigation
Enforce a minimum price in both the createSellOrder
and amendSellOrder
functions to ensure a fee is always collected. The minimum price should be 34, as this is the lowest value that results in a non-zero fee.
// In OrderBook.sol
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 < 34) revert InvalidPrice(); // Price must be high enough to generate a fee
if (_deadlineDuration == 0 || _deadlineDuration > MAX_DEADLINE_DURATION) revert InvalidDeadline();
// ...
}
function amendSellOrder(
uint256 _orderId,
uint256 _newAmountToSell,
uint256 _newPriceInUSDC,
uint256 _newDeadlineDuration
) public {
Order storage order = orders[_orderId];
// Validation checks
if (order.seller == address(0)) revert OrderNotFound(); // Check if order exists
if (order.seller != msg.sender) revert NotOrderSeller();
if (!order.isActive) revert OrderAlreadyInactive();
if (block.timestamp >= order.deadlineTimestamp) revert OrderExpired(); // Cannot amend expired order
if (_newAmountToSell == 0) revert InvalidAmount();
- if (_newPriceInUSDC == 0) revert InvalidPrice();
+ if (_newPriceInUSDC < 34) revert InvalidPrice(); // Price must be high enough to generate a fee
if (_newDeadlineDuration == 0 || _newDeadlineDuration > MAX_DEADLINE_DURATION) revert InvalidDeadline();
// ...
}