From 737173ed5aaafbc4cf4a32d7dff6a506c8c38eb0 Mon Sep 17 00:00:00 2001 From: Ariella Vu <20778143+digiwand@users.noreply.github.com> Date: Mon, 24 Jul 2023 08:54:17 +0200 Subject: [PATCH] New BlockaidBannerAlert component (#20051) --- app/_locales/en/messages.json | 24 +++ shared/constants/security-provider.ts | 36 ++++ .../blockaid-banner-alert.test.js.snap | 179 ++++++++++++++++++ .../blockaid-banner-alert.js | 90 +++++++++ .../blockaid-banner-alert.stories.js | 42 ++++ .../blockaid-banner-alert.test.js | 144 ++++++++++++++ .../blockaid-banner-alert/index.js | 1 + 7 files changed, 516 insertions(+) create mode 100644 ui/components/app/security-provider-banner-alert/blockaid-banner-alert/__snapshots__/blockaid-banner-alert.test.js.snap create mode 100644 ui/components/app/security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert.js create mode 100644 ui/components/app/security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert.stories.js create mode 100644 ui/components/app/security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert.test.js create mode 100644 ui/components/app/security-provider-banner-alert/blockaid-banner-alert/index.js diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 7f024a49d..de66494e2 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -565,6 +565,30 @@ "blockaid": { "message": "Blockaid" }, + "blockaidDescriptionApproveFarming": { + "message": "If you approve this request, a third party known for scams might take all your assets." + }, + "blockaidDescriptionBlurFarming": { + "message": "If you approve this request, someone can steal your assets listed on Blur." + }, + "blockaidDescriptionMaliciousDomain": { + "message": "You're interacting with a malicious domain. If you approve this request, you might lose your assets." + }, + "blockaidDescriptionMightLoseAssets": { + "message": "If you approve this request, you might lose your assets." + }, + "blockaidDescriptionSeaportFarming": { + "message": "If you approve this request, someone can steal your assets listed on OpenSea." + }, + "blockaidDescriptionTransferFarming": { + "message": "If you approve this request, a third party known for scams will take all your assets." + }, + "blockaidTitleDeceptive": { + "message": "This is a deceptive request" + }, + "blockaidTitleSuspicious": { + "message": "This is a suspicious request" + }, "blockies": { "message": "Blockies" }, diff --git a/shared/constants/security-provider.ts b/shared/constants/security-provider.ts index 420cecf03..b956a8e75 100644 --- a/shared/constants/security-provider.ts +++ b/shared/constants/security-provider.ts @@ -19,6 +19,42 @@ export const SECURITY_PROVIDER_CONFIG: Readonly = { }, }; +/** The reason, also referred to as the attack type, provided in the PPOM Response */ +export enum BlockaidReason { + /** Approval for a malicious spender */ + approvalFarming = 'approval_farming', + /** Malicious signature on Blur order */ + blurFarming = 'blur_farming', + /** A known malicous site invoked that transaction */ + maliciousDomain = 'malicious_domain', + /** Malicious signature on a Permit order */ + permitFarming = 'permit_farming', + /** Direct theft of native assets (ETH/MATIC/AVAX/ etc …) */ + rawNativeTokenTransfer = 'raw_native_token_transfer', + /** Malicious raw signature from the user */ + rawSignatureFarming = 'raw_signature_farming', + /** Malicious signature on a Seaport order */ + seaportFarming = 'seaport_farming', + /** setApprovalForAll for a malicious operator */ + setApprovalForAll = 'set_approval_for_all', + /** Malicious signature on other type of trade order (Zero-X / Rarible / etc..) */ + tradeOrderFarming = 'trade_order_farming', + /** Direct theft of assets using transfer */ + transferFarming = 'transfer_farming', + /** Direct theft of assets using transferFrom */ + transferFromFarming = 'transfer_from_farming', + /** Malicious trade that results in the victim being rained */ + unfairTrade = 'unfair_trade', + + other = 'other', +} + +export enum BlockaidResultType { + Malicious = 'Malicious', + Warning = 'Warning', + Benign = 'Benign', +} + /** * @typedef {object} SecurityProviderMessageSeverity * @property {0} NOT_MALICIOUS - Indicates message is not malicious diff --git a/ui/components/app/security-provider-banner-alert/blockaid-banner-alert/__snapshots__/blockaid-banner-alert.test.js.snap b/ui/components/app/security-provider-banner-alert/blockaid-banner-alert/__snapshots__/blockaid-banner-alert.test.js.snap new file mode 100644 index 000000000..ca18324d9 --- /dev/null +++ b/ui/components/app/security-provider-banner-alert/blockaid-banner-alert/__snapshots__/blockaid-banner-alert.test.js.snap @@ -0,0 +1,179 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Blockaid Banner Alert should render 'danger' UI when ppomResponse.resultType is 'Malicious 1`] = ` +
+ +
+
+ This is a deceptive request +
+

+ If you approve this request, a third party known for scams might take all your assets. +

+

+ + + + Security advice by + + Blockaid + + + + +

+
+
+`; + +exports[`Blockaid Banner Alert should render 'warning' UI when ppomResponse.resultType is 'Warning 1`] = ` +
+ +
+
+ This is a deceptive request +
+

+ If you approve this request, a third party known for scams might take all your assets. +

+

+ + + + Security advice by + + Blockaid + + + + +

+
+
+`; + +exports[`Blockaid Banner Alert should render details when provided 1`] = ` +
+
+ +
+
+ This is a deceptive request +
+

+ If you approve this request, a third party known for scams might take all your assets. +

+
+
+ +

+ See details +

+ +
+
+
    +
  • + • + Operator is an EOA +
  • +
  • + • + Operator is untrusted according to previous activity +
  • +
+
+
+
+

+ + + + Security advice by + + Blockaid + + + + +

+
+
+
+`; diff --git a/ui/components/app/security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert.js b/ui/components/app/security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert.js new file mode 100644 index 000000000..1ced5b805 --- /dev/null +++ b/ui/components/app/security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert.js @@ -0,0 +1,90 @@ +import React, { useContext } from 'react'; +import PropTypes from 'prop-types'; +import { captureException } from '@sentry/browser'; + +import { Text } from '../../../component-library'; +import { Severity } from '../../../../helpers/constants/design-system'; +import { I18nContext } from '../../../../contexts/i18n'; + +import { + BlockaidReason, + BlockaidResultType, + SecurityProvider, +} from '../../../../../shared/constants/security-provider'; +import SecurityProviderBannerAlert from '../security-provider-banner-alert'; + +/** Reason to description translation key mapping. Grouped by translations. */ +const REASON_TO_DESCRIPTION_TKEY = Object.freeze({ + [BlockaidReason.approvalFarming]: 'blockaidDescriptionApproveFarming', + [BlockaidReason.permitFarming]: 'blockaidDescriptionApproveFarming', + [BlockaidReason.setApprovalForAll]: 'blockaidDescriptionApproveFarming', + + [BlockaidReason.blurFarming]: 'blockaidDescriptionBlurFarming', + + [BlockaidReason.seaportFarming]: 'blockaidDescriptionSeaportFarming', + + [BlockaidReason.maliciousDomain]: 'blockaidDescriptionMaliciousDomain', + + [BlockaidReason.rawSignatureFarming]: 'blockaidDescriptionMightLoseAssets', + [BlockaidReason.tradeOrderFarming]: 'blockaidDescriptionMightLoseAssets', + [BlockaidReason.unfairTrade]: 'blockaidDescriptionMightLoseAssets', + + [BlockaidReason.rawNativeTokenTransfer]: 'blockaidDescriptionTransferFarming', + [BlockaidReason.transferFarming]: 'blockaidDescriptionTransferFarming', + [BlockaidReason.transferFromFarming]: 'blockaidDescriptionTransferFarming', + + [BlockaidReason.other]: 'blockaidDescriptionMightLoseAssets', +}); + +/** List of suspicious reason(s). Other reasons will be deemed as deceptive. */ +const SUSPCIOUS_REASON = [BlockaidReason.rawSignatureFarming]; + +function BlockaidBannerAlert({ + ppomResponse: { reason, resultType, features }, +}) { + const t = useContext(I18nContext); + + if (resultType === BlockaidResultType.Benign) { + return null; + } + + if (!REASON_TO_DESCRIPTION_TKEY[reason]) { + captureException(`BlockaidBannerAlert: Unidentified reason '${reason}'`); + } + + const description = t(REASON_TO_DESCRIPTION_TKEY[reason] || 'other'); + + const details = Boolean(features?.length) && ( + + {features.map((feature, i) => ( +
  • • {feature}
  • + ))} +
    + ); + + const severity = + resultType === BlockaidResultType.Malicious + ? Severity.Danger + : Severity.Warning; + + const title = + SUSPCIOUS_REASON.indexOf(reason) > -1 + ? t('blockaidTitleSuspicious') + : t('blockaidTitleDeceptive'); + + return ( + + ); +} + +BlockaidBannerAlert.propTypes = { + ppomResponse: PropTypes.object, +}; + +export default BlockaidBannerAlert; diff --git a/ui/components/app/security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert.stories.js b/ui/components/app/security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert.stories.js new file mode 100644 index 000000000..c12130871 --- /dev/null +++ b/ui/components/app/security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert.stories.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { + BlockaidReason, + BlockaidResultType, +} from '../../../../../shared/constants/security-provider'; +import BlockaidBannerAlert from '.'; + +const mockFeatures = [ + 'Operator is an EOA', + 'Operator is untrusted according to previous activity', +]; + +export default { + title: 'Components/App/SecurityProviderBannerAlert/BlockaidBannerAlert', + argTypes: { + features: { + control: 'array', + description: + 'ppomResponse.features value which is a list displayed as SecurityProviderBannerAlert details', + }, + reason: { + control: 'select', + options: Object.values(BlockaidReason), + description: 'ppomResponse.reason value', + }, + resultType: { + control: 'select', + options: Object.values(BlockaidResultType), + description: 'ppomResponse.resultType value', + }, + }, + args: { + features: mockFeatures, + reason: BlockaidReason.setApprovalForAll, + resultType: BlockaidResultType.Warning, + }, +}; + +export const DefaultStory = (args) => ( + +); +DefaultStory.storyName = 'Default'; diff --git a/ui/components/app/security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert.test.js b/ui/components/app/security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert.test.js new file mode 100644 index 000000000..7e7188645 --- /dev/null +++ b/ui/components/app/security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert.test.js @@ -0,0 +1,144 @@ +import React from 'react'; +import { renderWithLocalization } from '../../../../../test/lib/render-helpers'; +import { Severity } from '../../../../helpers/constants/design-system'; +import { + BlockaidReason, + BlockaidResultType, +} from '../../../../../shared/constants/security-provider'; +import BlockaidBannerAlert from '.'; + +const mockPpomResponse = { + resultType: BlockaidResultType.Warning, + reason: BlockaidReason.setApprovalForAll, + description: + 'A SetApprovalForAll request was made on {contract}. We found the operator {operator} to be malicious', + args: { + contract: '0xa7206d878c5c3871826dfdb42191c49b1d11f466', + operator: '0x92a3b9773b1763efa556f55ccbeb20441962d9b2', + }, +}; + +describe('Blockaid Banner Alert', () => { + it(`should not render when ppomResponse.resultType is '${BlockaidResultType.Benign}'`, () => { + const { container } = renderWithLocalization( + , + ); + + expect(container.querySelector('.mm-banner-alert')).toBeNull(); + }); + + it(`should render '${Severity.Danger}' UI when ppomResponse.resultType is '${BlockaidResultType.Malicious}`, () => { + const { container } = renderWithLocalization( + , + ); + const dangerBannerAlert = container.querySelector( + '.mm-banner-alert--severity-danger', + ); + + expect(dangerBannerAlert).toBeInTheDocument(); + expect(dangerBannerAlert).toMatchSnapshot(); + }); + + it(`should render '${Severity.Warning}' UI when ppomResponse.resultType is '${BlockaidResultType.Warning}`, () => { + const { container } = renderWithLocalization( + , + ); + const warningBannerAlert = container.querySelector( + '.mm-banner-alert--severity-warning', + ); + + expect(warningBannerAlert).toBeInTheDocument(); + expect(warningBannerAlert).toMatchSnapshot(); + }); + + it('should render title, "This is a deceptive request"', () => { + const { getByText } = renderWithLocalization( + , + ); + + expect(getByText('This is a deceptive request')).toBeInTheDocument(); + }); + + it('should render title, "This is a suspicious request", when the reason is "raw_signature_farming"', () => { + const { getByText } = renderWithLocalization( + , + ); + + expect(getByText('This is a suspicious request')).toBeInTheDocument(); + }); + + it('should render details when provided', () => { + const mockFeatures = [ + 'Operator is an EOA', + 'Operator is untrusted according to previous activity', + ]; + + const { container, getByText } = renderWithLocalization( + , + ); + + expect(container).toMatchSnapshot(); + expect(container.querySelector('.disclosure')).toBeInTheDocument(); + mockFeatures.forEach((feature) => { + expect(getByText(`• ${feature}`)).toBeInTheDocument(); + }); + }); + + describe('when rendering description', () => { + Object.entries({ + [BlockaidReason.approvalFarming]: + 'If you approve this request, a third party known for scams might take all your assets.', + [BlockaidReason.blurFarming]: + 'If you approve this request, someone can steal your assets listed on Blur.', + [BlockaidReason.maliciousDomain]: + "You're interacting with a malicious domain. If you approve this request, you might lose your assets.", + [BlockaidReason.other]: + 'If you approve this request, you might lose your assets.', + [BlockaidReason.permitFarming]: + 'If you approve this request, a third party known for scams might take all your assets.', + [BlockaidReason.rawNativeTokenTransfer]: + 'If you approve this request, a third party known for scams will take all your assets.', + [BlockaidReason.rawSignatureFarming]: + 'If you approve this request, you might lose your assets.', + [BlockaidReason.seaportFarming]: + 'If you approve this request, someone can steal your assets listed on OpenSea.', + [BlockaidReason.setApprovalForAll]: + 'If you approve this request, a third party known for scams might take all your assets.', + [BlockaidReason.tradeOrderFarming]: + 'If you approve this request, you might lose your assets.', + [BlockaidReason.transferFromFarming]: + 'If you approve this request, a third party known for scams will take all your assets.', + [BlockaidReason.transferFarming]: + 'If you approve this request, a third party known for scams will take all your assets.', + [BlockaidReason.unfairTrade]: + 'If you approve this request, you might lose your assets.', + }).forEach(([reason, expectedDescription]) => { + it(`should render for '${reason}' correctly`, () => { + const { getByText } = renderWithLocalization( + , + ); + + expect(getByText(expectedDescription)).toBeInTheDocument(); + }); + }); + }); +}); diff --git a/ui/components/app/security-provider-banner-alert/blockaid-banner-alert/index.js b/ui/components/app/security-provider-banner-alert/blockaid-banner-alert/index.js new file mode 100644 index 000000000..693cf2748 --- /dev/null +++ b/ui/components/app/security-provider-banner-alert/blockaid-banner-alert/index.js @@ -0,0 +1 @@ +export { default } from './blockaid-banner-alert';