Description
The `buyOrder()` function enables any user to purchase an active order at a pre-defined USDC price. However, this price (priceInUSDC) is static and manually set by the seller, and not validated or updated based on on-chain price feeds (e.g., Chainlink or Uniswap TWAP).
This leads to a critical vulnerability:
If the price of the underlying asset (e.g., wETH, wBTC) fluctuates between order creation and fulfillment, an attacker can monitor mempool transactions and frontrun users by executing the order ahead of them if it's profitable based on real-time market price.
Example Scenario:
A user creates a sell order for 1 wETH at a static price of 3,000 USDC.
The price of wETH drops to 2,800 USDC on the open market.
A MEV bot notices that the on-chain price is now lower than the priceInUSDC in the order.
The bot frontruns any other buyer and purchases the order, then sells the wETH on the open market for a guaranteed profit.
This is a form of arbitrage-based frontrunning, and the protocol is giving away value to anyone who can act faster or closer to miners.
Risk
Likelihood: High
-
Reason 1
Anyone observing the mempool can copy the `buyOrder` transaction and attempt to execute it faster (with higher gas fees), effectively "sniping" the order.
Impact:
Users can suffer unintended economic loss due to stale pricing.
Protocol may suffer from reputation damage due to manipulation opportunities.
Whitelisted tokens can be targeted during volatile price swings, depleting seller capital.
Proof of Concept
Let’s simulate a frontrunning attack.
Scenario:
Raj creates a sell order for 1 wETH for 3,000 USDC (i.e., priceInUSDC = 3000e6).
- At the time of creation, this reflects fair market price.
- Later, wETH price drops to 2,800 USDC on-chain.
- Khushi, a normal user, attempts to buy the order, thinking it’s still good value.
- An MEV bot (Nisarg) detects this order in the mempool, and sends a priority transaction to buy the same order before Khushi.
- Nisarg succeeds due to higher gas fee.
- Nisarg now sells the wETH on-chain for 2,800 USDC, pocketing 200 USDC in arbitrage.
Consequences:
- Khushi’s transaction fails or wastes gas.
- Nisarg profits off arbitrage without risk.
- Seller doesn’t care – their order was filled.
- Protocol loses user trust.
Recommended Mitigation
Option 1: Add Buyer Slippage Control (Simple + Effective)
Add a maxUSDCAmount parameter that the buyer is willing to spend.
Update buyOrder():
```diff
- function buyOrder(uint256 _orderId) public {
+ function buyOrder(uint256 _orderId, uint256 maxUSDCAmount) 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 > maxUSDCAmount) revert InvalidPrice(); // Slippage check
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);
}
```
Benefits:
Buyer is protected from stale pricing.
Simple to implement with minimal gas overhead.
No additional trust assumption