From 07478fca0de3fa273e10e7a26572497f07bcbe78 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Mon, 1 Jun 2026 15:33:57 -0300 Subject: [PATCH 1/5] fix: minor cleanups from grant review (COW-996, COW-999, COW-1003) - COW-1003 [F2]: add CirclesBackingOrder to DETERMINISTIC_ORDER_TYPES; it was already precomputed in uidPrecompute.ts but missing from the set, causing spurious non-deterministic log warnings - COW-996 [F3]: replace nonexistent ConditionalOrderCancelled event reference with accurate description of actual cancellation detection (SingleOrderNotAuthed + C5 singleOrders() sweep) - COW-999 [F10]: remove decode-only-for-logging dead code in settlement.ts; decodeAbiParameters block and console.log were decoding Trade log fields solely for a log line, with no downstream use Co-Authored-By: Claude Sonnet 4.6 --- src/application/handlers/composableCow.ts | 6 ++-- src/application/handlers/settlement.ts | 21 +----------- src/utils/order-types.ts | 2 +- tests/utils/order-types.test.ts | 39 +++++++++++++++++++++++ 4 files changed, 44 insertions(+), 24 deletions(-) create mode 100644 tests/utils/order-types.test.ts diff --git a/src/application/handlers/composableCow.ts b/src/application/handlers/composableCow.ts index 44f7858..ced8075 100644 --- a/src/application/handlers/composableCow.ts +++ b/src/application/handlers/composableCow.ts @@ -24,9 +24,9 @@ * * This affects only EIP-1271 composable orders where the user cancels through * the API rather than calling ComposableCoW.remove() on-chain. In practice - * this is rare — the standard cancellation path for composable orders is - * on-chain, which emits ConditionalOrderCancelled (handled elsewhere) or - * triggers PollNever in the block handler. + * this is rare — the standard on-chain cancellation path is detected via + * SingleOrderNotAuthed (C1 block handler) and the C5 singleOrders() sweep, + * both of which work correctly. * * If this gap proves significant in production, a lightweight periodic check * can be added for owners with open orders. Track via issue tracker if needed. diff --git a/src/application/handlers/settlement.ts b/src/application/handlers/settlement.ts index 7304b07..60ffda5 100644 --- a/src/application/handlers/settlement.ts +++ b/src/application/handlers/settlement.ts @@ -1,7 +1,7 @@ import { ponder } from "ponder:registry"; import { AddressType, conditionalOrderGenerator, ownerMapping, transaction } from "ponder:schema"; import { and, eq } from "ponder"; -import { decodeAbiParameters, keccak256, toBytes } from "viem"; +import { keccak256, toBytes } from "viem"; import { AaveV3AdapterHelperAbi } from "../../../abis/AaveV3AdapterHelperAbi"; import { AAVE_V3_ADAPTER_FACTORY_ADDRESSES, @@ -191,20 +191,6 @@ ponder.on("GPv2Settlement:Settlement", async ({ event, context }) => { ), ); - // Decode non-indexed Trade log fields: sellToken, buyToken, amounts, orderUid - const [sellToken, buyToken, sellAmount, buyAmount, , orderUid] = - decodeAbiParameters( - [ - { type: "address" }, - { type: "address" }, - { type: "uint256" }, - { type: "uint256" }, - { type: "uint256" }, - { type: "bytes" }, - ], - log.data, - ); - stats.mapped++; logStatsIfIntervalPassed(); @@ -212,11 +198,6 @@ ponder.on("GPv2Settlement:Settlement", async ({ event, context }) => { `[COW:SETTLEMENT:TRADE] AAVE_ADAPTER_MAPPED` + ` adapter=${ownerAddress}` + ` eoa=${eoaOwner.toLowerCase()}` + - ` orderUid=${orderUid}` + - ` sellToken=${sellToken.toLowerCase()}` + - ` buyToken=${buyToken.toLowerCase()}` + - ` sellAmount=${sellAmount}` + - ` buyAmount=${buyAmount}` + ` block=${event.block.number}` + ` chain=${chainId}`, ); diff --git a/src/utils/order-types.ts b/src/utils/order-types.ts index 707eabf..a9540df 100644 --- a/src/utils/order-types.ts +++ b/src/utils/order-types.ts @@ -77,7 +77,7 @@ export function getOrderTypeFromHandler( // Single source of truth for which order types have UIDs computable from staticInput // alone (no on-chain calls). Keep in sync with the switch in `precomputeOrderUids`. -export const DETERMINISTIC_ORDER_TYPES = new Set(["TWAP", "StopLoss"]); +export const DETERMINISTIC_ORDER_TYPES = new Set(["TWAP", "StopLoss", "CirclesBackingOrder"]); export function isDeterministicOrderType(orderType: string): boolean { return DETERMINISTIC_ORDER_TYPES.has(orderType as OrderType); diff --git a/tests/utils/order-types.test.ts b/tests/utils/order-types.test.ts new file mode 100644 index 0000000..cd0c5ad --- /dev/null +++ b/tests/utils/order-types.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect } from "vitest"; +import { + DETERMINISTIC_ORDER_TYPES, + isDeterministicOrderType, +} from "../../src/utils/order-types"; + +describe("DETERMINISTIC_ORDER_TYPES", () => { + it("includes TWAP", () => { + expect(DETERMINISTIC_ORDER_TYPES.has("TWAP")).toBe(true); + }); + + it("includes StopLoss", () => { + expect(DETERMINISTIC_ORDER_TYPES.has("StopLoss")).toBe(true); + }); + + // Regression guard for COW-1003 (F2): CirclesBackingOrder is deterministic + // (precomputed in uidPrecompute.ts) but was missing from this set, causing + // spurious non-deterministic warnings in logs. + it("includes CirclesBackingOrder (COW-1003)", () => { + expect(DETERMINISTIC_ORDER_TYPES.has("CirclesBackingOrder")).toBe(true); + }); + + it("does not include non-deterministic types", () => { + expect(DETERMINISTIC_ORDER_TYPES.has("PerpetualSwap")).toBe(false); + expect(DETERMINISTIC_ORDER_TYPES.has("GoodAfterTime")).toBe(false); + expect(DETERMINISTIC_ORDER_TYPES.has("TradeAboveThreshold")).toBe(false); + }); + + it("isDeterministicOrderType returns true for all members", () => { + for (const type of DETERMINISTIC_ORDER_TYPES) { + expect(isDeterministicOrderType(type)).toBe(true); + } + }); + + it("isDeterministicOrderType returns false for unknown types", () => { + expect(isDeterministicOrderType("Unknown")).toBe(false); + expect(isDeterministicOrderType("")).toBe(false); + }); +}); From 864586845425d6cebcf10120d9b49aa9a099efed Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Tue, 2 Jun 2026 17:08:28 -0300 Subject: [PATCH 2/5] fix: update stale C1/C5 names in comments, verify CirclesBackingOrder precompute (COW-996/999/1003) Co-Authored-By: Claude Sonnet 4.6 --- src/application/handlers/composableCow.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/application/handlers/composableCow.ts b/src/application/handlers/composableCow.ts index ced8075..4aa348f 100644 --- a/src/application/handlers/composableCow.ts +++ b/src/application/handlers/composableCow.ts @@ -25,7 +25,7 @@ * This affects only EIP-1271 composable orders where the user cancels through * the API rather than calling ComposableCoW.remove() on-chain. In practice * this is rare — the standard on-chain cancellation path is detected via - * SingleOrderNotAuthed (C1 block handler) and the C5 singleOrders() sweep, + * SingleOrderNotAuthed (OrderDiscoveryPoller) and the CancellationWatcher, * both of which work correctly. * * If this gap proves significant in production, a lightweight periodic check From ff2338b9641c359c57dac369677ecc9289d79bb3 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Thu, 4 Jun 2026 10:47:19 -0300 Subject: [PATCH 3/5] fix: replace stale C1-C4 block handler references with semantic names in comments Co-Authored-By: Claude Sonnet 4.6 --- src/application/handlers/composableCow.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/application/handlers/composableCow.ts b/src/application/handlers/composableCow.ts index 4aa348f..b619a17 100644 --- a/src/application/handlers/composableCow.ts +++ b/src/application/handlers/composableCow.ts @@ -8,8 +8,8 @@ * * For deterministic types (TWAP, StopLoss, CirclesBackingOrder), precomputeAndDiscover * computes all UIDs, fetches their status from the API, upserts discrete orders, and marks - * allCandidatesKnown=true. Non-deterministic types are left for the C1-C4 block handlers to - * discover at live sync. + * allCandidatesKnown=true. Non-deterministic types are left for the OrderDiscoveryPoller + * block handler to discover at live sync. * * CirclesBackingOrder (Gnosis only) additionally reads two constructor immutables * (SELL_TOKEN, SELL_AMOUNT) from the handler contract at creation time and merges them @@ -253,7 +253,7 @@ ponder.on( // ─── Live handler (ComposableCowLive — startBlock: "latest") ──────────────── // Same as backfill: pre-compute covers deterministic types. -// Non-deterministic types are discovered by C1-C4 block handlers at live sync. +// Non-deterministic types are discovered by the OrderDiscoveryPoller block handler at live sync. ponder.on( "ComposableCowLive:ConditionalOrderCreated", From 48347385c7686da018267d7de0e4e2399a9a8370 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Thu, 4 Jun 2026 18:19:46 -0300 Subject: [PATCH 4/5] docs: note ConditionalOrderRemoved in newer ComposableCoW contract (COW-1005) Co-Authored-By: Claude Sonnet 4.6 --- src/application/handlers/composableCow.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/application/handlers/composableCow.ts b/src/application/handlers/composableCow.ts index b619a17..61b72a0 100644 --- a/src/application/handlers/composableCow.ts +++ b/src/application/handlers/composableCow.ts @@ -28,8 +28,10 @@ * SingleOrderNotAuthed (OrderDiscoveryPoller) and the CancellationWatcher, * both of which work correctly. * - * If this gap proves significant in production, a lightweight periodic check - * can be added for owners with open orders. Track via issue tracker if needed. + * A newer ComposableCoW contract version (nullislabs/composable-cow#1) emits a + * ConditionalOrderRemoved event from remove(), which would allow the indexer to + * detect on-chain cancellations directly without polling. Supporting this contract + * version is tracked as a future improvement. * */ From 04181adcfb692a6cb7b7e847dd99812f10cd8930 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Thu, 4 Jun 2026 18:25:37 -0300 Subject: [PATCH 5/5] refactor: replace isDeterministicOrderType with Record and tighten orderType typing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - order-types.ts: replace DETERMINISTIC_ORDER_TYPES Set + isDeterministicOrderType() with DETERMINISTIC_ORDER_TYPE: Record — exhaustive record ensures TypeScript catches missing entries when new OrderTypes are added - uidPrecompute.ts: orderType: string → OrderType in both function signatures; isDeterministicOrderType() → DETERMINISTIC_ORDER_TYPE[] lookup - blockHandler.ts: NON_DETERMINISTIC_TYPES and SINGLE_SHOT_NON_DETERMINISTIC typed as readonly OrderType[]; three inline query result casts string → OrderType; removes as readonly string[] workaround on .includes() - orderbookClient.ts: orderType: string → OrderType in ComposableOrder and query result cast; empty string placeholder → "Unknown" - tests/utils/order-types.test.ts: rewrite to test DETERMINISTIC_ORDER_TYPE record Co-Authored-By: Claude Sonnet 4.6 --- src/application/handlers/blockHandler.ts | 13 +++--- src/application/helpers/orderbookClient.ts | 7 ++-- src/application/helpers/uidPrecompute.ts | 8 ++-- src/utils/order-types.ts | 16 ++++--- tests/utils/order-types.test.ts | 49 +++++++++------------- 5 files changed, 46 insertions(+), 47 deletions(-) diff --git a/src/application/handlers/blockHandler.ts b/src/application/handlers/blockHandler.ts index 817453c..054b9cc 100644 --- a/src/application/handlers/blockHandler.ts +++ b/src/application/handlers/blockHandler.ts @@ -41,9 +41,10 @@ import { parsePollError, } from "../helpers/pollResultErrors"; import { computeOrderUid, type GPv2OrderData } from "../helpers/orderUid"; +import { type OrderType } from "../../utils/order-types"; -const NON_DETERMINISTIC_TYPES = ["PerpetualSwap", "GoodAfterTime", "TradeAboveThreshold", "Unknown"] as const; -const SINGLE_SHOT_NON_DETERMINISTIC = ["GoodAfterTime", "TradeAboveThreshold"] as const; +const NON_DETERMINISTIC_TYPES: readonly OrderType[] = ["PerpetualSwap", "GoodAfterTime", "TradeAboveThreshold", "Unknown"]; +const SINGLE_SHOT_NON_DETERMINISTIC: readonly OrderType[] = ["GoodAfterTime", "TradeAboveThreshold"]; const BLOCK_NEVER = 2n ** 63n - 1n; // sentinel for epoch-scheduled generators (PollTryAtEpoch) const VALID_DISCRETE_STATUSES = new Set(["fulfilled", "unfilled", "expired", "cancelled"]); @@ -112,7 +113,7 @@ ponder.on("ContractPoller:block", async ({ event, context }) => { handler: Hex; salt: Hex; staticInput: Hex; - orderType: string; + orderType: OrderType; decodedParams: Record | null; consecutiveTryNextBlock: number; }[]; @@ -197,7 +198,7 @@ ponder.on("ContractPoller:block", async ({ event, context }) => { .onConflictDoNothing(), ); - const isSingleShot = (SINGLE_SHOT_NON_DETERMINISTIC as readonly string[]).includes(order.orderType); + const isSingleShot = SINGLE_SHOT_NON_DETERMINISTIC.includes(order.orderType); successPromises.push( updateGeneratorPollState(context, chainId, order.generatorId, currentBlock, { nextCheckBlock: currentBlock + RECHECK_INTERVAL, @@ -728,7 +729,7 @@ ponder.on("HistoricalBootstrap:block", async ({ event, context }) => { ) as { generatorId: string; owner: Hex; - orderType: string; + orderType: OrderType; }[]; // Exclude owners already retried above — they were just attempted this run @@ -820,7 +821,7 @@ ponder.on("DeterministicCancellationSweeper:block", async ({ event, context }) = generatorId: string; owner: Hex; hash: Hex; - orderType: string; + orderType: OrderType; }[]; if (dueGenerators.length === 0) return; diff --git a/src/application/helpers/orderbookClient.ts b/src/application/helpers/orderbookClient.ts index 5ef7b2d..0dd0771 100644 --- a/src/application/helpers/orderbookClient.ts +++ b/src/application/helpers/orderbookClient.ts @@ -20,6 +20,7 @@ import { } from "ponder:schema"; import { and, eq, sql } from "ponder"; import { encodeAbiParameters, keccak256, type Hex } from "viem"; +import { type OrderType } from "../../utils/order-types"; import { COMPOSABLE_COW_HANDLER_ADDRESSES, ORDERBOOK_API_URLS } from "../../data"; import { ORDERBOOK_HTTP_TIMEOUT_MS, SIGNING_SCHEME_EIP1271 } from "../../constants"; import { decodeEip1271Signature } from "../decoders/erc1271Signature"; @@ -52,7 +53,7 @@ export type ComposableOrder = Pick< uid: string; generatorId: string; generatorHash: string; - orderType: string; + orderType: OrderType; creationDate: number; }; @@ -274,7 +275,7 @@ export async function fetchOrderStatusByUids( status: order.status as ComposableOrder["status"], generatorId: "", generatorHash: "", - orderType: "", + orderType: "Unknown", sellAmount: order.sellAmount, buyAmount: order.buyAmount, feeAmount: order.feeAmount, @@ -432,7 +433,7 @@ async function filterAndProcess( ) .limit(1)) as { eventId: string; - orderType: string; + orderType: OrderType; }[]; if (generators.length === 0) continue; diff --git a/src/application/helpers/uidPrecompute.ts b/src/application/helpers/uidPrecompute.ts index 80ca37e..bd25086 100644 --- a/src/application/helpers/uidPrecompute.ts +++ b/src/application/helpers/uidPrecompute.ts @@ -20,7 +20,7 @@ import { and, eq } from "ponder"; import { candidateDiscreteOrder, conditionalOrderGenerator, discreteOrder } from "ponder:schema"; import { computeOrderUid, type GPv2OrderData } from "./orderUid"; import { fetchOrderStatusByUids } from "./orderbookClient"; -import { isDeterministicOrderType } from "../../utils/order-types"; +import { type OrderType, DETERMINISTIC_ORDER_TYPE } from "../../utils/order-types"; // GPv2Order.sol constant hashes const KIND_SELL = "0xf3b277728b3fee749481eb3e0b3b48980dbbab78658fc419025cb16eee346775" as Hex; @@ -55,12 +55,12 @@ export interface PrecomputedOrder { export function precomputeOrderUids( chainId: number, owner: Hex, - orderType: string, + orderType: OrderType, decodedParams: Record | null, blockTimestamp: bigint, ): PrecomputedOrder[] | null { if (!decodedParams) { - if (isDeterministicOrderType(orderType)) { + if (DETERMINISTIC_ORDER_TYPE[orderType]) { console.warn(`[COW:PRECOMPUTE] SKIP type=${orderType} owner=${owner} chain=${chainId} reason=decodedParams_null`); } return null; @@ -93,7 +93,7 @@ export async function precomputeAndDiscover( chainId: number, generatorEventId: string, owner: Hex, - orderType: string, + orderType: OrderType, decodedParams: Record | null, blockTimestamp: bigint, ): Promise { diff --git a/src/utils/order-types.ts b/src/utils/order-types.ts index a9540df..93cd1b3 100644 --- a/src/utils/order-types.ts +++ b/src/utils/order-types.ts @@ -77,8 +77,14 @@ export function getOrderTypeFromHandler( // Single source of truth for which order types have UIDs computable from staticInput // alone (no on-chain calls). Keep in sync with the switch in `precomputeOrderUids`. -export const DETERMINISTIC_ORDER_TYPES = new Set(["TWAP", "StopLoss", "CirclesBackingOrder"]); - -export function isDeterministicOrderType(orderType: string): boolean { - return DETERMINISTIC_ORDER_TYPES.has(orderType as OrderType); -} +export const DETERMINISTIC_ORDER_TYPE: Record = { + TWAP: true, + StopLoss: true, + CirclesBackingOrder: true, + PerpetualSwap: false, + GoodAfterTime: false, + TradeAboveThreshold: false, + SwapOrderHandler: false, + ERC4626CowSwapFeeBurner: false, + Unknown: false, +}; diff --git a/tests/utils/order-types.test.ts b/tests/utils/order-types.test.ts index cd0c5ad..46e39d0 100644 --- a/tests/utils/order-types.test.ts +++ b/tests/utils/order-types.test.ts @@ -1,39 +1,30 @@ import { describe, it, expect } from "vitest"; import { - DETERMINISTIC_ORDER_TYPES, - isDeterministicOrderType, + DETERMINISTIC_ORDER_TYPE, + type OrderType, } from "../../src/utils/order-types"; -describe("DETERMINISTIC_ORDER_TYPES", () => { - it("includes TWAP", () => { - expect(DETERMINISTIC_ORDER_TYPES.has("TWAP")).toBe(true); +describe("DETERMINISTIC_ORDER_TYPE", () => { + it("covers every OrderType (exhaustive record)", () => { + // If a new OrderType is added to the union without updating the record, + // TypeScript will catch it at compile time. This test documents the intent. + const types = Object.keys(DETERMINISTIC_ORDER_TYPE) as OrderType[]; + expect(types.length).toBeGreaterThan(0); }); - it("includes StopLoss", () => { - expect(DETERMINISTIC_ORDER_TYPES.has("StopLoss")).toBe(true); + it("marks TWAP, StopLoss, CirclesBackingOrder as deterministic", () => { + expect(DETERMINISTIC_ORDER_TYPE["TWAP"]).toBe(true); + expect(DETERMINISTIC_ORDER_TYPE["StopLoss"]).toBe(true); + // Regression guard for COW-1003: CirclesBackingOrder must be deterministic + expect(DETERMINISTIC_ORDER_TYPE["CirclesBackingOrder"]).toBe(true); }); - // Regression guard for COW-1003 (F2): CirclesBackingOrder is deterministic - // (precomputed in uidPrecompute.ts) but was missing from this set, causing - // spurious non-deterministic warnings in logs. - it("includes CirclesBackingOrder (COW-1003)", () => { - expect(DETERMINISTIC_ORDER_TYPES.has("CirclesBackingOrder")).toBe(true); - }); - - it("does not include non-deterministic types", () => { - expect(DETERMINISTIC_ORDER_TYPES.has("PerpetualSwap")).toBe(false); - expect(DETERMINISTIC_ORDER_TYPES.has("GoodAfterTime")).toBe(false); - expect(DETERMINISTIC_ORDER_TYPES.has("TradeAboveThreshold")).toBe(false); - }); - - it("isDeterministicOrderType returns true for all members", () => { - for (const type of DETERMINISTIC_ORDER_TYPES) { - expect(isDeterministicOrderType(type)).toBe(true); - } - }); - - it("isDeterministicOrderType returns false for unknown types", () => { - expect(isDeterministicOrderType("Unknown")).toBe(false); - expect(isDeterministicOrderType("")).toBe(false); + it("marks non-deterministic types as false", () => { + expect(DETERMINISTIC_ORDER_TYPE["PerpetualSwap"]).toBe(false); + expect(DETERMINISTIC_ORDER_TYPE["GoodAfterTime"]).toBe(false); + expect(DETERMINISTIC_ORDER_TYPE["TradeAboveThreshold"]).toBe(false); + expect(DETERMINISTIC_ORDER_TYPE["SwapOrderHandler"]).toBe(false); + expect(DETERMINISTIC_ORDER_TYPE["ERC4626CowSwapFeeBurner"]).toBe(false); + expect(DETERMINISTIC_ORDER_TYPE["Unknown"]).toBe(false); }); });