OrderBook

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

Order Cannot be Bought By Seller

Root + Impact

Description

Description

In a typical token trading system, the buyOrder() function allows a buyer to purchase tokens from a seller at a fixed price. The expected behavior is that only external users (non-sellers) are able to fill an order posted by a seller.

However, the current implementation of buyOrder() does not prevent the seller from calling the function themselves, effectively letting them buy their own order. This opens up the door for protocol fee manipulation, wash trading, and false volume generation.

function buyOrder(uint256 _orderId) public nonReentrant {
Order storage order = orders[_orderId];
...
@> // ❌ No check to ensure msg.sender ≠ seller
...
}

Risk

Likelihood:

Likelihood: High

  • This occurs whenever a seller deliberately calls buyOrder() on their own order.

  • This behavior is likely during protocol fee farming, volume manipulation, or testing price behavior.

Impact:

  • Protocol Fee Evasion: The seller pays themselves, effectively recycling funds, but still earns protocol rewards.

  • Wash Trading: Creates fake trading volume, harming protocol analytics and misleading users.

  • Misuse in Incentive Programs: If rewards are tied to buy volume or activity, this can be exploited.

Proof of Concept

Step-by-step walkthrough:

  1. A user (0xSeller) creates a sell order offering to sell 100 tokens for 100 USDC:

    orderId = createSellOrder(tokenToSell, 100 * 1e18, 100 * 1e6);
  2. The order is now active and available in the marketplace.

  3. The seller (0xSeller) then calls:

    buyOrder(orderId);
  4. Since there's no restriction in the code to prevent the seller from buying their own order, the transaction succeeds:

    • 0xSeller pays 100 USDC to themselves.

    • The protocol still collects a protocol fee.

    • Trading volume is recorded even though no actual trade happened.

    • If the protocol has a reward system based on volume or participation, the seller falsely earns rewards.

Why this is a problem:

  • Misrepresents real volume (wash trading)

  • Abuses incentives tied to buys/sells

  • Bypasses intent of fees or buyer-seller separation

This can lead to manipulation, protocol misuse, or bad analytics that impact both governance and users.

// Assume seller created Order #42
// Seller executes:
buyOrder(42);
// As there's no restriction, the order goes through,
// and the seller ends up paying themselves the same amount.
// If protocol pays out rewards per fill, they can exploit that.

Recommended Mitigation

Add a simple check in buyOrder():

function buyOrder(uint256 _orderId) public nonReentrant {
Order storage order = orders[_orderId];
+ if (msg.sender == order.seller) revert OrderCannotBeBoughtBySeller();

This ensures sellers cannot fill their own orders and enforces correct trade flows.


Final Code Snippet (Mitigated Version)

function buyOrder(uint256 _orderId) public nonReentrant {
Order storage order = orders[_orderId];
if (order.seller == address(0)) revert OrderNotFound();
if (!order.isActive) revert OrderNotActive();
if (block.timestamp >= order.deadlineTimestamp) revert OrderExpired();
+ if (msg.sender == order.seller) revert OrderCannotBeBoughtBySeller();
order.isActive = false;
order.buyLockExpiresAt = 0;
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
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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