OrderBook

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

Missing Token Allowance Check in `amendSellOrder()` Allows Previously Created Orders for Delisted Tokens to Persist

Finding Title

Missing Token Allowance Check in amendSellOrder() Allows Previously Created Orders for Delisted Tokens to Persist

Summary

The amendSellOrder() function allows a seller to modify an existing order. However, it fails to verify if the token being sold (order.tokenToSell) is still in the allowedSellToken mapping. This oversight allows a seller to amend and effectively keep their order active in the marketplace, even after the protocol owner has explicitly delisted the token via setAllowedSellToken(..., false). This undermines the owner's administrative control and contradicts the intended function of the token whitelist.

Finding Description

The protocol owner has the ability to manage which tokens are permitted for trading using the setAllowedSellToken(address _token, bool _isAllowed) function. When a new order is created, the createSellOrder() function correctly checks if the token is allowed:

// src/OrderBook.sol:113
if (!allowedSellToken[_tokenToSell]) revert InvalidToken();

However, this crucial check is missing in the amendSellOrder() function. If an order was created when a token was allowed, and the owner later decides to delist that token for security or other reasons, the existing order remains active. A seller can then call amendSellOrder() to change its price or deadline, effectively refreshing it and keeping a potentially unwanted or unsafe token listed for sale.

Scenario of Exploit:

  1. Token A is initially whitelisted by the owner.

  2. A seller creates an order to sell 100 units of Token A.

  3. The owner discovers a vulnerability in Token A and decides to delist it by calling setAllowedSellToken(address(TokenA), false).

  4. No new orders for Token A can be created, which is the intended behavior.

  5. However, the original seller can still call amendSellOrder() on their existing order. They can update the price and extend the deadline, keeping the vulnerable token available for purchase by unsuspecting buyers. The protocol fails to enforce the delisting on existing, amendable orders.

Impact

This vulnerability directly undermines the protocol's primary safety mechanism for managing supported assets. The consequences include:

  • Exposure to Risky Assets: Buyers may be exposed to purchasing tokens that the protocol owner has deemed unsafe, insecure, or no longer viable.

  • Incomplete Administrative Control: The owner's ability to manage the market is incomplete. Delisting a token does not fully remove it from active trading as intended, creating a discrepancy between the owner's actions and the protocol's state.

  • Erosion of Trust: Users may lose trust in the protocol's curation and safety measures if they find that delisted tokens are still actively being traded.

Likelihood

Medium. The scenario requires an owner to delist a token while active orders for it still exist, which is a plausible administrative action. A seller of such an order can then easily perform this action without any special knowledge.

Proof of Concept

The following test demonstrates that an order for a delisted token can be successfully amended.

Test File: test/PoC/AmendDelistedToken.t.sol

// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
import {Test} from "forge-std/Test.sol";
import {OrderBook} from "../../src/OrderBook.sol";
import {MockUSDC} from "../mocks/MockUSDC.sol";
import {MockWETH} from "../mocks/MockWETH.sol";
contract AmendDelistedTokenTest is Test {
OrderBook book;
MockWETH weth; // This will be the token we delist
MockUSDC usdc;
address owner;
address seller;
function setUp() public {
owner = makeAddr("owner");
seller = makeAddr("seller");
weth = new MockWETH(18);
usdc = new MockUSDC(6);
vm.prank(owner);
book = new OrderBook(address(weth), address(weth), address(weth), address(usdc), owner);
weth.mint(seller, 1e18);
}
/// @notice This PoC demonstrates that a seller can amend an order for a token
/// that has been delisted by the owner after the order's creation.
function test_PoC_CanAmendDelistedTokenOrder() public {
// --- Phase 1: Create order while token is allowed ---
vm.prank(owner);
book.setAllowedSellToken(address(weth), true);
vm.startPrank(seller);
weth.approve(address(book), 1e18);
uint256 orderId = book.createSellOrder(address(weth), 1e18, 1000e6, 1 days);
vm.stopPrank();
// --- Phase 2: Owner delists the token ---
vm.prank(owner);
book.setAllowedSellToken(address(weth), false);
// Verify that creating a NEW order for this token now fails.
vm.startPrank(seller);
weth.approve(address(book), 1e18);
vm.expectRevert(bytes("InvalidToken()"));
book.createSellOrder(address(weth), 1e18, 1000e6, 1 days);
vm.stopPrank();
// --- Phase 3: Seller amends the existing order for the delisted token ---
uint256 newPrice = 2000e6;
vm.startPrank(seller);
// This call should ideally revert, but it succeeds due to the missing check.
book.amendSellOrder(orderId, 1e18, newPrice, 1 days);
vm.stopPrank();
// --- Assertion Phase ---
OrderBook.Order memory orderAfter = book.getOrder(orderId);
assertEq(orderAfter.priceInUSDC, newPrice, "FAIL: Order for delisted token was amended.");
}
}

Execution Command & Result:
The test was executed using the command forge test --match-path test/PoC/AmendDelistedToken.t.sol. The test passed, confirming the vulnerability.

Running 1 test for test/PoC/AmendDelistedToken.t.sol:AmendDelistedTokenTest
[PASS] test_PoC_CanAmendDelistedTokenOrder() (gas: 283607)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.00ms

The successful result ([PASS]) proves that the call to amendSellOrder for the delisted token did not revert and successfully updated the order's state, which is incorrect behavior.

Recommended Mitigation

Add a validation check at the beginning of the amendSellOrder function to ensure the token associated with the order is still allowed for trading.

// src/OrderBook.sol:146-149
function amendSellOrder(/*...*/) public {
Order storage order = orders[_orderId];
// Validation checks
+ if (!allowedSellToken[order.tokenToSell]) revert InvalidToken();
if (order.seller == address(0)) revert OrderNotFound(); // Check if order exists
if (order.seller != msg.sender) revert NotOrderSeller();
//...
}

This simple check ensures that the owner's administrative action of delisting a token is enforced consistently across all functions, preventing unwanted assets from persisting in the marketplace.

Updates

Lead Judging Commences

yeahchibyke Lead Judge
about 1 month ago
yeahchibyke Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Current orders with delisted tokens can still be amended

Any current order with a delisted token can be amended.

Support

FAQs

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