← Index
Source: docs/issues/issue-99-bug-partially-filled-orders-cannot-be-matched-again-orderfil.md (auto-generated by scripts/generate-docs-html.mjs — edit the .md, not this file)

[BUG] Partially Filled Orders Cannot Be Matched Again - OrderFilledOrCancelled Error

Issue #99 | State: OPEN | Created: 2026-01-29T15:08:14Z

Assignees: linked0, Abdulkarim4u

Updated: 2026-01-29T15:08:14Z | Closed: N/A


๐Ÿ› Bug Description

When a limit order is partially filled, subsequent attempts to match the remaining shares fail with contract error 0x7b38b76e (OrderFilledOrCancelled()). This prevents the remaining portion of partially filled orders from being executed.

๐Ÿ”„ Steps to Reproduce

  1. **Alice/ Trader 1 ** places a Buy Limit order: 20 shares at 51ยข ($10.20 total)
  2. **Bob/Trader2 who has shares ** Market Sells 10 shares โ†’ โœ… SUCCESS (partial fill)
    • Database state: originalSize: 20, remainingSize: 10, status: PARTIALLY_FILLED
  3. Bob tries to Market Sell another 10 shares โ†’ โŒ FAILS
    • Error: execution reverted: 0x7b38b76e (OrderFilledOrCancelled)

๐Ÿ“Š Expected Behavior

โŒ Actual Behavior

๐Ÿ” Root Cause Analysis

The Problem: Duplicate Order Hash

The CTFExchange contract uses order hash-based tracking:

mapping(bytes32 => uint256) public filled; // orderHash => filledAmount

First fill (10 shares):

  1. Contract calculates orderHash = keccak256(abi.encode(order))
  2. Records filled[orderHash] = 10 shares
  3. โœ… Transaction succeeds

Second fill attempt (remaining 10 shares):

  1. System retrieves order from database: status: PARTIALLY_FILLED, remainingSize: 10
  2. Constructs order struct with SAME nonce, salt, and signature
  3. Calculates SAME orderHash
  4. Contract checks: filled[orderHash] > 0 โ†’ Already processed! โ†’ โŒ REVERT

Why Our Current Approach Fails

Database correctly updates:

{
  orderId: "abc-123",
  remainingSize: 10,        // โœ… Correct
  status: "PARTIALLY_FILLED",
  nonce: 0,                 // โŒ Same nonce = same hash
  salt: "...",              // โŒ Same salt = same hash
  signature: "..."          // โŒ Same signature = same hash
}

But on-chain:

filled[orderHash] = 10;  // Contract: "This order was already processed!"

The contract doesn't differentiate between "fully filled" and "partially filled" - it only knows if an order hash has been used.

โœ… Proposed Solution

How it works:

Why this approach:

Code Changes Required:

1. Exclude PARTIALLY_FILLED from order matching

File: /api/src/routes/orders.ts (line ~385)

const matchingDbOrders = await prisma.order.findMany({
  where: {
    outcomeId,
    side: oppositeSide,
    status: 'OPEN',  // โœ… CHANGE: Remove 'PARTIALLY_FILLED' from array
    isActive: true,
    price: signedOrder.side === 0
      ? { lte: price }
      : { gte: price },
  },
  orderBy: {
    price: signedOrder.side === 0 ? 'asc' : 'desc',
  },
});

Before:

status: { in: ['OPEN', 'PARTIALLY_FILLED'] },  // โŒ Includes partially filled

After:

status: 'OPEN',  // โœ… Only open orders

2. Always mark orders as FILLED after any fill

File: /api/src/routes/orders.ts (line ~697)

await prisma.order.update({
  where: { id: matchOrder.id },
  data: {
    remainingSize: { decrement: fillShares },
    status: 'FILLED',           // โœ… CHANGE: Always FILLED (not PARTIALLY_FILLED)
    isActive: false,            // โœ… CHANGE: Deactivate order
  },
});

Before:

status: fillShares >= matchShares ? 'FILLED' : 'PARTIALLY_FILLED',  // โŒ Allows partial
isActive: fillShares >= matchShares ? false : true,

After:

status: 'FILLED',      // โœ… Always mark as FILLED
isActive: false,       // โœ… Always deactivate

๐Ÿ”ฌ Alternative Solutions Considered

Option 2: Increment Nonce After Partial Fill

Option 3: Smart Contract Upgrade

๐Ÿ“ Testing Plan

After implementing the fix:

  1. โœ… Alice places buy limit: 20 shares at 51ยข
  2. โœ… Bob market sells 10 shares
    • Expected: Order marked as FILLED, isActive: false
  3. โœ… Bob market sells 10 more shares
    • Expected: Matches against OTHER orders (not Alice's closed order)
  4. โœ… Alice's portfolio shows 10 shares (from first fill)
  5. โœ… Alice's open orders tab is empty (order closed after first fill)
  6. โœ… Alice can place NEW order for remaining shares if desired

๐Ÿ“š References

๐Ÿ’ก Additional Notes

UX Consideration

After this fix, users will need to place a new order for remaining shares. Consider adding UI messaging:

โ„น๏ธ Your limit order was partially filled (10/20 shares).
   The order has been closed. Place a new order for the remaining 10 shares.

Impact Assessment

๐ŸŽฏ Success Criteria

๐Ÿ“ธ Error Logs

โŒ Error executing batch orders: Error: execution reverted (unknown custom error)
   (action="estimateGas", data="0x7b38b76e", reason=null, ...)

Error code decoded: OrderFilledOrCancelled()

Priority: High ๐Ÿ”ด Estimated Effort: Small (2 lines of code changes) Risk: Low (minimal changes, no contract upgrade needed)