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: usdt token approval #1812

Merged
merged 8 commits into from
Feb 4, 2025
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
23 changes: 23 additions & 0 deletions apps/web/src/lib/wagmi/hooks/approvals/hooks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,26 @@ import type { Address } from 'sushi/types'

export type ERC20ApproveABI = typeof erc20Abi_approve
export type ERC20ApproveArgs = [Address, bigint]

export const old_erc20Abi_approve = [
{
constant: false,
inputs: [
{
name: '_spender',
type: 'address',
},
{
name: '_value',
type: 'uint256',
},
],
name: 'approve',
outputs: {},
payable: false,
stateMutability: 'nonpayable',
type: 'function',
},
] as const

export type OLD_ERC20ApproveABI = typeof old_erc20Abi_approve
60 changes: 49 additions & 11 deletions apps/web/src/lib/wagmi/hooks/approvals/hooks/useTokenApproval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { erc20Abi_approve } from 'sushi/abi'
import type { Amount, Type } from 'sushi/currency'
import {
type Address,
ContractFunctionZeroDataError,
type SendTransactionReturnType,
UserRejectedRequestError,
maxUint256,
Expand All @@ -17,8 +18,12 @@ import {
useSimulateContract,
useWriteContract,
} from 'wagmi'

import type { ERC20ApproveABI, ERC20ApproveArgs } from './types'
import {
type ERC20ApproveABI,
type ERC20ApproveArgs,
type OLD_ERC20ApproveABI,
old_erc20Abi_approve,
} from './types'
import { useTokenAllowance } from './useTokenAllowance'

export enum ApprovalState {
Expand Down Expand Up @@ -57,7 +62,13 @@ export const useTokenApproval = ({
enabled: Boolean(amount?.currency?.isToken && enabled),
})

const { data: simulation } = useSimulateContract<
const [fallback, setFallback] = useState(false)

const simulationEnabled = Boolean(
amount && spender && address && allowance && enabled && !isAllowanceLoading,
)

const standardSimulation = useSimulateContract<
ERC20ApproveABI,
'approve',
ERC20ApproveArgs
Expand All @@ -70,18 +81,45 @@ export const useTokenApproval = ({
spender as Address,
approveMax ? maxUint256 : amount ? amount.quotient : 0n,
],
scopeKey: 'approve-std',
query: {
enabled: simulationEnabled && !fallback,
retry: (failureCount, error) => {
if (
error instanceof ContractFunctionZeroDataError ||
error.cause instanceof ContractFunctionZeroDataError
) {
setFallback(true)
return false
}
return failureCount < 2
},
},
})

const fallbackSimulation = useSimulateContract<
OLD_ERC20ApproveABI,
'approve',
ERC20ApproveArgs
>({
chainId: amount?.currency.chainId,
abi: old_erc20Abi_approve,
address: amount?.currency?.wrapped?.address as Address,
functionName: 'approve',
args: [
spender as Address,
approveMax ? maxUint256 : amount ? amount.quotient : 0n,
],
scopeKey: 'approve-fallback',
query: {
enabled: Boolean(
amount &&
spender &&
address &&
allowance &&
enabled &&
!isAllowanceLoading,
),
enabled: simulationEnabled && fallback,
},
})

const { data: simulation } = fallback
? fallbackSimulation
: standardSimulation

const onSuccess = useCallback(
async (data: SendTransactionReturnType) => {
if (!amount) return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,43 @@ import { createErrorToast, createToast } from '@sushiswap/notifications'
import { useCallback, useMemo, useState } from 'react'
import { erc20Abi_approve } from 'sushi/abi'
import type { Token } from 'sushi/currency'
import { type Address, UserRejectedRequestError } from 'viem'
import {
type Address,
ContractFunctionZeroDataError,
UserRejectedRequestError,
} from 'viem'
import { usePublicClient, useSimulateContract, useWriteContract } from 'wagmi'
import type { SendTransactionReturnType } from 'wagmi/actions'
import type { ERC20ApproveABI, ERC20ApproveArgs } from './types'
import {
type ERC20ApproveABI,
type ERC20ApproveArgs,
type OLD_ERC20ApproveABI,
old_erc20Abi_approve,
} from './types'

interface UseTokenRevokeApproval {
account: Address | undefined
spender: Address
spender: Address | undefined
token: Omit<Token, 'wrapped'> | undefined
enabled?: boolean
}

export const useTokenRevokeApproval = ({
account,
spender,
token,
enabled = true,
}: UseTokenRevokeApproval) => {
const [isPending, setPending] = useState(false)
const client = usePublicClient()
const { data: simulation } = useSimulateContract<

const [fallback, setFallback] = useState(false)

const simulationEnabled = Boolean(
enabled && account && spender && token?.address,
)

const standardSimulation = useSimulateContract<
ERC20ApproveABI,
'approve',
ERC20ApproveArgs
Expand All @@ -31,10 +49,43 @@ export const useTokenRevokeApproval = ({
abi: erc20Abi_approve,
chainId: token?.chainId,
functionName: 'approve',
args: [spender, 0n],
query: { enabled: Boolean(account && spender && token?.address) },
args: [spender as Address, 0n],
scopeKey: 'revoke-std',
query: {
enabled: simulationEnabled && !fallback,
retry: (failureCount, error) => {
if (
error instanceof ContractFunctionZeroDataError ||
error.cause instanceof ContractFunctionZeroDataError
) {
setFallback(true)
return false
}
return failureCount < 2
},
},
})

const fallbackSimulation = useSimulateContract<
OLD_ERC20ApproveABI,
'approve',
ERC20ApproveArgs
>({
address: token?.address as Address,
abi: old_erc20Abi_approve,
chainId: token?.chainId,
functionName: 'approve',
args: [spender as Address, 0n],
scopeKey: 'revoke-fallback',
query: {
enabled: simulationEnabled && fallback,
},
})

const { data: simulation } = fallback
? fallbackSimulation
: standardSimulation

const onSuccess = useCallback(
async (data: SendTransactionReturnType) => {
if (!token) return
Expand Down
11 changes: 10 additions & 1 deletion apps/web/src/lib/wagmi/systems/Checker/ApproveERC20.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
ApprovalState,
useTokenApproval,
} from '../../hooks/approvals/hooks/useTokenApproval'
import { RevokeApproveERC20 } from './RevokeApproveERC20'

interface ApproveERC20Props extends ButtonProps {
id: string
Expand All @@ -35,7 +36,15 @@ interface ApproveERC20Props extends ButtonProps {
enabled?: boolean
}

const ApproveERC20: FC<ApproveERC20Props> = ({
const ApproveERC20: FC<ApproveERC20Props> = (props) => {
return (
<RevokeApproveERC20 {...props} id={`revoke-${props.id}`}>
<_ApproveERC20 {...props} />
</RevokeApproveERC20>
)
}

const _ApproveERC20: FC<ApproveERC20Props> = ({
id,
amount,
contract,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
} from '../../hooks/approvals/hooks/useTokenPermit'
import { ApproveERC20 } from './ApproveERC20'
import { useApprovedActions } from './Provider'
import { RevokeApproveERC20 } from './RevokeApproveERC20'

enum ApprovalType {
Approve = 'approve',
Expand Down Expand Up @@ -63,7 +64,9 @@ const isPermitSupportedChainId = (chainId: number) =>

const ApproveERC20WithPermit: FC<ApproveERC20WithPermitProps> = (props) => {
return isPermitSupportedChainId(props.chainId) ? (
<_ApproveERC20WithPermit {...props} />
<RevokeApproveERC20 {...props} id={`revoke-${props.id}`}>
<_ApproveERC20WithPermit {...props} />
</RevokeApproveERC20>
) : (
<ApproveERC20 {...props} />
)
Expand Down
124 changes: 124 additions & 0 deletions apps/web/src/lib/wagmi/systems/Checker/RevokeApproveERC20.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { InformationCircleIcon } from '@heroicons/react/24/solid'
import {
Button,
type ButtonProps,
CardDescription,
CardHeader,
CardTitle,
HoverCard,
HoverCardContent,
HoverCardTrigger,
classNames,
} from '@sushiswap/ui'
import type { FC } from 'react'
import { EvmChainId } from 'sushi/chain'
import { type Amount, LDO, type Token, type Type, USDT } from 'sushi/currency'
import type { Address } from 'sushi/types'
import { useAccount } from 'wagmi'
import { useTokenAllowance } from '../../hooks/approvals/hooks/useTokenAllowance'
import { useTokenRevokeApproval } from '../../hooks/approvals/hooks/useTokenRevokeApproval'

interface RevokeApproveERC20Props extends ButtonProps {
id: string
amount: Amount<Type> | undefined
contract: Address | undefined
enabled?: boolean
}

// Tokens that require resetting allowance to zero before setting a new amount
const RESET_APPROVAL_TOKENS = {
[EvmChainId.ETHEREUM]: [USDT[EvmChainId.ETHEREUM], LDO[EvmChainId.ETHEREUM]],
}

const isResetApprovalToken = (token: Token) => {
const tokensForChain =
RESET_APPROVAL_TOKENS[token.chainId as keyof typeof RESET_APPROVAL_TOKENS]
if (!tokensForChain) return false

return tokensForChain.some((_token) => _token.equals(token))
}

const RevokeApproveERC20: FC<RevokeApproveERC20Props> = ({
id,
amount,
contract,
children,
className,
fullWidth = true,
size = 'xl',
enabled = true,
...props
}) => {
const allowanceEnabled =
enabled &&
amount?.currency?.chainId &&
isResetApprovalToken(amount.currency.wrapped)

const { address } = useAccount()

const { data: allowance, isLoading: isAllowanceLoading } = useTokenAllowance({
token: amount?.currency?.wrapped,
owner: address,
spender: contract,
chainId: amount?.currency.chainId,
enabled: Boolean(amount?.currency?.isToken && allowanceEnabled),
})

const revokeEnabled =
allowance &&
amount &&
allowance.quotient < amount.quotient &&
allowance.quotient !== 0n

const {
write,
isSuccess: isRevokeSuccess,
isPending: isRevokePending,
} = useTokenRevokeApproval({
account: address,
spender: contract,
token: amount?.currency?.wrapped,
enabled: revokeEnabled,
})

if (
!allowanceEnabled ||
(!isAllowanceLoading && !revokeEnabled) ||
isRevokeSuccess
)
return <>{children}</>

const loading = isAllowanceLoading || isRevokePending

return (
<HoverCard openDelay={0} closeDelay={0}>
<Button
disabled={!write}
className={classNames(className, 'group relative')}
loading={loading}
onClick={() => write?.()}
fullWidth={fullWidth}
size={size}
testId={id}
{...props}
>
Revoke {amount?.currency.symbol} approval
<HoverCardTrigger>
<InformationCircleIcon width={16} height={16} />
</HoverCardTrigger>
</Button>
<HoverCardContent className="!p-0 max-w-[320px]">
<CardHeader>
<CardTitle>Revoke ERC20 approval</CardTitle>
<CardDescription>
Revoke your allowance (set it to 0) before updating your approval
limit—some tokens (like USDT) require this.
</CardDescription>
</CardHeader>
</HoverCardContent>
</HoverCard>
)
}

export { RevokeApproveERC20 }
export type { RevokeApproveERC20Props }
Loading
Loading