From 578df822f761f720020563093dae38c85c366432 Mon Sep 17 00:00:00 2001 From: Leandro Date: Fri, 17 Nov 2023 02:25:14 -0800 Subject: [PATCH] feat(permit): fetch token name from chain (#3374) * chore: add PermitUtils type `unsupported` * chore: apply PermitInfo type to consumers * feat: add helper method to only check whether token is permittable useTokenSupportsPermit * refactor: rename useIsTokenPermittable to usePermitInfo * feat: add utils fn isSupportedPermitInfo * refactor: move inner private method getContract to external utils fn * feat: add minimal erc20 `name` method abi * feat: add utils fn `getTokenName` * feat: fetch token name when fetching permit info * feat: make tokenName optional on GetTokenPermitInfoParams * feat: migrate preGenerated permit info to new format * chore: more tokenName optional changes * chore: add TODOs * refactor: return error rather than throwing * chore: bump permit-utils package version * chore: bump permittableTokens atom version to v2 * refactor: use isSupportedPermitInfo in a few places * chore: do a type guard in the helper fn so there's no need for casting * chore: return error when name fetching fails due to network issues * chore: identify earlier when token is not dai-like and return received error * chore: only log debug msg if not dai-like * chore: mark some known error types as permanent errors * chore: log also tokenName when possible * chore: use a regex to catch connection issues * chore: use const for default obj return to avoid re-renders * refactor: remove redundant check. It's covered by isSupportedPermitInfo * chore: set permit-utils version to 0.0.1 --- .../limitOrders/hooks/useTradeFlowContext.ts | 4 +- .../limitOrders/services/tradeFlow/index.ts | 3 +- .../hooks/useAccountAgnosticPermitHookData.ts | 8 +- .../hooks/useCheckHasValidPendingPermit.ts | 2 +- .../permit/hooks/useGeneratePermitHook.ts | 9 ++- .../permit/hooks/usePermitCompatibleTokens.ts | 5 +- ...IsTokenPermittable.ts => usePermitInfo.ts} | 39 ++++----- .../permit/hooks/usePreGeneratedPermitInfo.ts | 26 +++++- .../permit/hooks/useTokenSupportsPermit.ts | 20 +++++ .../src/modules/permit/index.ts | 3 +- .../permit/state/permittableTokensAtom.ts | 5 +- .../src/modules/permit/utils/handlePermit.ts | 4 +- .../swap/hooks/useSwapButtonContext.ts | 4 +- .../modules/swap/hooks/useSwapFlowContext.ts | 4 +- .../modules/swap/services/swapFlow/index.ts | 3 +- .../hooks/useTradeFormValidationContext.ts | 4 +- libs/permit-utils/package.json | 2 +- libs/permit-utils/src/abi/erc20.json | 16 ++++ libs/permit-utils/src/index.ts | 10 +-- .../src/lib/checkIsCallDataAValidPermit.ts | 17 +++- .../src/lib/generatePermitHook.ts | 13 ++- .../src/lib/getTokenPermitInfo.ts | 79 ++++++++++++++++--- libs/permit-utils/src/types.ts | 19 +++-- .../src/utils/PermitProviderConnector.ts | 9 +-- libs/permit-utils/src/utils/fixTokenName.ts | 1 + libs/permit-utils/src/utils/getContract.ts | 7 ++ libs/permit-utils/src/utils/getTokenName.ts | 14 ++++ .../src/utils/isSupportedPermitInfo.ts | 5 ++ 28 files changed, 245 insertions(+), 90 deletions(-) rename apps/cowswap-frontend/src/modules/permit/hooks/{useIsTokenPermittable.ts => usePermitInfo.ts} (74%) create mode 100644 apps/cowswap-frontend/src/modules/permit/hooks/useTokenSupportsPermit.ts create mode 100644 libs/permit-utils/src/abi/erc20.json create mode 100644 libs/permit-utils/src/utils/getContract.ts create mode 100644 libs/permit-utils/src/utils/getTokenName.ts create mode 100644 libs/permit-utils/src/utils/isSupportedPermitInfo.ts diff --git a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useTradeFlowContext.ts b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useTradeFlowContext.ts index 488fa09af8..244a10c9ab 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useTradeFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useTradeFlowContext.ts @@ -15,7 +15,7 @@ import { useAppData } from 'modules/appData' import { useRateImpact } from 'modules/limitOrders/hooks/useRateImpact' import { TradeFlowContext } from 'modules/limitOrders/services/types' import { limitOrdersSettingsAtom } from 'modules/limitOrders/state/limitOrdersSettingsAtom' -import { useGeneratePermitHook, useIsTokenPermittable } from 'modules/permit' +import { useGeneratePermitHook, usePermitInfo } from 'modules/permit' import { useEnoughBalanceAndAllowance } from 'modules/tokens' import { TradeType } from 'modules/trade' import { useTradeQuote } from 'modules/tradeQuote' @@ -34,7 +34,7 @@ export function useTradeFlowContext(): TradeFlowContext | null { const quoteState = useTradeQuote() const rateImpact = useRateImpact() const settingsState = useAtomValue(limitOrdersSettingsAtom) - const permitInfo = useIsTokenPermittable(state.inputCurrency, TradeType.LIMIT_ORDER) + const permitInfo = usePermitInfo(state.inputCurrency, TradeType.LIMIT_ORDER) const checkAllowanceAddress = GP_VAULT_RELAYER[chainId] const { enoughAllowance } = useEnoughBalanceAndAllowance({ diff --git a/apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts b/apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts index e6c7baf500..7b59c53fd3 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts @@ -1,4 +1,5 @@ import { OrderClass } from '@cowprotocol/cow-sdk' +import { isSupportedPermitInfo } from '@cowprotocol/permit-utils' import { Percent } from '@uniswap/sdk-core' import { PriceImpact } from 'legacy/hooks/usePriceImpact' @@ -57,7 +58,7 @@ export async function tradeFlow( try { logTradeFlow('LIMIT ORDER FLOW', 'STEP 2: handle permit') - if (permitInfo) beforePermit() + if (isSupportedPermitInfo(permitInfo)) beforePermit() postOrderParams.appData = await handlePermit({ permitInfo, diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/useAccountAgnosticPermitHookData.ts b/apps/cowswap-frontend/src/modules/permit/hooks/useAccountAgnosticPermitHookData.ts index 580e46f2ee..a4e2c94754 100644 --- a/apps/cowswap-frontend/src/modules/permit/hooks/useAccountAgnosticPermitHookData.ts +++ b/apps/cowswap-frontend/src/modules/permit/hooks/useAccountAgnosticPermitHookData.ts @@ -1,13 +1,13 @@ import { useEffect, useState } from 'react' -import { PermitHookData } from '@cowprotocol/permit-utils' +import { isSupportedPermitInfo, PermitHookData } from '@cowprotocol/permit-utils' import { useDerivedTradeState } from 'modules/trade' import { useSafeMemo } from 'common/hooks/useSafeMemo' import { useGeneratePermitHook } from './useGeneratePermitHook' -import { useIsTokenPermittable } from './useIsTokenPermittable' +import { usePermitInfo } from './usePermitInfo' import { GeneratePermitHookParams } from '../types' @@ -41,10 +41,10 @@ function useGeneratePermitHookParams(): GeneratePermitHookParams | undefined { const { state } = useDerivedTradeState() const { inputCurrency, tradeType } = state || {} - const permitInfo = useIsTokenPermittable(inputCurrency, tradeType) + const permitInfo = usePermitInfo(inputCurrency, tradeType) return useSafeMemo(() => { - if (!inputCurrency || !('address' in inputCurrency) || !permitInfo) return undefined + if (!inputCurrency || !('address' in inputCurrency) || !isSupportedPermitInfo(permitInfo)) return undefined return { inputToken: { address: inputCurrency.address, name: inputCurrency.name }, diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/useCheckHasValidPendingPermit.ts b/apps/cowswap-frontend/src/modules/permit/hooks/useCheckHasValidPendingPermit.ts index b32c0410ad..045e31ad8f 100644 --- a/apps/cowswap-frontend/src/modules/permit/hooks/useCheckHasValidPendingPermit.ts +++ b/apps/cowswap-frontend/src/modules/permit/hooks/useCheckHasValidPendingPermit.ts @@ -64,7 +64,7 @@ async function checkHasValidPendingPermit( const eip2162Utils = getPermitUtilsInstance(chainId, provider, order.owner) const tokenAddress = order.inputToken.address - const tokenName = order.inputToken.name || tokenAddress + const tokenName = order.inputToken.name const checkedHooks = await Promise.all( preHooks.map(({ callData }) => diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/useGeneratePermitHook.ts b/apps/cowswap-frontend/src/modules/permit/hooks/useGeneratePermitHook.ts index 2a821920e9..2110d9f934 100644 --- a/apps/cowswap-frontend/src/modules/permit/hooks/useGeneratePermitHook.ts +++ b/apps/cowswap-frontend/src/modules/permit/hooks/useGeneratePermitHook.ts @@ -2,7 +2,12 @@ import { useAtomValue, useSetAtom } from 'jotai' import { useCallback } from 'react' import { GP_VAULT_RELAYER } from '@cowprotocol/common-const' -import { generatePermitHook, getPermitUtilsInstance, PermitHookData } from '@cowprotocol/permit-utils' +import { + generatePermitHook, + getPermitUtilsInstance, + isSupportedPermitInfo, + PermitHookData, +} from '@cowprotocol/permit-utils' import { useWalletInfo } from '@cowprotocol/wallet' import { useWeb3React } from '@web3-react/core' @@ -38,7 +43,7 @@ export function useGeneratePermitHook(): GeneratePermitHook { async (params: GeneratePermitHookParams): Promise => { const { inputToken, account, permitInfo } = params - if (!provider) { + if (!provider || !isSupportedPermitInfo(permitInfo)) { return } diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/usePermitCompatibleTokens.ts b/apps/cowswap-frontend/src/modules/permit/hooks/usePermitCompatibleTokens.ts index bbcdd4ca2b..2fd0801779 100644 --- a/apps/cowswap-frontend/src/modules/permit/hooks/usePermitCompatibleTokens.ts +++ b/apps/cowswap-frontend/src/modules/permit/hooks/usePermitCompatibleTokens.ts @@ -1,6 +1,7 @@ import { useAtomValue } from 'jotai' import { useMemo, useRef } from 'react' +import { isSupportedPermitInfo } from '@cowprotocol/permit-utils' import { useWalletInfo } from '@cowprotocol/wallet' import { useIsPermitEnabled } from 'common/hooks/featureFlags/useIsPermitEnabled' @@ -33,11 +34,11 @@ export function usePermitCompatibleTokens(): PermitCompatibleTokens { const permitCompatibleTokens: PermitCompatibleTokens = {} for (const address of Object.keys(preGeneratedPermitInfoRef.current)) { - permitCompatibleTokens[address.toLowerCase()] = !!preGeneratedPermitInfoRef.current[address] + permitCompatibleTokens[address.toLowerCase()] = isSupportedPermitInfo(preGeneratedPermitInfoRef.current[address]) } for (const address of Object.keys(localPermitInfoRef.current)) { - permitCompatibleTokens[address.toLowerCase()] = !!localPermitInfoRef.current[address] + permitCompatibleTokens[address.toLowerCase()] = isSupportedPermitInfo(localPermitInfoRef.current[address]) } return permitCompatibleTokens diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/useIsTokenPermittable.ts b/apps/cowswap-frontend/src/modules/permit/hooks/usePermitInfo.ts similarity index 74% rename from apps/cowswap-frontend/src/modules/permit/hooks/useIsTokenPermittable.ts rename to apps/cowswap-frontend/src/modules/permit/hooks/usePermitInfo.ts index 025b281819..636b006b00 100644 --- a/apps/cowswap-frontend/src/modules/permit/hooks/useIsTokenPermittable.ts +++ b/apps/cowswap-frontend/src/modules/permit/hooks/usePermitInfo.ts @@ -4,7 +4,7 @@ import { useEffect, useMemo } from 'react' import { GP_VAULT_RELAYER } from '@cowprotocol/common-const' import { getIsNativeToken, getWrappedToken } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' -import { getTokenPermitInfo } from '@cowprotocol/permit-utils' +import { getTokenPermitInfo, PermitInfo } from '@cowprotocol/permit-utils' import { useWalletInfo } from '@cowprotocol/wallet' import { Currency } from '@uniswap/sdk-core' import { useWeb3React } from '@web3-react/core' @@ -21,24 +21,25 @@ import { ORDER_TYPE_SUPPORTS_PERMIT } from '../const' import { addPermitInfoForTokenAtom, permittableTokensAtom } from '../state/permittableTokensAtom' import { IsTokenPermittableResult } from '../types' +const UNSUPPORTED: PermitInfo = { type: 'unsupported', name: 'native' } + /** - * Checks whether the token is permittable, and caches the result on localStorage + * Check whether the token is permittable, and returns the permit info for it + * Tries to find it out from the pre-generated list + * If not found, tries to load the info from chain + * The result will be cached on localStorage if a final conclusion is found * * When it is, returned type is `{type: 'dai'|'permit', gasLimit: number} - * When it is not, returned type is `false` + * When it is not, returned type is `{type: 'unsupported'}` * When it is unknown, returned type is `undefined` - * */ -export function useIsTokenPermittable( - token: Nullish, - tradeType: Nullish -): IsTokenPermittableResult { +export function usePermitInfo(token: Nullish, tradeType: Nullish): IsTokenPermittableResult { const { chainId } = useWalletInfo() const { provider } = useWeb3React() const lowerCaseAddress = token ? getWrappedToken(token).address?.toLowerCase() : undefined const isNative = !!token && getIsNativeToken(token) - const tokenName = token?.name || lowerCaseAddress || '' + const tokenName = token?.name // Avoid building permit info in the first place if order type is not supported const isPermitSupported = !!tradeType && ORDER_TYPE_SUPPORTS_PERMIT[tradeType] @@ -46,7 +47,7 @@ export function useIsTokenPermittable( const isPermitEnabled = useIsPermitEnabled() && isPermitSupported const addPermitInfo = useAddPermitInfo() - const permitInfo = usePermitInfo(chainId, isPermitEnabled ? lowerCaseAddress : undefined) + const permitInfo = _usePermitInfo(chainId, isPermitEnabled ? lowerCaseAddress : undefined) const { permitInfo: preGeneratedInfo, isLoading: preGeneratedIsLoading } = usePreGeneratedPermitInfoForToken( isPermitEnabled ? token : undefined ) @@ -70,16 +71,13 @@ export function useIsTokenPermittable( } getTokenPermitInfo({ spender, tokenAddress: lowerCaseAddress, tokenName, chainId, provider }).then((result) => { - if (!result) { - // When falsy, we know it doesn't support permit. Cache it. - addPermitInfo({ chainId, tokenAddress: lowerCaseAddress, permitInfo: false }) - } else if ('error' in result) { + if ('error' in result) { // When error, we don't know. Log and don't cache. console.debug( `useIsTokenPermittable: failed to check whether token ${lowerCaseAddress} is permittable: ${result.error}` ) } else { - // Otherwise, we know it is permittable. Cache it. + // Otherwise, we know it is permittable or not. Cache it. addPermitInfo({ chainId, tokenAddress: lowerCaseAddress, permitInfo: result }) } }) @@ -98,7 +96,7 @@ export function useIsTokenPermittable( ]) if (isNative) { - return false + return UNSUPPORTED } return preGeneratedInfo ?? permitInfo @@ -111,14 +109,7 @@ function useAddPermitInfo() { return useSetAtom(addPermitInfoForTokenAtom) } -/** - * Returns whether a token is permittable. - * - * When it is, returned type is `{type: 'dai'|'permit', gasLimit: number}` - * When it is not, returned type is `false` - * When it is unknown, returned type is `undefined` - */ -function usePermitInfo(chainId: SupportedChainId, tokenAddress: string | undefined): IsTokenPermittableResult { +function _usePermitInfo(chainId: SupportedChainId, tokenAddress: string | undefined): IsTokenPermittableResult { const permittableTokens = useAtomValue(permittableTokensAtom) return useMemo(() => { diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/usePreGeneratedPermitInfo.ts b/apps/cowswap-frontend/src/modules/permit/hooks/usePreGeneratedPermitInfo.ts index 3ea379ee01..856515fa8e 100644 --- a/apps/cowswap-frontend/src/modules/permit/hooks/usePreGeneratedPermitInfo.ts +++ b/apps/cowswap-frontend/src/modules/permit/hooks/usePreGeneratedPermitInfo.ts @@ -21,9 +21,33 @@ export function usePreGeneratedPermitInfo(): { const { data, isLoading } = useSWR( url, - (url: string): Promise> => fetch(url).then((r) => r.json()), + (url: string): Promise> => + fetch(url) + .then((r) => r.json()) + .then(migrateData), { ...SWR_NO_REFRESH_OPTIONS, fallbackData: {} } ) return { allPermitInfo: data, isLoading } } + +type OldPermitInfo = PermitInfo | false + +const UNSUPPORTED: PermitInfo = { type: 'unsupported' } + +/** + * Handles data migration from former way of storing unsupported tokens to the new one + */ +function migrateData(data: Record): Record { + const migrated: Record = {} + + for (const [k, v] of Object.entries(data)) { + if (v === false) { + migrated[k] = UNSUPPORTED + } else { + migrated[k] = v + } + } + + return migrated +} diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/useTokenSupportsPermit.ts b/apps/cowswap-frontend/src/modules/permit/hooks/useTokenSupportsPermit.ts new file mode 100644 index 0000000000..993ccf4937 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/permit/hooks/useTokenSupportsPermit.ts @@ -0,0 +1,20 @@ +import { isSupportedPermitInfo } from '@cowprotocol/permit-utils' +import { Currency } from '@uniswap/sdk-core' + +import { Nullish } from 'types' + +import { TradeType } from 'modules/trade' + +import { usePermitInfo } from './usePermitInfo' + +/** + * Whether the token supports permit for given trade type + * + * @param token + * @param tradeType + */ +export function useTokenSupportsPermit(token: Nullish, tradeType: Nullish): boolean { + const permitInfo = usePermitInfo(token, tradeType) + + return isSupportedPermitInfo(permitInfo) +} diff --git a/apps/cowswap-frontend/src/modules/permit/index.ts b/apps/cowswap-frontend/src/modules/permit/index.ts index c7e91b4054..bc55f16c71 100644 --- a/apps/cowswap-frontend/src/modules/permit/index.ts +++ b/apps/cowswap-frontend/src/modules/permit/index.ts @@ -1,8 +1,9 @@ export * from './hooks/useAccountAgnosticPermitHookData' export * from './hooks/useGeneratePermitHook' -export * from './hooks/useIsTokenPermittable' +export * from './hooks/usePermitInfo' export * from './hooks/useOrdersPermitStatus' export * from './hooks/usePermitCompatibleTokens' +export * from './hooks/useTokenSupportsPermit' export * from './types' export * from './updaters/PendingPermitUpdater' export * from './utils/handlePermit' diff --git a/apps/cowswap-frontend/src/modules/permit/state/permittableTokensAtom.ts b/apps/cowswap-frontend/src/modules/permit/state/permittableTokensAtom.ts index aeebdf1716..c3b0727ffb 100644 --- a/apps/cowswap-frontend/src/modules/permit/state/permittableTokensAtom.ts +++ b/apps/cowswap-frontend/src/modules/permit/state/permittableTokensAtom.ts @@ -9,10 +9,9 @@ import { AddPermitTokenParams, PermittableTokens } from '../types' * Atom that stores the permittable tokens info for each chain on localStorage. * It's meant to be shared across different tabs, thus no special storage handling. * - * Contains either the permit info with `type` and `gasLimit` when supported or - * `false` when not supported + * Contains either the permit info for every token checked locally */ -export const permittableTokensAtom = atomWithStorage('permittableTokens:v1', { +export const permittableTokensAtom = atomWithStorage('permittableTokens:v2', { [SupportedChainId.MAINNET]: {}, [SupportedChainId.GOERLI]: {}, [SupportedChainId.GNOSIS_CHAIN]: {}, diff --git a/apps/cowswap-frontend/src/modules/permit/utils/handlePermit.ts b/apps/cowswap-frontend/src/modules/permit/utils/handlePermit.ts index ce63421a0e..be16388c94 100644 --- a/apps/cowswap-frontend/src/modules/permit/utils/handlePermit.ts +++ b/apps/cowswap-frontend/src/modules/permit/utils/handlePermit.ts @@ -1,3 +1,5 @@ +import { isSupportedPermitInfo } from '@cowprotocol/permit-utils' + import { AppDataInfo, buildAppDataHooks, updateHooksOnAppData } from 'modules/appData' import { HandlePermitParams } from '../types' @@ -15,7 +17,7 @@ import { HandlePermitParams } from '../types' export async function handlePermit(params: HandlePermitParams): Promise { const { permitInfo, inputToken, account, appData, generatePermitHook } = params - if (permitInfo && 'address' in inputToken) { + if (isSupportedPermitInfo(permitInfo) && 'address' in inputToken) { // permitInfo will only be set if there's NOT enough allowance const permitData = await generatePermitHook({ diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts index a95c5937d5..f4caabb7fa 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts @@ -15,7 +15,7 @@ import { useGetQuoteAndStatus, useIsBestQuoteLoading } from 'legacy/state/price/ import { Field } from 'legacy/state/types' import { useExpertModeManager } from 'legacy/state/user/hooks' -import { useIsTokenPermittable } from 'modules/permit' +import { useTokenSupportsPermit } from 'modules/permit' import { getSwapButtonState } from 'modules/swap/helpers/getSwapButtonState' import { useEthFlowContext } from 'modules/swap/hooks/useEthFlowContext' import { useHandleSwap } from 'modules/swap/hooks/useHandleSwap' @@ -93,7 +93,7 @@ export function useSwapButtonContext(input: SwapButtonInput): SwapButtonsContext const isSwapUnsupported = useIsTradeUnsupported(currencyIn, currencyOut) const isSmartContractWallet = useIsSmartContractWallet() const isBundlingSupported = useIsBundlingSupported() - const isPermitSupported = !!useIsTokenPermittable(currencyIn, TradeType.SWAP) + const isPermitSupported = useTokenSupportsPermit(currencyIn, TradeType.SWAP) const swapButtonState = getSwapButtonState({ account, diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts index 13bc3ef6b6..c341019305 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts @@ -4,7 +4,7 @@ import { getWrappedToken } from '@cowprotocol/common-utils' import { OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk' import { TradeType as UniTradeType } from '@uniswap/sdk-core' -import { useGeneratePermitHook, useIsTokenPermittable } from 'modules/permit' +import { useGeneratePermitHook, usePermitInfo } from 'modules/permit' import { FlowType, getFlowContext, useBaseFlowContextSetup } from 'modules/swap/hooks/useFlowContext' import { SwapFlowContext } from 'modules/swap/services/types' import { useEnoughBalanceAndAllowance } from 'modules/tokens' @@ -14,7 +14,7 @@ export function useSwapFlowContext(): SwapFlowContext | null { const contract = useGP2SettlementContract() const baseProps = useBaseFlowContextSetup() const sellCurrency = baseProps.trade?.inputAmount?.currency - const permitInfo = useIsTokenPermittable(sellCurrency, TradeType.SWAP) + const permitInfo = usePermitInfo(sellCurrency, TradeType.SWAP) const generatePermitHook = useGeneratePermitHook() const checkAllowanceAddress = GP_VAULT_RELAYER[baseProps.chainId || SupportedChainId.MAINNET] diff --git a/apps/cowswap-frontend/src/modules/swap/services/swapFlow/index.ts b/apps/cowswap-frontend/src/modules/swap/services/swapFlow/index.ts index 48e61f8b67..a329f86e8d 100644 --- a/apps/cowswap-frontend/src/modules/swap/services/swapFlow/index.ts +++ b/apps/cowswap-frontend/src/modules/swap/services/swapFlow/index.ts @@ -1,3 +1,4 @@ +import { isSupportedPermitInfo } from '@cowprotocol/permit-utils' import { Percent } from '@uniswap/sdk-core' import { PriceImpact } from 'legacy/hooks/usePriceImpact' @@ -26,7 +27,7 @@ export async function swapFlow( try { logTradeFlow('SWAP FLOW', 'STEP 2: handle permit') - if (input.permitInfo) input.swapConfirmManager.requestPermitSignature() + if (isSupportedPermitInfo(input.permitInfo)) input.swapConfirmManager.requestPermitSignature() input.orderParams.appData = await handlePermit({ appData: input.orderParams.appData, diff --git a/apps/cowswap-frontend/src/modules/tradeFormValidation/hooks/useTradeFormValidationContext.ts b/apps/cowswap-frontend/src/modules/tradeFormValidation/hooks/useTradeFormValidationContext.ts index 003bd0cc33..2c469ad357 100644 --- a/apps/cowswap-frontend/src/modules/tradeFormValidation/hooks/useTradeFormValidationContext.ts +++ b/apps/cowswap-frontend/src/modules/tradeFormValidation/hooks/useTradeFormValidationContext.ts @@ -5,7 +5,7 @@ import { useIsTradeUnsupported } from '@cowprotocol/tokens' import { useGnosisSafeInfo, useIsBundlingSupported, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' import { isUnsupportedTokenInQuote } from 'modules/limitOrders/utils/isUnsupportedTokenInQuote' -import { useIsTokenPermittable } from 'modules/permit' +import { useTokenSupportsPermit } from 'modules/permit' import { useDerivedTradeState } from 'modules/trade/hooks/useDerivedTradeState' import { useIsWrapOrUnwrap } from 'modules/trade/hooks/useIsWrapOrUnwrap' import { useTradeQuote } from 'modules/tradeQuote' @@ -32,7 +32,7 @@ export function useTradeFormValidationContext(): TradeFormValidationCommonContex const isSafeReadonlyUser = gnosisSafeInfo?.isReadOnly || false - const isPermitSupported = !!useIsTokenPermittable(inputCurrency, tradeType) + const isPermitSupported = useTokenSupportsPermit(inputCurrency, tradeType) const commonContext = { account, diff --git a/libs/permit-utils/package.json b/libs/permit-utils/package.json index c9f38ccce8..4a27ce06db 100644 --- a/libs/permit-utils/package.json +++ b/libs/permit-utils/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/permit-utils", - "version": "0.0.1-RC.1", + "version": "0.0.1", "type": "module", "dependencies": { "ethers": "^5.7.2", diff --git a/libs/permit-utils/src/abi/erc20.json b/libs/permit-utils/src/abi/erc20.json new file mode 100644 index 0000000000..2433c82123 --- /dev/null +++ b/libs/permit-utils/src/abi/erc20.json @@ -0,0 +1,16 @@ +[ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + } +] diff --git a/libs/permit-utils/src/index.ts b/libs/permit-utils/src/index.ts index 3edad2af5e..06bb0ccd7e 100644 --- a/libs/permit-utils/src/index.ts +++ b/libs/permit-utils/src/index.ts @@ -2,12 +2,6 @@ export { checkIsCallDataAValidPermit } from './lib/checkIsCallDataAValidPermit' export { generatePermitHook } from './lib/generatePermitHook' export { getPermitUtilsInstance } from './lib/getPermitUtilsInstance' export { getTokenPermitInfo } from './lib/getTokenPermitInfo' +export { isSupportedPermitInfo } from './utils/isSupportedPermitInfo' -export type { - PermitHookData, - PermitHookParams, - PermitInfo, - PermitType, - SupportedPermitInfo, - GetTokenPermitIntoResult, -} from './types' +export type { PermitHookData, PermitHookParams, PermitInfo, PermitType, GetTokenPermitIntoResult } from './types' diff --git a/libs/permit-utils/src/lib/checkIsCallDataAValidPermit.ts b/libs/permit-utils/src/lib/checkIsCallDataAValidPermit.ts index 7c9c4cd48c..f249bfcfd1 100644 --- a/libs/permit-utils/src/lib/checkIsCallDataAValidPermit.ts +++ b/libs/permit-utils/src/lib/checkIsCallDataAValidPermit.ts @@ -1,6 +1,6 @@ import { DAI_PERMIT_SELECTOR, Eip2612PermitUtils, EIP_2612_PERMIT_SELECTOR } from '@1inch/permit-signed-approvals-utils' -import { SupportedPermitInfo } from '../types' +import { PermitInfo } from '../types' import { fixTokenName } from '../utils/fixTokenName' export async function checkIsCallDataAValidPermit( @@ -8,10 +8,21 @@ export async function checkIsCallDataAValidPermit( chainId: number, eip2162Utils: Eip2612PermitUtils, tokenAddress: string, - tokenName: string, + _tokenName: string | undefined, callData: string, - { version }: SupportedPermitInfo + { version, type, name }: PermitInfo ): Promise { + // TODO: take name only from PermitInfo + const tokenName = name || _tokenName + + if (type === 'unsupported') { + return false + } + + if (!tokenName) { + throw new Error(`No token name for ${tokenAddress}`) + } + const params = { chainId, tokenName: fixTokenName(tokenName), tokenAddress, callData, version } let recoverPermitOwnerPromise: Promise | undefined = undefined diff --git a/libs/permit-utils/src/lib/generatePermitHook.ts b/libs/permit-utils/src/lib/generatePermitHook.ts index 4299748a92..d8e36c8ab9 100644 --- a/libs/permit-utils/src/lib/generatePermitHook.ts +++ b/libs/permit-utils/src/lib/generatePermitHook.ts @@ -4,6 +4,7 @@ import { DEFAULT_PERMIT_GAS_LIMIT, DEFAULT_PERMIT_VALUE, PERMIT_SIGNER } from '. import { PermitHookData, PermitHookParams } from '../types' import { buildDaiLikePermitCallData, buildEip2162PermitCallData } from '../utils/buildPermitCallData' import { getPermitDeadline } from '../utils/getPermitDeadline' +import { isSupportedPermitInfo } from '../utils/isSupportedPermitInfo' const REQUESTS_CACHE: { [permitKey: string]: Promise } = {} @@ -35,8 +36,18 @@ export async function generatePermitHook(params: PermitHookParams): Promise { const { inputToken, spender, chainId, permitInfo, provider, account, eip2162Utils, nonce: preFetchedNonce } = params + const tokenAddress = inputToken.address - const tokenName = inputToken.name || tokenAddress + // TODO: remove the need for `name` from input token. Should come from permitInfo instead + const tokenName = permitInfo.name || inputToken.name + + if (!isSupportedPermitInfo(permitInfo)) { + throw new Error(`Trying to generate permit hook for unsupported token: ${tokenAddress}`) + } + + if (!tokenName) { + throw new Error(`No token name for token: ${tokenAddress}`) + } const owner = account || PERMIT_SIGNER.address diff --git a/libs/permit-utils/src/lib/getTokenPermitInfo.ts b/libs/permit-utils/src/lib/getTokenPermitInfo.ts index cb347da253..6ef486b717 100644 --- a/libs/permit-utils/src/lib/getTokenPermitInfo.ts +++ b/libs/permit-utils/src/lib/getTokenPermitInfo.ts @@ -6,9 +6,10 @@ import { SupportedChainId } from '@cowprotocol/cow-sdk' import { getPermitUtilsInstance } from './getPermitUtilsInstance' import { DEFAULT_PERMIT_VALUE, PERMIT_GAS_LIMIT_MIN, PERMIT_SIGNER, TOKENS_TO_SKIP_VERSION } from '../const' -import { GetTokenPermitInfoParams, GetTokenPermitIntoResult, PermitType } from '../types' +import { GetTokenPermitInfoParams, GetTokenPermitIntoResult, PermitInfo, PermitType } from '../types' import { buildDaiLikePermitCallData, buildEip2162PermitCallData } from '../utils/buildPermitCallData' import { getPermitDeadline } from '../utils/getPermitDeadline' +import { getTokenName } from '../utils/getTokenName' const EIP_2162_PERMIT_PARAMS = { value: DEFAULT_PERMIT_VALUE, @@ -24,6 +25,8 @@ const DAI_LIKE_PERMIT_PARAMS = { const REQUESTS_CACHE: Record> = {} +const UNSUPPORTED: PermitInfo = { type: 'unsupported' } + export async function getTokenPermitInfo(params: GetTokenPermitInfoParams): Promise { const { tokenAddress, chainId } = params @@ -43,25 +46,46 @@ export async function getTokenPermitInfo(params: GetTokenPermitInfoParams): Prom } async function actuallyCheckTokenIsPermittable(params: GetTokenPermitInfoParams): Promise { - const { spender, tokenAddress, tokenName, chainId, provider } = params + const { spender, tokenAddress, tokenName: _tokenName, chainId, provider } = params const eip2612PermitUtils = getPermitUtilsInstance(chainId, provider) const owner = PERMIT_SIGNER.address + // TODO: potentially remove the need for the name input + let tokenName = _tokenName + + try { + tokenName = await getTokenName(tokenAddress, chainId, provider) + } catch (e) { + if (/ETIMEDOUT/.test(e) && !tokenName) { + // Network issue or another temporary failure, return error + return { error: `Failed to fetch token name from contract. RPC connection error` } + } + console.debug( + `[checkTokenIsPermittable] Couldn't fetch token name from the contract for token ${tokenAddress}, using provided '${tokenName}'`, + e + ) + } + + if (!tokenName) { + const error = `Token name could not be determined for ${tokenAddress}` + return { error } + } + let nonce: number try { nonce = await eip2612PermitUtils.getTokenNonce(tokenAddress, owner) } catch (e) { if (e === 'nonce not supported' || e.message === 'nonce is NaN') { - console.debug(`[checkTokenIsPermittable] Not a permittable token ${tokenAddress}`, e?.message || e) - // Here we know it's not supported, return false + console.debug(`[checkTokenIsPermittable] Not a permittable token ${tokenAddress} - ${tokenName}`, e?.message || e) + // Here we know it's not supported, return unsupported // See https://github.com/1inch/permit-signed-approvals-utils/blob/b190197a45c3289867ee4e6da93f10dea51ef276/src/eip-2612-permit.utils.ts#L309 // and https://github.com/1inch/permit-signed-approvals-utils/blob/b190197a45c3289867ee4e6da93f10dea51ef276/src/eip-2612-permit.utils.ts#L325 - return false + return { ...UNSUPPORTED, name: tokenName } } - console.debug(`[checkTokenIsPermittable] Failed to get nonce for ${tokenAddress}`, e) + console.debug(`[checkTokenIsPermittable] Failed to get nonce for ${tokenAddress} - ${tokenName}`, e) // Otherwise, it might have been a network issue or another temporary failure, return error return { error: e.message || e.toString() } @@ -79,7 +103,7 @@ async function actuallyCheckTokenIsPermittable(params: GetTokenPermitInfoParams) version = await eip2612PermitUtils.getTokenVersion(tokenAddress) } catch (e) { // Not a problem, we can (try to) continue without it, and will default to `1` (part of the 1inch lib) - console.debug(`[checkTokenIsPermittable] Failed to get version for ${tokenAddress}`, e) + console.debug(`[checkTokenIsPermittable] Failed to get version for ${tokenAddress} - ${tokenName}`, e) } } @@ -99,12 +123,36 @@ async function actuallyCheckTokenIsPermittable(params: GetTokenPermitInfoParams) return await estimateTokenPermit({ ...baseParams, type: 'eip-2612', provider }) } catch (e) { // Not eip-2612, try dai-like - console.debug(`[checkTokenIsPermittable] Failed to estimate eip-2612 permit for ${tokenAddress}`, e) try { + const isDaiLike = await isDaiLikeTypeHash(tokenAddress, eip2612PermitUtils) + + if (!isDaiLike) { + // These might be supported, as they have nonces, but we don't know why the permit call fails + // TODO: further investigate this kind of token + // For now mark them as unsupported and don't check it again + if (/invalid signature/.test(e) || e?.code === 'UNPREDICTABLE_GAS_LIMIT') { + console.debug( + `[checkTokenIsPermittable] Token ${tokenAddress} - ${tokenName} might be permittable, but it's not supported for now. Reason:`, + e?.reason + ) + return { ...UNSUPPORTED, name: tokenName } + } + + // Maybe a temporary failure + console.debug( + `[checkTokenIsPermittable] Failed to estimate eip-2612 permit for ${tokenAddress} - ${tokenName}`, + e + ) + return { error: e.message || e.toString() } + } + return await estimateTokenPermit({ ...baseParams, type: 'dai-like', provider }) } catch (e) { // Not dai-like either, return error - console.debug(`[checkTokenIsPermittable] Failed to estimate dai-like permit for ${tokenAddress}`, e) + console.debug( + `[checkTokenIsPermittable] Failed to estimate dai-like permit for ${tokenAddress} - ${tokenName}`, + e + ) return { error: e.message || e.toString() } } } @@ -127,14 +175,14 @@ type EstimateParams = BaseParams & { } async function estimateTokenPermit(params: EstimateParams): Promise { - const { provider, chainId, walletAddress, tokenAddress, type, version } = params + const { provider, chainId, walletAddress, tokenAddress, tokenName, type, version } = params const getCallDataFn = type === 'eip-2612' ? getEip2612CallData : getDaiLikeCallData const data = await getCallDataFn(params) if (!data) { - return false + return { ...UNSUPPORTED, name: tokenName } } const estimatedGas = await provider.estimateGas({ @@ -149,8 +197,9 @@ async function estimateTokenPermit(params: EstimateParams): Promise { @@ -172,6 +221,12 @@ async function getEip2612CallData(params: BaseParams): Promise { }) } +async function isDaiLikeTypeHash(tokenAddress: string, eip2612PermitUtils: Eip2612PermitUtils): Promise { + const permitTypeHash = await eip2612PermitUtils.getPermitTypeHash(tokenAddress) + + return permitTypeHash === DAI_LIKE_PERMIT_TYPEHASH +} + async function getDaiLikeCallData(params: BaseParams): Promise { const { eip2612PermitUtils, tokenAddress, walletAddress, spender, nonce, chainId, tokenName, version } = params diff --git a/libs/permit-utils/src/types.ts b/libs/permit-utils/src/types.ts index 054aa5cef4..bb6d754549 100644 --- a/libs/permit-utils/src/types.ts +++ b/libs/permit-utils/src/types.ts @@ -2,18 +2,19 @@ import { Eip2612PermitUtils } from '@1inch/permit-signed-approvals-utils' import { latest } from '@cowprotocol/app-data' import { JsonRpcProvider } from '@ethersproject/providers' -export type PermitType = 'dai-like' | 'eip-2612' +export type PermitType = 'dai-like' | 'eip-2612' | 'unsupported' -export type SupportedPermitInfo = { +export type PermitInfo = { type: PermitType - version: string | undefined // Some tokens have it different than `1`, and won't work without it + // TODO: make it not optional once token-lists is migrated + name?: string + version?: string | undefined // Some tokens have it different than `1`, and won't work without it } -type UnsupportedPermitInfo = false -export type PermitInfo = SupportedPermitInfo | UnsupportedPermitInfo // Local TokenInfo definition to not depend on external libs just for this type TokenInfo = { address: string + // TODO: remove from token info name: string | undefined } @@ -21,7 +22,7 @@ export type PermitHookParams = { inputToken: TokenInfo spender: string chainId: number - permitInfo: SupportedPermitInfo + permitInfo: PermitInfo provider: JsonRpcProvider eip2162Utils: Eip2612PermitUtils account?: string | undefined @@ -34,11 +35,9 @@ type FailedToIdentify = { error: string } export type GetTokenPermitIntoResult = // When it's a permittable token: - | SupportedPermitInfo + | PermitInfo // When something failed: | FailedToIdentify - // When it's not permittable: - | UnsupportedPermitInfo type BasePermitCallDataParams = { eip2162Utils: Eip2612PermitUtils @@ -53,7 +52,7 @@ export type BuildDaiLikePermitCallDataParams = BasePermitCallDataParams & { export type GetTokenPermitInfoParams = { spender: string tokenAddress: string - tokenName: string + tokenName?: string | undefined chainId: number provider: JsonRpcProvider } diff --git a/libs/permit-utils/src/utils/PermitProviderConnector.ts b/libs/permit-utils/src/utils/PermitProviderConnector.ts index fa0715cb9e..a430744194 100644 --- a/libs/permit-utils/src/utils/PermitProviderConnector.ts +++ b/libs/permit-utils/src/utils/PermitProviderConnector.ts @@ -4,18 +4,15 @@ import { AbiInput, AbiItem, EIP712TypedData, ProviderConnector } from '@1inch/pe import { defaultAbiCoder, ParamType } from '@ethersproject/abi' import { TypedDataField } from '@ethersproject/abstract-signer' import { BigNumber } from '@ethersproject/bignumber' -import { Contract, ContractInterface } from '@ethersproject/contracts' import { Wallet } from '@ethersproject/wallet' +import { getContract } from './getContract' + export class PermitProviderConnector implements ProviderConnector { constructor(private provider: JsonRpcProvider, private walletSigner?: Wallet | undefined) {} - private getContract(address: string, abi: ContractInterface, provider: JsonRpcProvider): Contract { - return new Contract(address, abi, provider) - } - contractEncodeABI(abi: AbiItem[], address: string | null, methodName: string, methodParams: unknown[]): string { - const contract = this.getContract(address || '', abi, this.provider) + const contract = getContract(address || '', abi, this.provider) return contract.interface.encodeFunctionData(methodName, methodParams) } diff --git a/libs/permit-utils/src/utils/fixTokenName.ts b/libs/permit-utils/src/utils/fixTokenName.ts index 3b16d300d4..6b7a5bd8f1 100644 --- a/libs/permit-utils/src/utils/fixTokenName.ts +++ b/libs/permit-utils/src/utils/fixTokenName.ts @@ -1,3 +1,4 @@ +// TODO: remove this once permitInfo contains token names export function fixTokenName(tokenName: string): string { // TODO: this is ugly and I'm not happy with it either // It'll probably go away when the tokens overhaul is implemented diff --git a/libs/permit-utils/src/utils/getContract.ts b/libs/permit-utils/src/utils/getContract.ts new file mode 100644 index 0000000000..498f92b762 --- /dev/null +++ b/libs/permit-utils/src/utils/getContract.ts @@ -0,0 +1,7 @@ +import type { JsonRpcProvider } from '@ethersproject/providers' + +import { Contract, ContractInterface } from '@ethersproject/contracts' + +export function getContract(address: string, abi: ContractInterface, provider: JsonRpcProvider): Contract { + return new Contract(address, abi, provider) +} diff --git a/libs/permit-utils/src/utils/getTokenName.ts b/libs/permit-utils/src/utils/getTokenName.ts new file mode 100644 index 0000000000..3581b81bcb --- /dev/null +++ b/libs/permit-utils/src/utils/getTokenName.ts @@ -0,0 +1,14 @@ +import type { JsonRpcProvider } from '@ethersproject/providers' + +import { getAddress } from '@ethersproject/address' + +import { getContract } from './getContract' + +import Erc20Abi from '../abi/erc20.json' + +export async function getTokenName(tokenAddress: string, chainId: number, provider: JsonRpcProvider): Promise { + const formattedAddress = getAddress(tokenAddress) + const erc20Contract = getContract(formattedAddress, Erc20Abi, provider) + + return erc20Contract.callStatic['name']() +} diff --git a/libs/permit-utils/src/utils/isSupportedPermitInfo.ts b/libs/permit-utils/src/utils/isSupportedPermitInfo.ts new file mode 100644 index 0000000000..5396c038a2 --- /dev/null +++ b/libs/permit-utils/src/utils/isSupportedPermitInfo.ts @@ -0,0 +1,5 @@ +import { PermitInfo } from '../types' + +export function isSupportedPermitInfo(p: PermitInfo | undefined): p is PermitInfo { + return !!p && p.type !== 'unsupported' +}