OrderBook

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

Expired Order Fund Lock Vulnerability

Summary

The OrderBook contract allows orders to expire while remaining marked as active (isActive = true), causing seller funds to be indefinitely locked in the contract. Expired orders cannot be purchased, modified, or automatically cancelled, requiring manual intervention from sellers who may forget or be unable to cancel their orders.

Root Cause

The contract design allows orders to exist in an inconsistent state where:

  1. order.isActive = true (order appears active)

  2. block.timestamp >= order.deadlineTimestamp (order is expired)

This creates "zombie orders" that cannot be interacted with:

function buyOrder(uint256 _orderId) public {
// ...
if (block.timestamp >= order.deadlineTimestamp) revert OrderExpired(); // Cannot buy
}
function amendSellOrder(uint256 _orderId, ...) public {
// ...
if (block.timestamp >= order.deadlineTimestamp) revert OrderExpired(); // Cannot modify
}
// Only cancelSellOrder works for expired orders

Internal pre-conditions

  1. Order must be created with a deadline timestamp

  2. Current block timestamp must exceed the order's deadline

  3. Order must still have isActive = true

  4. Seller funds are locked in the contract

External pre-conditions

  1. Seller creates an order and forgets about it

  2. Market conditions change, making the order unattractive

  3. Seller becomes inactive or loses access to their account

  4. Time passes beyond the order deadline

Attack Path

  1. Natural Expiration: Orders naturally expire due to time passage

  2. Fund Lock: Seller funds remain locked in contract

  3. Interaction Failure: All attempts to buy/modify expired orders fail

  4. Manual Recovery Required: Only seller can manually cancel to recover funds

  5. Potential Permanent Lock: If seller is inactive, funds may be permanently locked

Impact

  1. Fund Lock Risk: Seller funds can be indefinitely locked

  2. Poor User Experience: Users must manually track and cancel expired orders

  3. Capital Inefficiency: Locked funds cannot be used for other purposes

  4. Protocol Liability: Contract holds funds that cannot be accessed

  5. Scalability Issues: Accumulation of expired orders clutters the system

PoC

Test demonstrates the fund lock scenario:

Scenario:

  1. Seller creates order with 1 hour deadline

  2. Time advances beyond deadline (order expires)

  3. Order remains isActive = true but cannot be purchased

  4. Seller funds remain locked until manual cancellation

Test Results:

  • Expired order cannot be purchased (reverts with "OrderExpired")

  • Expired order cannot be modified (reverts with "OrderExpired")

  • Funds remain locked in contract until seller manually cancels

  • Multiple expired orders accumulate locked funds

Critical Finding:

If sellers forget to cancel expired orders or become inactive, their funds may be permanently locked in the contract.

Mitigation

Option 1: Auto-Cancel Expired Orders in buyOrder

function buyOrder(uint256 _orderId) public {
Order storage order = orders[_orderId];
if (order.seller == address(0)) revert OrderNotFound();
if (!order.isActive) revert OrderNotActive();
// Auto-cancel expired orders
if (block.timestamp >= order.deadlineTimestamp) {
order.isActive = false;
IERC20(order.tokenToSell).safeTransfer(order.seller, order.amountToSell);
emit OrderCancelled(_orderId, order.seller);
revert OrderExpired();
}
// Continue with normal purchase logic...
}

Option 2: Separate Cleanup Function

function cleanupExpiredOrder(uint256 _orderId) external {
Order storage order = orders[_orderId];
if (order.seller == address(0)) revert OrderNotFound();
if (!order.isActive) revert OrderAlreadyInactive();
if (block.timestamp < order.deadlineTimestamp) revert OrderNotExpired();
order.isActive = false;
IERC20(order.tokenToSell).safeTransfer(order.seller, order.amountToSell);
emit OrderCancelled(_orderId, order.seller);
}

Option 3: Batch Cleanup Function

function cleanupExpiredOrders(uint256[] calldata orderIds) external {
for (uint256 i = 0; i < orderIds.length; i++) {
uint256 orderId = orderIds[i];
Order storage order = orders[orderId];
if (order.isActive && block.timestamp >= order.deadlineTimestamp) {
order.isActive = false;
IERC20(order.tokenToSell).safeTransfer(order.seller, order.amountToSell);
emit OrderCancelled(orderId, order.seller);
}
}
}

Recommended Solution:

Implement Option 1 (auto-cancel in buyOrder) combined with Option 2 (cleanup function) to provide both automatic and manual cleanup mechanisms.

Updates

Lead Judging Commences

yeahchibyke Lead Judge 11 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Expired orders can cause backlog

By design only `seller` can call `cancelSellOrder()` on their `order`. But when an `order` expires, and the `seller` doesn't have access to the protocol, the expired `order `should be be able to be cancelled by an `admin`.

Support

FAQs

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