OrderBook

First Flight #43
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Impact: high
Likelihood: low
Invalid

Predictable `orderId` Enables Order Impersonation

Predictable orderId Enables Order Impersonation

Description

  • The protocol generates orderIds using a simple, sequential counter (_nextOrderId). This makes the ID of the next order to be created completely predictable.

  • A malicious actor can exploit this predictability to conduct an "Order ID Preemption" or "Order Impersonation" attack. By front-running a legitimate seller's createSellOrder transaction, an attacker can claim an anticipated orderId for their own malicious order, tricking a buyer into executing a trade with the wrong party under unfavorable terms.

function createSellOrder(...) {
...
@> uint256 orderId = _nextOrderId++;
}
@> function buyOrder(uint256 _orderId) public {
@> Order storage order = orders[_orderId];
...
}

Risk

Likelihood:

The vulnerability stems from the _nextOrderId++ logic in the createSellOrder function. Because the next ID is public knowledge, a sophisticated attacker can execute the following attack:

  1. An attacker observes the mempool and sees two transactions submitted: one from a legitimate Seller calling createSellOrder, and another from a Buyer calling buyOrder with the predicted, upcoming orderId. This scenario is most likely when the buyer and seller are coordinating off-chain or in bot-driven activity.

  2. The attacker submits their own createSellOrder transaction with a higher gas fee. They can set malicious terms for this order (e.g., a very high price or low amount).

  3. The miner includes the attacker's transaction first. The attacker's order is created and assigned the predicted orderId.

  4. The legitimate Seller's transaction is included next. It is now assigned the next orderId.

  5. The Buyer's buyOrder transaction is included last. It executes against the orderId it specified, which now points to the attacker's malicious order, not the legitimate seller's order.

Impact:

This attack undermines the integrity of the entire order book, as buyers can no longer be certain who they are trading with.

Proof of Concept

The following scenario demonstrates an attacker (Bob) preempting an orderId that a buyer (Dan) intended to fill from a legitimate seller (Alice).

function test_audit_predictableId() public {
// Dan intends to buy Alice's upcoming order (which he predicts will be ID 1)
// and pre-approves the USDC transfer.
vm.startPrank(dan);
usdc.approve(address(book), 100_000e6);
vm.stopPrank();
// In the mempool, Alice's createSellOrder and Dan's buyOrder(1) are visible.
// Bob (attacker) sees this and front-runs Alice to claim order ID 1.
vm.startPrank(bob);
weth.approve(address(book), 1); // Bob's malicious order will only sell 1 wei of WETH
// Bob's transaction gets mined first.
uint256 bobOrderId = book.createSellOrder(address(weth), 1, 100_000e6, 2 days);
assertEq(bobOrderId, 1, "Bob should successfully claim Order ID 1");
vm.stopPrank();
// Alice's transaction is mined next. Her order gets the next ID.
vm.startPrank(alice);
wbtc.approve(address(book), 1e8);
uint256 aliceOrderId = book.createSellOrder(address(wbtc), 1e8, 100_000e6, 2 days);
assertEq(aliceOrderId, 2, "Alice's order gets bumped to ID 2");
vm.stopPrank();
// Dan's transaction is mined last. He unknowingly buys Bob's malicious order.
vm.startPrank(dan);
book.buyOrder(1);
vm.stopPrank();
// Assert the outcome: Dan paid the full price for Bob's malicious order.
assertEq(weth.balanceOf(dan), 1, "Dan was tricked into buying 1 wei of WETH");
assertEq(wbtc.balanceOf(dan), 0, "Dan did not receive the WBTC he expected from Alice");
}

Recommended Mitigation

Let the orderId be non-sequential and unpredictable. The best practice is to derive the orderId from a hash of order-specific details that are unique to the seller. This can be achieved by hashing the seller's address with a personal, incrementing nonce.

struct Order {
- uint256 id;
+ bytes32 id;
...
}
- mapping(uint256 => Order) public orders;
- uint256 private _nextOrderId;
+ mapping(bytes32 => Order) public orders;
+ mapping(address => uint256) public userNonces;
+ // Needs to modify events from uint256 orderId to bytes32 orderId
function createSellOrder(
address _tokenToSell,
uint256 _amountToSell,
uint256 _priceInUSDC,
uint256 _deadlineDuration
- ) public returns (uint256) {
+ ) public returns (bytes32) {
...
uint256 deadlineTimestamp = block.timestamp + _deadlineDuration;
- uint256 orderId = _nextOrderId++;
+ uint256 nonce = userNonces[msg.sender]++;
+ bytes32 orderId = keccak256(abi.encode(msg.sender, nonce));
+ if(orders[orderId].seller == address(0)) revert OrderIdCollided();
IERC20(_tokenToSell).safeTransferFrom(msg.sender, address(this), _amountToSell);
...
}
+ // Needs to update other functions using orderId
Updates

Lead Judging Commences

yeahchibyke Lead Judge
about 1 month ago
yeahchibyke Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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