From 6e45ac29e73ff55ec8c42bca983b179ee3cc5814 Mon Sep 17 00:00:00 2001 From: Aaron Cook Date: Wed, 18 Oct 2023 18:55:13 +0200 Subject: [PATCH] feat: block/warn about bridges accordingly (#2652) * feat: block/warn about bridges accordingly * fix: abstract constants + rename variables --- .../ProposalForm/ChainWarning.tsx | 64 ---------- .../ProposalForm/CompatibilityWarning.tsx | 43 +++++++ .../__tests__/useCompatibilityWarning.test.ts | 118 ++++++++++++++++++ .../walletconnect/ProposalForm/bridges.ts | 11 ++ .../walletconnect/ProposalForm/index.tsx | 17 +-- .../ProposalForm/useCompatibilityWarning.ts | 64 ++++++++++ src/components/walletconnect/constants.ts | 37 ++++++ 7 files changed, 282 insertions(+), 72 deletions(-) delete mode 100644 src/components/walletconnect/ProposalForm/ChainWarning.tsx create mode 100644 src/components/walletconnect/ProposalForm/CompatibilityWarning.tsx create mode 100644 src/components/walletconnect/ProposalForm/__tests__/useCompatibilityWarning.test.ts create mode 100644 src/components/walletconnect/ProposalForm/bridges.ts create mode 100644 src/components/walletconnect/ProposalForm/useCompatibilityWarning.ts create mode 100644 src/components/walletconnect/constants.ts diff --git a/src/components/walletconnect/ProposalForm/ChainWarning.tsx b/src/components/walletconnect/ProposalForm/ChainWarning.tsx deleted file mode 100644 index adb19de72b..0000000000 --- a/src/components/walletconnect/ProposalForm/ChainWarning.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Alert, Typography } from '@mui/material' -import type { AlertColor } from '@mui/material' -import type { ReactElement } from 'react' -import type { Web3WalletTypes } from '@walletconnect/web3wallet' - -import ChainIndicator from '@/components/common/ChainIndicator' -import useChains from '@/hooks/useChains' -import useSafeInfo from '@/hooks/useSafeInfo' - -import css from './styles.module.css' - -const ChainInformation: Record = { - UNSUPPORTED: { - severity: 'error', - message: - 'This dApp does not support the Safe Account network. If you want to interact with this dApp, please switch to a Safe Account on a supported network.', - }, - WRONG: { - severity: 'info', - message: 'Please make sure that the dApp is connected to %%chain%%.', - }, -} - -export const ChainWarning = ({ - proposal, - chainIds, -}: { - proposal: Web3WalletTypes.SessionProposal - chainIds: Array -}): ReactElement => { - const { configs } = useChains() - const { safe } = useSafeInfo() - - const supportsCurrentChain = chainIds.includes(safe.chainId) - - if (supportsCurrentChain) { - const chainName = configs.find((chain) => chain.chainId === safe.chainId)?.chainName ?? '' - ChainInformation.WRONG.message = ChainInformation.WRONG.message.replace('%%chain%%', chainName) - } - - const { severity, message } = supportsCurrentChain ? ChainInformation.WRONG : ChainInformation.UNSUPPORTED - - return ( - <> - - {message} - - - {!supportsCurrentChain && ( - <> - - Supported networks - - -
- {chainIds.map((chainId) => ( - - ))} -
- - )} - - ) -} diff --git a/src/components/walletconnect/ProposalForm/CompatibilityWarning.tsx b/src/components/walletconnect/ProposalForm/CompatibilityWarning.tsx new file mode 100644 index 0000000000..17eb254755 --- /dev/null +++ b/src/components/walletconnect/ProposalForm/CompatibilityWarning.tsx @@ -0,0 +1,43 @@ +import { Alert, Typography } from '@mui/material' +import type { ReactElement } from 'react' +import type { Web3WalletTypes } from '@walletconnect/web3wallet' + +import ChainIndicator from '@/components/common/ChainIndicator' +import { useCompatibilityWarning } from './useCompatibilityWarning' +import useSafeInfo from '@/hooks/useSafeInfo' + +import css from './styles.module.css' + +export const CompatibilityWarning = ({ + proposal, + chainIds, +}: { + proposal: Web3WalletTypes.SessionProposal + chainIds: Array +}): ReactElement => { + const { safe } = useSafeInfo() + const isUnsupportedChain = !chainIds.includes(safe.chainId) + const { severity, message } = useCompatibilityWarning(proposal, isUnsupportedChain) + + return ( + <> + + {message} + + + {isUnsupportedChain && ( + <> + + Supported networks + + +
+ {chainIds.map((chainId) => ( + + ))} +
+ + )} + + ) +} diff --git a/src/components/walletconnect/ProposalForm/__tests__/useCompatibilityWarning.test.ts b/src/components/walletconnect/ProposalForm/__tests__/useCompatibilityWarning.test.ts new file mode 100644 index 0000000000..d32cc8510b --- /dev/null +++ b/src/components/walletconnect/ProposalForm/__tests__/useCompatibilityWarning.test.ts @@ -0,0 +1,118 @@ +import { renderHook } from '@/tests/test-utils' +import type { ChainInfo, SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' +import type { Web3WalletTypes } from '@walletconnect/web3wallet' + +import * as bridges from '../bridges' +import { useCompatibilityWarning } from '../useCompatibilityWarning' + +describe('useCompatibilityWarning', () => { + it('should return an error for a dangerous bridge', () => { + jest.spyOn(bridges, 'isStrictAddressBridge').mockReturnValue(true) + + const proposal = { + params: { proposer: { metadata: { name: 'Fake Bridge' } } }, + verifyContext: { verified: { origin: '' } }, + } as unknown as Web3WalletTypes.SessionProposal + + const { result } = renderHook(() => useCompatibilityWarning(proposal, false)) + + expect(result.current).toEqual({ + message: + 'Fake Bridge is a bridge that is unusable in Safe{Wallet} due to the current implementation of WalletConnect — the bridged funds will be lost. Consider using a different bridge.', + severity: 'error', + }) + }) + + it('should return a warning for a risky bridge', () => { + jest.spyOn(bridges, 'isStrictAddressBridge').mockReturnValue(false) + jest.spyOn(bridges, 'isDefaultAddressBridge').mockReturnValue(true) + + const proposal = { + params: { proposer: { metadata: { name: 'Fake Bridge' } } }, + verifyContext: { verified: { origin: '' } }, + } as unknown as Web3WalletTypes.SessionProposal + + const { result } = renderHook(() => useCompatibilityWarning(proposal, false)) + + expect(result.current).toEqual({ + message: + 'While using Fake Bridge, please make sure that the desination address you send funds to matches the Safe address you have on the respective chain. Otherwise, the funds will be lost.', + severity: 'warning', + }) + }) + + it('should return an error for an unsupported chain', () => { + jest.spyOn(bridges, 'isStrictAddressBridge').mockReturnValue(false) + jest.spyOn(bridges, 'isDefaultAddressBridge').mockReturnValue(false) + + const proposal = { + params: { proposer: { metadata: { name: 'Fake dApp' } } }, + verifyContext: { verified: { origin: '' } }, + } as unknown as Web3WalletTypes.SessionProposal + + const { result } = renderHook(() => useCompatibilityWarning(proposal, true)) + + expect(result.current).toEqual({ + message: + 'Fake dApp does not support the Safe Account network. If you want to interact with Fake dApp, please switch to a Safe Account on a supported network.', + severity: 'error', + }) + }) + + describe('should otherwise return info', () => { + it('if chains are loaded', () => { + jest.spyOn(bridges, 'isStrictAddressBridge').mockReturnValue(false) + jest.spyOn(bridges, 'isDefaultAddressBridge').mockReturnValue(false) + + const proposal = { + params: { proposer: { metadata: { name: 'Fake dApp' } } }, + verifyContext: { verified: { origin: '' } }, + } as unknown as Web3WalletTypes.SessionProposal + + const { result } = renderHook(() => useCompatibilityWarning(proposal, false), { + initialReduxState: { + chains: { + loading: false, + error: undefined, + data: [ + { + chainId: '1', + chainName: 'Ethereum', + }, + ] as unknown as Array, + }, + safeInfo: { + loading: false, + error: undefined, + data: { + address: {}, + chainId: '1', + } as unknown as SafeInfo, + }, + }, + }) + + expect(result.current).toEqual({ + message: 'Please make sure that the dApp is connected to Ethereum.', + severity: 'info', + }) + }) + + it("if chains aren't loaded", () => { + jest.spyOn(bridges, 'isStrictAddressBridge').mockReturnValue(false) + jest.spyOn(bridges, 'isDefaultAddressBridge').mockReturnValue(false) + + const proposal = { + params: { proposer: { metadata: { name: 'Fake dApp' } } }, + verifyContext: { verified: { origin: '' } }, + } as unknown as Web3WalletTypes.SessionProposal + + const { result } = renderHook(() => useCompatibilityWarning(proposal, false)) + + expect(result.current).toEqual({ + message: 'Please make sure that the dApp is connected to this network.', + severity: 'info', + }) + }) + }) +}) diff --git a/src/components/walletconnect/ProposalForm/bridges.ts b/src/components/walletconnect/ProposalForm/bridges.ts new file mode 100644 index 0000000000..b595f105c1 --- /dev/null +++ b/src/components/walletconnect/ProposalForm/bridges.ts @@ -0,0 +1,11 @@ +import { StrictAddressBridges, DefaultAddressBridges } from '../constants' + +// Bridge enforces the same address on destination chain +export const isStrictAddressBridge = (origin: string) => { + return StrictAddressBridges.some((bridge) => origin.includes(bridge)) +} + +// Bridge defaults to same address on destination chain but allows changing it +export const isDefaultAddressBridge = (origin: string) => { + return DefaultAddressBridges.some((bridge) => origin.includes(bridge)) +} diff --git a/src/components/walletconnect/ProposalForm/index.tsx b/src/components/walletconnect/ProposalForm/index.tsx index ac15a1cac1..709d7816d6 100644 --- a/src/components/walletconnect/ProposalForm/index.tsx +++ b/src/components/walletconnect/ProposalForm/index.tsx @@ -6,10 +6,11 @@ import type { Web3WalletTypes } from '@walletconnect/web3wallet' import SafeAppIconCard from '@/components/safe-apps/SafeAppIconCard' import css from './styles.module.css' import ProposalVerification from './ProposalVerification' -import { ChainWarning } from './ChainWarning' +import { CompatibilityWarning } from './CompatibilityWarning' import useChains from '@/hooks/useChains' import useSafeInfo from '@/hooks/useSafeInfo' import { getSupportedChainIds } from '@/services/walletconnect/utils' +import { isStrictAddressBridge, isDefaultAddressBridge } from './bridges' type ProposalFormProps = { proposal: Web3WalletTypes.SessionProposal @@ -22,13 +23,13 @@ const ProposalForm = ({ proposal, onApprove, onReject }: ProposalFormProps): Rea const { safe } = useSafeInfo() const [understandsRisk, setUnderstandsRisk] = useState(false) const { proposer } = proposal.params - const { isScam } = proposal.verifyContext.verified + const { isScam, origin } = proposal.verifyContext.verified const chainIds = useMemo(() => getSupportedChainIds(configs, proposal.params), [configs, proposal.params]) - const supportsCurrentChain = chainIds.includes(safe.chainId) + const isUnsupportedChain = !chainIds.includes(safe.chainId) - const isHighRisk = proposal.verifyContext.verified.validation === 'INVALID' - const disabled = !supportsCurrentChain || isScam || (isHighRisk && !understandsRisk) + const isHighRisk = proposal.verifyContext.verified.validation === 'INVALID' || isDefaultAddressBridge(origin) + const disabled = isUnsupportedChain || isScam || isStrictAddressBridge(origin) || (isHighRisk && !understandsRisk) return (
@@ -54,14 +55,14 @@ const ProposalForm = ({ proposal, onApprove, onReject }: ProposalFormProps): Rea
- +
- {supportsCurrentChain && isHighRisk && ( + {!isUnsupportedChain && isHighRisk && ( setUnderstandsRisk(checked)} />} - label="I understand the risks of interacting with this dApp and would like to continue." + label="I understand the risks associated with interacting with this dApp and would like to continue." /> )} diff --git a/src/components/walletconnect/ProposalForm/useCompatibilityWarning.ts b/src/components/walletconnect/ProposalForm/useCompatibilityWarning.ts new file mode 100644 index 0000000000..3b1e68bb35 --- /dev/null +++ b/src/components/walletconnect/ProposalForm/useCompatibilityWarning.ts @@ -0,0 +1,64 @@ +import { useMemo } from 'react' +import type { AlertColor } from '@mui/material' +import type { Web3WalletTypes } from '@walletconnect/web3wallet' + +import useChains from '@/hooks/useChains' +import useSafeInfo from '@/hooks/useSafeInfo' +import { isStrictAddressBridge, isDefaultAddressBridge } from './bridges' + +const NAME_PLACEHOLDER = '%%name%%' +const CHAIN_PLACEHOLDER = '%%chain%%' + +const Warnings: Record = { + BLOCKED_BRIDGE: { + severity: 'error', + message: `${NAME_PLACEHOLDER} is a bridge that is unusable in Safe{Wallet} due to the current implementation of WalletConnect — the bridged funds will be lost. Consider using a different bridge.`, + }, + WARNED_BRIDGE: { + severity: 'warning', + message: `While using ${NAME_PLACEHOLDER}, please make sure that the desination address you send funds to matches the Safe address you have on the respective chain. Otherwise, the funds will be lost.`, + }, + UNSUPPORTED_CHAIN: { + severity: 'error', + message: `${NAME_PLACEHOLDER} does not support the Safe Account network. If you want to interact with ${NAME_PLACEHOLDER}, please switch to a Safe Account on a supported network.`, + }, + WRONG_CHAIN: { + severity: 'info', + message: `Please make sure that the dApp is connected to ${CHAIN_PLACEHOLDER}.`, + }, +} + +export const useCompatibilityWarning = ( + proposal: Web3WalletTypes.SessionProposal, + isUnsupportedChain: boolean, +): (typeof Warnings)[string] => { + const { configs } = useChains() + const { safe } = useSafeInfo() + + return useMemo(() => { + const { origin } = proposal.verifyContext.verified + const { proposer } = proposal.params + + let { message, severity } = isStrictAddressBridge(origin) + ? Warnings.DANGEROUS_BRIDGE + : isDefaultAddressBridge(origin) + ? Warnings.RISKY_BRIDGE + : isUnsupportedChain + ? Warnings.UNSUPPORTED_CHAIN + : Warnings.WRONG_CHAIN + + if (message.includes(NAME_PLACEHOLDER)) { + message = message.replaceAll(NAME_PLACEHOLDER, proposer.metadata.name) + } + + if (message.includes(CHAIN_PLACEHOLDER)) { + const chainName = configs.find((chain) => chain.chainId === safe.chainId)?.chainName ?? 'this network' + message = message.replaceAll(CHAIN_PLACEHOLDER, chainName) + } + + return { + message, + severity, + } + }, [configs, isUnsupportedChain, proposal.params, proposal.verifyContext.verified, safe.chainId]) +} diff --git a/src/components/walletconnect/constants.ts b/src/components/walletconnect/constants.ts new file mode 100644 index 0000000000..1b406ab4f6 --- /dev/null +++ b/src/components/walletconnect/constants.ts @@ -0,0 +1,37 @@ +// Bridges enforcing same address on destination chains +export const StrictAddressBridges = [ + 'bridge.arbitrum.io', + 'bridge.base.org', + 'cbridge.celer.network', + 'www.orbiter.finance', + 'zksync-era.l2scan.co', + 'app.optimism.io', + 'www.portalbridge.com', + 'wallet.polygon.technology', + 'app.rhino.fi', +] + +// Bridges that initially select the same address on the destination chain but allow changing it +export const DefaultAddressBridges = [ + 'across.to', + 'app.allbridge.io', + 'core.allbridge.io', + 'bungee.exchange', + 'www.carrier.so', + 'app.chainport.io', + 'bridge.gnosischain.com', + 'app.hop.exchange', + 'app.interport.fi', + 'jumper.exchange', + 'www.layerswap.io', + 'meson.fi', + 'satellite.money', + 'stargate.finance', + 'app.squidrouter.com', + 'app.symbiosis.finance', + 'www.synapseprotocol.com', + 'app.thevoyager.io', + 'portal.txsync.io', + 'bridge.wanchain.org', + 'app.xy.finance', +]