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' ...@@ -12,6 +12,7 @@ import Badge from 'components/Badge'
import Modal, { MODAL_TRANSITION_DURATION } from 'components/Modal' import Modal, { MODAL_TRANSITION_DURATION } from 'components/Modal'
import { RowFixed } from 'components/Row' import { RowFixed } from 'components/Row'
import { getChainInfo } from 'constants/chainInfo' import { getChainInfo } from 'constants/chainInfo'
import { USDT as USDT_MAINNET } from 'constants/tokens'
import { useMaxAmountIn } from 'hooks/useMaxAmountIn' import { useMaxAmountIn } from 'hooks/useMaxAmountIn'
import { Allowance, AllowanceState } from 'hooks/usePermit2Allowance' import { Allowance, AllowanceState } from 'hooks/usePermit2Allowance'
import usePrevious from 'hooks/usePrevious' import usePrevious from 'hooks/usePrevious'
...@@ -34,6 +35,7 @@ import SwapModalHeader from './SwapModalHeader' ...@@ -34,6 +35,7 @@ import SwapModalHeader from './SwapModalHeader'
export enum ConfirmModalState { export enum ConfirmModalState {
REVIEWING, REVIEWING,
RESETTING_USDT,
APPROVING_TOKEN, APPROVING_TOKEN,
PERMITTING, PERMITTING,
PENDING_CONFIRMATION, PENDING_CONFIRMATION,
...@@ -49,7 +51,11 @@ const StyledL2Logo = styled.img` ...@@ -49,7 +51,11 @@ const StyledL2Logo = styled.img`
` `
function isInApprovalPhase(confirmModalState: ConfirmModalState) { 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({ function useConfirmModalState({
...@@ -74,6 +80,15 @@ function useConfirmModalState({ ...@@ -74,6 +80,15 @@ function useConfirmModalState({
// at the bottom of the modal, even after they complete steps 1 and 2. // at the bottom of the modal, even after they complete steps 1 and 2.
const generateRequiredSteps = useCallback(() => { const generateRequiredSteps = useCallback(() => {
const steps: PendingConfirmModalState[] = [] 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) { if (allowance.state === AllowanceState.REQUIRED && allowance.needsSetupApproval) {
steps.push(ConfirmModalState.APPROVING_TOKEN) steps.push(ConfirmModalState.APPROVING_TOKEN)
} }
...@@ -98,6 +113,11 @@ function useConfirmModalState({ ...@@ -98,6 +113,11 @@ function useConfirmModalState({
const performStep = useCallback( const performStep = useCallback(
async (step: ConfirmModalState) => { async (step: ConfirmModalState) => {
switch (step) { 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: case ConfirmModalState.APPROVING_TOKEN:
setConfirmModalState(ConfirmModalState.APPROVING_TOKEN) setConfirmModalState(ConfirmModalState.APPROVING_TOKEN)
invariant(allowance.state === AllowanceState.REQUIRED, 'Allowance should be required') invariant(allowance.state === AllowanceState.REQUIRED, 'Allowance should be required')
...@@ -155,6 +175,15 @@ function useConfirmModalState({ ...@@ -155,6 +175,15 @@ function useConfirmModalState({
} }
}, [allowance, performStep, previousSetupApprovalNeeded]) }, [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(() => { useEffect(() => {
// Automatically triggers the next phase if the local modal state still thinks we're in the approval phase, // 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. // but the allowance has been set. This will automaticaly trigger the swap.
...@@ -282,6 +311,7 @@ export default function ConfirmSwapModal({ ...@@ -282,6 +311,7 @@ export default function ConfirmSwapModal({
trade={trade} trade={trade}
swapTxHash={txHash} swapTxHash={txHash}
tokenApprovalPending={allowance.state === AllowanceState.REQUIRED && allowance.isApprovalPending} tokenApprovalPending={allowance.state === AllowanceState.REQUIRED && allowance.isApprovalPending}
revocationPending={allowance.state === AllowanceState.REQUIRED && allowance.isRevocationPending}
/> />
) )
}, [ }, [
......
...@@ -87,7 +87,10 @@ const StepTitleAnimationContainer = styled(Column)<{ disableEntranceAnimation?: ...@@ -87,7 +87,10 @@ const StepTitleAnimationContainer = styled(Column)<{ disableEntranceAnimation?:
// This component is used for all steps after ConfirmModalState.REVIEWING // This component is used for all steps after ConfirmModalState.REVIEWING
export type PendingConfirmModalState = Extract< export type PendingConfirmModalState = Extract<
ConfirmModalState, ConfirmModalState,
ConfirmModalState.APPROVING_TOKEN | ConfirmModalState.PERMITTING | ConfirmModalState.PENDING_CONFIRMATION | ConfirmModalState.APPROVING_TOKEN
| ConfirmModalState.PERMITTING
| ConfirmModalState.PENDING_CONFIRMATION
| ConfirmModalState.RESETTING_USDT
> >
interface PendingModalStep { interface PendingModalStep {
...@@ -105,6 +108,7 @@ interface PendingModalContentProps { ...@@ -105,6 +108,7 @@ interface PendingModalContentProps {
swapTxHash?: string swapTxHash?: string
hideStepIndicators?: boolean hideStepIndicators?: boolean
tokenApprovalPending?: boolean tokenApprovalPending?: boolean
revocationPending?: boolean
} }
interface ContentArgs { interface ContentArgs {
...@@ -114,13 +118,30 @@ interface ContentArgs { ...@@ -114,13 +118,30 @@ interface ContentArgs {
swapConfirmed: boolean swapConfirmed: boolean
swapPending: boolean swapPending: boolean
tokenApprovalPending: boolean tokenApprovalPending: boolean
revocationPending: boolean
swapTxHash?: string swapTxHash?: string
chainId?: number chainId?: number
} }
function getContent(args: ContentArgs): PendingModalStep { 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) { 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: case ConfirmModalState.APPROVING_TOKEN:
return { return {
title: t`Enable spending ${approvalCurrency?.symbol ?? 'this token'} on Uniswap`, title: t`Enable spending ${approvalCurrency?.symbol ?? 'this token'} on Uniswap`,
...@@ -167,6 +188,7 @@ export function PendingModalContent({ ...@@ -167,6 +188,7 @@ export function PendingModalContent({
swapTxHash, swapTxHash,
hideStepIndicators, hideStepIndicators,
tokenApprovalPending = false, tokenApprovalPending = false,
revocationPending = false,
}: PendingModalContentProps) { }: PendingModalContentProps) {
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
const swapConfirmed = useIsTransactionConfirmed(swapTxHash) const swapConfirmed = useIsTransactionConfirmed(swapTxHash)
...@@ -177,6 +199,7 @@ export function PendingModalContent({ ...@@ -177,6 +199,7 @@ export function PendingModalContent({
swapConfirmed, swapConfirmed,
swapPending, swapPending,
tokenApprovalPending, tokenApprovalPending,
revocationPending,
swapTxHash, swapTxHash,
trade, trade,
chainId, chainId,
...@@ -194,26 +217,27 @@ export function PendingModalContent({ ...@@ -194,26 +217,27 @@ export function PendingModalContent({
return ( return (
<PendingModalContainer gap="lg"> <PendingModalContainer gap="lg">
<LogoContainer> <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 />} {currentStep === ConfirmModalState.APPROVING_TOKEN && <PaperIcon />}
{/* Shown during the first step as a small badge. */} {/* Shown during the setup approval step as a small badge. */}
{/* Scales up once we transition from first to second step. */} {/* Scales up once we transition from setup approval to permit signature. */}
{/* Fades out after the second step. */} {/* Fades out after the permit signature. */}
{currentStep !== ConfirmModalState.PENDING_CONFIRMATION && ( {currentStep !== ConfirmModalState.PENDING_CONFIRMATION && (
<CurrencyLoader <CurrencyLoader
currency={trade?.inputAmount.currency} currency={trade?.inputAmount.currency}
asBadge={currentStep === ConfirmModalState.APPROVING_TOKEN} 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 />} {currentStep === ConfirmModalState.PENDING_CONFIRMATION && showSuccess && <AnimatedEntranceConfirmationIcon />}
{/* Scales in for the first step if the approval is pending onchain confirmation. */} {/* Scales in for the USDT revoke allowance step if the revoke is pending onchain confirmation. */}
{/* Scales in for the third step if the swap is pending user signature or onchain confirmation. */} {/* Scales in for the setup approval step if the approval is pending onchain confirmation. */}
{((currentStep === ConfirmModalState.PENDING_CONFIRMATION && !showSuccess) || tokenApprovalPending) && ( {/* Scales in for the final step if the swap is pending user signature or onchain confirmation. */}
<LoadingIndicatorOverlay /> {((currentStep === ConfirmModalState.PENDING_CONFIRMATION && !showSuccess) ||
)} tokenApprovalPending ||
revocationPending) && <LoadingIndicatorOverlay />}
</LogoContainer> </LogoContainer>
<HeaderContainer gap="md" $disabled={tokenApprovalPending || (swapPending && !showSuccess)}> <HeaderContainer gap="md" $disabled={revocationPending || tokenApprovalPending || (swapPending && !showSuccess)}>
<AnimationWrapper> <AnimationWrapper>
{steps.map((step) => { {steps.map((step) => {
const { title, subtitle } = getContent({ const { title, subtitle } = getContent({
...@@ -222,6 +246,7 @@ export function PendingModalContent({ ...@@ -222,6 +246,7 @@ export function PendingModalContent({
swapConfirmed, swapConfirmed,
swapPending, swapPending,
tokenApprovalPending, tokenApprovalPending,
revocationPending,
swapTxHash, swapTxHash,
trade, trade,
}) })
......
...@@ -48,7 +48,8 @@ export function useUpdateTokenAllowance( ...@@ -48,7 +48,8 @@ export function useUpdateTokenAllowance(
if (!contract) throw new Error('missing contract') if (!contract) throw new Error('missing contract')
if (!spender) throw new Error('missing spender') 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) const response = await contract.approve(spender, allowance)
return { return {
response, 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