OrderBook

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

## [M-#] Disabled Allowed Tokens Can Still Be Bought (Missing Validation + Potential Invalid Trades)

[M-#] Disabled Allowed Tokens Can Still Be Bought (Missing Validation + Potential Invalid Trades)

Description:

The OrderBook::buyOrder function lacks a check for OrderBook::allowedSellToken[order.tokenToSell], allowing buyers to purchase orders for tokens that have been disabled via OrderBook::setAllowedSellToken(_token, false). While OrderBook::createSellOrder enforce the OrderBook::allowedSellToken check, OrderBook::buyOrder does not, enabling the fulfillment of existing orders for disabled tokens.

Impact:

Buyers can purchase orders for COMPROMISED tokens ALREADY marked as disallowed, violating intended token restrictions. This undermines the Orderbook's integrity, potentially leading to invalid trades and risking user funds when compromised tokens are no longer supported.

Proof of Concept:

  1. Deploy the OrderBook contract with valid addresses for wETH, wBTC, wSOL, and USDC.

  2. Alice creates a sell order for wETH via createSellOrder(address(iWETH), 1e18, 2500e6, 1 days), transferring 1 wETH to the contract.

  3. Owner disables wETH by calling setAllowedSellToken(address(iWETH), false).

  4. Dan calls buyOrder(orderId) to purchase Alice’s wETH order, which succeeds, transferring wETH to Dan and USDC to Alice.

Add this to test/TestOrderBook.t.sol

PoC Test Code:
function test_buyDisabledToken() public {
// Bob creates a sell order for wETH
vm.startPrank(bob);
weth.approve(address(book), 1e18);
uint256 orderId = book.createSellOrder(address(weth), 1e18, 2500e6, 1 days);
vm.stopPrank();
assertEq(weth.balanceOf(address(book)), 1e18, "Contract should hold 1 wETH");
// Owner disables wETH
vm.prank(owner);
book.setAllowedSellToken(address(weth), false);
assertFalse(book.allowedSellToken(address(weth)), "wETH should be disabled");
// Dan buys Bob's wETH order
vm.startPrank(dan);
usdc.approve(address(book), 2500e6);
book.buyOrder(orderId);
vm.stopPrank();
// Verify Dan received wETH and Bob received USDC
assertEq(weth.balanceOf(dan), 1e18, "Dan should receive 1 wETH");
assertEq(usdc.balanceOf(bob), 2425e6, "Bob should receive 2425 USDC (after fees)");
assertEq(book.totalFees(), 75e6, "Contract should collect 75 USDC fees");
}

Recommended Mitigation:

Add a check in OrderBook::buyOrder to ensure the token is still allowed in OrderBook::allowedSellToken.

Recommended solution:
function buyOrder(uint256 _orderId) public {
Order storage order = orders[_orderId];
// Validation checks
if (order.seller == address(0)) revert OrderNotFound();
if (!order.isActive) revert OrderNotActive();
if (block.timestamp >= order.deadlineTimestamp) revert OrderExpired();
+ if (!allowedSellToken[order.tokenToSell]) revert InvalidToken();
order.isActive = false;
uint256 protocolFee = (order.priceInUSDC * FEE) / PRECISION;
uint256 sellerReceives = order.priceInUSDC - protocolFee;
iUSDC.safeTransferFrom(msg.sender, address(this), protocolFee);
iUSDC.safeTransferFrom(msg.sender, order.seller, sellerReceives);
IERC20(order.tokenToSell).safeTransfer(msg.sender, order.amountToSell);
totalFees += protocolFee;
emit OrderFilled(_orderId, msg.sender, order.seller);
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge
about 1 month ago
yeahchibyke Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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