OrderBook

First Flight #43
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: high
Valid

Protocol is Vulnerable to Front-Running on Key Actions

Protocol is Vulnerable to Front-Running on Key Actions

Description

  • The protocol's core functions for interacting with orders (buyOrder, cancelSellOrder, amendSellOrder) are susceptible to front-running attacks. Due to the public and transparent nature of the blockchain mempool, a malicious actor (e.g., a MEV bot) can observe these pending transactions and execute their own transactions first. This allows attackers to steal profitable opportunities from both buyers and sellers, forcing them into unwanted trades or causing their intended actions to fail.

// functions are just using the orderId parameter for key actions
function amendSellOrder(
@> uint256 _orderId,
uint256 _newAmountToSell,
uint256 _newPriceInUSDC,
uint256 _newDeadlineDuration
) public { }
@> function cancelSellOrder(uint256 _orderId) public { }
@> function buyOrder(uint256 _orderId) public { }

Risk

Likelihood:

  • Blocking the amendOrder or cancelOrder: attacker adds buyOrder in front of the sellers' transactions.

  • Preempting the buyOrder: attacker preoccupies the good chance for buying tokens by duplicating the transaction.

Impact:

  • Sellers' Profit Stolen: Sellers might unwilling to sell the tokens at previous lower price expecting more profit later, but tokens are forced to sell at previous price.

  • Buyers' Profit Stolen: Previous buyer's order is replaced with the attacker's order, the profit is transferred to the attacker.

Proof of Concept

Below code shows both problems.

  1. Seller's Profit Stolen: Alice is willing to sell 1e8 WBTC with 100_000e6 USDC, after she want to cancel due to the rise. Dan sees the tx in mempool and buy with the previous price blocking the cancel.

  2. Buyers' Profit Stolen: Bob created order that selling 1e18 WETH in 2_000e6 USDC, which is flavorable to Alice. Alice sends buyOrder tx and is intercepted by Dan who steals the profit.

function test_audit_frontRunning() public {
// 1. blocks sellers' interactions
// alice sells 1e8 wbtc with 100_000e6 usdc
uint256 sellingWBTCAmount = 1e8;
uint256 usdcPrice = 100_000e6;
vm.startPrank(alice);
wbtc.approve(address(book), sellingWBTCAmount);
uint256 aliceId = book.createSellOrder(address(wbtc), sellingWBTCAmount, usdcPrice, 2 days);
vm.stopPrank();
// btc goes up 105_000e6 usdc, so alice want to cancel(or amend)
// while the cancelSellOrder tx is in mempool, dan frontruns alice's order
// dan buys 1e8 wbtc in 100_000e6 usdc
vm.startPrank(dan);
usdc.approve(address(book), usdcPrice);
book.buyOrder(aliceId);
vm.stopPrank();
vm.prank(alice);
// cancellation is reverted because matched already
vm.expectRevert(OrderBook.OrderAlreadyInactive.selector);
book.cancelSellOrder(aliceId);
// 2. replaces buyers' interactions
// bob sells 1e18 weth with 2_000e6 usdc
uint256 sellingWETHAmount = 1e18;
usdcPrice = 2_000e6;
vm.startPrank(bob);
weth.approve(address(book), sellingWETHAmount);
uint256 bobId = book.createSellOrder(address(weth), sellingWETHAmount, usdcPrice, 2 days);
vm.stopPrank();
// alice wants to buy weth, so submitted buyOrder
// while the buyOrder tx is in mempool, dan frontruns alice's order
vm.startPrank(dan);
usdc.approve(address(book), usdcPrice);
book.buyOrder(bobId);
vm.stopPrank();
vm.startPrank(alice);
usdc.approve(address(book), usdcPrice);
// purchase is reverted because matched already
vm.expectRevert(OrderBook.OrderNotActive.selector);
book.buyOrder(bobId);
vm.stopPrank();
}

Recommended Mitigation

Use the commit-reveal scheme for hiding the intention of users. Below is the sample code for replacing the buyOrder without making the orderId public.

+ mapping(address => bytes32) public orderCommitments;
+ event OrderCommitted(address indexed buyer, bytes32 indexed commitment);
+ function commitToBuyOrder(bytes32 _commitment) public {
+ require(_commitment != bytes32(0), "Commitment cannot be empty");
+ orderCommitments[msg.sender] = _commitment;
+ emit OrderCommitted(msg.sender, _commitment);
+ }
+ function revealAndBuyOrder(uint256 _orderId, bytes32 _secretNonce) public {
+ bytes32 commitment = orderCommitments[msg.sender];
+ require(commitment != bytes32(0), "No commitment found");
+
+ bytes32 recalculatedHash = keccak256(abi.encodePacked(_orderId, _secretNonce));
+
+ require(recalculatedHash == commitment, "Invalid reveal");
+ orderCommitments[msg.sender] = bytes32(0);
...
+ }
Updates

Lead Judging Commences

yeahchibyke Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

Amends or cancellation of sell orders can be front-run

When a seller wants to amend or cancel their sell orders, a malicious entity can front-run their transactions and buy out the orders. This can be especially harmful when real-world prices of listed assets fluctuate and sellers want to adjust the prices listed in their orders.

Support

FAQs

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