Indefinite Order Extension Breaks Core Protocol Invariant
Description
The protocol is designed to enforce a maximum lifetime for all orders, as specified by the MAX_DEADLINE_DURATION constant, to prevent stale listings. This is a key feature advertised in the project's README.md.
However, the amendSellOrder function calculates the new deadline based on the current block.timestamp instead of the order's original creation time. This flaw allows sellers to repeatedly reset the deadline countdown, enabling them to keep their orders active indefinitely and breaking the core protocol invariant.
function amendSellOrder(
uint256 _orderId,
uint256 _newAmountToSell,
uint256 _newPriceInUSDC,
uint256 _newDeadlineDuration
) public {
if (_newDeadlineDuration == 0 || _newDeadlineDuration > MAX_DEADLINE_DURATION) revert InvalidDeadline();
@> uint256 newDeadlineTimestamp = block.timestamp + _newDeadlineDuration;
IERC20 token = IERC20(order.tokenToSell);
}
Risk
Likelihood: High
-
When a seller of an active order decides to amend it, they can perpetually extend its lifetime before it expires.
-
This action does not require any special permissions beyond being the owner of the order.
Impact: Medium
-
The protocol's core, documented invariant of having a maximum order lifetime is broken, making the system's behavior unpredictable.
-
User trust is eroded as the "Deadline enforcement" feature, a key promise made in the README.md, is rendered ineffective.
Proof of Concept
The following test proves that a seller can extend an order's lifetime repeatedly, causing its total lifetime to exceed the MAX_DEADLINE_DURATION defined by the protocol.
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 {MockWETH} from "../mocks/MockWETH.sol";
contract DeadlineExtensionTest is Test {
OrderBook book;
MockWETH weth;
address owner = makeAddr("owner");
address seller = makeAddr("seller");
uint256 maxDeadlineDuration;
function setUp() public {
weth = new MockWETH(18);
MockUSDC usdc = new MockUSDC(6);
vm.prank(owner);
book = new OrderBook(address(weth), address(weth), address(weth), address(usdc), owner);
weth.mint(seller, 1e18);
maxDeadlineDuration = book.MAX_DEADLINE_DURATION();
}
function test_PoC_IndefiniteDeadlineExtension() public {
uint256 initialAmount = 1e18;
uint256 initialPrice = 1000e6;
uint256 initialDuration = 1 days;
vm.startPrank(seller);
weth.approve(address(book), initialAmount);
uint256 orderId = book.createSellOrder(address(weth), initialAmount, initialPrice, initialDuration);
vm.stopPrank();
uint256 creationTime = block.timestamp;
vm.warp(block.timestamp + initialDuration - 1 hours);
vm.prank(seller);
book.amendSellOrder(orderId, initialAmount, initialPrice, maxDeadlineDuration);
vm.warp(block.timestamp + maxDeadlineDuration - 1 hours);
vm.prank(seller);
book.amendSellOrder(orderId, initialAmount, initialPrice, maxDeadlineDuration);
OrderBook.Order memory finalOrder = book.getOrder(orderId);
uint256 totalLifetime = finalOrder.deadlineTimestamp - creationTime;
console2.log("Protocol's Maximum Allowed Lifetime (Invariant):", maxDeadlineDuration);
console2.log("Actual Order Lifetime After Two Extensions:", totalLifetime);
assertTrue(
totalLifetime > maxDeadlineDuration,
"INVARIANT BROKEN: Order lifetime exceeded the protocol's stated maximum."
);
}
}
Execution Command & Result:
$ forge test --match-path test/PoC/DeadlineExtension.t.sol
[PASS] test_PoC_IndefiniteDeadlineExtension() (gas: 267980)
Logs:
Protocol's Maximum Allowed Lifetime (Invariant): 259200
Actual Order Lifetime After Two Extensions: 597600
Recommended Mitigation
To fix this flaw, the order's creationTimestamp must be stored and checked against MAX_DEADLINE_DURATION during amendments.
1. Modify the Order struct to include creationTimestamp:
// src/OrderBook.sol:34-41
struct Order {
uint256 id;
address seller;
address tokenToSell;
uint256 amountToSell;
uint256 priceInUSDC;
- uint256 deadlineTimestamp;
+ uint256 creationTimestamp;
+ uint256 deadlineTimestamp;
bool isActive;
}
2. Store the creationTimestamp when an order is created:
// src/OrderBook.sol:154-162
orders[orderId] = Order({
id: orderId,
seller: msg.sender,
tokenToSell: _tokenToSell,
amountToSell: _amountToSell,
priceInUSDC: _priceInUSDC,
- deadlineTimestamp: deadlineTimestamp,
+ creationTimestamp: block.timestamp,
+ deadlineTimestamp: deadlineTimestamp,
isActive: true
});
3. Add a validation check in amendSellOrder:
// src/OrderBook.sol:198-202
if (_newDeadlineDuration == 0 || _newDeadlineDuration > MAX_DEADLINE_DURATION) revert InvalidDeadline();
uint256 newDeadlineTimestamp = block.timestamp + _newDeadlineDuration;
+ // Enforce that the new deadline does not exceed the maximum lifetime from the original creation time.
+ if (newDeadlineTimestamp > order.creationTimestamp + MAX_DEADLINE_DURATION) {
+ revert InvalidDeadline();
+ }
IERC20 token = IERC20(order.tokenToSell);