Commit 35b83ab8 authored by eddie's avatar eddie Committed by GitHub

feat: revoke USDT approvals in ConfirmSwapModal (#6730)

parent c0d42ade
......@@ -12,6 +12,7 @@ import Badge from 'components/Badge'
import Modal, { MODAL_TRANSITION_DURATION } from 'components/Modal'
import { RowFixed } from 'components/Row'
import { getChainInfo } from 'constants/chainInfo'
import { USDT as USDT_MAINNET } from 'constants/tokens'
import { useMaxAmountIn } from 'hooks/useMaxAmountIn'
import { Allowance, AllowanceState } from 'hooks/usePermit2Allowance'
import usePrevious from 'hooks/usePrevious'
......@@ -34,6 +35,7 @@ import SwapModalHeader from './SwapModalHeader'
export enum ConfirmModalState {
REVIEWING,
RESETTING_USDT,
APPROVING_TOKEN,
PERMITTING,
PENDING_CONFIRMATION,
......@@ -49,7 +51,11 @@ const StyledL2Logo = styled.img`
`
function isInApprovalPhase(confirmModalState: ConfirmModalState) {
return confirmModalState === ConfirmModalState.APPROVING_TOKEN || confirmModalState === ConfirmModalState.PERMITTING
return (
confirmModalState === ConfirmModalState.RESETTING_USDT ||
confirmModalState === ConfirmModalState.APPROVING_TOKEN ||
confirmModalState === ConfirmModalState.PERMITTING
)
}
function useConfirmModalState({
......@@ -74,6 +80,15 @@ function useConfirmModalState({
// at the bottom of the modal, even after they complete steps 1 and 2.
const generateRequiredSteps = useCallback(() => {
const steps: PendingConfirmModalState[] = []
// Any existing USDT allowance needs to be reset before we can approve the new amount (mainnet only).
// See the `approve` function here: https://etherscan.io/address/0xdAC17F958D2ee523a2206206994597C13D831ec7#code
if (
allowance.state === AllowanceState.REQUIRED &&
allowance.token.equals(USDT_MAINNET) &&
allowance.allowedAmount.greaterThan(0)
) {
steps.push(ConfirmModalState.RESETTING_USDT)
}
if (allowance.state === AllowanceState.REQUIRED && allowance.needsSetupApproval) {
steps.push(ConfirmModalState.APPROVING_TOKEN)
}
......@@ -98,6 +113,11 @@ function useConfirmModalState({
const performStep = useCallback(
async (step: ConfirmModalState) => {
switch (step) {
case ConfirmModalState.RESETTING_USDT:
setConfirmModalState(ConfirmModalState.RESETTING_USDT)
invariant(allowance.state === AllowanceState.REQUIRED, 'Allowance should be required')
allowance.revoke().catch((e) => catchUserReject(e, PendingModalError.TOKEN_APPROVAL_ERROR))
break
case ConfirmModalState.APPROVING_TOKEN:
setConfirmModalState(ConfirmModalState.APPROVING_TOKEN)
invariant(allowance.state === AllowanceState.REQUIRED, 'Allowance should be required')
......@@ -155,6 +175,15 @@ function useConfirmModalState({
}
}, [allowance, performStep, previousSetupApprovalNeeded])
const previousRevocationPending = usePrevious(
allowance.state === AllowanceState.REQUIRED && allowance.isRevocationPending
)
useEffect(() => {
if (allowance.state === AllowanceState.REQUIRED && previousRevocationPending && !allowance.isRevocationPending) {
performStep(ConfirmModalState.APPROVING_TOKEN)
}
}, [allowance, performStep, previousRevocationPending])
useEffect(() => {
// Automatically triggers the next phase if the local modal state still thinks we're in the approval phase,
// but the allowance has been set. This will automaticaly trigger the swap.
......@@ -282,6 +311,7 @@ export default function ConfirmSwapModal({
trade={trade}
swapTxHash={txHash}
tokenApprovalPending={allowance.state === AllowanceState.REQUIRED && allowance.isApprovalPending}
revocationPending={allowance.state === AllowanceState.REQUIRED && allowance.isRevocationPending}
/>
)
}, [
......
......@@ -87,7 +87,10 @@ const StepTitleAnimationContainer = styled(Column)<{ disableEntranceAnimation?:
// This component is used for all steps after ConfirmModalState.REVIEWING
export type PendingConfirmModalState = Extract<
ConfirmModalState,
ConfirmModalState.APPROVING_TOKEN | ConfirmModalState.PERMITTING | ConfirmModalState.PENDING_CONFIRMATION
| ConfirmModalState.APPROVING_TOKEN
| ConfirmModalState.PERMITTING
| ConfirmModalState.PENDING_CONFIRMATION
| ConfirmModalState.RESETTING_USDT
>
interface PendingModalStep {
......@@ -105,6 +108,7 @@ interface PendingModalContentProps {
swapTxHash?: string
hideStepIndicators?: boolean
tokenApprovalPending?: boolean
revocationPending?: boolean
}
interface ContentArgs {
......@@ -114,13 +118,30 @@ interface ContentArgs {
swapConfirmed: boolean
swapPending: boolean
tokenApprovalPending: boolean
revocationPending: boolean
swapTxHash?: string
chainId?: number
}
function getContent(args: ContentArgs): PendingModalStep {
const { step, approvalCurrency, swapConfirmed, swapPending, tokenApprovalPending, trade, swapTxHash, chainId } = args
const {
step,
approvalCurrency,
swapConfirmed,
swapPending,
tokenApprovalPending,
revocationPending,
trade,
swapTxHash,
chainId,
} = args
switch (step) {
case ConfirmModalState.RESETTING_USDT:
return {
title: t`Reset USDT`,
subtitle: t`USDT requires resetting approval when spending limits are too low.`,
label: revocationPending ? t`Pending...` : t`Proceed in your wallet`,
}
case ConfirmModalState.APPROVING_TOKEN:
return {
title: t`Enable spending ${approvalCurrency?.symbol ?? 'this token'} on Uniswap`,
......@@ -167,6 +188,7 @@ export function PendingModalContent({
swapTxHash,
hideStepIndicators,
tokenApprovalPending = false,
revocationPending = false,
}: PendingModalContentProps) {
const { chainId } = useWeb3React()
const swapConfirmed = useIsTransactionConfirmed(swapTxHash)
......@@ -177,6 +199,7 @@ export function PendingModalContent({
swapConfirmed,
swapPending,
tokenApprovalPending,
revocationPending,
swapTxHash,
trade,
chainId,
......@@ -194,26 +217,27 @@ export function PendingModalContent({
return (
<PendingModalContainer gap="lg">
<LogoContainer>
{/* Shown during the first step, and fades out afterwards. */}
{/* Shown during the setup approval step, and fades out afterwards. */}
{currentStep === ConfirmModalState.APPROVING_TOKEN && <PaperIcon />}
{/* Shown during the first step as a small badge. */}
{/* Scales up once we transition from first to second step. */}
{/* Fades out after the second step. */}
{/* Shown during the setup approval step as a small badge. */}
{/* Scales up once we transition from setup approval to permit signature. */}
{/* Fades out after the permit signature. */}
{currentStep !== ConfirmModalState.PENDING_CONFIRMATION && (
<CurrencyLoader
currency={trade?.inputAmount.currency}
asBadge={currentStep === ConfirmModalState.APPROVING_TOKEN}
/>
)}
{/* Shown only during the third step under "success" conditions, and scales in. */}
{/* Shown only during the final step under "success" conditions, and scales in. */}
{currentStep === ConfirmModalState.PENDING_CONFIRMATION && showSuccess && <AnimatedEntranceConfirmationIcon />}
{/* Scales in for the first step if the approval is pending onchain confirmation. */}
{/* Scales in for the third step if the swap is pending user signature or onchain confirmation. */}
{((currentStep === ConfirmModalState.PENDING_CONFIRMATION && !showSuccess) || tokenApprovalPending) && (
<LoadingIndicatorOverlay />
)}
{/* Scales in for the USDT revoke allowance step if the revoke is pending onchain confirmation. */}
{/* Scales in for the setup approval step if the approval is pending onchain confirmation. */}
{/* Scales in for the final step if the swap is pending user signature or onchain confirmation. */}
{((currentStep === ConfirmModalState.PENDING_CONFIRMATION && !showSuccess) ||
tokenApprovalPending ||
revocationPending) && <LoadingIndicatorOverlay />}
</LogoContainer>
<HeaderContainer gap="md" $disabled={tokenApprovalPending || (swapPending && !showSuccess)}>
<HeaderContainer gap="md" $disabled={revocationPending || tokenApprovalPending || (swapPending && !showSuccess)}>
<AnimationWrapper>
{steps.map((step) => {
const { title, subtitle } = getContent({
......@@ -222,6 +246,7 @@ export function PendingModalContent({
swapConfirmed,
swapPending,
tokenApprovalPending,
revocationPending,
swapTxHash,
trade,
})
......
......@@ -48,7 +48,8 @@ export function useUpdateTokenAllowance(
if (!contract) throw new Error('missing contract')
if (!spender) throw new Error('missing spender')
const allowance = MaxUint256.toString()
const maxAllowance = MaxUint256.toString()
const allowance = amount.equalTo(0) ? '0' : maxAllowance
const response = await contract.approve(spender, allowance)
return {
response,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment