OrderBook

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

Race Condition Between Order Amendment and Purchase

Root + Impact

Description

  • The OrderBook contract is designed to allow buyers to purchase orders at the price they see when submitting their transaction, ensuring fair and predictable trading where the execution price matches the expected price at transaction submission time.

  • A race condition exists between the amendSellOrder() and buyOrder() functions where sellers can modify order parameters (price, amount) while buyer transactions are pending in the mempool, causing buyers to execute transactions with different parameters than originally intended, leading to potential financial losses.

// Root cause in the codebase
function amendSellOrder(uint256 _orderId, uint256 _newAmountToSell, uint256 _newPriceInUSDC, uint256 _newDeadlineDuration) public {
Order storage order = orders[_orderId];
// ... validation checks
@> order.amountToSell = _newAmountToSell; // Can change while buyer tx pending
@> order.priceInUSDC = _newPriceInUSDC; // Price modified without buyer consent
order.deadlineTimestamp = newDeadlineTimestamp;
// ... token adjustments
}
function buyOrder(uint256 _orderId) public {
Order storage order = orders[_orderId];
// ... validation checks
@> uint256 protocolFee = (order.priceInUSDC * FEE) / PRECISION; // Uses current price
@> uint256 sellerReceives = order.priceInUSDC - protocolFee; // Not original price
iUSDC.safeTransferFrom(msg.sender, address(this), protocolFee);
@> iUSDC.safeTransferFrom(msg.sender, order.seller, sellerReceives); // Buyer pays modified amount
IERC20(order.tokenToSell).safeTransfer(msg.sender, order.amountToSell);
}

Risk

Likelihood:

  • Reason 1: Can occur whenever there's network congestion causing transaction delays, giving sellers opportunity to front-run

  • Reason 2: No protection mechanism exists to prevent order modifications during pending transactions

Impact:

  • Impact 1: Financial loss for buyers who pay more than intended due to last-minute price increases

  • Impact 2: Platform reputation damage and potential regulatory issues due to unfair trading practices

Proof of Concept

Attack Scenario Explanation: The race condition occurs when a seller can amend their order after a buyer submits a purchase transaction but before it gets mined. This timing window allows sellers to front-run buyer transactions by changing the price to their advantage.

// Vulnerable amendment function
function amendSellOrder(uint256 _orderId, uint256 _newAmountToSell, uint256 _newPriceInUSDC, uint256 _newDeadlineDuration) public {
Order storage order = orders[_orderId];
// ... validation checks
// ❌ No protection against pending transactions
order.amountToSell = _newAmountToSell;
order.priceInUSDC = _newPriceInUSDC; // Can be changed while buyer transaction is pending
order.deadlineTimestamp = newDeadlineTimestamp;
// ... token adjustments
}
// Buyer transaction that can be affected
function buyOrder(uint256 _orderId) public {
Order storage order = orders[_orderId];
// ... validation
// ❌ Uses current order price, not the price when transaction was submitted
uint256 protocolFee = (order.priceInUSDC * FEE) / PRECISION;
iUSDC.safeTransferFrom(msg.sender, order.seller, order.priceInUSDC - protocolFee);
}

Recommended Mitigation

Add slippage protection by requiring buyers to specify their maximum acceptable price, preventing execution at unfavorable prices due to race conditions.

- function buyOrder(uint256 _orderId) public {
+ function buyOrder(uint256 _orderId, uint256 _maxPriceWillingToPay) 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();
+ if (order.priceInUSDC > _maxPriceWillingToPay) revert PriceTooHigh();
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);
}
+ // Add custom error
+ error PriceTooHigh();
Updates

Lead Judging Commences

yeahchibyke Lead Judge
about 1 month ago
yeahchibyke Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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