diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenImportDialog.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenImportDialog.tsx index 2806fdaf07..9c01112c6d 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenImportDialog.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenImportDialog.tsx @@ -17,7 +17,6 @@ import { ERC20BridgeToken } from '../../hooks/arbTokenBridge.types' import { warningToast } from '../common/atoms/Toast' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' -import { isWithdrawOnlyToken } from '../../util/WithdrawOnlyUtils' import { isTransferDisabledToken } from '../../util/TokenTransferDisabledUtils' import { useTransferDisabledDialogStore } from './TransferDisabledDialog' import { TokenInfo } from './TokenInfo' @@ -300,12 +299,6 @@ export function TokenImportDialog({ }) } - // do not allow import of withdraw-only tokens at deposit mode - if (isDepositMode && isWithdrawOnlyToken(l1Address, childChain.id)) { - openTransferDisabledDialog() - return - } - if (isTransferDisabledToken(l1Address, childChain.id)) { openTransferDisabledDialog() return diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx index 1497979dfc..93af1c6977 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx @@ -38,7 +38,6 @@ import { TokenRow } from './TokenRow' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { useTransferDisabledDialogStore } from './TransferDisabledDialog' -import { isWithdrawOnlyToken } from '../../util/WithdrawOnlyUtils' import { isTransferDisabledToken } from '../../util/TokenTransferDisabledUtils' import { useTokenFromSearchParams } from './TransferPanelUtils' import { Switch } from '../common/atoms/Switch' @@ -371,6 +370,8 @@ function TokensPanel({ isArbitrumOne, isArbitrumSepolia, isOrbitChain, + isParentChainArbitrumOne, + isParentChainArbitrumSepolia, getBalance, nativeCurrency ]) @@ -535,7 +536,6 @@ export function TokenSearch({ childChainProvider, parentChain, parentChainProvider, - isDepositMode, isTeleportMode } = useNetworksRelationship(networks) const { updateUSDCBalances } = useUpdateUSDCBalances({ walletAddress }) @@ -631,12 +631,6 @@ export function TokenSearch({ }) } - // do not allow import of withdraw-only tokens at deposit mode - if (isDepositMode && isWithdrawOnlyToken(_token.address, childChain.id)) { - openTransferDisabledDialog() - return - } - if (isTransferDisabledToken(_token.address, childChain.id)) { openTransferDisabledDialog() return diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferDisabledDialog.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferDisabledDialog.tsx index 672b666ba8..12b9dcfdd0 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferDisabledDialog.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferDisabledDialog.tsx @@ -10,6 +10,7 @@ import { ChainId, getNetworkName } from '../../util/networks' import { getL2ConfigForTeleport } from '../../token-bridge-sdk/teleport' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { withdrawOnlyTokens } from '../../util/WithdrawOnlyUtils' +import { useSelectedTokenIsWithdrawOnly } from './hooks/useSelectedTokenIsWithdrawOnly' type TransferDisabledDialogStore = { isOpen: boolean @@ -26,14 +27,17 @@ export const useTransferDisabledDialogStore = export function TransferDisabledDialog() { const [networks] = useNetworks() - const { isTeleportMode } = useNetworksRelationship(networks) + const { isDepositMode, isTeleportMode } = useNetworksRelationship(networks) const { app } = useAppState() const { selectedToken } = app const { app: { setSelectedToken } } = useActions() + const { isSelectedTokenWithdrawOnly, isSelectedTokenWithdrawOnlyLoading } = + useSelectedTokenIsWithdrawOnly() const { isOpen: isOpenTransferDisabledDialog, + openDialog: openTransferDisabledDialog, closeDialog: closeTransferDisabledDialog } = useTransferDisabledDialogStore() const unsupportedToken = sanitizeTokenSymbol(selectedToken?.symbol ?? '', { @@ -57,6 +61,22 @@ export function TransferDisabledDialog() { updateL2ChainIdForTeleport() }, [isTeleportMode, networks.destinationChainProvider]) + useEffect(() => { + // do not allow import of withdraw-only tokens at deposit mode + if ( + isDepositMode && + isSelectedTokenWithdrawOnly && + !isSelectedTokenWithdrawOnlyLoading + ) { + openTransferDisabledDialog() + } + }, [ + isSelectedTokenWithdrawOnly, + isDepositMode, + openTransferDisabledDialog, + isSelectedTokenWithdrawOnlyLoading + ]) + const onClose = () => { setSelectedToken(null) closeTransferDisabledDialog() diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMainInput.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMainInput.tsx index 32e8d9b4fb..414dc3303f 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMainInput.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMainInput.tsx @@ -185,17 +185,16 @@ function ErrorMessage({ case TransferReadinessRichErrorMessage.TOKEN_WITHDRAW_ONLY: case TransferReadinessRichErrorMessage.TOKEN_TRANSFER_DISABLED: return ( - <> - - This token can't be bridged over. - {' '} +
+ This token can't be bridged over.{' '} - + . +
) } } diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useSelectedTokenIsWithdrawOnly.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useSelectedTokenIsWithdrawOnly.ts new file mode 100644 index 0000000000..df55d80a5f --- /dev/null +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useSelectedTokenIsWithdrawOnly.ts @@ -0,0 +1,45 @@ +import useSWRImmutable from 'swr/immutable' +import { useMemo } from 'react' + +import { useAppState } from '../../../state' +import { useNetworks } from '../../../hooks/useNetworks' +import { useNetworksRelationship } from '../../../hooks/useNetworksRelationship' +import { isWithdrawOnlyToken } from '../../../util/WithdrawOnlyUtils' + +export function useSelectedTokenIsWithdrawOnly() { + const { + app: { selectedToken } + } = useAppState() + const [networks] = useNetworks() + const { isDepositMode, parentChain, childChain } = + useNetworksRelationship(networks) + + const queryKey = useMemo(() => { + if (!selectedToken) { + return null + } + if (!isDepositMode) { + return null + } + return [ + selectedToken.address.toLowerCase(), + parentChain.id, + childChain.id + ] as const + }, [selectedToken, isDepositMode, parentChain.id, childChain.id]) + + const { data: isSelectedTokenWithdrawOnly, isLoading } = useSWRImmutable( + queryKey, + ([parentChainErc20Address, parentChainId, childChainId]) => + isWithdrawOnlyToken({ + parentChainErc20Address, + parentChainId, + childChainId + }) + ) + + return { + isSelectedTokenWithdrawOnly, + isSelectedTokenWithdrawOnlyLoading: isLoading + } +} diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/useTransferReadiness.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/useTransferReadiness.ts index ad61553ad2..87b05093c8 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/useTransferReadiness.ts +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/useTransferReadiness.ts @@ -11,7 +11,6 @@ import { } from '../../util/TokenUtils' import { useAppContextState } from '../App/AppContext' import { useDestinationAddressStore } from './AdvancedSettings' -import { isWithdrawOnlyToken } from '../../util/WithdrawOnlyUtils' import { TransferReadinessRichErrorMessage, getInsufficientFundsErrorMessage, @@ -32,6 +31,7 @@ import { isNetwork } from '../../util/networks' import { useBalances } from '../../hooks/useBalances' import { useArbQueryParams } from '../../hooks/useArbQueryParams' import { formatAmount } from '../../util/NumberUtils' +import { useSelectedTokenIsWithdrawOnly } from './hooks/useSelectedTokenIsWithdrawOnly' // Add chains IDs that are currently down or disabled // It will block transfers and display an info box in the transfer panel @@ -136,6 +136,8 @@ export function useTransferReadiness(): UseTransferReadinessResult { isTeleportMode } = useNetworksRelationship(networks) + const { isSelectedTokenWithdrawOnly, isSelectedTokenWithdrawOnlyLoading } = + useSelectedTokenIsWithdrawOnly() const gasSummary = useGasSummary() const { address: walletAddress } = useAccount() const { isSmartContractWallet } = useAccountType() @@ -293,11 +295,6 @@ export function useTransferReadiness(): UseTransferReadinessResult { // ERC-20 if (selectedToken) { - const selectedTokenIsWithdrawOnly = isWithdrawOnlyToken( - selectedToken.address, - childChain.id - ) - const selectedTokenIsDisabled = isTransferDisabledToken(selectedToken.address, childChain.id) || (isTeleportMode && @@ -307,7 +304,11 @@ export function useTransferReadiness(): UseTransferReadinessResult { childChain.id )) - if (isDepositMode && selectedTokenIsWithdrawOnly) { + if ( + isDepositMode && + isSelectedTokenWithdrawOnly && + !isSelectedTokenWithdrawOnlyLoading + ) { return notReady({ errorMessages: { inputAmount1: TransferReadinessRichErrorMessage.TOKEN_WITHDRAW_ONLY diff --git a/packages/arb-token-bridge-ui/src/util/WithdrawOnlyUtils.ts b/packages/arb-token-bridge-ui/src/util/WithdrawOnlyUtils.ts index c3036fe0c8..cf9f7d8aca 100644 --- a/packages/arb-token-bridge-ui/src/util/WithdrawOnlyUtils.ts +++ b/packages/arb-token-bridge-ui/src/util/WithdrawOnlyUtils.ts @@ -1,6 +1,9 @@ // tokens that can't be bridged to Arbitrum (maybe coz they have their native protocol bridges and custom implementation or they are being discontinued) // the UI doesn't let users deposit such tokens. If bridged already, these can only be withdrawn. +import { ethers } from 'ethers' +import { getProviderForChainId } from '@/token-bridge-sdk/utils' + import { ChainId, isNetwork } from '../util/networks' import { isTokenArbitrumOneUSDCe, @@ -130,12 +133,6 @@ export const withdrawOnlyTokens: { [chainId: number]: WithdrawOnlyToken[] } = { l1Address: '0x137dDB47Ee24EaA998a535Ab00378d6BFa84F893', l2Address: '0xa4431f62db9955bfd056c30e5ae703bf0d0eaec8' }, - { - symbol: 'GSWIFT', - l2CustomAddr: '0x580e933d90091b9ce380740e3a4a39c67eb85b4c', - l1Address: '0x580e933d90091b9ce380740e3a4a39c67eb85b4c', - l2Address: '0x88e5369f73312eba739dcdf83bdb8bad3d08f4c8' - }, { symbol: 'eETH', l2CustomAddr: '', @@ -196,6 +193,13 @@ export const withdrawOnlyTokens: { [chainId: number]: WithdrawOnlyToken[] } = { l1Address: '0x0B7f0e51Cd1739D6C96982D55aD8fA634dd43A9C', l2Address: '0x6Ab317237cc72B2cdb54EcfcC180b61E00F7df76' }, + // LayerZero tokens + { + symbol: 'GSWIFT', + l2CustomAddr: '0x580e933d90091b9ce380740e3a4a39c67eb85b4c', + l1Address: '0x580e933d90091b9ce380740e3a4a39c67eb85b4c', + l2Address: '0x88e5369f73312eba739dcdf83bdb8bad3d08f4c8' + }, { symbol: 'ZRO', l2CustomAddr: '0x6985884c4392d348587b19cb9eaaf157f13271cd', @@ -206,15 +210,43 @@ export const withdrawOnlyTokens: { [chainId: number]: WithdrawOnlyToken[] } = { [ChainId.ArbitrumNova]: [] } +async function isLayerZeroToken( + parentChainErc20Address: string, + parentChainId: number +) { + const parentProvider = getProviderForChainId(parentChainId) + + // https://github.com/LayerZero-Labs/LayerZero-v2/blob/592625b9e5967643853476445ffe0e777360b906/packages/layerzero-v2/evm/oapp/contracts/oft/OFT.sol#L37 + const layerZeroTokenOftContract = new ethers.Contract( + parentChainErc20Address, + [ + 'function oftVersion() external pure virtual returns (bytes4 interfaceId, uint64 version)' + ], + parentProvider + ) + + try { + const _isLayerZeroToken = await layerZeroTokenOftContract.oftVersion() + return !!_isLayerZeroToken + } catch (error) { + return false + } +} + /** * * @param erc20L1Address * @param childChainId */ -export function isWithdrawOnlyToken( - parentChainErc20Address: string, +export async function isWithdrawOnlyToken({ + parentChainErc20Address, + parentChainId, + childChainId +}: { + parentChainErc20Address: string + parentChainId: number childChainId: number -) { +}) { // disable USDC.e deposits for Orbit chains if ( (isTokenArbitrumOneUSDCe(parentChainErc20Address) || @@ -224,7 +256,17 @@ export function isWithdrawOnlyToken( return true } - return (withdrawOnlyTokens[childChainId] ?? []) + const inWithdrawOnlyList = (withdrawOnlyTokens[childChainId] ?? []) .map(token => token.l1Address.toLowerCase()) .includes(parentChainErc20Address.toLowerCase()) + + if (inWithdrawOnlyList) { + return true + } + + if (await isLayerZeroToken(parentChainErc20Address, parentChainId)) { + return true + } + + return false }