Root + Impact
Description
-
The OrderBook contract is designed to allow buyers to purchase orders at the price they see when submitting their transaction, ensuring fair and predictable trading where the execution price matches the expected price at transaction submission time.
-
A race condition exists between the amendSellOrder()
and buyOrder()
functions where sellers can modify order parameters (price, amount) while buyer transactions are pending in the mempool, causing buyers to execute transactions with different parameters than originally intended, leading to potential financial losses.
function amendSellOrder(uint256 _orderId, uint256 _newAmountToSell, uint256 _newPriceInUSDC, uint256 _newDeadlineDuration) public {
Order storage order = orders[_orderId];
@> order.amountToSell = _newAmountToSell;
@> order.priceInUSDC = _newPriceInUSDC;
order.deadlineTimestamp = newDeadlineTimestamp;
}
function buyOrder(uint256 _orderId) public {
Order storage order = orders[_orderId];
@> 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);
}
Risk
Likelihood:
-
Reason 1: Can occur whenever there's network congestion causing transaction delays, giving sellers opportunity to front-run
-
Reason 2: No protection mechanism exists to prevent order modifications during pending transactions
Impact:
Proof of Concept
Attack Scenario Explanation: The race condition occurs when a seller can amend their order after a buyer submits a purchase transaction but before it gets mined. This timing window allows sellers to front-run buyer transactions by changing the price to their advantage.
function amendSellOrder(uint256 _orderId, uint256 _newAmountToSell, uint256 _newPriceInUSDC, uint256 _newDeadlineDuration) public {
Order storage order = orders[_orderId];
order.amountToSell = _newAmountToSell;
order.priceInUSDC = _newPriceInUSDC;
order.deadlineTimestamp = newDeadlineTimestamp;
}
function buyOrder(uint256 _orderId) public {
Order storage order = orders[_orderId];
uint256 protocolFee = (order.priceInUSDC * FEE) / PRECISION;
iUSDC.safeTransferFrom(msg.sender, order.seller, order.priceInUSDC - protocolFee);
}
Recommended Mitigation
Add slippage protection by requiring buyers to specify their maximum acceptable price, preventing execution at unfavorable prices due to race conditions.
- function buyOrder(uint256 _orderId) public {
+ function buyOrder(uint256 _orderId, uint256 _maxPriceWillingToPay) public {
Order storage order = orders[_orderId];
if (order.seller == address(0)) revert OrderNotFound();
if (!order.isActive) revert OrderNotActive();
if (block.timestamp >= order.deadlineTimestamp) revert OrderExpired();
+ if (order.priceInUSDC > _maxPriceWillingToPay) revert PriceTooHigh();
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 custom error
+ error PriceTooHigh();