Critical Fee Logic Bug in buyOrder() Causes Buyers to Overpay
Description
-
When a buyer executes buyOrder()
, they should pay the full order.priceInUSDC
, and the protocol deducts a 3% fee from the seller’s proceeds and the seller should receive 97% of the order price, and the protocol should retain 3% as fees.
-
The current implementation forces the buyer to pay an extra 3% fee on top of the order price, while the seller still receives the full amount, resulting in buyers overpaying by 3%, while the protocol loses its intended fee revenue.
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:
-
Buyers lose 3% more funds than intended.
-
Protocol fails to collect fees, losing revenue in intended way
Proof of Concept
This test:
Creates a sell order (1 WBTC for 50,000 USDC)
Records initial balances
Executes the buyOrder function
Calculates and logs the actual transfers
Shows the buyer is paying 51,500 USDC (50,000 + 1,500 fee) instead of just 50,000 USDC
The key proof is in the final assertion where we see:
Buyer paid 103% of order price (51,500 instead of 50,000)
While seller and protocol amounts are mathematically correct, the economic burden is wrong
This confirms the fee is being added to buyer's payment rather than deducted from seller's proceeds
function test_BuyerOverpaysDueToFeeBug() public {
vm.prank(alice);
wbtc.approve(address(book), 1e8);
vm.prank(alice);
uint256 orderId = book.createSellOrder(address(wbtc), 1e8, 50_000e6, mdd);
uint256 buyerInitialUSDC = usdc.balanceOf(dan);
uint256 sellerInitialUSDC = usdc.balanceOf(alice);
uint256 protocolInitialUSDC = usdc.balanceOf(address(book));
vm.prank(dan);
usdc.approve(address(book), type(uint256).max);
vm.prank(dan);
book.buyOrder(orderId);
uint256 buyerPaid = buyerInitialUSDC - usdc.balanceOf(dan);
uint256 sellerReceived = usdc.balanceOf(alice) - sellerInitialUSDC;
uint256 protocolReceived = usdc.balanceOf(address(book)) - protocolInitialUSDC;
console2.log("Buyer paid:", buyerPaid);
console2.log("Seller received:", sellerReceived);
console2.log("Protocol received:", protocolReceived);
assertEq(buyerPaid, 51_500e6, "Buyer overpaid by 3%");
assertEq(sellerReceived, 48_500e6, "Seller received correct amount");
assertEq(protocolReceived, 1_500e6, "Protocol received correct fee");
}
Recommended Mitigation
Bug:
The original code made buyers pay twice:
3% fee to protocol
97% to seller
→ Buyer overpaid by 3% (paid 103% total)
Fix:
Now buyers pay once (100% to contract), then:
Contract sends 97% to seller
Keeps 3% as fee
→ Buyer pays exact price (100%), seller effectively pays the fee
function buyOrder(uint256 _orderId) public {
// ...
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);
+ iUSDC.safeTransferFrom(msg.sender, address(this), order.priceInUSDC);
+ iUSDC.safeTransfer(order.seller, sellerReceives);
totalFees += protocolFee;
// ...
}