From 74b8ad8427b860fbf8fa5bf3fd80d2c44020046a Mon Sep 17 00:00:00 2001 From: Nexory Date: Thu, 11 Jun 2026 12:26:32 +0200 Subject: [PATCH] fix(cancelOrders): reject orders that span different protocol addresses cancelOrders sends a single transaction to one Seaport contract, but it derived the target protocol with a last-write-wins loop over the orders' protocolAddress. Two valid protocols exist (CROSS_CHAIN_SEAPORT_V1_6 and ALTERNATE_SEAPORT_V1_6), so a batch mixing them passed validation and cancelled only the last protocol's orders. The rest were sent to the wrong contract and silently left live (still fillable), while the caller believed every order was cancelled. Reject up front when the provided orders span more than one protocol address, so the caller cancels each protocol separately instead of getting a silent partial cancel. Adds a regression test. --- src/sdk/cancellation.ts | 17 ++++++++++++++++ test/sdk/cancelOrders.spec.ts | 38 +++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/sdk/cancellation.ts b/src/sdk/cancellation.ts index aff66eb0d..51ab9b3d8 100644 --- a/src/sdk/cancellation.ts +++ b/src/sdk/cancellation.ts @@ -167,6 +167,23 @@ export class CancellationManager { throw new Error("At least one order hash must be provided") } + // Cancellation is a single transaction to one Seaport contract, so every + // order must live on the same protocol. Mixing protocol addresses would + // send the others to the wrong contract and silently leave them live. + if (orders) { + const protocols = new Set( + orders + .filter(order => "protocolData" in order) + .map(order => (order as OrderV2).protocolAddress.toLowerCase()), + ) + if (protocols.size > 1) { + throw new Error( + "All orders must use the same protocolAddress to be cancelled in one call. " + + "Cancel orders from different protocols separately.", + ) + } + } + requireValidProtocol(protocolAddress) // Check account availability after parameter validation diff --git a/test/sdk/cancelOrders.spec.ts b/test/sdk/cancelOrders.spec.ts index a8ba1ff47..b117d9d8d 100644 --- a/test/sdk/cancelOrders.spec.ts +++ b/test/sdk/cancelOrders.spec.ts @@ -1,6 +1,7 @@ import type { OrderComponents } from "@opensea/seaport-js/lib/types" import { ethers } from "ethers" import { describe, expect, test } from "vitest" +import { ALTERNATE_SEAPORT_V1_6_ADDRESS } from "../../src/constants" import type { OrderV2 } from "../../src/orders/types" import { DEFAULT_SEAPORT_CONTRACT_ADDRESS } from "../../src/orders/utils" import { sdk } from "../utils/sdk" @@ -84,6 +85,43 @@ describe("SDK: cancelOrders", () => { } }) + test("Should throw when orders span different protocol addresses", async () => { + const mockOrderComponents: OrderComponents = { + offerer: "0x0000000000000000000000000000000000000001", + zone: "0x0000000000000000000000000000000000000000", + offer: [], + consideration: [], + orderType: 0, + startTime: "0", + endTime: "0", + zoneHash: + "0x0000000000000000000000000000000000000000000000000000000000000000", + salt: "0", + conduitKey: + "0x0000000000000000000000000000000000000000000000000000000000000000", + totalOriginalConsiderationItems: 0, + counter: 0, + } + const makeOrderV2 = (protocolAddress: string) => + ({ + protocolData: { parameters: mockOrderComponents }, + protocolAddress, + }) as unknown as OrderV2 + + try { + await sdk.cancelOrders({ + orders: [ + makeOrderV2(DEFAULT_SEAPORT_CONTRACT_ADDRESS), + makeOrderV2(ALTERNATE_SEAPORT_V1_6_ADDRESS), + ], + accountAddress, + }) + throw new Error("should have thrown") + } catch (e) { + expect((e as Error).message).toContain("same protocolAddress") + } + }) + test("Should attempt to fetch orders from API when using orderHashes", async () => { try { await sdk.cancelOrders({