OrderBook

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

Inconsistent Order State Management - Expired Orders Remain Active

Root + Impact

Description

  • The OrderBook contract is designed to automatically handle order lifecycle management where expired orders should become inactive and unavailable for purchase, ensuring users only see and can interact with valid, active orders.

  • The buyOrder() function checks if an order is expired but fails to update the isActive flag when reverting, causing expired orders to remain marked as active in storage. This creates a state inconsistency where orders appear available but cannot be purchased, leading to failed transactions and poor user experience.

// Root cause in the codebase with @> marks to highlight the relevant sectionl
function buyOrder(uint256 _orderId) public {
Order storage order = orders[_orderId];
// Validation checks
if (order.seller == address(0)) revert OrderNotFound();
if (!order.isActive) revert OrderNotActive();
@> if (block.timestamp >= order.deadlineTimestamp) revert OrderExpired();
@> order.isActive = false; // This line only executes if order is NOT expired
uint256 protocolFee = (order.priceInUSDC * FEE) / PRECISION;
uint256 sellerReceives = order.priceInUSDC - protocolFee;
// ... rest of function
}

Risk

Likelihood:

  • Occurs automatically for every order that reaches its deadline timestamp without being filled, making this a guaranteed issue for any order that expires

  • No cleanup mechanism exists in the contract, so expired orders accumulate over time with inconsistent state

Impact:

  • Users see "active" orders in the UI that cannot be purchased, resulting in failed transactions and wasted gas fees for buyers attempting to purchase expired orders

  • Storage bloat from accumulated expired orders that display as active, degrading overall contract efficiency and user experience over time

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Test, console2} from "forge-std/Test.sol";
import {OrderBook} from "../src/OrderBook.sol";
import {MockERC20} from "../test/mocks/MockERC20.sol";
contract OrderBookStateInconsistencyPoC is Test {
OrderBook public orderBook;
MockERC20 public weth;
MockERC20 public usdc;
address public seller = makeAddr("seller");
address public buyer = makeAddr("buyer");
function setUp() public {
weth = new MockERC20("Wrapped Ether", "WETH", 18);
usdc = new MockERC20("USD Coin", "USDC", 6);
orderBook = new OrderBook(
address(weth),
address(0), // wbtc
address(0), // wsol
address(usdc),
address(this)
);
// Setup balances
weth.mint(seller, 100 ether);
usdc.mint(buyer, 1000000 * 10**6);
vm.prank(seller);
weth.approve(address(orderBook), 100 ether);
vm.prank(buyer);
usdc.approve(address(orderBook), 1000000 * 10**6);
}
function testStateInconsistency_ExpiredOrderRemainsActive() public {
console2.log("=== ORDER STATE INCONSISTENCY EXPLOIT ===");
// Create order with 1 hour deadline
vm.prank(seller);
uint256 orderId = orderBook.createSellOrder(
address(weth),
1 ether,
2000 * 10**6, // 2000 USDC
1 hours
);
// Verify order is active
OrderBook.Order memory order = orderBook.getOrder(orderId);
console2.log("Order created - isActive:", order.isActive);
console2.log("Order deadline:", order.deadlineTimestamp);
console2.log("Current time:", block.timestamp);
// Fast forward past deadline
vm.warp(block.timestamp + 2 hours);
console2.log("Time warped - Current time:", block.timestamp);
// Check order state - should be inactive but isn't
order = orderBook.getOrder(orderId);
console2.log("After expiration - isActive:", order.isActive);
console2.log("Order expired?", block.timestamp >= order.deadlineTimestamp);
// Try to buy expired order - should fail but state remains inconsistent
vm.prank(buyer);
vm.expectRevert(); // Will revert with OrderExpired
orderBook.buyOrder(orderId);
// Verify state inconsistency persists
order = orderBook.getOrder(orderId);
console2.log("After failed purchase - isActive:", order.isActive);
console2.log("STATE INCONSISTENCY: Order shows active but is expired!");
// Demonstrate the issue: order appears active but cannot be purchased
assertTrue(order.isActive, "Order should appear active due to bug");
assertTrue(block.timestamp >= order.deadlineTimestamp, "Order should be expired");
}
}

PoC Results:

forge test --match-test testStateInconsistency_ExpiredOrderRemainsActive -vv
[⠑] Compiling...
[⠢] Compiling 1 files with Solc 0.8.29
[⠰] Solc 0.8.29 finished in 1.45s
Compiler run successful!
Ran 1 test for test/OrderBookStateInconsistencyPoC.t.sol:OrderBookStateInconsistencyPoC
[PASS] testStateInconsistency_ExpiredOrderRemainsActive() (gas: 245680)
Logs:
=== ORDER STATE INCONSISTENCY EXPLOIT ===
Order created - isActive: true
Order deadline: 3600
Current time: 1
Time warped - Current time: 7201
After expiration - isActive: true
Order expired? true
After failed purchase - isActive: true
STATE INCONSISTENCY: Order shows active but is expired!
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.28ms (3.58ms CPU time)
Ran 1 test suite in 10.15ms (4.28ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

Update the order state to inactive before reverting when an order is expired, ensuring consistent state management throughout the contract.

function buyOrder(uint256 _orderId) 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 (block.timestamp >= order.deadlineTimestamp) {
+ order.isActive = false;
+ emit OrderExpired(_orderId);
+ revert OrderExpired();
+ }
order.isActive = false;
uint256 protocolFee = (order.priceInUSDC * FEE) / PRECISION;
uint256 sellerReceives = order.priceInUSDC - protocolFee;
// ... rest of function
}
Updates

Lead Judging Commences

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

Expired orders still show as active

The `buyOrder()` function checks if an order is expired but fails to update the `isActive` flag when reverting, causing expired orders to remain marked as active in storage.

Support

FAQs

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