diff --git a/packages/arb-token-bridge-ui/src/hooks/arbTokenBridge.types.ts b/packages/arb-token-bridge-ui/src/hooks/arbTokenBridge.types.ts index 2b62dd9bc8..ec28d40cef 100644 --- a/packages/arb-token-bridge-ui/src/hooks/arbTokenBridge.types.ts +++ b/packages/arb-token-bridge-ui/src/hooks/arbTokenBridge.types.ts @@ -124,6 +124,7 @@ export interface AddressToDecimals { export type GasEstimates = { estimatedParentChainGas: BigNumber estimatedChildChainGas: BigNumber + isError?: boolean } export type DepositGasEstimates = GasEstimates & { diff --git a/packages/arb-token-bridge-ui/src/token-bridge-sdk/Erc20WithdrawalStarter.ts b/packages/arb-token-bridge-ui/src/token-bridge-sdk/Erc20WithdrawalStarter.ts index 8ee9f51ac2..b56c8d9541 100644 --- a/packages/arb-token-bridge-ui/src/token-bridge-sdk/Erc20WithdrawalStarter.ts +++ b/packages/arb-token-bridge-ui/src/token-bridge-sdk/Erc20WithdrawalStarter.ts @@ -1,4 +1,4 @@ -import { Erc20Bridger } from '@arbitrum/sdk' +import { Erc20Bridger, getArbitrumNetwork } from '@arbitrum/sdk' import { BigNumber, constants } from 'ethers' import { ERC20__factory } from '@arbitrum/sdk/dist/lib/abi/factories/ERC20__factory' import { @@ -11,6 +11,7 @@ import { TransferType } from './BridgeTransferStarter' import { + fetchErc20Allowance, fetchErc20L2GatewayAddress, getL1ERC20Address } from '../util/TokenUtils' @@ -19,7 +20,6 @@ import { getChainIdFromProvider, percentIncrease } from './utils' -import { tokenRequiresApprovalOnL2 } from '../util/L2ApprovalUtils' import { withdrawInitTxEstimateGas } from '../util/WithdrawalUtils' import { addressIsSmartContract } from '../util/AddressUtils' @@ -86,6 +86,15 @@ export class Erc20WithdrawalStarter extends BridgeTransferStarter { // no-op } + /** + * Most tokens inherently allows the token gateway to burn whichever amount + * on the child chain for withdrawal because they inherited the + * IArbToken interface that allows the gateway to burn without allowance approval + * + * if the token does not have the bridgeBurn method, approval is required + * https://github.com/OffchainLabs/token-bridge-contracts/blob/d54877598e80a00d264d2b4353968faafd6f534d/contracts/tokenbridge/arbitrum/IArbToken.sol + * + */ public requiresTokenApproval = async ({ amount, signer @@ -94,38 +103,45 @@ export class Erc20WithdrawalStarter extends BridgeTransferStarter { throw Error('Erc20 token address not found') } - const destinationChainErc20Address = - await this.getDestinationChainErc20Address() - const address = await getAddressFromSigner(signer) const sourceChainId = await getChainIdFromProvider(this.sourceChainProvider) - const destinationChainId = await getChainIdFromProvider( - this.destinationChainProvider - ) + const gatewayAddress = await this.getSourceChainGatewayAddress() + + const sourceChainTokenBridge = getArbitrumNetwork(sourceChainId).tokenBridge - // check first if token is even eligible for allowance check on l2 - if ( - (await tokenRequiresApprovalOnL2({ - tokenAddressOnParentChain: destinationChainErc20Address, - parentChainId: destinationChainId, - childChainId: sourceChainId - })) && - this.sourceChainErc20Address - ) { - const token = ERC20__factory.connect( - this.sourceChainErc20Address, - this.sourceChainProvider - ) - - const gatewayAddress = await this.getSourceChainGatewayAddress() - const allowance = await token.allowance(address, gatewayAddress) - - return allowance.lt(amount) + // tokens that use the standard gateways do not require approval on child chain + const standardGateways = [ + sourceChainTokenBridge?.childErc20Gateway.toLowerCase(), + sourceChainTokenBridge?.childWethGateway.toLowerCase() + ] + + if (standardGateways.includes(gatewayAddress.toLowerCase())) { + return false } - return false + // the below checks are only run for tokens using custom gateway / custom custom gateway + // + // check if token withdrawal gas estimation fails + // if it fails, the token gateway is not allowed to burn the token without additional approval + const transferEstimateGasResult = await this.transferEstimateGas({ + amount, + signer + }) + + if (!transferEstimateGasResult.isError) { + return false + } + + const allowanceForSourceChainGateway = await fetchErc20Allowance({ + address: this.sourceChainErc20Address, + provider: this.sourceChainProvider, + owner: address, + spender: gatewayAddress + }) + + return allowanceForSourceChainGateway.lt(amount) } public async approveTokenEstimateGas({ signer, amount }: ApproveTokenProps) { diff --git a/packages/arb-token-bridge-ui/src/util/L2ApprovalUtils.ts b/packages/arb-token-bridge-ui/src/util/L2ApprovalUtils.ts deleted file mode 100644 index b5bfe7d038..0000000000 --- a/packages/arb-token-bridge-ui/src/util/L2ApprovalUtils.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { ChainId } from '../util/networks' -import { xErc20RequiresApprovalOnChildChain } from './xErc20Utils' - -export type RequireL2ApproveToken = { - symbol: string - l1Address: string - l2Address: string -} - -const L2ApproveTokens: { [chainId: number]: RequireL2ApproveToken[] } = { - [ChainId.ArbitrumOne]: [ - { - symbol: 'LPT', - l1Address: '0x58b6A8A3302369DAEc383334672404Ee733aB239', - l2Address: '0x289ba1701C2F088cf0faf8B3705246331cB8A839' - }, - { - symbol: 'GRT', - l1Address: '0xc944e90c64b2c07662a292be6244bdf05cda44a7', - l2Address: '0x9623063377AD1B27544C965cCd7342f7EA7e88C7' - }, - { - symbol: 'ARB', - l1Address: '0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1', - l2Address: '0x912CE59144191C1204E64559FE8253a0e49E6548' - }, - { - symbol: 'saETH', - l1Address: '0xF1617882A71467534D14EEe865922de1395c9E89', - l2Address: '0xF1617882A71467534D14EEe865922de1395c9E89' - } - ], - [ChainId.ArbitrumNova]: [ - { - symbol: 'ARB', - l1Address: '0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1', - l2Address: '0xf823C3cD3CeBE0a1fA952ba88Dc9EEf8e0Bf46AD' - }, - { - symbol: 'MOON', - l1Address: '0xb2490e357980cE57bF5745e181e537a64Eb367B1', - l2Address: '0x0057Ac2d777797d31CD3f8f13bF5e927571D6Ad0' - } - ], - [ChainId.ArbitrumSepolia]: [ - { - symbol: 'ARB', - l1Address: '0xfa898E8d38B008F3bAc64dce019A9480d4F06863', - l2Address: '0xc275b23c035a9d4ec8867b47f55427e0bdce14cb' - } - ], - // xai mainnet - [660279]: [ - { - symbol: 'wCU', - l1Address: '0x89c49a3fa372920ac23ce757a029e6936c0b8e02', - l2Address: '0x89c49a3fa372920ac23ce757a029e6936c0b8e02' - } - ], - // xai testnet - [37714555429]: [ - { - symbol: 'CU', - l1Address: '0xd781cea0b8D5dDd0aeeD1dF7aC109C974A221B00', - l2Address: '0xe267c440dbfb1e185d506c2cc3c44eb21340e046' - } - ], - // pop apex - [70700]: [ - { - symbol: 'USDC.e', - l1Address: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', - l2Address: '0x094ae96521f35284e04fdf023ca06fe878e307fd' - } - ] -} - -export type TokenWithdrawalApprovalParams = { - tokenAddressOnParentChain: string - parentChainId: ChainId - childChainId: ChainId -} - -export async function tokenRequiresApprovalOnL2( - params: TokenWithdrawalApprovalParams -) { - if (await xErc20RequiresApprovalOnChildChain(params)) { - return true - } - - const { tokenAddressOnParentChain, childChainId } = params - - return (L2ApproveTokens[childChainId] ?? []) - .map(token => token.l1Address.toLowerCase()) - .includes(tokenAddressOnParentChain.toLowerCase()) -} diff --git a/packages/arb-token-bridge-ui/src/util/TokenApprovalUtils.ts b/packages/arb-token-bridge-ui/src/util/TokenApprovalUtils.ts deleted file mode 100644 index 8c6619e493..0000000000 --- a/packages/arb-token-bridge-ui/src/util/TokenApprovalUtils.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ERC20__factory } from '@arbitrum/sdk/dist/lib/abi/factories/ERC20__factory' -import { MaxUint256 } from '@ethersproject/constants' -import { Provider } from '@ethersproject/providers' -import { fetchErc20ParentChainGatewayAddress } from './TokenUtils' - -export const approveTokenEstimateGas = async ({ - erc20L1Address, - address, - l1Provider, - l2Provider -}: { - erc20L1Address: string - address: string - l1Provider: Provider - l2Provider: Provider -}) => { - const l1GatewayAddress = await fetchErc20ParentChainGatewayAddress({ - erc20ParentChainAddress: erc20L1Address, - parentChainProvider: l1Provider, - childChainProvider: l2Provider - }) - - const contract = ERC20__factory.connect(erc20L1Address, l1Provider) - - return contract.estimateGas.approve(l1GatewayAddress, MaxUint256, { - from: address - }) -} diff --git a/packages/arb-token-bridge-ui/src/util/WithdrawalUtils.ts b/packages/arb-token-bridge-ui/src/util/WithdrawalUtils.ts index ac29f2c02c..4ceec6cb05 100644 --- a/packages/arb-token-bridge-ui/src/util/WithdrawalUtils.ts +++ b/packages/arb-token-bridge-ui/src/util/WithdrawalUtils.ts @@ -103,7 +103,8 @@ export async function withdrawInitTxEstimateGas({ // https://arbiscan.io/tx/0xb9c866257b6f8861c2323ae902f681f7ffa313c3a3b93347f1ecaa0aa5c9b59e estimatedChildChainGas: isToken ? BigNumber.from(1_400_000) - : BigNumber.from(800_000) + : BigNumber.from(800_000), + isError: true } } } diff --git a/packages/arb-token-bridge-ui/src/util/xErc20Utils.ts b/packages/arb-token-bridge-ui/src/util/xErc20Utils.ts deleted file mode 100644 index 267763edaf..0000000000 --- a/packages/arb-token-bridge-ui/src/util/xErc20Utils.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { getProviderForChainId } from '@/token-bridge-sdk/utils' -import { fetchErc20L2GatewayAddress } from './TokenUtils' -import { ChainId } from './networks' -import { TokenWithdrawalApprovalParams } from './L2ApprovalUtils' - -export const xErc20Gateways: { - [chainId: number]: { - parentChainId: ChainId - parentGateway: string - childGateway: string - } -} = { - [ChainId.ArbitrumSepolia]: { - parentChainId: ChainId.Sepolia, - parentGateway: '0x30BEc9c7C2d102aF63F23712bEAc69cdF013f062', - childGateway: '0x30BEc9c7C2d102aF63F23712bEAc69cdF013f062' - } -} - -export async function xErc20RequiresApprovalOnChildChain({ - tokenAddressOnParentChain, - parentChainId, - childChainId -}: TokenWithdrawalApprovalParams): Promise { - const gatewayData = xErc20Gateways[childChainId] - - if (gatewayData?.parentChainId !== parentChainId) { - return false - } - - const childChainProvider = getProviderForChainId(childChainId) - - const childChainGatewayAddress = await fetchErc20L2GatewayAddress({ - erc20L1Address: tokenAddressOnParentChain, - l2Provider: childChainProvider - }) - - return ( - childChainGatewayAddress.toLowerCase() === - gatewayData.childGateway.toLowerCase() - ) -}