OrderBook

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

Lack of reentrancy protection in buyOrder() and cancelSellOrder()

Description

The function buyOrder() performs external calls (safeTransferFrom and safeTransfer) before updating state variables, which is dangerous. Although ERC20 tokens are generally safe, a malicious token (non-compliant ERC20) could re-enter the contract and exploit logic or steal funds.

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; // <-- Should happen *before* any external calls for safety
uint256 protocolFee = (order.priceInUSDC * FEE) / PRECISION;
uint256 sellerReceives = order.priceInUSDC - protocolFee;
@> iUSDC.safeTransferFrom(msg.sender, address(this), protocolFee); // external call (could reenter)
@> iUSDC.safeTransferFrom(msg.sender, order.seller, sellerReceives); // external call
@> IERC20(order.tokenToSell).safeTransfer(msg.sender, order.amountToSell); // external call
totalFees += protocolFee;
emit OrderFilled(_orderId, msg.sender, order.seller);
}

These external token calls could reenter if the token is malicious or not ERC20-compliant, especially before order.isActive = false is set.

Impact:

  • A malicious ERC20 token could exploit the lack of ReentrancyGuard by reentering into the contract during a token transfer (safeTransferFrom or safeTransfer) and invoking functions like buyOrder() or cancelSellOrder() again.


Proof of Concept

  1. Deploy a malicious ERC20 token with a transfer or transferFrom hook that calls back into the OrderBook contract (e.g., calls buyOrder() again).

  2. Create a sell order using this malicious token.

  3. Call buyOrder() on the order.

  4. During the execution of safeTransfer, the malicious token reenters and calls buyOrder() again before order.isActive is set to false.

  5. Now both calls think the order is still active — the order can be filled twice.

Recommended Mitigation

Add the ReentrancyGuard modifier from OpenZeppelin and apply nonReentrant to:

  • buyOrder()

  • cancelSellOrder()

  • amendSellOrder()

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract OrderBook is Ownable, ReentrancyGuard {
...
function buyOrder(uint256 _orderId) public nonReentrant {
...
}
function cancelSellOrder(uint256 _orderId) public nonReentrant {
...
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge
4 months ago
yeahchibyke Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Too generic

Support

FAQs

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