diff --git a/packages/bridge-ui/src/components/Bridge/SharedBridgeComponents/Actions.svelte b/packages/bridge-ui/src/components/Bridge/SharedBridgeComponents/Actions.svelte index fba7eefb18..41c45b352b 100644 --- a/packages/bridge-ui/src/components/Bridge/SharedBridgeComponents/Actions.svelte +++ b/packages/bridge-ui/src/components/Bridge/SharedBridgeComponents/Actions.svelte @@ -2,6 +2,7 @@ import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; + import { Alert } from '$components/Alert'; import { allApproved, computingBalance, @@ -10,6 +11,7 @@ errorComputingBalance, insufficientAllowance, insufficientBalance, + needsApprovalReset, recipientAddress, selectedToken, tokenBalance, @@ -24,9 +26,11 @@ export let approve: () => Promise; export let bridge: () => Promise; + export let resetApproval: () => Promise; export let approving = false; export let bridging = false; + export let resetting = false; export let disabled = false; @@ -47,6 +51,12 @@ bridge(); } + const onResetApproveClick = async () => { + resetting = true; + await resetApproval(); + resetting = false; + }; + onMount(async () => { if ($selectedToken) { $allApproved = false; @@ -85,6 +95,11 @@ $: validApprovalStatus = $allApproved; + // USDT specific, L1 address of USDT contract + $: resetRequired = + $selectedToken?.addresses[$connectedSourceChain.id] === '0xdAC17F958D2ee523a2206206994597C13D831ec7' && + $needsApprovalReset; + $: commonConditions = validApprovalStatus && !bridging && @@ -106,6 +121,8 @@ $: ethConditionsSatisfied = commonConditions && $enteredAmount && $enteredAmount > 0; + $: disableReset = !resetRequired || resetting; + $: disableBridge = isERC20 ? !erc20ConditionsSatisfied : isERC721 @@ -119,24 +136,42 @@
{#if $selectedToken && !isETH} - - {#if approving} - {$t('bridge.button.approving')} - {:else if $allApproved} -
- - {$t('bridge.button.approved')} -
- {:else if checking} - {$t('bridge.button.validating')} - {:else} - {$t('bridge.button.approve')} - {/if} -
+ {#if resetRequired} + {$t('bridge.usdt_approval.info')} + + {#if resetting} + {$t('bridge.button.resetting')} + {:else if $allApproved} +
+ + {$t('bridge.button.reset')} +
+ {:else if checking} + {$t('bridge.button.validating')} + {:else} + {$t('bridge.button.reset_approval')} + {/if} +
+ {:else} + + {#if approving} + {$t('bridge.button.approving')} + {:else if $allApproved} +
+ + {$t('bridge.button.approved')} +
+ {:else if checking} + {$t('bridge.button.validating')} + {:else} + {$t('bridge.button.approve')} + {/if} +
+ {/if} {/if} {#if bridging} diff --git a/packages/bridge-ui/src/components/Bridge/SharedBridgeComponents/ConfirmationStep/ConfirmationStep.svelte b/packages/bridge-ui/src/components/Bridge/SharedBridgeComponents/ConfirmationStep/ConfirmationStep.svelte index 1adbcb96d9..2d72fdedda 100644 --- a/packages/bridge-ui/src/components/Bridge/SharedBridgeComponents/ConfirmationStep/ConfirmationStep.svelte +++ b/packages/bridge-ui/src/components/Bridge/SharedBridgeComponents/ConfirmationStep/ConfirmationStep.svelte @@ -48,6 +48,7 @@ let bridging: boolean; let approving: boolean; let checking: boolean; + let resetting: boolean; let icon: IconType; @@ -173,6 +174,25 @@ } }; + async function resetApproval() { + if (!$selectedToken || !$connectedSourceChain || !$destNetwork?.id) return; + try { + let tokenAddress = $selectedToken.addresses[$connectedSourceChain.id]; + const type: TokenType = $selectedToken.type; + + const spenderAddress = routingContractsMap[$connectedSourceChain.id][$destNetwork?.id].erc20VaultAddress; + const walletClient = await getConnectedWallet($connectedSourceChain.id); + + const args: ApproveArgs = { tokenAddress, spenderAddress, wallet: walletClient, amount: 0n }; + approveTxHash = await (bridges[type] as ERC20Bridge).approve(args, true); + + if (approveTxHash) await handleApproveTxHash(approveTxHash); + } catch (err) { + console.error(err); + handleBridgeError(err as Error); + } + } + async function approve() { isBridgePaused().then((paused) => { if (paused) throw new BridgePausedError('Bridge is paused'); @@ -301,7 +321,7 @@ {#if bridgingStatus === BridgingStatus.PENDING}
- +
{/if}
diff --git a/packages/bridge-ui/src/components/Bridge/state.ts b/packages/bridge-ui/src/components/Bridge/state.ts index 711886bbe6..2b33509e22 100644 --- a/packages/bridge-ui/src/components/Bridge/state.ts +++ b/packages/bridge-ui/src/components/Bridge/state.ts @@ -49,6 +49,7 @@ export const insufficientBalance = writable(false); export const insufficientAllowance = writable(false); export const allApproved = writable(false); +export const needsApprovalReset = writable(false); // Derived state export const bridgeService = derived(selectedToken, (token) => (token ? bridges[token.type] : null)); diff --git a/packages/bridge-ui/src/i18n/en.json b/packages/bridge-ui/src/i18n/en.json index f9e04b28ce..a4b2d86a25 100644 --- a/packages/bridge-ui/src/i18n/en.json +++ b/packages/bridge-ui/src/i18n/en.json @@ -53,6 +53,9 @@ "bridging": "Bridging", "fetch": "Fetch NFT data", "import": "Import", + "reset": "Reset", + "reset_approval": "Reset Approval", + "resetting": "Resetting", "validating": "Validating..." }, "description": { @@ -205,6 +208,9 @@ "recipient": "Recipient", "review": "Review" } + }, + "usdt_approval": { + "info": "You have previously approved a lower amount of USDT. To adjust the approval, you must first reset the previous amount to 0. This requirement is unique to USDT." } }, "chain_selector": { diff --git a/packages/bridge-ui/src/libs/bridge/ERC20Bridge.ts b/packages/bridge-ui/src/libs/bridge/ERC20Bridge.ts index 3eb95ee70a..f1c06c0324 100644 --- a/packages/bridge-ui/src/libs/bridge/ERC20Bridge.ts +++ b/packages/bridge-ui/src/libs/bridge/ERC20Bridge.ts @@ -111,7 +111,7 @@ export class ERC20Bridge extends Bridge { return estimatedGas; } - async requireAllowance({ amount, tokenAddress, ownerAddress, spenderAddress }: RequireAllowanceArgs) { + async getAllowance({ amount, tokenAddress, ownerAddress, spenderAddress }: RequireAllowanceArgs) { isBridgePaused().then((paused) => { if (paused) throw new BridgePausedError('Bridge is paused'); }); @@ -125,6 +125,14 @@ export class ERC20Bridge extends Bridge { chainId: (await getConnectedWallet()).chain.id, }); + return allowance; + } + async requireAllowance({ amount, tokenAddress, ownerAddress, spenderAddress }: RequireAllowanceArgs, reset = false) { + const allowance = await this.getAllowance({ amount, tokenAddress, ownerAddress, spenderAddress }); + + if (reset) { + return true; + } const requiresAllowance = allowance < amount; log('Allowance is', allowance, 'requires allowance?', requiresAllowance); @@ -132,15 +140,18 @@ export class ERC20Bridge extends Bridge { return requiresAllowance; } - async approve(args: ApproveArgs) { + async approve(args: ApproveArgs, reset = false) { const { amount, tokenAddress, spenderAddress, wallet } = args; if (!wallet || !wallet.account) throw new Error('No wallet found'); - const requireAllowance = await this.requireAllowance({ - amount, - tokenAddress, - ownerAddress: wallet.account.address, - spenderAddress, - }); + const requireAllowance = await this.requireAllowance( + { + amount, + tokenAddress, + ownerAddress: wallet.account.address, + spenderAddress, + }, + reset, + ); if (!requireAllowance) { throw new NoAllowanceRequiredError(`no allowance required for the amount ${amount}`); @@ -148,10 +159,31 @@ export class ERC20Bridge extends Bridge { try { log(`Calling approve for spender "${spenderAddress}" for token "${tokenAddress}" with amount`, amount); + // USDT does not play nice with the default ERC20 ABI, this works for both + const approvalABI = [ + { + constant: false, + inputs: [ + { + name: '_spender', + type: 'address', + }, + { + name: '_value', + type: 'uint256', + }, + ], + name: 'approve', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + ]; const { request } = await simulateContract(config, { address: tokenAddress, - abi: erc20Abi, + abi: approvalABI, functionName: 'approve', args: [spenderAddress, amount], }); diff --git a/packages/bridge-ui/src/libs/token/getTokenApprovalStatus.ts b/packages/bridge-ui/src/libs/token/getTokenApprovalStatus.ts index 94a5cbcf88..0fa606db40 100644 --- a/packages/bridge-ui/src/libs/token/getTokenApprovalStatus.ts +++ b/packages/bridge-ui/src/libs/token/getTokenApprovalStatus.ts @@ -6,6 +6,7 @@ import { destNetwork, enteredAmount, insufficientAllowance, + needsApprovalReset, selectedToken, } from '$components/Bridge/state'; import { bridges, ContractType, type RequireApprovalArgs } from '$libs/bridge'; @@ -27,6 +28,7 @@ export enum ApprovalStatus { ETH_NO_APPROVAL_REQUIRED, APPROVAL_REQUIRED, NO_APPROVAL_REQUIRED, + RESET_REQUIRED, } export const getTokenApprovalStatus = async (token: Maybe): Promise => { @@ -57,6 +59,7 @@ export const getTokenApprovalStatus = async (token: Maybe): Promise } if (token.type === TokenType.ERC20) { log('checking approval status for ERC20'); + needsApprovalReset.set(false); const tokenVaultAddress = routingContractsMap[currentChainId][destinationChainId].erc20VaultAddress; const bridge = bridges[TokenType.ERC20] as ERC20Bridge; @@ -72,6 +75,19 @@ export const getTokenApprovalStatus = async (token: Maybe): Promise insufficientAllowance.set(requireAllowance); allApproved.set(!requireAllowance); if (requireAllowance) { + // specific check for USDT + if (get(selectedToken)?.symbol === 'tUSDT') { + const allowance = await bridge.getAllowance({ + amount: get(enteredAmount), + tokenAddress, + ownerAddress, + spenderAddress: tokenVaultAddress, + }); + if (allowance > 0n) { + needsApprovalReset.set(true); + return ApprovalStatus.RESET_REQUIRED; + } + } return ApprovalStatus.APPROVAL_REQUIRED; } return ApprovalStatus.NO_APPROVAL_REQUIRED;