Root + Impact
Description
## Summary
The OrderBook contract has inconsistent expiry logic that can lead to permanent
token locking. While expired orders cannot be bought (correctly blocked), they
remain in an "active" state and can only be cancelled by the seller. If a seller
forgets to cancel an expired order or loses access to their wallet, their tokens
remain locked in the contract indefinitely.
## Vulnerability Details
The contract implements expiry checks inconsistently across different functions:
1. --buyOrder() - Correctly prevents buying expired orders with `OrderExpired()` revert
2. --amendSellOrder() - Correctly prevents amending expired orders with `OrderExpired()` revert
3. --cancelSellOrder()** - **MISSING expiry check** - allows cancellation of expired orders
The `cancelSellOrder()` function only checks if the order is active (`isActive == true`) but doesn't
verify if the order has expired. This means:
- Expired orders remain in an "active" state (`isActive = true`)
- Expired orders cannot be bought (correctly blocked by `buyOrder()`)
- Expired orders cannot be amended (correctly blocked by `amendSellOrder()`)
- **Only the original seller can cancel expired orders**
- No mechanism exists for automatic cancellation or public cancellation
- Tokens can be locked forever if seller is unavailable
## Impact
- Permanent token locking: Seller's tokens can be locked indefinitely
- Poor user experience: Users must remember to cancel expired orders
- Lost funds: If seller loses wallet access, tokens are unrecoverable
- Contract bloat: Accumulation of expired but uncancelled orders
- Inconsistent state: Orders remain "active" even after expiry
## Affected Functions
- `cancelSellOrder()` - Lines 176-192 (missing expiry check)
- `getOrderDetailsString()` - Lines 220-250 (shows confusing status for expired orders)
Risk
Likelihood:
Impact:
Proof of Concept
# Proof of Concept
1. User creates order with 1-day deadline
2. Order expires after 1 day
3. Order remains "active" but cannot be bought
4. Only seller can cancel the expired order
5. If seller forgets or loses wallet access, tokens are locked forever
## The Problem
The `cancelSellOrder()` function doesn't check if an order has expired:
```solidity
function cancelSellOrder(uint256 _orderId) public {
if (!order.isActive) revert OrderAlreadyInactive();
order.isActive = false;
IERC20(order.tokenToSell).safeTransfer(order.seller, order.amountToSell);
}
```
## What Happens
- Expired orders remain `isActive = true`
- Cannot be bought** (correctly blocked by `buyOrder()`)
- Cannot be amended** (correctly blocked by `amendSellOrder()`)
- Can only be cancelled by seller** (the problem)
- If seller unavailable → tokens locked forever
Recommended Mitigation
# Mitigation
## What's Wrong
The `cancelSellOrder()` function doesn't check if an order has expired, creating inconsistent expiry logic. While expired orders cannot be bought or amended, they remain active and can only be cancelled by the seller, potentially leading to permanent token locking.
**Vulnerable Code:**
// Lines 176-192: cancelSellOrder()
function cancelSellOrder(uint256 _orderId) public {
Order storage order = orders[_orderId];
// Validation checks
if (order.seller == address(0)) revert OrderNotFound();
if (order.seller != msg.sender) revert NotOrderSeller();
if (!order.isActive) revert OrderAlreadyInactive(); // Only checks if active, not if expired
// No expiry validation here!
order.isActive = false;
IERC20(order.tokenToSell).safeTransfer(order.seller, order.amountToSell);
}
--The Problem:
Expired orders remain `isActive = true` and can only be cancelled by the seller.
If the seller forgets or loses wallet access, tokens are locked forever.
## How to Fix
### Solution 1: Add Expiry Check (Recommended)
function cancelSellOrder(uint256 _orderId) public {
Order storage order = orders[_orderId];
// Validation checks
if (order.seller == address(0)) revert OrderNotFound();
if (order.seller != msg.sender) revert NotOrderSeller();
if (!order.isActive) revert OrderAlreadyInactive();
if (block.timestamp >= order.deadlineTimestamp) revert OrderExpired(); // Add expiry check
order.isActive = false;
IERC20(order.tokenToSell).safeTransfer(order.seller, order.amountToSell);
}
### Solution 2: Automatically cleanup orders
// In buyOrder() - automatically clean up expired orders
function buyOrder(uint256 _orderId) public {
Order storage order = orders[_orderId];
// Auto-cleanup expired orders
if (block.timestamp >= order.deadlineTimestamp && order.isActive) {
order.isActive = false;
IERC20(order.tokenToSell).safeTransfer(order.seller, order.amountToSell);
emit OrderCancelled(_orderId, order.seller);
revert OrderExpired();
}
// ... rest of function
}