Root + Impact
Description
-
Order book systems should prevent sellers from buying their own orders to avoid unnecessary transactions and maintain logical trading behavior.
-
The buyOrder function lacks validation to prevent sellers from purchasing their own orders, allowing them to pay protocol fees unnecessarily while essentially canceling their own order through a more expensive route.
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:
-
Sellers pay unnecessary protocol fees to retrieve their own tokens instead of using the free cancelSellOrder function
-
Wasteful gas consumption for a transaction that achieves the same result as order cancellation
-
Potential user confusion about order book mechanics and available actions
-
Protocol receives fees from non-genuine trading activity, potentially inflating fee metrics
Proof of Concept
The missing validation allows sellers to buy their own orders, resulting in unnecessary fee payments and gas costs:
Scenario Demonstration: A seller can buy back their own order and pay protocol fees unnecessarily:
function test_POC_buyOrder() public {
usdc.mint(alice, 200_000e6);
vm.startPrank(alice);
wbtc.approve(address(book), 2e8);
uint256 aliceId = book.createSellOrder(address(wbtc), 2e8, 180_000e6, 2 days);
vm.stopPrank();
assert(aliceId == 1);
assert(wbtc.balanceOf(alice) == 0);
assert(wbtc.balanceOf(address(book)) == 2e8);
vm.startPrank(alice);
usdc.approve(address(book), 200_000e6);
book.buyOrder(aliceId);
vm.stopPrank();
assert(wbtc.balanceOf(alice) == 2e8);
assert(usdc.balanceOf(alice) == 199999994600000000);
}
Recommended Mitigation
Add validation to prevent sellers from buying their own orders and guide them toward the proper cancellation mechanism:
+ // Add custom error for self-buy attempts
+ error CannotBuyOwnOrder();
function buyOrder(uint256 _orderId) public {
Order storage order = orders[_orderId];
// Validation checks
if (order.seller == address(0)) revert OrderNotFound();
+ if (order.seller == msg.sender) revert CannotBuyOwnOrder();
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);
}