diff --git a/src/components/tx-flow/flows/UpsertRecovery/GuardianSmartContractWarning.tsx b/src/components/tx-flow/flows/UpsertRecovery/GuardianSmartContractWarning.tsx new file mode 100644 index 0000000000..19ce57dfa9 --- /dev/null +++ b/src/components/tx-flow/flows/UpsertRecovery/GuardianSmartContractWarning.tsx @@ -0,0 +1,59 @@ +import { Alert } from '@mui/material' +import { getSafeInfo } from '@safe-global/safe-gateway-typescript-sdk' +import { useState, useEffect } from 'react' +import { useWatch } from 'react-hook-form' +import { isAddress } from 'ethers/lib/utils' +import type { ReactElement } from 'react' + +import { isSmartContractWallet } from '@/utils/wallets' +import useDebounce from '@/hooks/useDebounce' +import useSafeInfo from '@/hooks/useSafeInfo' +import { UpsertRecoveryFlowFields } from '.' +import { sameAddress } from '@/utils/addresses' + +export function GuardianWarning(): ReactElement | null { + const { safe, safeAddress } = useSafeInfo() + const [warning, setWarning] = useState() + + const guardian = useWatch({ name: UpsertRecoveryFlowFields.guardian }) + const debouncedGuardian = useDebounce(guardian, 500) + + useEffect(() => { + setWarning(undefined) + + if (!isAddress(debouncedGuardian) || sameAddress(debouncedGuardian, safeAddress)) { + return + } + + ;(async () => { + let isSmartContract = false + + try { + isSmartContract = await isSmartContractWallet(safe.chainId, debouncedGuardian) + } catch { + return + } + + // EOA + if (!isSmartContract) { + return + } + + try { + await getSafeInfo(safe.chainId, debouncedGuardian) + } catch { + setWarning('The given address is a smart contract. Please ensure that it can sign transactions.') + } + })() + }, [debouncedGuardian, safe.chainId, safeAddress]) + + if (!warning) { + return null + } + + return ( + + {warning} + + ) +} diff --git a/src/components/tx-flow/flows/UpsertRecovery/UpsertRecoveryFlowSettings.tsx b/src/components/tx-flow/flows/UpsertRecovery/UpsertRecoveryFlowSettings.tsx index 6539944728..59613b9429 100644 --- a/src/components/tx-flow/flows/UpsertRecovery/UpsertRecoveryFlowSettings.tsx +++ b/src/components/tx-flow/flows/UpsertRecovery/UpsertRecoveryFlowSettings.tsx @@ -26,6 +26,7 @@ import AddressBookInput from '@/components/common/AddressBookInput' import { sameAddress } from '@/utils/addresses' import useSafeInfo from '@/hooks/useSafeInfo' import InfoIcon from '@/public/images/notifications/info.svg' +import { GuardianWarning } from './GuardianSmartContractWarning' import type { UpsertRecoveryFlowProps } from '.' import commonCss from '@/components/tx-flow/common/styles.module.css' @@ -80,6 +81,8 @@ export function UpsertRecoveryFlowSettings({ validate={validateGuardian} /> + + Your Guardian will be able to modify your Account setup. Only select an address that you trust. diff --git a/src/components/tx/SignOrExecuteForm/hooks.ts b/src/components/tx/SignOrExecuteForm/hooks.ts index acf79a1baa..0976dd60d6 100644 --- a/src/components/tx/SignOrExecuteForm/hooks.ts +++ b/src/components/tx/SignOrExecuteForm/hooks.ts @@ -78,7 +78,7 @@ export const useTxActions = (): TxActions => { assertOnboard(onboard) // Smart contracts cannot sign transactions off-chain - if (await isSmartContractWallet(wallet)) { + if (await isSmartContractWallet(wallet.chainId, wallet.address)) { throw new Error('Cannot relay an unsigned transaction from a smart contract wallet') } return await dispatchTxSigning(safeTx, version, onboard, chainId, txId) @@ -90,7 +90,7 @@ export const useTxActions = (): TxActions => { assertOnboard(onboard) // Smart contract wallets must sign via an on-chain tx - if (await isSmartContractWallet(wallet)) { + if (await isSmartContractWallet(wallet.chainId, wallet.address)) { // If the first signature is a smart contract wallet, we have to propose w/o signatures // Otherwise the backend won't pick up the tx // The signature will be added once the on-chain signature is indexed diff --git a/src/hooks/useWalletCanRelay.ts b/src/hooks/useWalletCanRelay.ts index e7261e0226..64513475f6 100644 --- a/src/hooks/useWalletCanRelay.ts +++ b/src/hooks/useWalletCanRelay.ts @@ -13,7 +13,7 @@ const useWalletCanRelay = (tx: SafeTransaction | undefined) => { return useAsync(() => { if (!tx || !wallet) return - return isSmartContractWallet(wallet) + return isSmartContractWallet(wallet.chainId, wallet.address) .then((isSCWallet) => { if (!isSCWallet) return true diff --git a/src/utils/__tests__/wallets.test.ts b/src/utils/__tests__/wallets.test.ts index 2ecf0f5224..e971bb5475 100644 --- a/src/utils/__tests__/wallets.test.ts +++ b/src/utils/__tests__/wallets.test.ts @@ -3,7 +3,6 @@ import type { Web3Provider } from '@ethersproject/providers' import * as web3 from '@/hooks/wallets/web3' import { isSmartContractWallet } from '@/utils/wallets' -import type { ConnectedWallet } from '@/services/onboard' describe('wallets', () => { describe('isSmartContractWallet', () => { @@ -24,7 +23,7 @@ describe('wallets', () => { it('should should only call the provider once per address on a chain', async () => { for await (const _ of Array.from({ length: 10 })) { - await isSmartContractWallet({ chainId: '1', address: hexZeroPad('0x1', 20) } as ConnectedWallet) + await isSmartContractWallet('1', hexZeroPad('0x1', 20)) } expect(getCodeMock).toHaveBeenCalledTimes(1) @@ -33,15 +32,15 @@ describe('wallets', () => { it('should not memoize different addresses on the same chain', async () => { const chainId = '1' - await isSmartContractWallet({ chainId, address: hexZeroPad('0x1', 20) } as ConnectedWallet) - await isSmartContractWallet({ chainId, address: hexZeroPad('0x2', 20) } as ConnectedWallet) + await isSmartContractWallet(chainId, hexZeroPad('0x1', 20)) + await isSmartContractWallet(chainId, hexZeroPad('0x2', 20)) expect(getCodeMock).toHaveBeenCalledTimes(2) }) it('should not memoize the same address on difference chains', async () => { for await (const i of Array.from({ length: 10 }, (_, i) => i + 1)) { - await isSmartContractWallet({ chainId: i.toString(), address: hexZeroPad('0x1', 20) } as ConnectedWallet) + await isSmartContractWallet(i.toString(), hexZeroPad('0x1', 20)) } expect(getCodeMock).toHaveBeenCalledTimes(10) diff --git a/src/utils/wallets.ts b/src/utils/wallets.ts index d149609fc8..272a1bbf7f 100644 --- a/src/utils/wallets.ts +++ b/src/utils/wallets.ts @@ -28,14 +28,14 @@ export const isHardwareWallet = (wallet: ConnectedWallet): boolean => { } export const isSmartContractWallet = memoize( - async (wallet: ConnectedWallet) => { + async (_chainId: string, address: string) => { const provider = getWeb3ReadOnly() if (!provider) { throw new Error('Provider not found') } - return isSmartContract(provider, wallet.address) + return isSmartContract(provider, address) }, - ({ chainId, address }) => chainId + address, + (chainId, address) => chainId + address, )