OrderBook

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

Front-run the buy transaction to amend the order to a minimum deposit greater than zero, receiving the full cost for a smaller deposit.

Root + Impact

Description

  • Normal behavior: A user creates an order by depositing collateral with the cost at their discretion and waits for the buyer.

  • User (attacker) creates a fake order, monitoring the mempool for a buy transaction of a specific order. They frontrun the buy transaction to amend the order to a smaller deposit, receive a refund, and obtain full payment for their order from the buyer's transaction.

...
@> function amendSellOrder(
uint256 _orderId,
uint256 _newAmountToSell,
uint256 _newPriceInUSDC,
uint256 _newDeadlineDuration
) public {
Order storage order = orders[_orderId];
// Validation checks
if (order.seller == address(0)) revert OrderNotFound(); // Check if order exists
if (order.seller != msg.sender) revert NotOrderSeller();
if (!order.isActive) revert OrderAlreadyInactive();
if (block.timestamp >= order.deadlineTimestamp) revert OrderExpired(); // Cannot amend expired order
if (_newAmountToSell == 0) revert InvalidAmount();
if (_newPriceInUSDC == 0) revert InvalidPrice();
if (_newDeadlineDuration == 0 || _newDeadlineDuration > MAX_DEADLINE_DURATION) revert InvalidDeadline();
uint256 newDeadlineTimestamp = block.timestamp + _newDeadlineDuration;
IERC20 token = IERC20(order.tokenToSell);
// Handle token amount changes
if (_newAmountToSell > order.amountToSell) {
// Increasing amount: Transfer additional tokens from seller
uint256 diff = _newAmountToSell - order.amountToSell;
token.safeTransferFrom(msg.sender, address(this), diff);
} else if (_newAmountToSell < order.amountToSell) {
// Decreasing amount: Transfer excess tokens back to seller
uint256 diff = order.amountToSell - _newAmountToSell;
token.safeTransfer(order.seller, diff);
}
// Update order details
order.amountToSell = _newAmountToSell;
order.priceInUSDC = _newPriceInUSDC;
order.deadlineTimestamp = newDeadlineTimestamp;
emit OrderAmended(_orderId, _newAmountToSell, _newPriceInUSDC, newDeadlineTimestamp);
}
...
@> 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;
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:

  • Attacker creates a fake order to attract a buyer, monitors the mempool for a buy transaction, and executes a front-run to reduce the collateral of the order.

Impact:

  • Huge potential for front-running exploitation, causing buyer funds loss.

Proof of Concept

This test shows that the buyOrder function lacks an order amount check, allowing the owner to modify the order before the buy.

function test_amendSellOrderToZero() public {
// alice creates sell order for wbtc
vm.startPrank(alice);
wbtc.approve(address(book), 2e8);
uint256 aliceId = book.createSellOrder(address(wbtc), 2e8, 180_000e6, 2 days);
vm.stopPrank();
vm.prank(alice);
book.amendSellOrder(aliceId, 1, 180_000e6, 2 days);
vm.stopPrank();
vm.startPrank(dan);
usdc.approve(address(book), 200_000e6);
book.buyOrder(aliceId);
vm.stopPrank();
assertEq(wbtc.balanceOf(alice), 199999999);
assertEq(usdc.balanceOf(alice), 180_000e6 - (180_000e6 * book.FEE() / book.PRECISION()));
assertEq(wbtc.balanceOf(address(dan)), 1);
}

Recommended Mitigation

Additional parameter _amountToBuy in the buyOrder function and a subsequent check for the expected amount ensure that the buyer receives the purchased amount.

- function buyOrder(uint256 _orderId) public {
+ function buyOrder(uint256 _orderId, uint256 _amountToBuy) public {
...
+ if (_amountToBuy != order.amountToSell) revert InvalidAmount();
Updates

Lead Judging Commences

yeahchibyke Lead Judge
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.