OrderBook

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

Expired orders remain marked as `isActive` onchain when they expire, this can mislead off-chain systems relying on order status

Expired orders remain marked as isActive onchain when they expire, this can mislead off-chain systems relying on order status

Description

The OrderBook contract does not mark expired orders as inactive once their deadlineTimestamp has passed. As a result, expired orders still return isActive == true even though they can no longer be filled. This causes inconsistency between the order’s actual status and its isActive flag.

struct Order {
uint256 id;
address seller;
address tokenToSell; // Address of wETH, wBTC, or wSOL
uint256 amountToSell; // Amount of tokenToSell
uint256 priceInUSDC; // Total USDC price for the entire amountToSell
uint256 deadlineTimestamp; // Block timestamp after which the order expires
bool isActive; // Flag indicating if the order is available to be bought
}
// @> Missing: automatic deactivation of expired orders based on block.timestamp

Risk

Likelihood:

  • This will occur when an order's deadlineTimestamp has passed. The order will remain flagged as isActive indefinitely, even though it is no longer fillable.

Impact:

  • Off-chain systems and frontends relying on isActive may incorrectly display expired orders as valid, leading to misleading user interface behavior and incorrect analytics.

Proof of Concept

This invariant test, designed to ensure that all expired orders retain their corresponding sell token balances within the contract, revealed a critical inconsistency. Orders with a deadlineTimestamp in the past remain flagged as isActive == true, despite having clearly expired on-chain.

The test reveals that the isActive flag does not reflect the expiration status, expired orders still appear active even though they cannot be filled on chain due to deadline enforcement

function invariant_expired_order_funds_should_be_held() public view {
uint256 totalOrders = book.getTotalOrders();
uint256 expectedLockedWETH = 0;
uint256 expectedLockedWBTC = 0;
uint256 expectedLockedWSOL = 0;
for (uint256 orderId = 1; orderId <= totalOrders; orderId++) {
(,, address tokenToSell, uint256 amountToSell,, uint256 deadlineTimestamp, bool isActive) = book.orders(orderId);
if (!isActive && amountToSell > 0 && block.timestamp > deadlineTimestamp) {
// `!isActive` returns false
if (tokenToSell == address(weth)) {
expectedLockedWETH += amountToSell;
} else if (tokenToSell == address(wbtc)) {
expectedLockedWBTC += amountToSell;
} else if (tokenToSell == address(wsol)) {
expectedLockedWSOL += amountToSell;
}
}
}
// Check the contract balances
// Check WETH
uint256 actualWETHBalance = IERC20(address(weth)).balanceOf(address(book));
if (actualWETHBalance != expectedLockedWETH) {
console.log("Invariant Failed for WETH");
console.log("Expected Locked WETH:", expectedLockedWETH);
console.log("Actual WETH Balance in Contract:", actualWETHBalance);
}
assert(actualWETHBalance == expectedLockedWETH);
// Check WBTC
uint256 actualWBTCBalance = IERC20(address(wbtc)).balanceOf(address(book));
if (actualWBTCBalance != expectedLockedWBTC) {
console.log("Invariant Failed for WBTC");
console.log("Expected Locked WBTC:", expectedLockedWBTC);
console.log("Actual WBTC Balance in Contract:", actualWBTCBalance);
}
assert(actualWBTCBalance == expectedLockedWBTC);
// Check WSOL
uint256 actualWSOLBalance = IERC20(address(wsol)).balanceOf(address(book));
if (actualWSOLBalance != expectedLockedWSOL) {
console.log("Invariant Failed for WSOL");
console.log("Expected Locked WSOL:", expectedLockedWSOL);
console.log("Actual WSOL Balance in Contract:", actualWSOLBalance);
}
assert(actualWSOLBalance == expectedLockedWSOL);
}
// Test Trace
[40362] Invariants::invariant_expired_order_funds_should_be_held()
├─ [2662] OrderBook::getTotalOrders() [staticcall]
│ └─ ← [Return] 1
├─ [16329] OrderBook::orders(1) [staticcall]
│ └─ ← [Return] 1, 0x0000000000000000000000000000000000001A1b, MockWETH: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 1774000000000000000000 [1.774e21], 8241, 2, true
├─ [2851] MockWETH::balanceOf(OrderBook: [0xc7183455a4C133Ae270771860664b6B7ec320bB1]) [staticcall]
│ └─ ← [Return] 1774000000000000000000 [1.774e21]

Recommended Mitigation

Use Chainlink Automation to periodically scan and cancel orders whose deadlineTimestamp has passed.

Updates

Lead Judging Commences

yeahchibyke Lead Judge 13 days 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.