From 4dd18e469b4f8e5f1688e75646c079e7757bef91 Mon Sep 17 00:00:00 2001 From: schmanu Date: Tue, 13 Aug 2024 17:58:40 +0200 Subject: [PATCH 1/6] feat: restrict safe pass safe app access for ofac blocked addresses --- .../common/BlockedAddress/index.tsx | 4 +- src/components/safe-apps/AppFrame/index.tsx | 19 ++++- src/features/swap/index.tsx | 2 +- .../__tests__/useSanctionedAddress.test.ts | 70 +++++++++++++++++++ src/hooks/useSanctionedAddress.ts | 22 ++++++ src/store/ofac.ts | 2 +- 6 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 src/hooks/__tests__/useSanctionedAddress.test.ts create mode 100644 src/hooks/useSanctionedAddress.ts diff --git a/src/components/common/BlockedAddress/index.tsx b/src/components/common/BlockedAddress/index.tsx index 4e2dc6d8ee..c5a0dcd465 100644 --- a/src/components/common/BlockedAddress/index.tsx +++ b/src/components/common/BlockedAddress/index.tsx @@ -5,7 +5,7 @@ import { useRouter } from 'next/router' import Disclaimer from '@/components/common/Disclaimer' import { AppRoutes } from '@/config/routes' -export const BlockedAddress = ({ address }: { address?: string }): ReactElement => { +export const BlockedAddress = ({ address, featureName }: { address?: string; featureName: string }): ReactElement => { const theme = useTheme() const isMobile = useMediaQuery(theme.breakpoints.down('sm')) const displayAddress = address && isMobile ? shortenAddress(address) : address @@ -19,7 +19,7 @@ export const BlockedAddress = ({ address }: { address?: string }): ReactElement ) diff --git a/src/components/safe-apps/AppFrame/index.tsx b/src/components/safe-apps/AppFrame/index.tsx index b2e1155ea6..489aa21a8d 100644 --- a/src/components/safe-apps/AppFrame/index.tsx +++ b/src/components/safe-apps/AppFrame/index.tsx @@ -4,7 +4,7 @@ import { type AddressBookItem, Methods } from '@safe-global/safe-apps-sdk' import type { ReactElement } from 'react' import { useMemo } from 'react' import { useCallback, useEffect } from 'react' -import { CircularProgress, Typography } from '@mui/material' +import { Box, CircularProgress, Typography } from '@mui/material' import { useRouter } from 'next/router' import Head from 'next/head' import type { RequestId } from '@safe-global/safe-apps-sdk' @@ -28,6 +28,8 @@ import { PermissionStatus, type SafeAppDataWithPermissions } from '@/components/ import css from './styles.module.css' import SafeAppIframe from './SafeAppIframe' import { useCustomAppCommunicator } from '@/hooks/safe-apps/useCustomAppCommunicator' +import { useSanctionedAddress } from '@/hooks/useSanctionedAddress' +import BlockedAddress from '@/components/common/BlockedAddress' const UNKNOWN_APP_NAME = 'Unknown Safe App' @@ -43,6 +45,8 @@ const AppFrame = ({ appUrl, allowedFeaturesList, safeAppFromManifest }: AppFrame const chainId = useChainId() const chain = useCurrentChain() const router = useRouter() + const sanctionedAddress = useSanctionedAddress() + const isSafePassApp = appUrl.startsWith('https://community.safe.global') const { expanded: queueBarExpanded, dismissedByUser: queueBarDismissed, @@ -116,6 +120,19 @@ const AppFrame = ({ appUrl, allowedFeaturesList, safeAppFromManifest }: AppFrame return
} + if (Boolean(sanctionedAddress) && isSafePassApp) { + return ( + <> + + {`Safe Apps - Viewer - ${remoteApp ? remoteApp.name : UNKNOWN_APP_NAME}`} + + + + + + ) + } + return ( <> diff --git a/src/features/swap/index.tsx b/src/features/swap/index.tsx index 1960b2761d..263534bcf5 100644 --- a/src/features/swap/index.tsx +++ b/src/features/swap/index.tsx @@ -288,7 +288,7 @@ const SwapWidget = ({ sell }: Params) => { useCustomAppCommunicator(iframeRef, appData, chain) if (blockedAddress) { - return + return } if (!isConsentAccepted) { diff --git a/src/hooks/__tests__/useSanctionedAddress.test.ts b/src/hooks/__tests__/useSanctionedAddress.test.ts new file mode 100644 index 0000000000..6d536740d0 --- /dev/null +++ b/src/hooks/__tests__/useSanctionedAddress.test.ts @@ -0,0 +1,70 @@ +import { renderHook } from '@/tests/test-utils' +import { useSanctionedAddress } from '../useSanctionedAddress' +import useSafeAddress from '../useSafeAddress' +import useWallet from '../wallets/useWallet' +import { faker } from '@faker-js/faker' +import { connectedWalletBuilder } from '@/tests/builders/wallet' +import * as ofac from '@/store/ofac' + +jest.mock('@/hooks/useSafeAddress') +jest.mock('@/hooks/wallets/useWallet') + +describe('useSanctionedAddress', () => { + const mockUseSafeAddress = useSafeAddress as jest.MockedFunction + const mockUseWallet = useWallet as jest.MockedFunction + + it('should return undefined without safeAddress and wallet', () => { + const { result } = renderHook(() => useSanctionedAddress()) + expect(result.current).toBeUndefined() + }) + + it('should return undefined if neither safeAddress nor wallet are sanctioned', () => { + mockUseSafeAddress.mockReturnValue(faker.finance.ethereumAddress()) + mockUseWallet.mockReturnValue(connectedWalletBuilder().build()) + + jest.spyOn(ofac, 'useGetIsSanctionedQuery').mockReturnValue({ data: false, refetch: jest.fn() }) + + const { result } = renderHook(() => useSanctionedAddress()) + expect(result.current).toBeUndefined() + }) + + it('should return safeAddress if it is sanctioned', () => { + const mockSafeAddress = faker.finance.ethereumAddress() + const mockWalletAddress = faker.finance.ethereumAddress() + mockUseSafeAddress.mockReturnValue(mockSafeAddress) + mockUseWallet.mockReturnValue(connectedWalletBuilder().with({ address: mockWalletAddress }).build()) + + jest + .spyOn(ofac, 'useGetIsSanctionedQuery') + .mockImplementation((address) => ({ data: address === mockSafeAddress, refetch: jest.fn() })) + + const { result } = renderHook(() => useSanctionedAddress()) + expect(result.current).toEqual(mockSafeAddress) + }) + + it('should return walletAddress if it is sanctioned', () => { + const mockSafeAddress = faker.finance.ethereumAddress() + const mockWalletAddress = faker.finance.ethereumAddress() + mockUseSafeAddress.mockReturnValue(mockSafeAddress) + mockUseWallet.mockReturnValue(connectedWalletBuilder().with({ address: mockWalletAddress }).build()) + + jest + .spyOn(ofac, 'useGetIsSanctionedQuery') + .mockImplementation((address) => ({ data: address === mockWalletAddress, refetch: jest.fn() })) + + const { result } = renderHook(() => useSanctionedAddress()) + expect(result.current).toEqual(mockWalletAddress) + }) + + it('should return safeAddress if both are sanctioned', () => { + const mockSafeAddress = faker.finance.ethereumAddress() + const mockWalletAddress = faker.finance.ethereumAddress() + mockUseSafeAddress.mockReturnValue(mockSafeAddress) + mockUseWallet.mockReturnValue(connectedWalletBuilder().with({ address: mockWalletAddress }).build()) + + jest.spyOn(ofac, 'useGetIsSanctionedQuery').mockReturnValue({ data: true, refetch: jest.fn() }) + + const { result } = renderHook(() => useSanctionedAddress()) + expect(result.current).toEqual(mockSafeAddress) + }) +}) diff --git a/src/hooks/useSanctionedAddress.ts b/src/hooks/useSanctionedAddress.ts new file mode 100644 index 0000000000..a1e5fd7fc6 --- /dev/null +++ b/src/hooks/useSanctionedAddress.ts @@ -0,0 +1,22 @@ +import { useGetIsSanctionedQuery } from '@/store/ofac' +import useSafeAddress from './useSafeAddress' +import useWallet from './wallets/useWallet' +import { skipToken } from '@reduxjs/toolkit/query/react' + +export const useSanctionedAddress = () => { + const wallet = useWallet() + const safeAddress = useSafeAddress() + + const { data: isWalletSanctioned } = useGetIsSanctionedQuery(wallet ? wallet.address : skipToken) + + const { data: isSafeSanctioned } = useGetIsSanctionedQuery(safeAddress !== '' ? safeAddress : skipToken) + + if (isSafeSanctioned) { + return safeAddress + } + if (isWalletSanctioned) { + return wallet?.address + } + + return undefined +} diff --git a/src/store/ofac.ts b/src/store/ofac.ts index 125969ff5b..b48e5e1c9e 100644 --- a/src/store/ofac.ts +++ b/src/store/ofac.ts @@ -44,7 +44,7 @@ export const ofacApi = createApi({ reducerPath: 'ofacApi', baseQuery: noopBaseQuery, endpoints: (builder) => ({ - getIsSanctioned: builder.query({ + getIsSanctioned: builder.query({ async queryFn(address, { getState }) { const state = getState() const chain = selectChainById(state as RootState, chains.eth) From 05ac7979aeb7c21b6925bf05ba720b4a128c24bb Mon Sep 17 00:00:00 2001 From: schmanu Date: Tue, 13 Aug 2024 18:29:10 +0200 Subject: [PATCH 2/6] feat: block wc requests for sanctioned addresses if from Safe{Pass} --- src/components/common/BlockedAddress/index.tsx | 12 ++++++++++-- .../components/WcProposalForm/index.tsx | 18 +++++++++++++++++- .../components/WcSessionManager/index.tsx | 11 ++++++++--- src/features/walletconnect/services/utils.ts | 4 ++++ 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/components/common/BlockedAddress/index.tsx b/src/components/common/BlockedAddress/index.tsx index c5a0dcd465..9d2928d8fa 100644 --- a/src/components/common/BlockedAddress/index.tsx +++ b/src/components/common/BlockedAddress/index.tsx @@ -5,7 +5,15 @@ import { useRouter } from 'next/router' import Disclaimer from '@/components/common/Disclaimer' import { AppRoutes } from '@/config/routes' -export const BlockedAddress = ({ address, featureName }: { address?: string; featureName: string }): ReactElement => { +export const BlockedAddress = ({ + address, + featureName, + onClose, +}: { + address?: string + featureName: string + onClose?: () => void +}): ReactElement => { const theme = useTheme() const isMobile = useMediaQuery(theme.breakpoints.down('sm')) const displayAddress = address && isMobile ? shortenAddress(address) : address @@ -20,7 +28,7 @@ export const BlockedAddress = ({ address, featureName }: { address?: string; fea title="Blocked address" subtitle={displayAddress} content={`The above address is part of the OFAC SDN list and the ${featureName} is unavailable for sanctioned addresses.`} - onAccept={handleAccept} + onAccept={onClose ?? handleAccept} /> ) } diff --git a/src/features/walletconnect/components/WcProposalForm/index.tsx b/src/features/walletconnect/components/WcProposalForm/index.tsx index f1189d9dff..5dd759b363 100644 --- a/src/features/walletconnect/components/WcProposalForm/index.tsx +++ b/src/features/walletconnect/components/WcProposalForm/index.tsx @@ -4,6 +4,7 @@ import { getPeerName, getSupportedChainIds, isBlockedBridge, + isSafePassApp, isWarnedBridge, } from '@/features/walletconnect/services/utils' import { WalletConnectContext } from '@/features/walletconnect/WalletConnectContext' @@ -20,6 +21,8 @@ import { type Dispatch, type SetStateAction, useCallback, useContext, useEffect, import { CompatibilityWarning } from './CompatibilityWarning' import ProposalVerification from './ProposalVerification' import css from './styles.module.css' +import { useSanctionedAddress } from '@/hooks/useSanctionedAddress' +import BlockedAddress from '@/components/common/BlockedAddress' type ProposalFormProps = { proposal: Web3WalletTypes.SessionProposal @@ -38,13 +41,22 @@ const WcProposalForm = ({ proposal, setProposal, onApprove }: ProposalFormProps) const { isScam, origin } = proposal.verifyContext.verified const url = proposer.metadata.url || origin + const sanctionedAddress = useSanctionedAddress() + const isSafePass = isSafePassApp(origin) + const chainIds = useMemo(() => getSupportedChainIds(configs, proposal.params), [configs, proposal.params]) const isUnsupportedChain = !chainIds.includes(chainId) const name = getPeerName(proposer) || 'Unknown dApp' const isHighRisk = proposal.verifyContext.verified.validation === 'INVALID' || isWarnedBridge(origin, name) const isBlocked = isScam || isBlockedBridge(origin) - const disabled = !safeLoaded || isUnsupportedChain || isBlocked || (isHighRisk && !understandsRisk) || !!isLoading + const disabled = + !safeLoaded || + isUnsupportedChain || + isBlocked || + (isHighRisk && !understandsRisk) || + !!isLoading || + (Boolean(sanctionedAddress) && isSafePass) // On session reject const onReject = useCallback(async () => { @@ -134,6 +146,10 @@ const WcProposalForm = ({ proposal, setProposal, onApprove }: ProposalFormProps) /> )} + {isSafePass && Boolean(sanctionedAddress) && ( + + )} +
diff --git a/src/features/walletconnect/components/WcSessionManager/index.tsx b/src/features/walletconnect/components/WcSessionManager/index.tsx index 689c95b11d..076d80c866 100644 --- a/src/features/walletconnect/components/WcSessionManager/index.tsx +++ b/src/features/walletconnect/components/WcSessionManager/index.tsx @@ -11,7 +11,8 @@ import WcErrorMessage from '../WcErrorMessage' import WcProposalForm from '../WcProposalForm' import { trackEvent } from '@/services/analytics' import { WALLETCONNECT_EVENTS } from '@/services/analytics/events/walletconnect' -import { splitError } from '@/features/walletconnect/services/utils' +import { isSafePassApp, splitError } from '@/features/walletconnect/services/utils' +import { useSanctionedAddress } from '@/hooks/useSanctionedAddress' type WcSessionManagerProps = { sessions: SessionTypes.Struct[] @@ -29,6 +30,7 @@ const WcSessionManager = ({ sessions, uri }: WcSessionManagerProps) => { const { safe, safeAddress } = useSafeInfo() const { chainId } = safe const [proposal, setProposal] = useState() + const sanctionedAddress = useSanctionedAddress() // On session approve const onApprove = useCallback( @@ -91,7 +93,10 @@ const WcSessionManager = ({ sessions, uri }: WcSessionManagerProps) => { return walletConnect.onSessionPropose((proposalData) => { setError(null) - if (autoApprove[chainId]?.[proposalData.verifyContext.verified.origin]) { + if ( + autoApprove[chainId]?.[proposalData.verifyContext.verified.origin] && + (!isSafePassApp(proposalData.verifyContext.verified.origin) || !sanctionedAddress) + ) { onApprove(proposalData) return } @@ -99,7 +104,7 @@ const WcSessionManager = ({ sessions, uri }: WcSessionManagerProps) => { setProposal(proposalData) setIsLoading(undefined) }) - }, [autoApprove, chainId, onApprove, setError, setIsLoading, walletConnect]) + }, [autoApprove, chainId, onApprove, setError, setIsLoading, walletConnect, sanctionedAddress]) // Track errors useEffect(() => { diff --git a/src/features/walletconnect/services/utils.ts b/src/features/walletconnect/services/utils.ts index 0c5d30da1f..e416588c5b 100644 --- a/src/features/walletconnect/services/utils.ts +++ b/src/features/walletconnect/services/utils.ts @@ -50,6 +50,10 @@ export const isBlockedBridge = (origin: string) => { return BlockedBridges.some((bridge) => origin.includes(bridge)) } +export const isSafePassApp = (origin: string) => { + return origin.includes('community.safe.global') +} + // Bridge defaults to same address on destination chain but allows changing it export const isWarnedBridge = (origin: string, name: string) => { return WarnedBridges.some((bridge) => origin.includes(bridge)) || WarnedBridgeNames.includes(name) From 4a9818d7240bb0219b7f13b5f9aead2c61aabf80 Mon Sep 17 00:00:00 2001 From: schmanu Date: Thu, 15 Aug 2024 14:15:42 +0200 Subject: [PATCH 3/6] fix: do not show SafePass widgets if sanctioned --- src/components/common/SafeTokenWidget/index.tsx | 5 ++++- src/components/dashboard/ActivityRewardsSection/index.tsx | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/common/SafeTokenWidget/index.tsx b/src/components/common/SafeTokenWidget/index.tsx index 79c4aa10fb..d9ad717b0f 100644 --- a/src/components/common/SafeTokenWidget/index.tsx +++ b/src/components/common/SafeTokenWidget/index.tsx @@ -12,6 +12,7 @@ import SafeTokenIcon from '@/public/images/common/safe-token.svg' import css from './styles.module.css' import UnreadBadge from '../UnreadBadge' import classnames from 'classnames' +import { useSanctionedAddress } from '@/hooks/useSanctionedAddress' const TOKEN_DECIMALS = 18 @@ -47,8 +48,10 @@ const SafeTokenWidget = () => { const [allocationData, , allocationDataLoading] = useSafeTokenAllocation() const [allocation, , allocationLoading] = useSafeVotingPower(allocationData) + const sanctionedAddress = useSanctionedAddress() + const tokenAddress = getSafeTokenAddress(chainId) - if (!tokenAddress) { + if (!tokenAddress || Boolean(sanctionedAddress)) { return null } diff --git a/src/components/dashboard/ActivityRewardsSection/index.tsx b/src/components/dashboard/ActivityRewardsSection/index.tsx index 9173225266..7f876fb4f1 100644 --- a/src/components/dashboard/ActivityRewardsSection/index.tsx +++ b/src/components/dashboard/ActivityRewardsSection/index.tsx @@ -16,6 +16,7 @@ import { useHasFeature } from '@/hooks/useChains' import { FEATURES } from '@/utils/chains' import useLocalStorage from '@/services/local-storage/useLocalStorage' import ExternalLink from '@/components/common/ExternalLink' +import { useSanctionedAddress } from '@/hooks/useSanctionedAddress' const Step = ({ active, title }: { active: boolean; title: ReactNode }) => { return ( @@ -43,12 +44,15 @@ const ActivityRewardsSection = () => { const isDarkMode = useDarkMode() const router = useRouter() + const sanctionedAddress = useSanctionedAddress() + const [widgetHidden = false, setWidgetHidden] = useLocalStorage(LOCAL_STORAGE_KEY_HIDE_WIDGET) const isSAPBannerEnabled = useHasFeature(FEATURES.SAP_BANNER) const governanceApp = matchingApps?.[0] - if (!governanceApp || !governanceApp?.url || !isSAPBannerEnabled || widgetHidden) return null + if (!governanceApp || !governanceApp?.url || !isSAPBannerEnabled || widgetHidden || Boolean(sanctionedAddress)) + return null const appUrl = getSafeAppUrl(router, governanceApp?.url) From 5480f2f310ec83003b8cd812621f469968023b1b Mon Sep 17 00:00:00 2001 From: schmanu Date: Mon, 19 Aug 2024 15:57:06 +0200 Subject: [PATCH 4/6] fix: return error directly --- src/store/ofac.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/ofac.ts b/src/store/ofac.ts index b48e5e1c9e..815bf93454 100644 --- a/src/store/ofac.ts +++ b/src/store/ofac.ts @@ -59,7 +59,7 @@ export const ofacApi = createApi({ const isAddressBlocked: boolean = await contract['isSanctioned'](address) return { data: isAddressBlocked } } catch (error) { - return { error: { status: 'CUSTOM_ERROR', data: (error as Error).message } } + return { error } } }, }), From 75a926ce57c031361aed14b3e9b0e1b998475fda Mon Sep 17 00:00:00 2001 From: schmanu Date: Mon, 19 Aug 2024 16:37:45 +0200 Subject: [PATCH 5/6] fix: pass token to skip sanctions check --- .../ActivityRewardsSection/index.tsx | 4 +-- src/components/safe-apps/AppFrame/index.tsx | 5 ++-- src/config/constants.ts | 2 ++ .../components/WcProposalForm/index.tsx | 2 +- .../components/WcSessionManager/index.tsx | 11 +++----- .../__tests__/useSanctionedAddress.test.ts | 26 +++++++++++++++++-- src/hooks/useSanctionedAddress.ts | 13 +++++++--- 7 files changed, 45 insertions(+), 18 deletions(-) diff --git a/src/components/dashboard/ActivityRewardsSection/index.tsx b/src/components/dashboard/ActivityRewardsSection/index.tsx index 7f876fb4f1..e7a87e5db0 100644 --- a/src/components/dashboard/ActivityRewardsSection/index.tsx +++ b/src/components/dashboard/ActivityRewardsSection/index.tsx @@ -44,13 +44,13 @@ const ActivityRewardsSection = () => { const isDarkMode = useDarkMode() const router = useRouter() - const sanctionedAddress = useSanctionedAddress() - const [widgetHidden = false, setWidgetHidden] = useLocalStorage(LOCAL_STORAGE_KEY_HIDE_WIDGET) const isSAPBannerEnabled = useHasFeature(FEATURES.SAP_BANNER) const governanceApp = matchingApps?.[0] + const sanctionedAddress = useSanctionedAddress(isSAPBannerEnabled && !widgetHidden && !!governanceApp) + if (!governanceApp || !governanceApp?.url || !isSAPBannerEnabled || widgetHidden || Boolean(sanctionedAddress)) return null diff --git a/src/components/safe-apps/AppFrame/index.tsx b/src/components/safe-apps/AppFrame/index.tsx index 489aa21a8d..c4b55d821b 100644 --- a/src/components/safe-apps/AppFrame/index.tsx +++ b/src/components/safe-apps/AppFrame/index.tsx @@ -30,6 +30,7 @@ import SafeAppIframe from './SafeAppIframe' import { useCustomAppCommunicator } from '@/hooks/safe-apps/useCustomAppCommunicator' import { useSanctionedAddress } from '@/hooks/useSanctionedAddress' import BlockedAddress from '@/components/common/BlockedAddress' +import { SAFE_PASS_URL } from '@/config/constants' const UNKNOWN_APP_NAME = 'Unknown Safe App' @@ -45,8 +46,8 @@ const AppFrame = ({ appUrl, allowedFeaturesList, safeAppFromManifest }: AppFrame const chainId = useChainId() const chain = useCurrentChain() const router = useRouter() - const sanctionedAddress = useSanctionedAddress() - const isSafePassApp = appUrl.startsWith('https://community.safe.global') + const isSafePassApp = appUrl.startsWith(SAFE_PASS_URL) + const sanctionedAddress = useSanctionedAddress(isSafePassApp) const { expanded: queueBarExpanded, dismissedByUser: queueBarDismissed, diff --git a/src/config/constants.ts b/src/config/constants.ts index f31a12033a..473e4ec131 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -107,3 +107,5 @@ export const REDEFINE_API = process.env.NEXT_PUBLIC_REDEFINE_API export const REDEFINE_ARTICLE = 'https://safe.mirror.xyz/rInLWZwD_sf7enjoFerj6FIzCYmVMGrrV8Nhg4THdwI' export const CHAINALYSIS_OFAC_CONTRACT = '0x40c57923924b5c5c5455c48d93317139addac8fb' + +export const SAFE_PASS_URL = 'community.safe.global' diff --git a/src/features/walletconnect/components/WcProposalForm/index.tsx b/src/features/walletconnect/components/WcProposalForm/index.tsx index 5dd759b363..cd1cadc4ef 100644 --- a/src/features/walletconnect/components/WcProposalForm/index.tsx +++ b/src/features/walletconnect/components/WcProposalForm/index.tsx @@ -41,8 +41,8 @@ const WcProposalForm = ({ proposal, setProposal, onApprove }: ProposalFormProps) const { isScam, origin } = proposal.verifyContext.verified const url = proposer.metadata.url || origin - const sanctionedAddress = useSanctionedAddress() const isSafePass = isSafePassApp(origin) + const sanctionedAddress = useSanctionedAddress(isSafePass) const chainIds = useMemo(() => getSupportedChainIds(configs, proposal.params), [configs, proposal.params]) const isUnsupportedChain = !chainIds.includes(chainId) diff --git a/src/features/walletconnect/components/WcSessionManager/index.tsx b/src/features/walletconnect/components/WcSessionManager/index.tsx index 076d80c866..689c95b11d 100644 --- a/src/features/walletconnect/components/WcSessionManager/index.tsx +++ b/src/features/walletconnect/components/WcSessionManager/index.tsx @@ -11,8 +11,7 @@ import WcErrorMessage from '../WcErrorMessage' import WcProposalForm from '../WcProposalForm' import { trackEvent } from '@/services/analytics' import { WALLETCONNECT_EVENTS } from '@/services/analytics/events/walletconnect' -import { isSafePassApp, splitError } from '@/features/walletconnect/services/utils' -import { useSanctionedAddress } from '@/hooks/useSanctionedAddress' +import { splitError } from '@/features/walletconnect/services/utils' type WcSessionManagerProps = { sessions: SessionTypes.Struct[] @@ -30,7 +29,6 @@ const WcSessionManager = ({ sessions, uri }: WcSessionManagerProps) => { const { safe, safeAddress } = useSafeInfo() const { chainId } = safe const [proposal, setProposal] = useState() - const sanctionedAddress = useSanctionedAddress() // On session approve const onApprove = useCallback( @@ -93,10 +91,7 @@ const WcSessionManager = ({ sessions, uri }: WcSessionManagerProps) => { return walletConnect.onSessionPropose((proposalData) => { setError(null) - if ( - autoApprove[chainId]?.[proposalData.verifyContext.verified.origin] && - (!isSafePassApp(proposalData.verifyContext.verified.origin) || !sanctionedAddress) - ) { + if (autoApprove[chainId]?.[proposalData.verifyContext.verified.origin]) { onApprove(proposalData) return } @@ -104,7 +99,7 @@ const WcSessionManager = ({ sessions, uri }: WcSessionManagerProps) => { setProposal(proposalData) setIsLoading(undefined) }) - }, [autoApprove, chainId, onApprove, setError, setIsLoading, walletConnect, sanctionedAddress]) + }, [autoApprove, chainId, onApprove, setError, setIsLoading, walletConnect]) // Track errors useEffect(() => { diff --git a/src/hooks/__tests__/useSanctionedAddress.test.ts b/src/hooks/__tests__/useSanctionedAddress.test.ts index 6d536740d0..4c79e822d8 100644 --- a/src/hooks/__tests__/useSanctionedAddress.test.ts +++ b/src/hooks/__tests__/useSanctionedAddress.test.ts @@ -5,6 +5,7 @@ import useWallet from '../wallets/useWallet' import { faker } from '@faker-js/faker' import { connectedWalletBuilder } from '@/tests/builders/wallet' import * as ofac from '@/store/ofac' +import { skipToken } from '@reduxjs/toolkit/query' jest.mock('@/hooks/useSafeAddress') jest.mock('@/hooks/wallets/useWallet') @@ -62,9 +63,30 @@ describe('useSanctionedAddress', () => { mockUseSafeAddress.mockReturnValue(mockSafeAddress) mockUseWallet.mockReturnValue(connectedWalletBuilder().with({ address: mockWalletAddress }).build()) - jest.spyOn(ofac, 'useGetIsSanctionedQuery').mockReturnValue({ data: true, refetch: jest.fn() }) - + jest.spyOn(ofac, 'useGetIsSanctionedQuery').mockImplementation((arg) => { + if (arg === skipToken) { + return { data: undefined, refetch: jest.fn() } + } + return { data: true, refetch: jest.fn() } + }) const { result } = renderHook(() => useSanctionedAddress()) expect(result.current).toEqual(mockSafeAddress) }) + + it('should skip sanction check if isRestricted is false', () => { + const mockSafeAddress = faker.finance.ethereumAddress() + const mockWalletAddress = faker.finance.ethereumAddress() + mockUseSafeAddress.mockReturnValue(mockSafeAddress) + mockUseWallet.mockReturnValue(connectedWalletBuilder().with({ address: mockWalletAddress }).build()) + + jest.spyOn(ofac, 'useGetIsSanctionedQuery').mockImplementation((arg) => { + if (arg === skipToken) { + return { data: undefined, refetch: jest.fn() } + } + return { data: true, refetch: jest.fn() } + }) + + const { result } = renderHook(() => useSanctionedAddress(false)) + expect(result.current).toBeUndefined() + }) }) diff --git a/src/hooks/useSanctionedAddress.ts b/src/hooks/useSanctionedAddress.ts index a1e5fd7fc6..ee6257e57a 100644 --- a/src/hooks/useSanctionedAddress.ts +++ b/src/hooks/useSanctionedAddress.ts @@ -3,13 +3,20 @@ import useSafeAddress from './useSafeAddress' import useWallet from './wallets/useWallet' import { skipToken } from '@reduxjs/toolkit/query/react' -export const useSanctionedAddress = () => { +/** + * Checks if the opened Safe or the connected wallet are sanctioned and returns the sanctioned address. + * @param isRestricted the check is only performed if isRestricted is true. + * @returns address of sanctioned wallet or Safe + */ +export const useSanctionedAddress = (isRestricted = true) => { const wallet = useWallet() const safeAddress = useSafeAddress() - const { data: isWalletSanctioned } = useGetIsSanctionedQuery(wallet ? wallet.address : skipToken) + const { data: isWalletSanctioned } = useGetIsSanctionedQuery(isRestricted && wallet ? wallet.address : skipToken) - const { data: isSafeSanctioned } = useGetIsSanctionedQuery(safeAddress !== '' ? safeAddress : skipToken) + const { data: isSafeSanctioned } = useGetIsSanctionedQuery( + isRestricted && safeAddress !== '' ? safeAddress : skipToken, + ) if (isSafeSanctioned) { return safeAddress From da03cde76e79d9341a80bfb6c23589d8cbd96d32 Mon Sep 17 00:00:00 2001 From: schmanu Date: Mon, 19 Aug 2024 16:57:17 +0200 Subject: [PATCH 6/6] fix: reuse isSafePassApp util --- src/components/safe-apps/AppFrame/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/safe-apps/AppFrame/index.tsx b/src/components/safe-apps/AppFrame/index.tsx index c4b55d821b..44c1684f50 100644 --- a/src/components/safe-apps/AppFrame/index.tsx +++ b/src/components/safe-apps/AppFrame/index.tsx @@ -30,7 +30,7 @@ import SafeAppIframe from './SafeAppIframe' import { useCustomAppCommunicator } from '@/hooks/safe-apps/useCustomAppCommunicator' import { useSanctionedAddress } from '@/hooks/useSanctionedAddress' import BlockedAddress from '@/components/common/BlockedAddress' -import { SAFE_PASS_URL } from '@/config/constants' +import { isSafePassApp } from '@/features/walletconnect/services/utils' const UNKNOWN_APP_NAME = 'Unknown Safe App' @@ -46,8 +46,8 @@ const AppFrame = ({ appUrl, allowedFeaturesList, safeAppFromManifest }: AppFrame const chainId = useChainId() const chain = useCurrentChain() const router = useRouter() - const isSafePassApp = appUrl.startsWith(SAFE_PASS_URL) - const sanctionedAddress = useSanctionedAddress(isSafePassApp) + const isSafePass = isSafePassApp(appUrl) + const sanctionedAddress = useSanctionedAddress(isSafePass) const { expanded: queueBarExpanded, dismissedByUser: queueBarDismissed, @@ -121,7 +121,7 @@ const AppFrame = ({ appUrl, allowedFeaturesList, safeAppFromManifest }: AppFrame return
} - if (Boolean(sanctionedAddress) && isSafePassApp) { + if (Boolean(sanctionedAddress) && isSafePass) { return ( <>