CEI Pattern Violation in buyOrder() Enables Reentrancy and Fee Manipulation
Description
The buyOrder()
function should safely execute a token purchase by collecting USDC payment (including protocol fees), transferring tokens to the buyer, paying the seller, and updating fee accounting atomically without risk of reentrancy.
Specific Issue
The function violates the Checks-Effects-Interactions (CEI) pattern by making multiple external calls before completing the final state update (totalFees += protocolFee
), creating a reentrancy vulnerability that allows malicious contracts to manipulate fee accounting and potentially exploit the order execution process.
function buyOrder(uint256 _orderId) public {
Order storage order = orders[_orderId];
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:
-
USDC and established tokens (wETH, wBTC, wSOL) have standard implementations
-
These legitimate tokens are unlikely to contain malicious reentrancy code
-
However, the vulnerability exists and could be exploited if any involved token had non-standard behavior
-
Most critical function for value exchange makes this a high-priority fix
Impact:
totalFees
accounting manipulation - attacker could prevent proper fee tracking
Could potentially execute multiple orders or manipulate protocol revenue
Most severe impact among all CEI violations due to direct value exchange
Proof of Concept
contract MaliciousUSDC {
OrderBook orderBook;
uint256 targetOrderId;
bool attacking;
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
if (!attacking && to == address(orderBook)) {
attacking = true;
orderBook.buyOrder(targetOrderId);
}
return true;
}
}
contract MaliciousToken {
function transfer(address to, uint256 amount) external returns (bool) {
if (to != address(orderBook)) {
orderBook.buyOrder(anotherOrderId);
}
return true;
}
}
Recommended Mitigation
Follow the Checks-Effects-Interactions pattern by moving all state updates before external calls.
Alternatively, add a reentrancy guard.
Additional security consideration: Consider adding a self-purchase prevention check:
function buyOrder(uint256 _orderId) public {
Order storage order = orders[_orderId];
if (order.seller == address(0)) revert OrderNotFound();
+ if (order.seller == msg.sender) revert CannotBuyOwnOrder();
if (!order.isActive) revert OrderNotActive();
if (block.timestamp >= order.deadlineTimestamp) revert OrderExpired();
// ... rest of implementation
}