Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -28,6 +28,7 @@ import {
isNativeToken,
populateEthFlowTx,
sendOrder,
sendOrderForWrappingDelegate,
uploadAppData,
} from '../../helpers/cow';
import { useSwapGasEstimation } from '../../hooks/useSwapGasEstimation';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
98 changes: 98 additions & 0 deletions src/components/transactions/Swap/helpers/cow/orders.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
OrderClass,
OrderKind,
OrderParameters,
OrderSigningUtils,
OrderStatus,
QuoteAndPost,
SellTokenSource,
Expand Down Expand Up @@ -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, {
Expand Down
53 changes: 53 additions & 0 deletions src/helpers/eip7702.ts
Original file line number Diff line number Diff line change
@@ -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<string>();

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<WalletKind> {
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';
}
Loading