OrderBook

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

No Reentrancy Guard on buyOrder() and cancelSellOrder()

No Reentrancy Guard on buyOrder

Description

  • The buyOrder function performs external calls (safeTransferFrom for USDC and safeTransfer for the token to sell) before updating the order.isActive state variable. This creates a reentrancy vulnerability, where a malicious contract acting as the buyer could re-enter the buyOrder function (via the safeTransferFrom call to the USDC contract) and repeatedly execute the function before the isActive flag is set to false, potentially draining funds or executing multiple unintended trades.

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;
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);
}

Risk

Likelihood:

  • A bad user could exploit this function anytime he wants for valid orders.

Impact:

  • A malicious buyer could exploit this to receive multiple transfers of the seller’s tokens while only paying once, or manipulate the contract state in unintended ways.

Proof of Concept

This is an example PoC code to simulate sample ReentrancyAttack in the problem and explain how does it work.

pragma solidity ^0.8.0;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract ReentrancyAttack {
OrderBook public orderBook;
IERC20 public usdc;
uint256 public attackOrderId;
bool private attacking;
constructor(address _orderBook, address _usdc) {
orderBook = OrderBook(_orderBook);
usdc = IERC20(_usdc);
}
// Malicious function to initiate the attack
function attack(uint256 _orderId) external {
attackOrderId = _orderId;
attacking = true;
// Approve a large amount to allow multiple transfers
usdc.approve(address(orderBook), type(uint256).max);
orderBook.buyOrder(_orderId);
}
// Simulate a reentrancy hook (e.g., via a malicious USDC-like token or callback)
// For simplicity, assume USDC has a hook or use a custom token
function onTransferReceived(address, address, uint256, bytes calldata) external returns (bytes4) {
if (attacking && address(msg.sender) == address(orderBook)) {
// Re-enter buyOrder while the order is still active
orderBook.buyOrder(attackOrderId);
}
return bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)"));
}
// Fallback to receive tokens
receive() external payable {}
}
interface OrderBook {
function buyOrder(uint256 _orderId) external;
}

Recommended Mitigation

Here is fix suggestion to import Openzeppelin's ReentrancyGuard library and use it in buyOrder function to prevent reentrance attack.

- remove this code
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;
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 this code
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
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();
// Update state first (Checks-Effects)
order.isActive = false;
uint256 protocolFee = (order.priceInUSDC * FEE) / PRECISION;
uint256 sellerReceives = order.priceInUSDC - protocolFee;
// Interactions
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
6 days ago
yeahchibyke Lead Judge 5 days ago
Submission Judgement Published
Invalidated
Reason: Too generic

Support

FAQs

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