OrderBook

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

Insufficient Buyer Protections in buyOrder Leads to Unfavorable Trades

Insufficient Buyer Protections in buyOrder Leads to Unfavorable Trades

Description

  • A buyer calls the buyOrder function to fill a sell order, expecting to receive a specific amount of a designated token for a set USDC price.

  • There is insufficient buyer-side protections for slippage, deadline and output token validation in the buy order in contrast to the sell order. This exposes the buyer to several distinct risks, including price manipulation (slippage), unfavorable execution timing, and irrecoverable losses from user error.

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();
...
}

Risk

Likelihood:

  • Malicious Seller Action (Slippage Attack): A malicious seller observes a pending buyOrder transaction in the mempool and creates amendSellOrder transaction with a higher gas price.

  • Delayed Transaction: The buyer's transaction can be delayed due to network congestion and be executed without any deadline check.

  • Buyer Mistakes: An honest buyer can simply send a transaction for the wrong orderId. The contract treats it as a normal transaction due to the lack of slippage protection and output token validation.

Impact:

  • Price/Amount Risk (Slippage): The malicious seller can drain the buyer's USDC for a negligible amount of the sell tokens(e.g., 1 wei).

  • Timing Risk (Stale Orders): A legitimate order can become unfavorable due to market changes if the buyer's transaction is delayed.

  • Mistake Risk: The buyers are forced into a trade they never intended.

Proof of Concept

Below scenarios show the risks.

  • Case 1: Alice amends the amount of WBTC from 2e8 to 1 wei in the same USDC price.

  • Case 2: Dan's transaction is delayed and it is matched with the unfavorable price.

  • Case 3: Dan makes a mistake and the order is matched with the unintended selling order of Bob.

// Case 1. Malicious Seller Amends Sell Order
function test_audit_amendedSellOrder() public {
uint256 initSellingWBTCAmount = 2e8;
uint256 modifiedSellingWBTCAmount = 1;
uint256 usdcPrice = 200_000e6;
vm.startPrank(alice);
wbtc.approve(address(book), initSellingWBTCAmount);
// alice sells 2e8 wbtc with 200_000e6 usdc
uint256 aliceId = book.createSellOrder(address(wbtc), initSellingWBTCAmount, usdcPrice, 2 days);
// alice maliciously amends the amount to only 1 wbtc
book.amendSellOrder(aliceId, modifiedSellingWBTCAmount, usdcPrice, 2 days);
vm.stopPrank();
vm.startPrank(dan);
usdc.approve(address(book), usdcPrice);
// dan buys alice wbtc order expecting 2e8 wbtc
book.buyOrder(aliceId);
vm.stopPrank();
assertEq(usdc.balanceOf(alice), usdcPrice * 97 / 100);
assertEq(wbtc.balanceOf(alice), initSellingWBTCAmount-modifiedSellingWBTCAmount); // alice sold only 1 wbtc
assertEq(usdc.balanceOf(dan), 0);
assertEq(wbtc.balanceOf(dan), modifiedSellingWBTCAmount); // dan got only 1 wbtc
}
// Case 2: Buyer's Transaction is Delayed
function test_audit_delayedBuy() public {
// alice sells 1e8 wbtc with 100_000e6 usdc
uint256 sellingWBTCAmount = 1e8;
uint256 usdcPrice = 100_000e6;
vm.startPrank(alice);
wbtc.approve(address(book), sellingWBTCAmount);
uint256 aliceId = book.createSellOrder(
address(wbtc),
sellingWBTCAmount,
usdcPrice,
3 days
);
vm.stopPrank();
// btc goes down to 95_000e6 usdc in 2 days
// while the buyOrder tx is delayed, there is no way to prevent matching
// dan buys 1e8 wbtc in 100_000e6 usdc
vm.warp(block.timestamp + 2 days);
vm.startPrank(dan);
usdc.approve(address(book), usdcPrice);
book.buyOrder(aliceId);
vm.stopPrank();
}
// Case 3: Buyer Requests with Wrong Order Mistakenly
function test_audit_buyerMistakes() public {
// alice sells 1e8 wbtc with 100_000e6 usdc
uint256 sellingWBTCAmount = 1e8;
uint256 usdcPrice = 100_000e6;
vm.startPrank(alice);
wbtc.approve(address(book), sellingWBTCAmount);
book.createSellOrder(address(wbtc), sellingWBTCAmount, usdcPrice, 2 days);
vm.stopPrank();
// bob sells 1e18 weth with 10_000e6 usdc (4x larger than general price)
uint256 sellingWETHAmount = 1e18;
usdcPrice = 10_000e6;
vm.startPrank(bob);
weth.approve(address(book), sellingWETHAmount);
uint256 bobId = book.createSellOrder(address(weth), sellingWETHAmount, usdcPrice, 2 days);
vm.stopPrank();
// dan mistakenly buy bob's weth without any protection
vm.startPrank(dan);
usdc.approve(address(book), 100_000e6);
book.buyOrder(bobId);
vm.stopPrank();
}

Recommended Mitigation

Apply followings in the buyOrder function.

  • Slippage Protection: Add _minOutputAmount and _maxInputAmount parameter to prevent malicious amending sell orders and buyer mistakes.

  • Transaction Deadline: Add buyer-defined _deadline parameter to prevent later matching.

  • Expected Asset Verification: Add _exepctedToken to prevent buyer mistakes.

+ error OutputTooLow();
+ error InputTooHigh();
- function buyOrder(uint256 _orderId) public {
+ function buyOrder(uint256 _orderId, address _expectedToken, uint256 _minOutputAmount, uint256 _maxInputAmount, uint256 _deadline) public {
Order storage order = orders[_orderId];
// Validation checks
...
+ // Output(Selling) Token Validation
+ if (order.tokenToSell != _expectedToken) revert InvalidToken();
+ // Slippage Protection
+ if (order.amountToSell < _minOutputAmount) revert OutputTooLow();
+ if (order.priceInUSDC > _maxInputAmount) revert InputTooHigh();
+ // Delaying Protection
+ if (block.timestamp >= _deadline) revert OrderExpired();
order.isActive = false;
...
}
Updates

Lead Judging Commences

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

Buy orders can be front-run and amended maliciously

A malicious seller can front-run a buy order for their order, and decrease the amount of assets to be sold. If the price is unchanged, the buy transaction fulfills, but the buyer gets lesser amount than expected.

Support

FAQs

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