Root + Impact
Description
-
The OrderBook contract is designed to provide fair trading where buyers can confidently purchase orders knowing they won't pay more than expected, similar to how traditional DEXs provide slippage protection to prevent unfavorable execution prices.
-
The buyOrder()
function lacks slippage protection, forcing buyers to accept whatever price is current at execution time without allowing them to specify a maximum acceptable price. This creates vulnerability to price manipulation and unfavorable execution, especially during volatile market conditions or network congestion.
function buyOrder(uint256 _orderId) 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();
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);
}
Risk
Likelihood:
-
Reason 1: Occurs whenever there's network congestion or sellers amend orders while buyer transactions are pending
-
Reason 2: No protection mechanism exists in the current implementation, making this inevitable during price volatility
Impact:
-
Impact 1: Financial losses for buyers who pay more than intended due to lack of price protection
-
Impact 2: Platform becomes unusable during market volatility, losing competitive advantage to DEXs with proper slippage protection
Proof of Concept
The vulnerability becomes critical when combined with order amendments or during network congestion, where buyers cannot control their execution price and may face significant unexpected costs.
contract SlippageVulnerabilityPoC {
function testNoSlippageProtection() public {
uint256 orderId = orderBook.createSellOrder(
address(weth), 1 ether, 200000e6, 1 hours
);
uint256 expectedPrice = 200000e6;
uint256 buyerBalance = usdc.balanceOf(buyer);
orderBook.amendSellOrder(orderId, 1 ether, 250000e6, 1 hours);
vm.prank(buyer);
orderBook.buyOrder(orderId);
uint256 actualCost = buyerBalance - usdc.balanceOf(buyer);
uint256 unexpectedLoss = actualCost - expectedPrice;
console.log("Expected cost:", expectedPrice);
console.log("Actual cost paid:", actualCost);
console.log("Unexpected slippage loss:", unexpectedLoss);
assert(actualCost > expectedPrice);
assert(unexpectedLoss == 50000e6);
}
function testVolatileMarketConditions() public {
uint256 orderId = orderBook.createSellOrder(
address(weth), 1 ether, 200000e6, 1 hours
);
orderBook.amendSellOrder(orderId, 1 ether, 220000e6, 1 hours);
orderBook.amendSellOrder(orderId, 1 ether, 250000e6, 1 hours);
orderBook.amendSellOrder(orderId, 1 ether, 300000e6, 1 hours);
vm.prank(buyer);
orderBook.buyOrder(orderId);
console.log("Final execution 50% above expected price - platform unusable");
}
}
Recommended Mitigation
Add slippage protection by requiring buyers to specify their maximum acceptable price, preventing execution at unfavorable prices.
- 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();