Repository: MetaMask/metamask-mobile
Pull request #14573: feat: add migration 082 to reset PhishingController phishingLists
Author: login: AugmentedMode
Labels: name: QA Passed
description: QA testing has been completed and passed
name: area-performance
description: Issues relating to slowness of app, cpu usage, and/or blank screens.
name: team-product-safety
description: Push issues to Product Safety team
name: release-7.48.0
description: Issue or pull request that will be included in release 7.48.0
Created: 2025-04-10T03:37:20Z | Merged: 2025-05-30T13:50:04Z

<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once.
-->

## **Description**

This PR adds migration 082 to reset the `phishingLists` array in the PhishingController while preserving all other controller state.

1. **Reason for change:** We have introduced real-time dapp scanning to replace the traditional eth phishing detection functionality. As a result, the large local phishing lists are no longer needed, which have been contributing to memory issues on low-end devices and increased app startup time.

2. **Improvement/solution:** This targeted migration removes the outdated phishing lists data by resetting only the `phishingLists` array to empty, while preserving all other user preferences and controller state. This approach:
   - Reduces app memory footprint and state size
   - Improves app startup performance
   - Prevents memory-related issues on low-end devices
   - Maintains all user customizations like whitelisted sites

## **Related issues**

Fixes: [Issue number/link]

## **Manual testing steps**

1. Install previous version of the app and observe app startup time
2. Check memory usage with the old phishing lists in place
3. Update to version with this migration
4. Verify improved startup time and reduced memory usage
5. Confirm dapp security scanning still functions correctly with the real-time implementation

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. -->

### **Before**

N/A - Performance improvement in background process

### **After**

N/A - Performance improvement in background process

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable
- [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.


Diff:
diff --git a/app/store/migrations/082.test.ts b/app/store/migrations/082.test.ts
new file mode 100644
index 00000000000..f0b1965f13b
--- /dev/null
+++ b/app/store/migrations/082.test.ts
@@ -0,0 +1,154 @@
+import migrate from './082';
+import { ensureValidState } from './util';
+import { captureException } from '@sentry/react-native';
+
+jest.mock('@sentry/react-native', () => ({
+  captureException: jest.fn(),
+}));
+
+jest.mock('./util', () => ({
+  ensureValidState: jest.fn(),
+}));
+
+const mockedCaptureException = jest.mocked(captureException);
+const mockedEnsureValidState = jest.mocked(ensureValidState);
+
+describe('Migration 082: Reset PhishingController phishingLists', () => {
+  beforeEach(() => {
+    jest.resetAllMocks();
+  });
+
+  it('returns state unchanged if ensureValidState fails', () => {
+    const state = { some: 'state' };
+
+    mockedEnsureValidState.mockReturnValue(false);
+
+    const migratedState = migrate(state);
+
+    expect(migratedState).toBe(state);
+    expect(mockedCaptureException).not.toHaveBeenCalled();
+  });
+
+  it('captures exception if engine state is invalid', () => {
+    const state = { invalidState: true };
+
+    mockedEnsureValidState.mockReturnValue(true);
+
+    const migratedState = migrate(state);
+
+    expect(migratedState).toEqual(state);
+    expect(mockedCaptureException).toHaveBeenCalledWith(expect.any(Error));
+    expect(mockedCaptureException.mock.calls[0][0].message).toContain(
+      'Migration 082: Invalid engine state structure',
+    );
+  });
+
+  it('captures exception if PhishingController state is invalid', () => {
+    const state = {
+      engine: {
+        backgroundState: {
+          // PhishingController is missing
+        },
+      },
+    };
+
+    mockedEnsureValidState.mockReturnValue(true);
+
+    const migratedState = migrate(state);
+
+    expect(migratedState).toEqual(state);
+    expect(mockedCaptureException).toHaveBeenCalledWith(expect.any(Error));
+    expect(mockedCaptureException.mock.calls[0][0].message).toContain(
+      'Migration 082: Invalid PhishingController state',
+    );
+  });
+
+  it('resets PhishingController phishingLists to empty array and stalelistLastFetched to 0 while preserving other fields', () => {
+    interface TestState {
+      engine: {
+        backgroundState: {
+          PhishingController: {
+            c2DomainBlocklistLastFetched: number;
+            phishingLists: string[];
+            whitelist: string[];
+            hotlistLastFetched: number;
+            stalelistLastFetched: number;
+            extraProperty?: string;
+          };
+          OtherController: {
+            shouldStayUntouched: boolean;
+          };
+        };
+      };
+    }
+
+    const state: TestState = {
+      engine: {
+        backgroundState: {
+          PhishingController: {
+            c2DomainBlocklistLastFetched: 123456789,
+            phishingLists: ['list1', 'list2'],
+            whitelist: ['site1', 'site2'],
+            hotlistLastFetched: 987654321,
+            stalelistLastFetched: 123123123,
+            extraProperty: 'should remain',
+          },
+          OtherController: {
+            shouldStayUntouched: true,
+          },
+        },
+      },
+    };
+
+    mockedEnsureValidState.mockReturnValue(true);
+
+    const migratedState = migrate(state) as typeof state;
+
+    // PhishingLists should be reset to empty array and stalelistLastFetched to 0
+    expect(migratedState.engine.backgroundState.PhishingController.phishingLists).toEqual([]);
+    expect(migratedState.engine.backgroundState.PhishingController.stalelistLastFetched).toBe(0);
+
+    // Other fields should remain unchanged
+    expect(migratedState.engine.backgroundState.PhishingController.c2DomainBlocklistLastFetched).toBe(123456789);
+    expect(migratedState.engine.backgroundState.PhishingController.whitelist).toEqual(['site1', 'site2']);
+    expect(migratedState.engine.backgroundState.PhishingController.hotlistLastFetched).toBe(987654321);
+    expect(migratedState.engine.backgroundState.PhishingController.extraProperty).toBe('should remain');
+
+    // Other controllers should remain untouched
+    expect(migratedState.engine.backgroundState.OtherController).toEqual({
+      shouldStayUntouched: true,
+    });
+
+    expect(mockedCaptureException).not.toHaveBeenCalled();
+  });
+
+  it('handles error during migration', () => {
+    // Create state with a PhishingController that throws when phishingLists is accessed
+    const state = {
+      engine: {
+        backgroundState: {
+          PhishingController: Object.defineProperty({}, 'phishingLists', {
+            get: () => {
+              throw new Error('Test error');
+            },
+            set: () => {
+              throw new Error('Test error');
+            },
+            configurable: true,
+            enumerable: true,
+          }),
+        },
+      },
+    };
+
+    mockedEnsureValidState.mockReturnValue(true);
+
+    const migratedState = migrate(state);
+
+    expect(migratedState).toEqual(state);
+    expect(mockedCaptureException).toHaveBeenCalledWith(expect.any(Error));
+    expect(mockedCaptureException.mock.calls[0][0].message).toContain(
+      'Migration 082: cleaning PhishingController state failed with error',
+    );
+  });
+}); 
\ No newline at end of file
diff --git a/app/store/migrations/082.ts b/app/store/migrations/082.ts
new file mode 100644
index 00000000000..96445fdf522
--- /dev/null
+++ b/app/store/migrations/082.ts
@@ -0,0 +1,64 @@
+import { hasProperty, isObject } from '@metamask/utils';
+import { ensureValidState } from './util';
+import { captureException } from '@sentry/react-native';
+
+/**
+ * Migration 82: Reset PhishingController phishingLists
+ *
+ * This migration resets only the phishingLists array in the PhishingController state
+ * while preserving all other state properties. This allows the app to rebuild the lists
+ * while maintaining user preferences and configuration.
+ */
+const migration = (state: unknown): unknown => {
+  const migrationVersion = 82;
+
+  if (!ensureValidState(state, migrationVersion)) {
+    return state;
+  }
+
+  try {
+    if (
+      !hasProperty(state, 'engine') ||
+      !isObject(state.engine) ||
+      !hasProperty(state.engine, 'backgroundState') ||
+      !isObject(state.engine.backgroundState)
+    ) {
+      captureException(
+        new Error(
+          `Migration 082: Invalid engine state structure`,
+        ),
+      );
+      return state;
+    }
+
+    if (
+      !hasProperty(state.engine.backgroundState, 'PhishingController') ||
+      !isObject(state.engine.backgroundState.PhishingController)
+    ) {
+      captureException(
+        new Error(
+          `Migration 082: Invalid PhishingController state: '${JSON.stringify(
+            state.engine.backgroundState.PhishingController,
+          )}'`,
+        ),
+      );
+      return state;
+    }
+
+    // Only reset the phishingLists field to an empty array
+    // while preserving all other fields
+    state.engine.backgroundState.PhishingController.phishingLists = [];
+    state.engine.backgroundState.PhishingController.stalelistLastFetched = 0;
+
+    return state;
+  } catch (error) {
+    captureException(
+      new Error(
+        `Migration 082: cleaning PhishingController state failed with error: ${error}`,
+      ),
+    );
+    return state;
+  }
+};
+
+export default migration; 
\ No newline at end of file
diff --git a/app/store/migrations/index.ts b/app/store/migrations/index.ts
index 39e9c61c5fb..9e9628ab18d 100644
--- a/app/store/migrations/index.ts
+++ b/app/store/migrations/index.ts
@@ -82,6 +82,7 @@ import migration78 from './078';
 import migration79 from './079';
 import migration80 from './080';
 import migration81 from './081';
+import migration82 from './082';
 
 // Add migrations above this line
 import { validatePostMigrationState } from '../validateMigration/validateMigration';
@@ -180,6 +181,7 @@ export const migrationList: MigrationsList = {
   79: migration79,
   80: migration80,
   81: migration81,
+  82: migration82,
 };
 
 // Enable both synchronous and asynchronous migrations


Reviews:
user: login: mindofmar
body: LGTM
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T21:00:33Z
user: login: sethkfman
body: LGTM
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T21:56:41Z
user: login: mindofmar
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T21:00:21Z
user: login: mindofmar
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T17:35:48Z
user: login: MarioAslau
body: This looks great! I'm looking forward to seeing how this change will impact our app Performance 🎉
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T12:57:05Z

Review comments:
diff_hunk: @@ -0,0 +1,63 @@
+import { hasProperty, isObject } from '@metamask/utils';
+import { ensureValidState } from './util';
+import { captureException } from '@sentry/react-native';
+
+/**
+ * Migration 74: Reset PhishingController phishingLists
+ *
+ * This migration resets only the phishingLists array in the PhishingController state
+ * while preserving all other state properties. This allows the app to rebuild the lists
+ * while maintaining user preferences and configuration.
+ */
+const migration = (state: unknown): unknown => {
+  const migrationVersion = 74;
+
+  if (!ensureValidState(state, migrationVersion)) {
+    return state;
+  }
+
+  try {
+    if (
+      !hasProperty(state, 'engine') ||
+      !isObject(state.engine) ||
+      !hasProperty(state.engine, 'backgroundState') ||
+      !isObject(state.engine.backgroundState)
+    ) {
+      captureException(
+        new Error(
+          `Migration 074: Invalid engine state structure`,
+        ),
+      );
+      return state;
+    }
+
+    if (
+      !hasProperty(state.engine.backgroundState, 'PhishingController') ||
+      !isObject(state.engine.backgroundState.PhishingController)
+    ) {
+      captureException(
+        new Error(
+          `Migration 074: Invalid PhishingController state: '${JSON.stringify(
+            state.engine.backgroundState.PhishingController,
+          )}'`,
+        ),
+      );
+      return state;
+    }
+
+    // Only reset the phishingLists field to an empty array
+    // while preserving all other fields
+    state.engine.backgroundState.PhishingController.phishingLists = [];
path: app/store/migrations/074.ts
user: login: mindofmar
body: Huge 🚀
created_at: 2025-04-10T20:59:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,154 @@
+import migrate from './079';
+import { ensureValidState } from './util';
+import { captureException } from '@sentry/react-native';
+
+jest.mock('@sentry/react-native', () => ({
+  captureException: jest.fn(),
+}));
+
+jest.mock('./util', () => ({
+  ensureValidState: jest.fn(),
+}));
+
+const mockedCaptureException = jest.mocked(captureException);
+const mockedEnsureValidState = jest.mocked(ensureValidState);
+
+describe('Migration 074: Reset PhishingController phishingLists', () => {
path: app/store/migrations/079.test.ts
user: login: mindofmar
body: ```suggestion
describe('Migration 079: Reset PhishingController phishingLists', () => {
```
created_at: 2025-05-15T20:59:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,64 @@
+import { hasProperty, isObject } from '@metamask/utils';
+import { ensureValidState } from './util';
+import { captureException } from '@sentry/react-native';
+
+/**
+ * Migration 74: Reset PhishingController phishingLists
+ *
+ * This migration resets only the phishingLists array in the PhishingController state
+ * while preserving all other state properties. This allows the app to rebuild the lists
+ * while maintaining user preferences and configuration.
+ */
+const migration = (state: unknown): unknown => {
+  const migrationVersion = 74;
path: app/store/migrations/079.ts
user: login: mindofmar
body: ```suggestion
  const migrationVersion = 79;
```

You have a lot of references to `74` throughout this PR - plz `ctrl + F` and fix 😉 
created_at: 2025-05-15T20:59:55Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-10T03:37:36Z
body: **CLA Signature Action:** All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-10T03:41:15Z
body: ## [<img alt="https://bitrise.io/" src="https://assets-global.website-files.com/5db35de024bb983af1b4e151/5e6f9ccc3e129dfd8a205e4e_Bitrise%20Logo%20-%20Eggplant%20Bg.png" height="20">](https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/6611b629-47a0-4dca-8f1e-f169c482f9a0) **Bitrise**

✅✅✅ `pr_smoke_e2e_pipeline` passed on Bitrise! ✅✅✅

Commit hash: fb4bf79b0062964ee1d0b9644c468dac0192ac46
Build link: https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/6611b629-47a0-4dca-8f1e-f169c482f9a0

>[!NOTE]
>- You can kick off another `pr_smoke_e2e_pipeline` on Bitrise by removing and re-applying the `Run Smoke E2E` label on the pull request

<!-- BITRISE_TAG -->
<!-- BITRISE_SUCCESS_TAG -->

<!-- fb4bf79b0062964ee1d0b9644c468dac0192ac46 -->
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-04-10T03:50:55Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=metamask-mobile&pullRequest=14573) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=metamask-mobile&pullRequest=14573&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=metamask-mobile&pullRequest=14573&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=metamask-mobile&pullRequest=14573&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=metamask-mobile&pullRequest=14573&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=metamask-mobile&pullRequest=14573&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=metamask-mobile&pullRequest=14573)


author_association: NONE
user: login: sethkfman
created_at: 2025-04-11T15:22:46Z
body: @mindofmar Does this need to be cherry-picked into 7.45.0? If so, can you add `cp-7.45.0` to the PR title
author_association: CONTRIBUTOR
user: login: hesterbruikman
created_at: 2025-05-01T13:34:44Z
body: @mindofmar Is there a dependency here to require a Do Not Merge label?
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-28T17:34:52Z
body: ## [<img alt="https://bitrise.io/" src="https://assets-global.website-files.com/5db35de024bb983af1b4e151/5e6f9ccc3e129dfd8a205e4e_Bitrise%20Logo%20-%20Eggplant%20Bg.png" height="20">](https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/c3bbf97c-63c2-411a-a952-2092698cedb3) **Bitrise**

❌❌❌ `pr_smoke_e2e_pipeline` failed on Bitrise! ❌❌❌

Commit hash: 4fa4c8d3982f24bc2c91c991f5f0229ae38f6d81
Build link: https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/c3bbf97c-63c2-411a-a952-2092698cedb3

>[!NOTE]
>- You can kick off another `pr_smoke_e2e_pipeline` on Bitrise by removing and re-applying the `Run Smoke E2E` label on the pull request

> [!TIP]
>- Check the [documentation](https://www.notion.so/metamask-consensys/Bitrise-Pipeline-Overview-43159500c43748a389556f0593e8834b#26052f2ea6e24f8c9cfdb57a7522dc1f) if you have any doubts on how to understand the failure on bitrise

<!-- BITRISE_TAG -->
<!-- BITRISE_FAIL_TAG -->

<!-- 4fa4c8d3982f24bc2c91c991f5f0229ae38f6d81 -->
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-05-28T17:46:26Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=metamask-mobile&pullRequest=14573) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=metamask-mobile&pullRequest=14573&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=metamask-mobile&pullRequest=14573&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=metamask-mobile&pullRequest=14573&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=metamask-mobile&pullRequest=14573&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=metamask-mobile&pullRequest=14573&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=metamask-mobile&pullRequest=14573)


author_association: NONE