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
}