Skip to content

Commit

Permalink
fix: detect smart contracts for every evm chains (#7358)
Browse files Browse the repository at this point in the history
* fix: detect smart contracts for every evm chains

* fix: handle non evm trades properly

* fix: handle smartcontractaddress return type properly

* chore: trigger ci

* fix: typo

* feat: fix button not disabled

* fix: disable if thorchain swap

* fix: big brain kev review

* fix: remove all disabled state

* fix: move isLoading to isLoading

---------

Co-authored-by: woody <[email protected]>
  • Loading branch information
NeOMakinG and woodenfurniture authored Jul 16, 2024
1 parent 0397564 commit 9f00c73
Show file tree
Hide file tree
Showing 14 changed files with 67 additions and 50 deletions.
2 changes: 1 addition & 1 deletion src/assets/translations/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -908,7 +908,7 @@
"numHops": "This trade includes %{numHops} hops",
"receiveAddress": "Receive Address",
"receiveAddressDescription": "No %{chainName} address found from connected wallet. Manually enter an address or",
"smartContractReceiveAddressDescription": "We detected you are swapping from a smart contract whose address may not exist on the destination chain. Please verify and manually enter desired recipient address on %{chainName}.",
"smartContractReceiveAddressDescription": "We detected you are swapping from a smart contract whose address may not exist on the destination chain. Please verify and manually enter desired recipient address on %{chainName}",
"toContinue": "to continue.",
"addressPlaceholder": "%{chainName} address",
"priceImpactWarning": "Due to the size of this trade relative to available liquidity, the expected price impact of this trade is %{priceImpactPercentage}%. Are you sure you want to trade?",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,22 +107,22 @@ export const ConfirmSummary = ({
}, [initialSellAssetAccountId])

const { data: _isSmartContractSellAddress, isLoading: isSellAddressByteCodeLoading } =
useIsSmartContractAddress(userAddress)
useIsSmartContractAddress(userAddress, sellAsset.chainId)

const { data: _isSmartContractReceiveAddress, isLoading: isReceiveAddressByteCodeLoading } =
useIsSmartContractAddress(receiveAddress ?? '')
useIsSmartContractAddress(receiveAddress ?? '', buyAsset.chainId)

const disableSmartContractSwap = useMemo(() => {
// Swappers other than THORChain shouldn't be affected by this limitation
if (activeSwapperName !== SwapperName.Thorchain) return false

// This is either a smart contract address, or the bytecode is still loading - disable confirm
if (_isSmartContractSellAddress !== false) return true
if (_isSmartContractSellAddress) return true
if (
[THORCHAIN_LONGTAIL_SWAP_SOURCE, THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE].includes(
tradeQuoteStep?.source!,
) &&
_isSmartContractReceiveAddress !== false
_isSmartContractReceiveAddress
)
return true

Expand Down Expand Up @@ -157,13 +157,12 @@ export const ConfirmSummary = ({
// don't execute trades for smart contract addresses where they aren't supported
disableSmartContractSwap ||
// don't allow non-existent quotes to be executed
!activeSwapperName ||
!activeQuote ||
!hasUserEnteredAmount ||
// don't allow users to execute trades while the quote is being updated
isTradeQuoteApiQueryPending[activeSwapperName] ||
// don't allow users to proceed until a swapper has been selected
!activeSwapperName
!activeSwapperName ||
// don't allow users to execute trades while the quote is being updated
isTradeQuoteApiQueryPending[activeSwapperName]
)
}, [
quoteHasError,
Expand Down Expand Up @@ -231,8 +230,12 @@ export const ConfirmSummary = ({
}, [activeQuote, buyAssetFeeAsset])

const shouldForceManualAddressEntry = useMemo(() => {
return Boolean(_isSmartContractSellAddress) && sellAsset?.chainId !== buyAsset.chainId
}, [_isSmartContractSellAddress, sellAsset, buyAsset])
return (
!disableSmartContractSwap &&
Boolean(_isSmartContractSellAddress) &&
sellAsset?.chainId !== buyAsset.chainId
)
}, [_isSmartContractSellAddress, sellAsset, buyAsset, disableSmartContractSwap])

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,11 +334,11 @@ export const Confirm: React.FC<ConfirmProps> = ({ accountId, onNext }) => {
)

const { data: _isSmartContractAddress, isLoading: isAddressByteCodeLoading } =
useIsSmartContractAddress(fromAddress ?? '')
useIsSmartContractAddress(fromAddress ?? '', chainId)

const disableSmartContractDeposit = useMemo(() => {
// This is either a smart contract address, or the bytecode is still loading - disable confirm
if (_isSmartContractAddress !== false) return true
if (_isSmartContractAddress) return true

// All checks passed - this is an EOA address
return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -451,11 +451,11 @@ export const Confirm: React.FC<ConfirmProps> = ({ accountId, onNext }) => {
)

const { data: _isSmartContractAddress, isLoading: isAddressByteCodeLoading } =
useIsSmartContractAddress(userAddress)
useIsSmartContractAddress(userAddress, chainId)

const disableSmartContractWithdraw = useMemo(() => {
// This is either a smart contract address, or the bytecode is still loading - disable confirm
if (_isSmartContractAddress !== false) return true
if (_isSmartContractAddress) return true

// All checks passed - this is an EOA address
return false
Expand Down
33 changes: 17 additions & 16 deletions src/hooks/useIsSmartContractAddress/useIsSmartContractAddress.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import { useQuery } from '@tanstack/react-query'
import type { ChainId } from '@shapeshiftoss/caip'
import { isEvmChainId } from '@shapeshiftoss/chain-adapters'
import { skipToken, useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import { isSmartContractAddress } from 'lib/address/utils'

export const useIsSmartContractAddress = (address: string) => {
export const useIsSmartContractAddress = (address: string, chainId: ChainId) => {
// Lowercase the address to ensure proper caching
const userAddress = useMemo(() => address.toLowerCase(), [address])

const queryParams = useMemo(() => {
return {
queryKey: [
'isSmartContractAddress',
{
userAddress,
},
],
queryFn: () => isSmartContractAddress(userAddress),
enabled: Boolean(userAddress.length),
}
}, [userAddress])

const query = useQuery(queryParams)
const query = useQuery({
queryKey: [
'isSmartContractAddress',
{
userAddress,
chainId,
},
],
queryFn:
isEvmChainId(chainId) && Boolean(userAddress.length)
? () => isSmartContractAddress(userAddress, chainId)
: skipToken,
})

return query
}
10 changes: 8 additions & 2 deletions src/lib/address/utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import type { ChainId } from '@shapeshiftoss/caip'
import { isEvmChainId } from '@shapeshiftoss/chain-adapters'
import { isAddress } from 'viem'
import { getEthersProvider } from 'lib/ethersProviderSingleton'

export const isEthAddress = (address: string): boolean => /^0x[0-9A-Fa-f]{40}$/.test(address)

export const isSmartContractAddress = async (address: string): Promise<boolean> => {
export const isSmartContractAddress = async (
address: string,
chainId: ChainId,
): Promise<boolean> => {
if (!isAddress(address)) return false
const bytecode = await getEthersProvider().getCode(address)
if (!isEvmChainId(chainId)) return false
const bytecode = await getEthersProvider(chainId).getCode(address)
return bytecode !== '0x'
}
4 changes: 1 addition & 3 deletions src/lib/ethersProviderSingleton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ export const rpcUrlByChainId = (chainId: EvmChainId): string => {
const ethersProviders: Map<ChainId, JsonRpcProvider> = new Map()
const ethersV5Providers: Map<ChainId, ethersV5.providers.StaticJsonRpcProvider> = new Map()

export const getEthersProvider = (
chainId: EvmChainId = KnownChainIds.EthereumMainnet,
): JsonRpcProvider => {
export const getEthersProvider = (chainId: EvmChainId): JsonRpcProvider => {
if (!ethersProviders.has(chainId)) {
const provider = new JsonRpcProvider(rpcUrlByChainId(chainId), undefined, {
staticNetwork: true,
Expand Down
3 changes: 2 additions & 1 deletion src/lib/fees/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { KnownChainIds } from '@shapeshiftoss/types'
import type { Block } from 'ethers'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { getEthersProvider } from 'lib/ethersProviderSingleton'
Expand All @@ -6,7 +7,7 @@ import { findClosestFoxDiscountDelayBlockNumber } from './utils'

vi.unmock('ethers')

const getBlockSpy = vi.spyOn(getEthersProvider(), 'getBlock')
const getBlockSpy = vi.spyOn(getEthersProvider(KnownChainIds.EthereumMainnet), 'getBlock')

describe('findClosestFoxDiscountDelayBlockNumber', () => {
beforeEach(() => {
Expand Down
7 changes: 4 additions & 3 deletions src/lib/fees/utils.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { KnownChainIds } from '@shapeshiftoss/types'
import { getEthersProvider } from 'lib/ethersProviderSingleton'

export const AVERAGE_BLOCK_TIME_BLOCKS = 1000

export const findClosestFoxDiscountDelayBlockNumber = async (
delayHours: number,
): Promise<number> => {
const latestBlock = await getEthersProvider().getBlock('latest')
const latestBlock = await getEthersProvider(KnownChainIds.EthereumMainnet).getBlock('latest')
if (!latestBlock) throw new Error('Could not get latest block')

// No-op - if delay is zero, we don't need to perform any logic to find the closest FOX discounts delay block number
// Since the block we're interested in is the current one
if (delayHours === 0) return latestBlock.number

const historicalBlock = await getEthersProvider().getBlock(
const historicalBlock = await getEthersProvider(KnownChainIds.EthereumMainnet).getBlock(
latestBlock.number - AVERAGE_BLOCK_TIME_BLOCKS,
)
if (!historicalBlock)
Expand All @@ -27,7 +28,7 @@ export const findClosestFoxDiscountDelayBlockNumber = async (

let blockNumber = latestBlock.number - targetBlocksToMove
while (true) {
const block = await getEthersProvider().getBlock(blockNumber)
const block = await getEthersProvider(KnownChainIds.EthereumMainnet).getBlock(blockNumber)
if (!block) throw new Error(`Could not get block ${blockNumber}`)

const timeDifference = targetTimestamp - block.timestamp
Expand Down
8 changes: 4 additions & 4 deletions src/pages/Lending/Pool/components/Borrow/BorrowInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -358,11 +358,11 @@ export const BorrowInput = ({
}, [collateralAccountId])

const { data: _isSmartContractAddress, isLoading: isAddressByteCodeLoading } =
useIsSmartContractAddress(userAddress)
useIsSmartContractAddress(userAddress, borrowAsset?.chainId ?? '')

const disableSmartContractDeposit = useMemo(() => {
// This is either a smart contract address, or the bytecode is still loading - disable confirm
if (_isSmartContractAddress !== false) return true
if (_isSmartContractAddress) return true

// All checks passed - this is an EOA address
return false
Expand Down Expand Up @@ -658,8 +658,8 @@ export const BorrowInput = ({
isEstimatedFeesDataLoading ||
isEstimatedSweepFeesDataLoading ||
isEstimatedSweepFeesDataLoading ||
isSweepNeededLoading ||
isAddressByteCodeLoading
isAddressByteCodeLoading ||
isSweepNeededLoading
}
isDisabled={Boolean(
isHardCapReached ||
Expand Down
4 changes: 2 additions & 2 deletions src/pages/Lending/Pool/components/Repay/RepayInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -461,11 +461,11 @@ export const RepayInput = ({
])

const { data: _isSmartContractAddress, isLoading: isAddressByteCodeLoading } =
useIsSmartContractAddress(userAddress)
useIsSmartContractAddress(userAddress, repaymentAsset?.chainId ?? '')

const disableSmartContractRepayment = useMemo(() => {
// This is either a smart contract address, or the bytecode is still loading - disable confirm
if (_isSmartContractAddress !== false) return true
if (_isSmartContractAddress) return true

// All checks passed - this is an EOA address
return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ export const AddLiquidityInput: React.FC<AddLiquidityInputProps> = ({
})

const { data: isSmartContractAccountAddress, isLoading: isSmartContractAccountAddressLoading } =
useIsSmartContractAddress(poolAssetAccountAddress ?? '')
useIsSmartContractAddress(poolAssetAccountAddress ?? '', poolAsset?.chainId ?? '')

const accountIdsByAssetId = useAppSelector(selectPortfolioAccountIdsByAssetId)

Expand Down Expand Up @@ -1572,6 +1572,7 @@ export const AddLiquidityInput: React.FC<AddLiquidityInputProps> = ({
isSweepNeededError ||
isEstimatedPoolAssetFeesDataError ||
isEstimatedRuneFeesDataError ||
isSmartContractAccountAddress ||
bnOrZero(actualAssetDepositAmountCryptoPrecision)
.plus(bnOrZero(actualRuneDepositAmountCryptoPrecision))
.isZero() ||
Expand Down
10 changes: 8 additions & 2 deletions src/state/apis/swapper/helpers/validateTradeQuote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,14 @@ export const validateTradeQuote = async (
if (swapperName !== SwapperName.Thorchain) return false

// This is either a smart contract address, or the bytecode is still loading - disable confirm
const _isSmartContractSellAddress = await isSmartContractAddress(sendAddress)
const _isSmartContractReceiveAddress = await isSmartContractAddress(quote.receiveAddress)
const _isSmartContractSellAddress = await isSmartContractAddress(
sendAddress,
firstHop.sellAsset.chainId,
)
const _isSmartContractReceiveAddress = await isSmartContractAddress(
quote.receiveAddress,
firstHop.buyAsset.chainId,
)
// For long-tails, the *destination* address cannot be a smart contract
// https://dev.thorchain.org/aggregators/aggregator-overview.html#admonition-warning
// This doesn't apply to regular THOR swaps however, which docs have no mention of *destination* having to be an EOA
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { calculateAPRFromToken0 } from './utils'

let _blockNumber: number | null = null
const getBlockNumber = async () => {
const ethersProvider = getEthersProvider()
const ethersProvider = getEthersProvider(KnownChainIds.EthereumMainnet)
if (_blockNumber) return _blockNumber
const blockNumber = await ethersProvider.getBlockNumber()
_blockNumber = blockNumber
Expand Down

0 comments on commit 9f00c73

Please sign in to comment.