Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(bridge-ui): fix USDT approvals #17539

Merged
merged 5 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';

import { Alert } from '$components/Alert';
import {
allApproved,
computingBalance,
Expand All @@ -10,6 +11,7 @@
errorComputingBalance,
insufficientAllowance,
insufficientBalance,
needsApprovalReset,
recipientAddress,
selectedToken,
tokenBalance,
Expand All @@ -24,9 +26,11 @@

export let approve: () => Promise<void>;
export let bridge: () => Promise<void>;
export let resetApproval: () => Promise<void>;

export let approving = false;
export let bridging = false;
export let resetting = false;

export let disabled = false;

Expand All @@ -47,6 +51,12 @@
bridge();
}

const onResetApproveClick = async () => {
resetting = true;
await resetApproval();
resetting = false;
};

onMount(async () => {
if ($selectedToken) {
$allApproved = false;
Expand Down Expand Up @@ -85,6 +95,11 @@

$: validApprovalStatus = $allApproved;

// USDT specific, L1 address of USDT contract
$: resetRequired =
$selectedToken?.addresses[$connectedSourceChain.id] === '0xdAC17F958D2ee523a2206206994597C13D831ec7' &&
$needsApprovalReset;

$: commonConditions =
validApprovalStatus &&
!bridging &&
Expand All @@ -106,6 +121,8 @@

$: ethConditionsSatisfied = commonConditions && $enteredAmount && $enteredAmount > 0;

$: disableReset = !resetRequired || resetting;

$: disableBridge = isERC20
? !erc20ConditionsSatisfied
: isERC721
Expand All @@ -119,24 +136,42 @@

<div class="f-col w-full gap-4">
{#if $selectedToken && !isETH}
<ActionButton
priority="primary"
disabled={disableApprove}
loading={approving || $validatingAmount || checking}
on:click={onApproveClick}>
{#if approving}
<span class="body-bold">{$t('bridge.button.approving')}</span>
{:else if $allApproved}
<div class="f-items-center">
<Icon type="check" />
<span class="body-bold">{$t('bridge.button.approved')}</span>
</div>
{:else if checking}
<span class="body-bold">{$t('bridge.button.validating')}</span>
{:else}
<span class="body-bold">{$t('bridge.button.approve')}</span>
{/if}
</ActionButton>
{#if resetRequired}
<Alert type="info">{$t('bridge.usdt_approval.info')}</Alert>
<ActionButton priority="primary" disabled={disableReset} loading={resetting} on:click={onResetApproveClick}>
{#if resetting}
<span class="body-bold">{$t('bridge.button.resetting')}</span>
{:else if $allApproved}
<div class="f-items-center">
<Icon type="check" />
<span class="body-bold">{$t('bridge.button.reset')}</span>
</div>
{:else if checking}
<span class="body-bold">{$t('bridge.button.validating')}</span>
{:else}
<span class="body-bold">{$t('bridge.button.reset_approval')}</span>
{/if}
</ActionButton>
{:else}
<ActionButton
priority="primary"
disabled={disableApprove}
loading={approving || $validatingAmount || checking}
on:click={onApproveClick}>
{#if approving}
<span class="body-bold">{$t('bridge.button.approving')}</span>
{:else if $allApproved}
<div class="f-items-center">
<Icon type="check" />
<span class="body-bold">{$t('bridge.button.approved')}</span>
</div>
{:else if checking}
<span class="body-bold">{$t('bridge.button.validating')}</span>
{:else}
<span class="body-bold">{$t('bridge.button.approve')}</span>
{/if}
</ActionButton>
{/if}
{/if}
<ActionButton priority="primary" disabled={disableBridge} loading={bridging} on:click={onBridgeClick}>
{#if bridging}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
let bridging: boolean;
let approving: boolean;
let checking: boolean;
let resetting: boolean;

let icon: IconType;

Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -301,7 +321,7 @@
{#if bridgingStatus === BridgingStatus.PENDING}
<section id="actions" class="f-col w-full">
<div class="h-sep mb-[30px]" />
<Actions {approve} {bridge} bind:bridging bind:approving bind:checking />
<Actions {approve} {bridge} bind:bridging bind:approving bind:checking bind:resetting {resetApproval} />
</section>
{/if}
</div>
1 change: 1 addition & 0 deletions packages/bridge-ui/src/components/Bridge/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const insufficientBalance = writable<boolean>(false);
export const insufficientAllowance = writable<boolean>(false);

export const allApproved = writable(<boolean>false);
export const needsApprovalReset = writable<boolean>(false);

// Derived state
export const bridgeService = derived(selectedToken, (token) => (token ? bridges[token.type] : null));
Expand Down
6 changes: 6 additions & 0 deletions packages/bridge-ui/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@
"bridging": "Bridging",
"fetch": "Fetch NFT data",
"import": "Import",
"reset": "Reset",
"reset_approval": "Reset Approval",
"resetting": "Resetting",
"validating": "Validating..."
},
"description": {
Expand Down Expand Up @@ -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": {
Expand Down
50 changes: 41 additions & 9 deletions packages/bridge-ui/src/libs/bridge/ERC20Bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand All @@ -125,33 +125,65 @@ 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);

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}`);
}

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],
});
Expand Down
16 changes: 16 additions & 0 deletions packages/bridge-ui/src/libs/token/getTokenApprovalStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
destNetwork,
enteredAmount,
insufficientAllowance,
needsApprovalReset,
selectedToken,
} from '$components/Bridge/state';
import { bridges, ContractType, type RequireApprovalArgs } from '$libs/bridge';
Expand All @@ -27,6 +28,7 @@ export enum ApprovalStatus {
ETH_NO_APPROVAL_REQUIRED,
APPROVAL_REQUIRED,
NO_APPROVAL_REQUIRED,
RESET_REQUIRED,
}

export const getTokenApprovalStatus = async (token: Maybe<Token | NFT>): Promise<ApprovalStatus> => {
Expand Down Expand Up @@ -57,6 +59,7 @@ export const getTokenApprovalStatus = async (token: Maybe<Token | NFT>): 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;
Expand All @@ -72,6 +75,19 @@ export const getTokenApprovalStatus = async (token: Maybe<Token | NFT>): 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;
Expand Down