OrderBook

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

Reentrancy Attack in buyOrder Function Allows Malicious Sellers to Prevent Order Execution

Author Revealed upon completion

Root + Impact

Description

  • When a buyer calls buyOrder(), the order should be marked as inactive, payments should be transferred, and the buyer should receive the tokens being sold.

  • The buyOrder() function updates the order state (isActive = false) before completing all external token transfers, creating a reentrancy vulnerability where malicious sellers can exploit the inconsistent state.

function buyOrder(uint256 _orderId) public {
Order storage order = orders[_orderId];
// ... validation checks ...
order.isActive = false; // @> State changed before external calls
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); // @> External call to potentially malicious seller
IERC20(order.tokenToSell).safeTransfer(msg.sender, order.amountToSell); // @> This fails if tokens were stolen
}

Risk

Likelihood:

  • Malicious sellers can easily deploy contracts that implement reentrancy attacks

  • No special permissions or conditions required to exploit this vulnerability

Impact:

  • Denial of service: Legitimate buyers cannot purchase from malicious sellers

  • Gas fee losses for buyers whose transactions fail

  • Protocol reputation damage due to failed transactions

Proof of Concept


A malicious seller deploys a contract with a receive() fallback that reenters the OrderBook.

  • The seller creates a sell order using that contract as the seller address.

  • A buyer calls buyOrder() on that order.

  • During safeTransferFrom(msg.sender, order.seller, ...), the malicious seller contract's fallback triggers.

  • The fallback executes cancelSellOrder(orderId) after isActive = false but before token transfer to buyer.

  • The malicious contract reclaims the tokens being sold before they are transferred to the buyer.

  • Once control returns to buyOrder(), the final transfer fails (tokens gone), but the buyer has already paid USDC.

// Malicious seller contract
contract MaliciousSeller {
IOrderBook public orderBook;
uint256 public myOrderId;
bool public attacked = false;
// Triggered when receiving USDC payment
receive() external payable {
if (!attacked) {
attacked = true;
// Order is marked inactive but buyer hasn't received tokens yet
// Cancel order to retrieve our tokens
orderBook.cancelSellOrder(myOrderId);
}
}
function attack(uint256 orderId) external {
myOrderId = orderId;
// Create order normally, then wait for someone to buy
}
}

Recommended Mitigation

#1 - Reorder State Change

Move order.isActive = false after all external token transfers:

#2- Use OpenZeppelin's ReentrancyGuard

function buyOrder(uint256 _orderId) public {
Order storage order = orders[_orderId];
// ... validation checks ...
- 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);
+ order.isActive = false; // State change after all external calls
totalFees += protocolFee;
emit OrderFilled(_orderId, msg.sender, order.seller);
}
// Alternative: Add the nonReentrant modifier from OpenZeppelin's ReentrancyGuard
Updates

Lead Judging Commences

yeahchibyke Lead Judge about 5 hours ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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