Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Confirmation page alerts #20125

Merged
merged 45 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
952b89e
Adding metrics events for blockaid
jpuri Jul 19, 2023
3c2e7fd
Adding blockaid alert on confirmation pages
jpuri Jul 21, 2023
997c035
Fix
jpuri Jul 26, 2023
bba6e59
Update
jpuri Jul 27, 2023
bd8e6f9
Update
jpuri Jul 27, 2023
e2fcb68
Update
jpuri Jul 27, 2023
6d26d7b
Update
jpuri Jul 27, 2023
353e0e0
Update
jpuri Jul 27, 2023
6e35f9a
Update
jpuri Jul 27, 2023
347d80f
Update
jpuri Jul 27, 2023
afef9fd
Adding metrics events for blockaid
jpuri Jul 19, 2023
f582b52
Merge
jpuri Jul 28, 2023
ba11b40
Merge
jpuri Jul 28, 2023
bc29cb7
Adding unit tests
jpuri Jul 28, 2023
d268a09
Update
jpuri Jul 28, 2023
5ac22df
Update
jpuri Jul 28, 2023
52d93fd
Merge branch 'develop' into blockaid_metrics
jpuri Jul 28, 2023
b6b06bb
Merge branch 'develop' into blockaid_metrics
jpuri Jul 28, 2023
75ae51b
merge
jpuri Jul 31, 2023
c09365e
Update
jpuri Jul 31, 2023
8c0d769
Merge branch 'blockaid_metrics' of github.com:MetaMask/metamask-exten…
jpuri Jul 31, 2023
cc6314e
Merge branch 'blockaid_metrics' into confirmation_page_alerts
jpuri Jul 31, 2023
e8927e6
Merge branch 'develop' into blockaid_metrics
jpuri Jul 31, 2023
76ea113
Update
jpuri Jul 31, 2023
a5c7964
Merge branch 'confirmation_page_alerts' of github.com:MetaMask/metama…
jpuri Jul 31, 2023
5cc93ee
Update
jpuri Jul 31, 2023
43d23eb
Merge branch 'develop' into blockaid_metrics
jpuri Jul 31, 2023
439e080
Merge branch 'develop' of github.com:MetaMask/metamask-extension into…
jpuri Aug 1, 2023
9c65250
Update
jpuri Aug 1, 2023
5d5561e
Update
jpuri Aug 1, 2023
98e14dc
Update
jpuri Aug 1, 2023
4a88a9c
Update
jpuri Aug 1, 2023
e7befbd
merge
jpuri Aug 1, 2023
521ff7e
Update
jpuri Aug 1, 2023
77d2266
Update
jpuri Aug 1, 2023
a673b8f
Update LavaMoat policies
metamaskbot Aug 1, 2023
f18118c
Merge branch 'confirmation_page_alerts' of github.com:MetaMask/metama…
jpuri Aug 1, 2023
c5205a8
Update
jpuri Aug 1, 2023
e499b26
Update
jpuri Aug 1, 2023
d8192ee
Update
jpuri Aug 1, 2023
9fd2df1
Merge branch 'develop' into confirmation_page_alerts
jpuri Aug 1, 2023
7f4a05f
Update
jpuri Aug 2, 2023
2ac201c
Merge branch 'confirmation_page_alerts' of github.com:MetaMask/metama…
jpuri Aug 2, 2023
6d6ce95
Merge branch 'develop' of github.com:MetaMask/metamask-extension into…
jpuri Aug 3, 2023
d88685e
Update
jpuri Aug 3, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .metamaskrc.dist
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ INFURA_PROJECT_ID=00000000000
; Set this to test changes to the phishing warning page.
;PHISHING_WARNING_PAGE_URL=
BLOCKAID_FILE_CDN=
BLOCKAID_PUBLIC_KEY=
19 changes: 14 additions & 5 deletions app/scripts/controllers/transactions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2415,13 +2415,22 @@ export default class TransactionController extends EventEmitter {

let uiCustomizations;

if (securityProviderResponse?.flagAsDangerous === 1) {
uiCustomizations = ['flagged_as_malicious'];
} else if (securityProviderResponse?.flagAsDangerous === 2) {
uiCustomizations = ['flagged_as_safety_unknown'];
///: BEGIN:ONLY_INCLUDE_IN(blockaid)
if (securityAlertResponse?.result_type === BlockaidResultType.Failed) {
uiCustomizations = ['security_alert_failed'];
} else {
uiCustomizations = null;
///: END:ONLY_INCLUDE_IN
// eslint-disable-next-line no-lonely-if
if (securityProviderResponse?.flagAsDangerous === 1) {
uiCustomizations = ['flagged_as_malicious'];
} else if (securityProviderResponse?.flagAsDangerous === 2) {
uiCustomizations = ['flagged_as_safety_unknown'];
} else {
uiCustomizations = null;
}
///: BEGIN:ONLY_INCLUDE_IN(blockaid)
}
///: END:ONLY_INCLUDE_IN

/** The transaction status property is not considered sensitive and is now included in the non-anonymous event */
let properties = {
Expand Down
77 changes: 77 additions & 0 deletions app/scripts/controllers/transactions/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2652,6 +2652,83 @@ describe('Transaction Controller', function () {
);
});

it('should call _trackMetaMetricsEvent with the correct payload when blockaid verification fails', async function () {
const txMeta = {
id: 1,
status: TransactionStatus.unapproved,
txParams: {
from: fromAccount.address,
to: '0x1678a085c290ebd122dc42cba69373b5953b831d',
gasPrice: '0x77359400',
gas: '0x7b0d',
nonce: '0x4b',
},
type: TransactionType.simpleSend,
origin: 'other',
chainId: currentChainId,
time: 1624408066355,
metamaskNetworkId: currentNetworkId,
securityAlertResponse: {
result_type: BlockaidResultType.Failed,
reason: 'some error',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] could use constant from shared/constants/security-provider.ts here. might be helpful to know there are finite values for this property

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For failed request reason is actual exception message.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm. odd. We typically use this value as a key reference e.g. when we get the description, we use this value, but now we might need to check result_type as well for our description.. It's adding slight complexity, but avoiding any complexity will help avoid build up and keep the code more scalable

@jpuri

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

transferring knowledge and convo from our DM's to continue here:

  • BlockaidResultType.Failed and related reason value are generated on our side

@jpuri's proposal:

  • provide error message as the reason value to be sent to metametrics (how the logic is written now)

my (@digiwand's) proposal:

  • provide error message in a new property e.g. securityAlertResponse.error. Send error as a separate property to metametrics
  • when BlockaidResultType.Failed happens, use a snake case key value for reason e.g. local_var_failed_request

the reason for my proposal is to have consistency in the values and avoid supporting more complexities as mentioned in the comment above.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alternatively, we could also provide the error message in the description property which is currently used for arbitrary text

},
};
const expectedPayload = {
actionId,
initialEvent: 'Transaction Added',
successEvent: 'Transaction Approved',
failureEvent: 'Transaction Rejected',
uniqueIdentifier: 'transaction-added-1',
persist: true,
category: MetaMetricsEventCategory.Transactions,
properties: {
network: '5',
referrer: 'other',
source: MetaMetricsTransactionEventSource.Dapp,
status: 'unapproved',
transaction_type: TransactionType.simpleSend,
chain_id: '0x5',
eip_1559_version: '0',
gas_edit_attempted: 'none',
gas_edit_type: 'none',
account_type: 'MetaMask',
asset_type: AssetType.native,
token_standard: TokenStandard.none,
device_model: 'N/A',
transaction_speed_up: false,
ui_customizations: ['security_alert_failed'],
security_alert_reason: 'some error',
security_alert_response: BlockaidResultType.Failed,
},
sensitiveProperties: {
baz: 3.0,
foo: 'bar',
gas_price: '2',
gas_limit: '0x7b0d',
transaction_contract_method: undefined,
transaction_replaced: undefined,
first_seen: 1624408066355,
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
},
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] I think rather than rebuilding the mock obj for each test, we could reuse the mock data e.g.

// above the initial describe call 

const MOCK_TX_DATA = { 
  // starting mock properties
};

// ..

it('', () => {
  const mockTxMeta = {
    ...MOCK_TX_DATA,
    securityAlertResponse: {
        result_type: BlockaidResultType.Failed,
        reason: 'some error',
     },
  }
})


await txController._trackTransactionMetricsEvent(
txMeta,
TransactionMetaMetricsEvent.added,
actionId,
{
baz: 3.0,
foo: 'bar',
},
);
assert.equal(createEventFragmentSpy.callCount, 1);
assert.equal(finalizeEventFragmentSpy.callCount, 0);
assert.deepEqual(
createEventFragmentSpy.getCall(0).args[0],
expectedPayload,
);
});

it('should call _trackMetaMetricsEvent with the correct payload (extra params) when flagAsDangerous is malicious', async function () {
const txMeta = {
id: 1,
Expand Down
116 changes: 102 additions & 14 deletions app/scripts/lib/ppom/ppom-middleware.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import {
BlockaidReason,
BlockaidResultType,
} from '../../../../shared/constants/security-provider';
import { createPPOMMiddleware } from './ppom-middleware';

Object.defineProperty(globalThis, 'fetch', {
Expand All @@ -13,10 +17,16 @@ Object.defineProperty(globalThis, 'performance', {
describe('PPOMMiddleware', () => {
it('should call ppomController.usePPOM for requests of type confirmation', async () => {
const useMock = jest.fn();
const controller = {
const ppomController = {
usePPOM: useMock,
};
const middlewareFunction = createPPOMMiddleware(controller as any);
const preferenceController = {
store: { getState: () => ({ securityAlertsEnabled: true }) },
};
const middlewareFunction = createPPOMMiddleware(
ppomController as any,
preferenceController as any,
);
await middlewareFunction(
{ method: 'eth_sendTransaction' },
undefined,
Expand All @@ -26,25 +36,85 @@ describe('PPOMMiddleware', () => {
});

it('should add validation response on confirmation requests', async () => {
const controller = {
const ppomController = {
usePPOM: async () => Promise.resolve('VALIDATION_RESULT'),
};
const preferenceController = {
store: { getState: () => ({ securityAlertsEnabled: true }) },
};
const middlewareFunction = createPPOMMiddleware(
ppomController as any,
preferenceController as any,
);
const req = {
method: 'eth_sendTransaction',
securityAlertResponse: undefined,
};
await middlewareFunction(req, undefined, () => undefined);
expect(req.securityAlertResponse).toBeDefined();
});

it('should not do validation if user has not enabled preference', async () => {
const ppomController = {
usePPOM: async () => Promise.resolve('VALIDATION_RESULT'),
};
const middlewareFunction = createPPOMMiddleware(controller as any);
const req = { method: 'eth_sendTransaction', ppomResponse: undefined };
const preferenceController = {
store: { getState: () => ({ securityAlertsEnabled: false }) },
};
const middlewareFunction = createPPOMMiddleware(
ppomController as any,
preferenceController as any,
);
const req = {
method: 'eth_sendTransaction',
securityAlertResponse: undefined,
};
await middlewareFunction(req, undefined, () => undefined);
expect(req.ppomResponse).toBeDefined();
expect(req.securityAlertResponse).toBeUndefined();
});

it('should set Failed type in response if usePPOM throw error', async () => {
const ppomController = {
usePPOM: async () => {
throw new Error('some error');
},
};
const preferenceController = {
store: { getState: () => ({ securityAlertsEnabled: true }) },
};
const middlewareFunction = createPPOMMiddleware(
ppomController as any,
preferenceController as any,
);
const req = {
method: 'eth_sendTransaction',
securityAlertResponse: undefined,
};
await middlewareFunction(req, undefined, () => undefined);
expect((req.securityAlertResponse as any)?.result_type).toBe(
BlockaidResultType.Failed,
);
expect((req.securityAlertResponse as any)?.reason).toBe(
BlockaidReason.failed,
);
});

it('should call next method when ppomController.usePPOM completes', async () => {
const ppom = {
validateJsonRpc: () => undefined,
};
const controller = {
const ppomController = {
usePPOM: async (callback: any) => {
callback(ppom);
},
};
const middlewareFunction = createPPOMMiddleware(controller as any);
const preferenceController = {
store: { getState: () => ({ securityAlertsEnabled: true }) },
};
const middlewareFunction = createPPOMMiddleware(
ppomController as any,
preferenceController as any,
);
const nextMock = jest.fn();
await middlewareFunction(
{ method: 'eth_sendTransaction' },
Expand All @@ -55,12 +125,18 @@ describe('PPOMMiddleware', () => {
});

it('should call next method when ppomController.usePPOM throws error', async () => {
const controller = {
const ppomController = {
usePPOM: async (_callback: any) => {
throw Error('Some error');
},
};
const middlewareFunction = createPPOMMiddleware(controller as any);
const preferenceController = {
store: { getState: () => ({ securityAlertsEnabled: true }) },
};
const middlewareFunction = createPPOMMiddleware(
ppomController as any,
preferenceController as any,
);
const nextMock = jest.fn();
await middlewareFunction(
{ method: 'eth_sendTransaction' },
Expand All @@ -75,12 +151,18 @@ describe('PPOMMiddleware', () => {
const ppom = {
validateJsonRpc: validateMock,
};
const controller = {
const ppomController = {
usePPOM: async (callback: any) => {
callback(ppom);
},
};
const middlewareFunction = createPPOMMiddleware(controller as any);
const preferenceController = {
store: { getState: () => ({ securityAlertsEnabled: true }) },
};
const middlewareFunction = createPPOMMiddleware(
ppomController as any,
preferenceController as any,
);
await middlewareFunction(
{ method: 'eth_sendTransaction' },
undefined,
Expand All @@ -94,12 +176,18 @@ describe('PPOMMiddleware', () => {
const ppom = {
validateJsonRpc: validateMock,
};
const controller = {
const ppomController = {
usePPOM: async (callback: any) => {
callback(ppom);
},
};
const middlewareFunction = createPPOMMiddleware(controller as any);
const preferenceController = {
store: { getState: () => ({ securityAlertsEnabled: true }) },
};
const middlewareFunction = createPPOMMiddleware(
ppomController as any,
preferenceController as any,
);
await middlewareFunction(
{ method: 'eth_someRequest' },
undefined,
Expand Down
35 changes: 28 additions & 7 deletions app/scripts/lib/ppom/ppom-middleware.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { PPOM } from '@blockaid/ppom';

import { PPOMController } from '@metamask/ppom-validator';

import {
BlockaidReason,
BlockaidResultType,
} from '../../../../shared/constants/security-provider';
import PreferencesController from 'app/scripts/controllers/preferences';

const { sentry } = global as any;

const ConfirmationMethods = Object.freeze([
'eth_sendRawTransaction',
'eth_sendTransaction',
Expand All @@ -23,19 +30,33 @@ const ConfirmationMethods = Object.freeze([
* the request will be forwarded to the next middleware, together with the PPOM response.
*
* @param ppomController - Instance of PPOMController.
* @param preferencesController - Instance of PreferenceController.
* @returns PPOMMiddleware function.
*/
export function createPPOMMiddleware(ppomController: PPOMController) {
export function createPPOMMiddleware(
ppomController: PPOMController,
preferencesController: PreferencesController,
) {
return async (req: any, _res: any, next: () => void) => {
try {
if (ConfirmationMethods.includes(req.method)) {
const securityAlertsEnabled =
preferencesController.store.getState()?.securityAlertsEnabled;
if (securityAlertsEnabled && ConfirmationMethods.includes(req.method)) {
// eslint-disable-next-line require-atomic-updates
req.ppomResponse = await ppomController.usePPOM(async (ppom: PPOM) => {
return ppom.validateJsonRpc(req);
});
req.securityAlertResponse = await ppomController.usePPOM(
async (ppom: PPOM) => {
return ppom.validateJsonRpc(req);
},
);
}
} catch (error: unknown) {
} catch (error: any) {
sentry?.captureException(error);
console.error('Error validating JSON RPC using PPOM: ', error);
req.securityAlertResponse = {
result_type: BlockaidResultType.Failed,
reason: BlockaidReason.failed,
description: 'Validating the confirmation failed by throwing error.',
};
} finally {
next();
}
Expand Down
Loading
Loading