OrderBook

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

Reentrancy Vulnerability

Root + Impact

Description

  • Normal Behavior: The buyOrder() function should atomically transfer USDC from the buyer to both the protocol (as fees) and the seller, then transfer the sold tokens to the buyer, marking the order as inactive exactly once.

  • Specific Issue: The function performs multiple external calls after marking the order as inactive but before completing all transfers, allowing malicious seller contracts to reenter the function and exploit the inconsistent state.

function buyOrder(uint256 _orderId) public {
Order storage order = orders[_orderId];
// Validation checks...
order.isActive = false; // @> State changed before external calls complete
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 contract
IERC20(order.tokenToSell).safeTransfer(msg.sender, order.amountToSell); // @> Additional external call
totalFees += protocolFee; // @> State update after external calls
emit OrderFilled(_orderId, msg.sender, order.seller);
}

Likelihood:

  • Any seller using a smart contract address (multisig, DAO, or malicious contract) creates a reentry opportunity during USDC payment reception

  • The vulnerability triggers automatically when the malicious seller contract implements reentrant logic in its receive() or fallback() functions

Impact:

  • Double spending allows attackers to receive multiple payments for the same order while only providing tokens once

  • Protocol fee theft through manipulation of the totalFees variable during reentrant calls

  • Complete order book state corruption leading to locked funds and market manipulation

Proof of Concept

contract MaliciousOrderSeller {
OrderBook public orderBook;
uint256 public targetOrderId;
bool public attacked = false;
function createMaliciousOrder() external {
// Create order with this contract as seller
targetOrderId = orderBook.createSellOrder(
wethAddress,
1 ether,
1000e6, // 1000 USDC
1 days
);
}
// Triggered when receiving USDC payment
receive() external payable {
if (!attacked) {
attacked = true;
// Reenter to buy the same order again
orderBook.buyOrder(targetOrderId); // Order marked inactive but transfer continues
}
}
}

Recommended Mitigation

- contract OrderBook is Ownable
+ contract OrderBook is Ownable, ReentrancyGuard
- function buyOrder(uint256 _orderId) public
+ function buyOrder(uint256 _orderId) public nonReentrant
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract OrderBook is Ownable, ReentrancyGuard {
function buyOrder(uint256 _orderId) public nonReentrant {
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();
// Calculate values
uint256 protocolFee = (order.priceInUSDC * FEE) / PRECISION;
uint256 sellerReceives = order.priceInUSDC - protocolFee;
// Effects - Update state before external calls
order.isActive = false;
totalFees += protocolFee;
// Interactions - External calls last
iUSDC.safeTransferFrom(msg.sender, address(this), protocolFee);
iUSDC.safeTransferFrom(msg.sender, order.seller, sellerReceives);
IERC20(order.tokenToSell).safeTransfer(msg.sender, order.amountToSell);
emit OrderFilled(_orderId, msg.sender, order.seller);
}
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge
4 months ago
yeahchibyke Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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