pragma solidity ^0.8.0;
import {Test, console2} from "forge-std/Test.sol";
import {OrderBook} from "../src/OrderBook.sol";
import {MockUSDC} from "./mocks/MockUSDC.sol";
import {MockWBTC} from "./mocks/MockWBTC.sol";
import {MockWETH} from "./mocks/MockWETH.sol";
import {MockWSOL} from "./mocks/MockWSOL.sol";
* @title State Inconsistency in Order Expiration
* @notice Demonstrates critical flaws in order expiration state management
* @dev Shows how expired orders remain "active" creating confusion and potential exploits
*/
contract ExpirationStateInconsistency is Test {
OrderBook book;
MockUSDC usdc;
MockWBTC wbtc;
MockWETH weth;
MockWSOL wsol;
address owner;
address seller;
address buyer;
function setUp() public {
owner = makeAddr("protocol_owner");
seller = makeAddr("seller");
buyer = makeAddr("buyer");
usdc = new MockUSDC(6);
wbtc = new MockWBTC(8);
weth = new MockWETH(18);
wsol = new MockWSOL(18);
vm.prank(owner);
book = new OrderBook(address(weth), address(wbtc), address(wsol), address(usdc), owner);
weth.mint(seller, 10e18);
usdc.mint(buyer, 50_000e6);
}
* @notice CRITICAL: Demonstrates state inconsistency in expired orders
* @dev Shows how orders remain "active" after expiration, causing confusion
*/
function test_ExpiredOrderStateInconsistency() public {
console2.log("\n=== EXPIRED ORDER STATE INCONSISTENCY EXPLOIT ===");
console2.log("Demonstrating how expired orders remain 'active' in state");
vm.startPrank(seller);
weth.approve(address(book), 1e18);
uint256 orderId = book.createSellOrder(
address(weth),
1e18,
3000e6,
1 hours
);
vm.stopPrank();
console2.log("Step 1: Order created with 1 hour deadline");
console2.log("Order ID:", orderId);
OrderBook.Order memory orderBefore = book.getOrder(orderId);
console2.log("Initial state - isActive:", orderBefore.isActive);
console2.log("Initial state - deadline:", orderBefore.deadlineTimestamp);
console2.log("Current timestamp:", block.timestamp);
vm.warp(block.timestamp + 2 hours);
console2.log("\nStep 2: Fast forwarded 2 hours past deadline");
console2.log("New timestamp:", block.timestamp);
OrderBook.Order memory orderAfter = book.getOrder(orderId);
console2.log("\n=== STATE INCONSISTENCY DETECTED ===");
console2.log("Order isActive:", orderAfter.isActive);
console2.log("Order deadline:", orderAfter.deadlineTimestamp);
console2.log("Current time:", block.timestamp);
console2.log("Is expired?", block.timestamp >= orderAfter.deadlineTimestamp);
console2.log("But isActive is still:", orderAfter.isActive);
string memory orderDetails = book.getOrderDetailsString(orderId);
console2.log("\nOrder details string:");
console2.log(orderDetails);
assertTrue(orderAfter.isActive, "Order should appear active in storage");
assertTrue(
block.timestamp >= orderAfter.deadlineTimestamp,
"Order should be past deadline"
);
vm.startPrank(buyer);
usdc.approve(address(book), 5000e6);
vm.expectRevert(OrderBook.OrderExpired.selector);
book.buyOrder(orderId);
vm.stopPrank();
console2.log("\n INCONSISTENCY CONFIRMED:");
console2.log("- Order appears active in storage (isActive = true)");
console2.log("- Order is past deadline (expired)");
console2.log("- buyOrder correctly reverts");
console2.log("- But other functions may behave unexpectedly with stale state");
}
* @notice Demonstrates amendment blocking on expired orders
* @dev Shows how expired orders can't be amended, creating operational issues
*/
function test_ExpiredOrderAmendmentBlocking() public {
console2.log("\n=== EXPIRED ORDER AMENDMENT BLOCKING ===");
vm.startPrank(seller);
weth.approve(address(book), 1e18);
uint256 orderId = book.createSellOrder(address(weth), 1e18, 3000e6, 1 hours);
vm.stopPrank();
vm.warp(block.timestamp + 2 hours);
vm.startPrank(seller);
vm.expectRevert(OrderBook.OrderExpired.selector);
book.amendSellOrder(orderId, 1e18, 3500e6, 1 hours);
vm.stopPrank();
console2.log(" Expired orders cannot be amended");
console2.log("Sellers must cancel and recreate, but state remains confusing");
}
* @notice Demonstrates the impact of stale active state on external integrations
* @dev Shows how external systems might misinterpret order availability
*/
function test_StaleStateImpactOnIntegrations() public {
console2.log("\n=== STALE STATE IMPACT ON EXTERNAL INTEGRATIONS ===");
vm.startPrank(seller);
weth.approve(address(book), 5e18);
uint256 orderId1 = book.createSellOrder(address(weth), 1e18, 3000e6, 30 minutes);
uint256 orderId2 = book.createSellOrder(address(weth), 1e18, 3100e6, 1 hours);
uint256 orderId3 = book.createSellOrder(address(weth), 1e18, 3200e6, 2 hours);
vm.stopPrank();
console2.log("Created 3 orders with 30min, 1hr, 2hr deadlines");
vm.warp(block.timestamp + 90 minutes);
console2.log("Fast forwarded 1.5 hours");
OrderBook.Order memory order1 = book.getOrder(orderId1);
OrderBook.Order memory order2 = book.getOrder(orderId2);
OrderBook.Order memory order3 = book.getOrder(orderId3);
console2.log("\n=== EXTERNAL SYSTEM VIEW ===");
console2.log("Order 1 - isActive:", order1.isActive, "| Actually expired:",
block.timestamp >= order1.deadlineTimestamp);
console2.log("Order 2 - isActive:", order2.isActive, "| Actually expired:",
block.timestamp >= order2.deadlineTimestamp);
console2.log("Order 3 - isActive:", order3.isActive, "| Actually expired:",
block.timestamp >= order3.deadlineTimestamp);
uint256 apparentlyActiveOrders = 0;
uint256 actuallyActiveOrders = 0;
if (order1.isActive) apparentlyActiveOrders++;
if (order2.isActive) apparentlyActiveOrders++;
if (order3.isActive) apparentlyActiveOrders++;
if (order1.isActive && block.timestamp < order1.deadlineTimestamp) actuallyActiveOrders++;
if (order2.isActive && block.timestamp < order2.deadlineTimestamp) actuallyActiveOrders++;
if (order3.isActive && block.timestamp < order3.deadlineTimestamp) actuallyActiveOrders++;
console2.log("\nApparently active orders:", apparentlyActiveOrders);
console2.log("Actually active orders:", actuallyActiveOrders);
assertEq(apparentlyActiveOrders, 3, "All orders appear active");
assertEq(actuallyActiveOrders, 1, "Only one order is actually active");
console2.log("\n INTEGRATION ISSUE CONFIRMED:");
console2.log("External systems see", apparentlyActiveOrders, "active orders");
console2.log("But only", actuallyActiveOrders, "can actually be filled");
}
* @notice Demonstrates the cancellation of expired orders
* @dev Shows how expired orders can still be cancelled, returning tokens
*/
function test_ExpiredOrderCancellation() public {
console2.log("\n=== EXPIRED ORDER CANCELLATION BEHAVIOR ===");
vm.startPrank(seller);
weth.approve(address(book), 1e18);
uint256 orderId = book.createSellOrder(address(weth), 1e18, 3000e6, 1 hours);
vm.stopPrank();
uint256 sellerBalanceBefore = weth.balanceOf(seller);
uint256 contractBalanceBefore = weth.balanceOf(address(book));
console2.log("Seller balance before:", sellerBalanceBefore / 1e18, "wETH");
console2.log("Contract balance before:", contractBalanceBefore / 1e18, "wETH");
vm.warp(block.timestamp + 2 hours);
vm.startPrank(seller);
book.cancelSellOrder(orderId);
vm.stopPrank();
uint256 sellerBalanceAfter = weth.balanceOf(seller);
uint256 contractBalanceAfter = weth.balanceOf(address(book));
console2.log("Seller balance after:", sellerBalanceAfter / 1e18, "wETH");
console2.log("Contract balance after:", contractBalanceAfter / 1e18, "wETH");
OrderBook.Order memory orderAfter = book.getOrder(orderId);
assertFalse(orderAfter.isActive, "Order should be inactive after cancellation");
console2.log("\n CANCELLATION SUCCESSFUL:");
console2.log("Expired orders can be cancelled normally");
console2.log("This is correct behavior, but state inconsistency remains an issue");
}
}
forge test --match-contract ExpirationStateInconsistency --via-ir -vv
[⠰] Compiling...
[⠑] Compiling 1 files with Solc 0.8.26
[⠃] Solc 0.8.26 finished in 5.20s
Compiler run successful!
Ran 4 tests for test/ExpirationStateInconsistency.t.sol:ExpirationStateInconsistency
[PASS] test_ExpiredOrderAmendmentBlocking() (gas: 225843)
Logs:
=== EXPIRED ORDER AMENDMENT BLOCKING ===
Expired orders cannot be amended
Sellers must cancel and recreate, but state remains confusing
[PASS] test_ExpiredOrderCancellation() (gas: 207370)
Logs:
=== EXPIRED ORDER CANCELLATION BEHAVIOR ===
Seller balance before: 9999999999999999999 wETH
Contract balance before: 1 wETH
Seller balance after: 10000000000000000000 wETH
Contract balance after: 0 wETH
CANCELLATION SUCCESSFUL:
Expired orders can be cancelled normally
This is correct behavior, but state inconsistency remains an issue
[PASS] test_ExpiredOrderStateInconsistency() (gas: 298797)
Logs:
=== EXPIRED ORDER STATE INCONSISTENCY EXPLOIT ===
Demonstrating how expired orders remain 'active' in state
Step 1: Order created with 1 hour deadline
Order ID: 1
Initial state - isActive: true
Initial state - deadline: 3601
Current timestamp: 1
Step 2: Fast forwarded 2 hours past deadline
New timestamp: 7201
=== STATE INCONSISTENCY DETECTED ===
Order isActive: true
Order deadline: 3601
Current time: 7201
Is expired? true
But isActive is still: true
Order details string:
Order ID: 1
Seller: 0xdfa97bfe5d2b2e8169b194eaa78fbb793346b174
Selling: 1000000000000000000 wETH
Asking Price: 3000000000 USDC
Deadline Timestamp: 3601
Status: Expired (Awaiting Cancellation)
INCONSISTENCY CONFIRMED:
- Order appears active in storage (isActive = true)
- Order is past deadline (expired)
- buyOrder correctly reverts
- But other functions may behave unexpectedly with stale state
[PASS] test_StaleStateImpactOnIntegrations() (gas: 590860)
Logs:
=== STALE STATE IMPACT ON EXTERNAL INTEGRATIONS ===
Created 3 orders with 30min, 1hr, 2hr deadlines
Fast forwarded 1.5 hours
=== EXTERNAL SYSTEM VIEW ===
Order 1 - isActive: true | Actually expired: true
Order 2 - isActive: true | Actually expired: true
Order 3 - isActive: true | Actually expired: false
Apparently active orders: 3
Actually active orders: 1
INTEGRATION ISSUE CONFIRMED:
External systems see 3 active orders
But only 1 can actually be filled
Suite result: ok. 4 passed; 0 failed; 0 skipped; finished in 3.78ms (7.21ms CPU time)
Ran 1 test suite in 8.24ms (3.78ms CPU time): 4 tests passed, 0 failed, 0 skipped (4 total tests)
This mitigation addresses the state inconsistency by implementing dynamic state updates in view functions and centralizing order validation logic. The getOrder
function now returns accurate isActive
status based on current time, while the new _isOrderActiveAndValid
internal function provides consistent validation across all contract functions. The markExpiredOrdersInactive
function allows for batch cleanup of expired orders to maintain accurate on-chain state. This approach ensures that external integrations receive reliable order data while maintaining gas efficiency by only updating storage when necessary, eliminating the confusion between displayed status and actual order availability.