OrderBook

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

Price Change Front Run prevention

Root + Impact

Description

Description

In a decentralized marketplace, a buyer typically reads order details (including price) off-chain and then submits a transaction to execute the trade. The expected behavior is that the trade executes at the exact price the buyer agreed to.

However, in the original buyOrder() implementation, there was no check to validate the current on-chain price against what the buyer expected. This opens up a critical vulnerability: the seller can front-run the buyer by updating the price via amendSellOrder() right before the buyer’s transaction is mined — causing the buyer to unknowingly pay more (or receive less).

function buyOrder(uint256 _orderId) public nonReentrant {
Order storage order = orders[_orderId];
@> // ❌ No check to confirm that the buyer still agrees with the price
...
}

Risk

Likelihood:

Likelihood: Medium to High

  • This happens when:

    • A buyer reads order data off-chain

    • Then the seller updates the price before the buyer's tx is mined

  • It is easier in a low-liquidity environment where one party controls both ends or monitors mempool activity.

Impact:

  • Buyers may overpay or get worse terms than expected

  • Loss of trust in the protocol

  • Potential financial losses to users

  • Cannot defend against mempool sniping or sandwiching

Proof of Concept

// Step 1: Seller creates order
createSellOrder(tokenA, 100 tokens, 100 USDC); // Order #1
// Step 2: Buyer reads order off-chain and submits tx:
buyOrder(1, expectedPrice = 100 * 1e6);
// Step 3: Seller sees tx in mempool and front-runs:
amendSellOrder(1, newAmount=100, newPrice=120 * 1e6);
// Step 4: Buyer's tx still executes at 120 USDC (since no on-chain check)
// Buyer pays more than intended — gets front-run

Recommended Mitigation

Add a price check in buyOrder():

function buyOrder(uint256 _orderId, uint256 expectedPrice) public nonReentrant {
Order storage order = orders[_orderId];
+ if (expectedPrice != order.priceInUSDC) revert OrderPriceHasBeenChanged();

This ensures that the price seen by the buyer off-chain matches the price stored on-chain at the time of trade execution.


✅ Final (Mitigated) Code Example

function buyOrder(uint256 _orderId, uint256 expectedPrice) public nonReentrant {
Order storage order = orders[_orderId];
if (order.seller == address(0)) revert OrderNotFound();
if (!order.isActive) revert OrderNotActive();
if (block.timestamp >= order.deadlineTimestamp) revert OrderExpired();
if (msg.sender == order.seller) revert OrderCannotBeBoughtBySeller();
+ if (expectedPrice != order.priceInUSDC) revert OrderPriceHasBeenChanged();
order.isActive = false;
order.buyLockExpiresAt = 0;
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);
}
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

Support

FAQs

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