diff --git a/src/components/transactions/Swap/actions/SwapActions/SwapActionsViaCoW.tsx b/src/components/transactions/Swap/actions/SwapActions/SwapActionsViaCoW.tsx index b15cff96c7..be940929e6 100644 --- a/src/components/transactions/Swap/actions/SwapActions/SwapActionsViaCoW.tsx +++ b/src/components/transactions/Swap/actions/SwapActions/SwapActionsViaCoW.tsx @@ -9,7 +9,7 @@ import { BigNumber } from 'ethers'; import stringify from 'json-stringify-deterministic'; import { Dispatch, useMemo } from 'react'; import { TxActionsWrapper } from 'src/components/transactions/TxActionsWrapper'; -import { isSmartContractWallet } from 'src/helpers/provider'; +import { classifyAccount } from 'src/helpers/eip7702'; import { useModalContext } from 'src/hooks/useModal'; import { useSwapOrdersTracking } from 'src/hooks/useSwapOrdersTracking'; import { useWeb3Context } from 'src/libs/hooks/useWeb3Context'; @@ -28,6 +28,7 @@ import { isNativeToken, populateEthFlowTx, sendOrder, + sendOrderForWrappingDelegate, uploadAppData, } from '../../helpers/cow'; import { useSwapGasEstimation } from '../../hooks/useSwapGasEstimation'; @@ -279,7 +280,12 @@ export const SwapActionsViaCoW = ({ } else { let orderId; try { - if (await isSmartContractWallet(user, provider)) { + // Three-way wallet branching: + // contract -> setPreSignature on-chain + // delegated-eoa-wrapping -> EIP-1271 off-chain, bytes forwarded raw + // eoa / delegated-eoa-plain -> EIP-712 off-chain + const walletKind = await classifyAccount(user, provider); + if (walletKind === 'contract') { const preSignTransaction = await getPreSignTransaction({ provider, validTo, @@ -333,6 +339,42 @@ export const SwapActionsViaCoW = ({ success: true, txHash: preSignTransaction.orderId, }); + } else if (walletKind === 'delegated-eoa-wrapping') { + orderId = await sendOrderForWrappingDelegate({ + validTo, + tokenSrc: state.sourceToken.addressToSwap, + tokenSrcDecimals: state.sourceToken.decimals, + tokenDest: state.destinationToken.addressToSwap, + tokenDestDecimals: state.destinationToken.decimals, + quote: state.swapRate?.order, + sellAmount: sellAmountAccountingCosts.toString(), + buyAmount: buyAmountAccountingCosts.toString(), + slippageBps, + smartSlippage, + orderType: state.orderType, + swapType: params.swapType, + market: currentMarket, + kind: + state.orderType === OrderType.MARKET + ? OrderKind.SELL + : state.side === 'buy' + ? OrderKind.BUY + : OrderKind.SELL, + chainId: state.chainId, + user, + provider, + inputSymbol: state.sourceToken.symbol, + outputSymbol: state.destinationToken.symbol, + appCode, + orderBookQuote: state.swapRate?.orderBookQuote, + signatureParams, + estimateGasLimit, + }); + setMainTxState({ + loading: false, + success: true, + txHash: orderId, + }); } else { orderId = await sendOrder({ validTo, diff --git a/src/components/transactions/Swap/helpers/cow/orders.helpers.ts b/src/components/transactions/Swap/helpers/cow/orders.helpers.ts index 361a791ec2..f014ab59a5 100644 --- a/src/components/transactions/Swap/helpers/cow/orders.helpers.ts +++ b/src/components/transactions/Swap/helpers/cow/orders.helpers.ts @@ -6,6 +6,7 @@ import { OrderClass, OrderKind, OrderParameters, + OrderSigningUtils, OrderStatus, QuoteAndPost, SellTokenSource, @@ -239,6 +240,103 @@ export const sendOrder = async ({ .then((orderResult) => orderResult.orderId); }; +/** + * Send a CoW order for an EIP-7702 delegated EOA whose delegate wraps + * signatures at sign time (ERC-7739 / ERC-7579 MA v2). The wallet's + * signTypedData returns bytes that verify via the owner's isValidSignature, + * which 7702 dispatches to the delegate. Submitted with + * `signingScheme: EIP1271` and `from = user EOA`; CoW resolves verification + * on-chain via the delegate. + */ +export const sendOrderForWrappingDelegate = async ({ + provider, + chainId, + user, + slippageBps, + inputSymbol, + outputSymbol, + smartSlippage, + appCode, + orderType, + sellAmount, + buyAmount, + tokenSrc, + tokenDest, + tokenSrcDecimals, + tokenDestDecimals, + kind, + signatureParams, + estimateGasLimit, + validTo, + swapType, + market, +}: CowProtocolActionParams) => { + const signer = provider?.getSigner(); + + if (!isChainIdSupportedByCoWProtocol(chainId)) { + throw new Error('Chain not supported.'); + } + if (!signer) { + throw new Error('No signer found in provider'); + } + + const permitHook = + signatureParams && estimateGasLimit + ? await getPermitHook({ tokenAddress: tokenSrc, signatureParams, estimateGasLimit, chainId }) + : undefined; + + const hooks = permitHook ? { pre: [permitHook] } : undefined; + + const appData = COW_APP_DATA( + inputSymbol, + outputSymbol, + slippageBps, + smartSlippage, + orderType, + appCode, + swapType, + market, + hooks + ); + + const tradingSdk = await getCowTradingSdkByChainIdAndAppCode(chainId, appCode); + + return tradingSdk + .postLimitOrder( + { + sellAmount, + buyAmount, + kind: kind == OrderKind.SELL ? OrderKind.SELL : OrderKind.BUY, + sellToken: tokenSrc, + buyToken: tokenDest, + sellTokenDecimals: tokenSrcDecimals, + buyTokenDecimals: tokenDestDecimals, + validTo, + owner: user as `0x${string}`, + env: COW_ENV, + }, + { + appData, + additionalParams: { + applyCostsSlippageAndFees: false, + signingScheme: SigningScheme.EIP1271, + // The wallet's signTypedData wraps internally per its delegate's + // spec (7739 / MA v2 / both). We forward bytes verbatim — no + // (order, sig) ABI tuple wrap. + customEIP1271Signature: async (orderToSign, walletSigner) => { + const result = await OrderSigningUtils.signOrder( + orderToSign, + chainId as SupportedChainId, + walletSigner + ); + return result.signature; + }, + }, + } + ) + .then((orderResult) => orderResult.orderId); +}; + export const getOrderStatus = async (orderId: string, chainId: number) => { const orderBookApi = new OrderBookApi({ chainId: chainId, env: COW_ENV }); const status = await orderBookApi.getOrderCompetitionStatus(orderId, { diff --git a/src/helpers/eip7702.ts b/src/helpers/eip7702.ts new file mode 100644 index 0000000000..77e9a5c947 --- /dev/null +++ b/src/helpers/eip7702.ts @@ -0,0 +1,53 @@ +import { JsonRpcProvider } from '@ethersproject/providers'; + +/** + * On-chain classification of an account, from CoW's signing-scheme + * perspective. + * + * Vendored from the cow-sdk proposal in + * https://github.com/cowprotocol/cow-sdk/pull/878. When that PR merges, drop + * this module and import `classifyAccount` from `@cowprotocol/sdk-common`. + */ +export type WalletKind = 'eoa' | 'delegated-eoa-plain' | 'delegated-eoa-wrapping' | 'contract'; + +const EIP7702_DELEGATION_PREFIX = '0xef0100'; +const EIP7702_DELEGATION_HEX_LENGTH = 2 + 23 * 2; // "0x" + 23 bytes + +/** + * Delegate addresses (lowercase, no `0x`) known to force signature wrapping + * at `signTypedData_v4` time (ERC-7739 / ERC-7579 MA v2 / both). Empty today; + * populate as wrapping delegates are confirmed in production. + */ +const WRAPPING_DELEGATES = new Set(); + +function isEip7702DelegationCode(code: string): boolean { + if (!code) return false; + const lower = code.toLowerCase(); + return ( + lower.length === EIP7702_DELEGATION_HEX_LENGTH && lower.startsWith(EIP7702_DELEGATION_PREFIX) + ); +} + +function extractEip7702Delegate(code: string): string | null { + if (!isEip7702DelegationCode(code)) return null; + return code.slice(2 + 6).toLowerCase(); +} + +/** + * Classify an account by inspecting its on-chain code. Drives CoW + * signing-scheme selection at the call-site of `sendOrder` / + * `getPreSignTransaction` / `sendOrderForWrappingDelegate`. + */ +export async function classifyAccount( + user: string, + provider: JsonRpcProvider +): Promise { + const code = await provider.getCode(user); + if (!code || code === '0x') return 'eoa'; + + const delegate = extractEip7702Delegate(code); + if (delegate !== null) { + return WRAPPING_DELEGATES.has(delegate) ? 'delegated-eoa-wrapping' : 'delegated-eoa-plain'; + } + return 'contract'; +}