OrderBook

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

Reentrancy Vulnerability in buyOrder() Function Leading to Potential Theft of Tokens

Root + Impact

Description

Normal Behavior

The OrderBook contract allows users to create sell orders for tokens (wETH, wBTC, or wSOL) and buy these orders using USDC. When a buy order is executed, the contract collects a fee, transfers USDC from the buyer to the seller, and transfers the ordered tokens to the buyer.

Specific Issue

The buyOrder() function violates the checks-effects-interactions pattern by modifying state variables after making external contract calls, creating a potential reentrancy vulnerability:

// @> marks the vulnerable section
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; // State change before external calls
uint256 protocolFee = (order.priceInUSDC * FEE) / PRECISION;
uint256 sellerReceives = order.priceInUSDC - protocolFee;
iUSDC.safeTransferFrom(msg.sender, address(this), protocolFee); // External call
iUSDC.safeTransferFrom(msg.sender, order.seller, sellerReceives); // External call
IERC20(order.tokenToSell).safeTransfer(msg.sender, order.amountToSell); // External call
totalFees += protocolFee; // State change after external calls
emit OrderFilled(_orderId, msg.sender, order.seller);
}

Risk

Likelihood:

  1. The vulnerability occurs when a malicious contract with a callback function attempts to buy an order.

  2. During the execution of safeTransferFrom or safeTransfer, the attacker's contract can call back into the buyOrder() function.

Impact:

  1. An attacker could potentially execute multiple purchases of the same order, obtaining the order's tokens multiple times while only paying once.

  2. This could lead to significant financial loss by draining tokens from the contract.

Proof of Concept

// Malicious contract to exploit reentrancy in buyOrder()
contract ReentrancyExploiter {
OrderBook private orderBook;
uint256 private targetOrderId;
bool private attacking = false;
IERC20 private usdc;
constructor(address _orderBook, address _usdc) {
orderBook = OrderBook(_orderBook);
usdc = IERC20(_usdc);
}
// Start the attack
function attack(uint256 _orderId, uint256 _price) external {
targetOrderId = _orderId;
// Approve USDC to be spent by OrderBook
usdc.approve(address(orderBook), _price);
// Buy the order to trigger reentrancy
orderBook.buyOrder(targetOrderId);
}
// This function is called during token transfer
function onERC20Transfer(address, address, uint256) external returns (bool) {
// Prevent infinite recursion
if (!attacking) {
attacking = true;
// Call buyOrder again during the first transfer callback
orderBook.buyOrder(targetOrderId);
attacking = false;
}
return true;
}
// Function to withdraw stolen tokens
function withdraw(address token) external {
IERC20(token).transfer(msg.sender, IERC20(token).balanceOf(address(this)));
}
}

Recommended Mitigation

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();
+ // Save values needed for transfers
+ address seller = order.seller;
+ address tokenToSell = order.tokenToSell;
+ uint256 amountToSell = order.amountToSell;
+ uint256 priceInUSDC = order.priceInUSDC;
// Update state before external calls
order.isActive = false;
- uint256 protocolFee = (order.priceInUSDC * FEE) / PRECISION;
- uint256 sellerReceives = order.priceInUSDC - protocolFee;
+ uint256 protocolFee = (priceInUSDC * FEE) / PRECISION;
+ uint256 sellerReceives = priceInUSDC - protocolFee;
+ totalFees += protocolFee;
+ // All state changes complete before external calls
iUSDC.safeTransferFrom(msg.sender, address(this), protocolFee);
- iUSDC.safeTransferFrom(msg.sender, order.seller, sellerReceives);
- IERC20(order.tokenToSell).safeTransfer(msg.sender, order.amountToSell);
+ iUSDC.safeTransferFrom(msg.sender, seller, sellerReceives);
+ IERC20(tokenToSell).safeTransfer(msg.sender, amountToSell);
- totalFees += protocolFee;
- emit OrderFilled(_orderId, msg.sender, order.seller);
+ emit OrderFilled(_orderId, msg.sender, seller);
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge
about 1 month ago
yeahchibyke Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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