Skip to content

Commit a38cb00

Browse files
committed
fix: Delete invalid SelectedNetworkController state (#26428)
## **Description** The `SelectedNetworkController` state is cleared if any invalid `networkConfigurationId`s are found in state. We are seeing reports of this happening in production in v12.0.1. The suspected cause is `NetworkController` state corruption. We resolved a few cases of this in v12.0.1, but for users that were affected by this, the invalid IDs may have propogated to the `SelectedNetworkController` state already. That is what this migration intends to fix. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/26428?quickstart=1) ## **Related issues** Fixes #26309 ## **Manual testing steps** We don't have clear reproduction steps for the bug itself. To artificially reproduce the scenario by changing extension state, this should work: * Create a dev build from v12.0.2 * Install the dev build from the `dist/chrome` directory and proceed through onboarding * Visit the test dapp, refresh, and connect to it. * Run this command in the background console: ``` chrome.storage.local.get( null, (state) => { state.data.SelectedNetworkController.domains['https://metamask.github.io'] = '123'; chrome.storage.local.set(state, () => chrome.runtime.reload()); } ); ``` * The linked error should now appear in the console of the popup * Disable the extension * Switch to this branch and create a dev build * Enable and reload the extension * The error should no longer appear. ## **Screenshots/Recordings** N/A ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.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 - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.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.
1 parent 2106727 commit a38cb00

File tree

2 files changed

+381
-0
lines changed

2 files changed

+381
-0
lines changed

app/scripts/migrations/120.5.test.ts

+255
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import { cloneDeep } from 'lodash';
2+
import { migrate, version } from './120.5';
3+
4+
const oldVersion = 120.4;
5+
6+
describe('migration #120.5', () => {
7+
afterEach(() => {
8+
jest.resetAllMocks();
9+
});
10+
11+
it('updates the version metadata', async () => {
12+
const oldStorage = {
13+
meta: { version: oldVersion },
14+
data: {},
15+
};
16+
17+
const newStorage = await migrate(cloneDeep(oldStorage));
18+
19+
expect(newStorage.meta).toStrictEqual({ version });
20+
});
21+
22+
it('does nothing if SelectedNetworkController state is not set', async () => {
23+
const oldState = {
24+
NetworkController: {
25+
networkConfigurations: {
26+
123: {},
27+
},
28+
},
29+
};
30+
31+
const transformedState = await migrate({
32+
meta: { version: oldVersion },
33+
data: cloneDeep(oldState),
34+
});
35+
36+
expect(transformedState.data).toEqual(oldState);
37+
});
38+
39+
it('deletes the SelectedNetworkController state if it is corrupted', async () => {
40+
const oldState = {
41+
NetworkController: {
42+
networkConfigurations: {
43+
123: {},
44+
},
45+
},
46+
SelectedNetworkController: 'invalid',
47+
};
48+
49+
const transformedState = await migrate({
50+
meta: { version: oldVersion },
51+
data: cloneDeep(oldState),
52+
});
53+
54+
expect(transformedState.data).toEqual({
55+
NetworkController: {
56+
networkConfigurations: {
57+
123: {},
58+
},
59+
},
60+
});
61+
});
62+
63+
it('deletes the SelectedNetworkController state if it is missing the domains state', async () => {
64+
const oldState = {
65+
NetworkController: {
66+
networkConfigurations: {
67+
123: {},
68+
},
69+
},
70+
SelectedNetworkController: {
71+
somethingElse: {},
72+
},
73+
};
74+
75+
const transformedState = await migrate({
76+
meta: { version: oldVersion },
77+
data: cloneDeep(oldState),
78+
});
79+
80+
expect(transformedState.data).toEqual({
81+
NetworkController: {
82+
networkConfigurations: {
83+
123: {},
84+
},
85+
},
86+
});
87+
});
88+
89+
it('deletes the SelectedNetworkController state if the domains state is corrupted', async () => {
90+
const oldState = {
91+
NetworkController: {
92+
networkConfigurations: {
93+
123: {},
94+
},
95+
},
96+
SelectedNetworkController: {
97+
domains: 'invalid',
98+
},
99+
};
100+
101+
const transformedState = await migrate({
102+
meta: { version: oldVersion },
103+
data: cloneDeep(oldState),
104+
});
105+
106+
expect(transformedState.data).toEqual({
107+
NetworkController: {
108+
networkConfigurations: {
109+
123: {},
110+
},
111+
},
112+
});
113+
});
114+
115+
it('deletes the SelectedNetworkController state if NetworkController state is missing', async () => {
116+
const oldState = {
117+
SelectedNetworkController: {
118+
domains: {},
119+
},
120+
};
121+
122+
const transformedState = await migrate({
123+
meta: { version: oldVersion },
124+
data: cloneDeep(oldState),
125+
});
126+
127+
expect(transformedState.data).toEqual({});
128+
});
129+
130+
it('deletes the SelectedNetworkController state if NetworkController state is corrupted', async () => {
131+
const oldState = {
132+
NetworkController: 'invalid',
133+
SelectedNetworkController: {
134+
domains: {},
135+
},
136+
};
137+
138+
const transformedState = await migrate({
139+
meta: { version: oldVersion },
140+
data: cloneDeep(oldState),
141+
});
142+
143+
expect(transformedState.data).toEqual({
144+
NetworkController: 'invalid',
145+
});
146+
});
147+
148+
it('deletes the SelectedNetworkController state if NetworkController has no networkConfigurations', async () => {
149+
const oldState = {
150+
NetworkController: {},
151+
SelectedNetworkController: {
152+
domains: {},
153+
},
154+
};
155+
156+
const transformedState = await migrate({
157+
meta: { version: oldVersion },
158+
data: cloneDeep(oldState),
159+
});
160+
161+
expect(transformedState.data).toEqual({
162+
NetworkController: {},
163+
});
164+
});
165+
166+
it('deletes the SelectedNetworkController state if NetworkController networkConfigurations state is corrupted', async () => {
167+
const oldState = {
168+
NetworkController: { networkConfigurations: 'invalid' },
169+
SelectedNetworkController: {
170+
domains: {},
171+
},
172+
};
173+
174+
const transformedState = await migrate({
175+
meta: { version: oldVersion },
176+
data: cloneDeep(oldState),
177+
});
178+
179+
expect(transformedState.data).toEqual({
180+
NetworkController: { networkConfigurations: 'invalid' },
181+
});
182+
});
183+
184+
it('does nothing if SelectedNetworkController domains state is empty', async () => {
185+
const oldState = {
186+
NetworkController: { networkConfigurations: {} },
187+
SelectedNetworkController: {
188+
domains: {},
189+
},
190+
};
191+
192+
const transformedState = await migrate({
193+
meta: { version: oldVersion },
194+
data: cloneDeep(oldState),
195+
});
196+
197+
expect(transformedState.data).toEqual(oldState);
198+
});
199+
200+
it('does nothing if SelectedNetworkController domains state is valid', async () => {
201+
const oldState = {
202+
NetworkController: {
203+
networkConfigurations: {
204+
123: {},
205+
},
206+
},
207+
SelectedNetworkController: {
208+
domains: {
209+
'example1.test': '123',
210+
'example2.test': 'mainnet',
211+
'example3.test': 'goerli',
212+
'example4.test': 'sepolia',
213+
'example5.test': 'linea-goerli',
214+
'example6.test': 'linea-sepolia',
215+
'example7.test': 'linea-mainnet',
216+
},
217+
},
218+
};
219+
220+
const transformedState = await migrate({
221+
meta: { version: oldVersion },
222+
data: cloneDeep(oldState),
223+
});
224+
225+
expect(transformedState.data).toEqual(oldState);
226+
});
227+
228+
it('deletes the SelectedNetworkController state if an invalid networkConfigurationId is found', async () => {
229+
const oldState = {
230+
NetworkController: {
231+
networkConfigurations: {
232+
123: {},
233+
},
234+
},
235+
SelectedNetworkController: {
236+
domains: {
237+
'domain.test': '456',
238+
},
239+
},
240+
};
241+
242+
const transformedState = await migrate({
243+
meta: { version: oldVersion },
244+
data: cloneDeep(oldState),
245+
});
246+
247+
expect(transformedState.data).toEqual({
248+
NetworkController: {
249+
networkConfigurations: {
250+
123: {},
251+
},
252+
},
253+
});
254+
});
255+
});

app/scripts/migrations/120.5.ts

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { hasProperty, isObject } from '@metamask/utils';
2+
import { cloneDeep } from 'lodash';
3+
4+
type VersionedData = {
5+
meta: { version: number };
6+
data: Record<string, unknown>;
7+
};
8+
9+
export const version = 120.5;
10+
11+
/**
12+
* This migration removes invalid network configuration IDs from the SelectedNetworkController.
13+
*
14+
* @param originalVersionedData - Versioned MetaMask extension state, exactly
15+
* what we persist to dist.
16+
* @param originalVersionedData.meta - State metadata.
17+
* @param originalVersionedData.meta.version - The current state version.
18+
* @param originalVersionedData.data - The persisted MetaMask state, keyed by
19+
* controller.
20+
* @returns Updated versioned MetaMask extension state.
21+
*/
22+
export async function migrate(
23+
originalVersionedData: VersionedData,
24+
): Promise<VersionedData> {
25+
const versionedData = cloneDeep(originalVersionedData);
26+
versionedData.meta.version = version;
27+
transformState(versionedData.data);
28+
return versionedData;
29+
}
30+
31+
/**
32+
* A list of InfuraNetworkType values from extension v12.0.1
33+
* This version of the extension uses `@metamask/[email protected]`, which in turn uses
34+
* the types from `@metamask/[email protected]`
35+
*
36+
* See https://github.com/MetaMask/core/blob/34542cf6e808f294fd83c7c5f70d1bc7418f8a9e/packages/controller-utils/src/types.ts#L4
37+
*
38+
* Hard-coded here rather than imported so that this migration continues to work correctly as these
39+
* constants get updated in the future.
40+
*/
41+
const infuraNetworkTypes = [
42+
'mainnet',
43+
'goerli',
44+
'sepolia',
45+
'linea-goerli',
46+
'linea-sepolia',
47+
'linea-mainnet',
48+
];
49+
50+
/**
51+
* Remove invalid network configuration IDs from the SelectedNetworkController.
52+
*
53+
* @param state - The persisted MetaMask state, keyed by controller.
54+
*/
55+
function transformState(state: Record<string, unknown>): void {
56+
if (!hasProperty(state, 'SelectedNetworkController')) {
57+
return;
58+
}
59+
if (!isObject(state.SelectedNetworkController)) {
60+
console.error(
61+
`Migration ${version}: Invalid SelectedNetworkController state of type '${typeof state.SelectedNetworkController}'`,
62+
);
63+
delete state.SelectedNetworkController;
64+
return;
65+
} else if (!hasProperty(state.SelectedNetworkController, 'domains')) {
66+
console.error(
67+
`Migration ${version}: Missing SelectedNetworkController domains state`,
68+
);
69+
delete state.SelectedNetworkController;
70+
return;
71+
} else if (!isObject(state.SelectedNetworkController.domains)) {
72+
console.error(
73+
`Migration ${version}: Invalid SelectedNetworkController domains state of type '${typeof state
74+
.SelectedNetworkController.domains}'`,
75+
);
76+
delete state.SelectedNetworkController;
77+
return;
78+
}
79+
80+
if (!hasProperty(state, 'NetworkController')) {
81+
delete state.SelectedNetworkController;
82+
return;
83+
} else if (!isObject(state.NetworkController)) {
84+
console.error(
85+
new Error(
86+
`Migration ${version}: Invalid NetworkController state of type '${typeof state.NetworkController}'`,
87+
),
88+
);
89+
delete state.SelectedNetworkController;
90+
return;
91+
} else if (!hasProperty(state.NetworkController, 'networkConfigurations')) {
92+
delete state.SelectedNetworkController;
93+
return;
94+
} else if (!isObject(state.NetworkController.networkConfigurations)) {
95+
console.error(
96+
new Error(
97+
`Migration ${version}: Invalid NetworkController networkConfigurations state of type '${typeof state.NetworkController}'`,
98+
),
99+
);
100+
delete state.SelectedNetworkController;
101+
return;
102+
}
103+
104+
const validNetworkConfigurationIds = [
105+
...infuraNetworkTypes,
106+
...Object.keys(state.NetworkController.networkConfigurations),
107+
];
108+
const domainMappedNetworkConfigurationIds = Object.values(
109+
state.SelectedNetworkController.domains,
110+
);
111+
112+
for (const configurationId of domainMappedNetworkConfigurationIds) {
113+
if (
114+
typeof configurationId !== 'string' ||
115+
!validNetworkConfigurationIds.includes(configurationId)
116+
) {
117+
console.error(
118+
new Error(
119+
`Migration ${version}: Invalid networkConfigurationId found in SelectedNetworkController state: '${configurationId}'`,
120+
),
121+
);
122+
delete state.SelectedNetworkController;
123+
return;
124+
}
125+
}
126+
}

0 commit comments

Comments
 (0)