Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@ export function mapTwapOrderToStoreOrder(order: TwapOrderItem, tokensByAddress:
status,
apiAdditionalInfo: enrichedOrder,
isCancelling: order.status === TwapOrderStatus.Cancelling,
isUnfillable: order.isUnfillable,
}
}
3 changes: 3 additions & 0 deletions apps/cowswap-frontend/src/modules/twap/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export interface TwapOrderItem {
id: string
safeTxParams?: SafeTransactionParams
executionInfo: TwapOrdersExecution
// True when an open order cannot be executed because the Safe's ComposableCoW
// fallback handler is missing/reset by the user (see issue #5426)
isUnfillable?: boolean
}

export type TwapOrdersAuthResult = { [key: string]: boolean | undefined }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { ComposableCowContractData } from 'modules/advancedOrders'

import { TWAP_PENDING_STATUSES } from '../const'
import { useAllTwapOrdersInfo } from '../hooks/useAllTwapOrdersInfo'
import { useIsFallbackHandlerRequired } from '../hooks/useFallbackHandlerVerification'
import { useFetchTwapOrdersFromSafe } from '../hooks/useFetchTwapOrdersFromSafe'
import { useTwapOrdersAuthMulticall } from '../hooks/useTwapOrdersAuthMulticall'
import { useTwapOrdersExecutions } from '../hooks/useTwapOrdersExecutions'
Expand Down Expand Up @@ -52,6 +53,12 @@ export function TwapOrdersUpdater(props: {
// eslint-disable-next-line react-hooks/refs
twapOrderExecutions.current = _twapOrderExecutions

// When the Safe's fallback handler is missing/reset, open orders will never execute
const isFallbackHandlerRequired = useIsFallbackHandlerRequired()
const isFallbackHandlerRequiredRef = useRef(isFallbackHandlerRequired)
// eslint-disable-next-line react-hooks/refs
isFallbackHandlerRequiredRef.current = isFallbackHandlerRequired

// Here we can split all orders in two groups: 1. Not signed + expired, 2. Open + cancelled
const pendingTwapOrderIds = useMemo(() => {
// eslint-disable-next-line react-hooks/refs
Expand Down Expand Up @@ -96,6 +103,7 @@ export function TwapOrdersUpdater(props: {
allOrdersInfo,
ordersAuthResult,
twapOrderExecutions.current,
isFallbackHandlerRequiredRef.current,
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
type TwapOrdersAuthResult,
type TwapOrdersExecution,
type TwapOrdersSafeData,
TwapOrderStatus,
} from '../types'

import type { Hex } from 'viem'
Expand All @@ -23,6 +24,7 @@ export function buildTwapOrdersItems(
ordersInfo: TwapOrderInfo[],
ordersAuthResult: TwapOrdersAuthResult,
twapOrderExecutions: TwapOrdersExecutionMap,
isFallbackHandlerBroken: boolean,
): TwapOrdersList {
return ordersInfo.reduce<TwapOrdersList>((acc, { safeData, id }) => {
acc[id] = getTwapOrderItem(
Expand All @@ -32,6 +34,7 @@ export function buildTwapOrdersItems(
id as `0x${string}`,
ordersAuthResult[id],
twapOrderExecutions[id] ?? DEFAULT_TWAP_EXECUTION,
isFallbackHandlerBroken,
)
return acc
}, {})
Expand All @@ -44,6 +47,7 @@ function getTwapOrderItem(
id: Hex,
authorized: boolean | undefined,
executionInfo: TwapOrdersExecution,
isFallbackHandlerBroken: boolean,
): TwapOrderItem {
const { conditionalOrderParams, safeTxParams } = safeData
const { isExecuted, submissionDate, executionDate: _executionDate } = safeTxParams
Expand All @@ -52,6 +56,10 @@ function getTwapOrderItem(
const order = parseTwapOrderStruct(conditionalOrderParams.staticInput as `0x${string}`)
const status = getTwapOrderStatus(order, isExecuted, executionDate, authorized, executionInfo)

// An open (Pending) order will never be picked up by watchtower when the Safe's
// ComposableCoW fallback handler has been reset/removed, so surface it as Unfillable.
const isUnfillable = isFallbackHandlerBroken && status === TwapOrderStatus.Pending

return {
order,
status,
Expand All @@ -62,5 +70,6 @@ function getTwapOrderItem(
executedDate: _executionDate || undefined,
safeTxParams,
executionInfo,
isUnfillable,
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { EnrichedOrder, getAddressKey } from '@cowprotocol/cow-sdk'
import { TokensByAddress } from '@cowprotocol/tokens'

import { Order } from 'legacy/state/orders/actions'
import { Order, OrderStatus } from 'legacy/state/orders/actions'

import { getIsLastPartOrder } from './getIsLastPartOrder'
import { getPartOrderStatus } from './getPartOrderStatus'
Expand All @@ -19,6 +19,9 @@ export function mapPartOrderToStoreOrder(
const isCancelling = item.isCancelling || parent.status === TwapOrderStatus.Cancelling
const status = getPartOrderStatus(enrichedOrder, parent, isVirtualPart)

// A broken fallback handler blocks the whole order, so mark still-open parts as unfillable too
const isUnfillable = !!parent.isUnfillable && (status === OrderStatus.PENDING || status === OrderStatus.SCHEDULED)

const inputToken = tokensByAddress[getAddressKey(enrichedOrder.sellToken)]
const outputToken = tokensByAddress[getAddressKey(enrichedOrder.buyToken)]

Expand All @@ -39,5 +42,6 @@ export function mapPartOrderToStoreOrder(
status,
apiAdditionalInfo: enrichedOrder,
isCancelling,
isUnfillable,
}
}
Loading