OrderBook

First Flight #43
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Orders can be extended beyond the maximum deadline set by the protocol due to the OrderBook::amendSellOrder function

Root + Impact

amendSellOrder ignores previous deadline, enabling orders to exceed the maximum allowed duration

Description

The amendSellOrder function enforces that the newDeadlineDuration is less than or equal to the maximum allowed duration (MAX_DEADLINE_DURATION), but it does not account for the time already elapsed since the order was created.
This allows the seller to repeatedly extend the order's deadline by calling amendSellOrder, effectively bypassing the maximum order lifetime. As a result, an order can persist indefinitely, breaking the protocol’s documented feature of “Deadline enforcement to prevent stale listings”, which is designed to protect market participants from interacting with outdated or misleading orders.

// Root cause in the codebase with @> marks to highlight the relevant section

function amendSellOrder(
uint256 _orderId,
uint256 _newAmountToSell,
uint256 _newPriceInUSDC,
uint256 _newDeadlineDuration
) public {
Order storage order = orders[_orderId];
// Validation checks
if (order.seller == address(0)) revert OrderNotFound(); // Check if order exists
if (order.seller != msg.sender) revert NotOrderSeller();
if (!order.isActive) revert OrderAlreadyInactive();
if (block.timestamp >= order.deadlineTimestamp) revert OrderExpired(); // Cannot amend expired order
if (_newAmountToSell == 0) revert InvalidAmount();
if (_newPriceInUSDC == 0) revert InvalidPrice();
if (
_newDeadlineDuration == 0 ||
_newDeadlineDuration > MAX_DEADLINE_DURATION
) revert InvalidDeadline();
@> uint256 newDeadlineTimestamp = block.timestamp + _newDeadlineDuration; // @audit it doesn't take into account the previous deadline, so it could make infinite orders
IERC20 token = IERC20(order.tokenToSell);
// Handle token amount changes
if (_newAmountToSell > order.amountToSell) {
// Increasing amount: Transfer additional tokens from seller
uint256 diff = _newAmountToSell - order.amountToSell;
token.safeTransferFrom(msg.sender, address(this), diff);
} else if (_newAmountToSell < order.amountToSell) {
// Decreasing amount: Transfer excess tokens back to seller
uint256 diff = order.amountToSell - _newAmountToSell;
token.safeTransfer(order.seller, diff);
}
// Update order details
order.amountToSell = _newAmountToSell; // @audit potential re-entrancy
order.priceInUSDC = _newPriceInUSDC;
order.deadlineTimestamp = newDeadlineTimestamp;
emit OrderAmended(
_orderId,
_newAmountToSell,
_newPriceInUSDC,
newDeadlineTimestamp
);
}

Risk

Likelihood: HIGH

  • The vulnerability can be exploited by any order creator without any special privileges or technical expertise. The seller simply calls the amendSellOrder function before the deadline expires, which is a normal function exposed by the protocol.

  • There are no protocol-level safeguards preventing repeated use of the amend function, meaning a malicious actor could easily automate deadline extensions to maintain stale orders indefinitely. This can happen continuously as long as the order is active.

Impact: HIGH

  • The exploit breaks a critical market protection mechanism—deadline enforcement—intended to remove stale or misleading orders from the order book. This undermines market trust and the integrity of the order book, as takers and bots rely on deadlines to assess order freshness and risk.

  • Malicious actors can leverage this flaw to execute liquidity baiting and market manipulation attacks, potentially causing financial losses for unsuspecting traders who take stale orders, and damaging the protocol’s reputation as a reliable exchange venue.

Proof of Concept

function test_deadlineExtensionExploit() public {
uint256 maxDeadline = book.MAX_DEADLINE_DURATION(); // 3 days
console2.log(
"Protocol max deadline: %s seconds (%s days)",
maxDeadline,
maxDeadline / 1 days
);
// Alice creates order with max deadline
vm.startPrank(alice);
wbtc.approve(address(book), 2e8);
uint256 orderId = book.createSellOrder(
address(wbtc),
2e8,
180_000e6,
maxDeadline
);
vm.stopPrank();
OrderBook.Order memory order = book.getOrder(orderId);
uint256 originalDeadline = order.deadlineTimestamp;
uint256 creationTime = block.timestamp;
console2.log("Order created at timestamp: %s", creationTime);
console2.log("Original deadline: %s", originalDeadline);
// Skip almost to deadline (2.5 days)
uint256 timeSkip = maxDeadline - 0.5 days;
vm.warp(block.timestamp + timeSkip);
console2.log(
"Time skipped: %s seconds (%s days)",
timeSkip,
timeSkip / 1 days
);
// Alice extends deadline by another max period
vm.prank(alice);
book.amendSellOrder(orderId, 2e8, 180_000e6, maxDeadline);
order = book.getOrder(orderId);
uint256 newDeadline = order.deadlineTimestamp;
uint256 totalDuration = newDeadline - creationTime;
console2.log("New deadline after extension: %s", newDeadline);
console2.log(
"Total duration from creation: %s seconds (%s days)",
totalDuration,
totalDuration / 1 days
);
console2.log(
"Exceeded protocol limit by: %s seconds (%s days)",
totalDuration - maxDeadline,
(totalDuration - maxDeadline) / 1 days
);
// Prove the exploit worked
assertTrue(
totalDuration > maxDeadline,
"Order should exceed max deadline"
);
assertEq(
totalDuration,
maxDeadline + timeSkip,
"Total duration calculation"
);
// Alice can repeat this indefinitely
vm.warp(block.timestamp + timeSkip);
vm.prank(alice);
book.amendSellOrder(orderId, 2e8, 180_000e6, maxDeadline);
order = book.getOrder(orderId);
uint256 finalDeadline = order.deadlineTimestamp;
uint256 finalTotalDuration = finalDeadline - creationTime;
console2.log(
"After second extension - Total duration: %s days",
finalTotalDuration / 1 days
);
assertTrue(
finalTotalDuration > maxDeadline * 2,
"Should exceed 2x max deadline"
);
}

Recommended Mitigation

- remove this code
uint256 newDeadlineTimestamp = block.timestamp + _newDeadlineDuration;
+ add this code
Add creationTimestamp to Order struct - Now we remember the full history, path-dependent not Markovian
In createSellOrder - Store block.timestamp as creationTimestamp
In amendSellOrder - The critical fix:
uint256 totalDurationFromCreation = newDeadlineTimestamp - order.creationTimestamp;
if (totalDurationFromCreation > MAX_DEADLINE_DURATION) {
revert InvalidDeadline();
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge
about 1 month ago
yeahchibyke Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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