Skip to content

Commit 66c1b92

Browse files
adonesky1MajorLift
authored andcommitted
Fix issue where mutex is acquired after state is read in addToken resulting in overwritten state with batched addToken/watchAsset calls (#1768)
Fixes a subtle bug: when multiple `wallet_watchAsset` calls of ERC20 tokens are batched, only the last of the tokens added was getting added because the calls were not properly single threaded, since the mutex was acquired after reading state in the `addToken` call. ## TokensController ### Fixes - Fixes bug where batched addToken calls overwrite each other because mutex is acquired after reading state
1 parent 37fafbf commit 66c1b92

File tree

2 files changed

+85
-5
lines changed

2 files changed

+85
-5
lines changed

packages/assets-controllers/src/TokensController.test.ts

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import type { AddApprovalRequest } from '@metamask/approval-controller';
1+
import {
2+
ApprovalController,
3+
type AddApprovalRequest,
4+
type ApprovalControllerEvents,
5+
} from '@metamask/approval-controller';
26
import { ControllerMessenger } from '@metamask/base-controller';
37
import contractMaps from '@metamask/contract-metadata';
48
import {
@@ -60,8 +64,25 @@ describe('TokensController', () => {
6064
let preferences: PreferencesController;
6165
const messenger = new ControllerMessenger<
6266
ApprovalActions,
67+
ApprovalControllerEvents
68+
>();
69+
70+
const approvalControllerMessenger = messenger.getRestricted({
71+
name: 'ApprovalController',
72+
allowedActions: ['ApprovalController:addRequest'],
73+
});
74+
75+
const approvalController = new ApprovalController({
76+
messenger: approvalControllerMessenger,
77+
showApprovalRequest: jest.fn(),
78+
typesExcludedFromRateLimiting: [ApprovalType.WatchAsset],
79+
});
80+
81+
const tokensControllerMessenger = messenger.getRestricted<
82+
typeof controllerName,
83+
ApprovalActions['type'],
6384
never
64-
>().getRestricted<typeof controllerName, ApprovalActions['type'], never>({
85+
>({
6586
name: controllerName,
6687
allowedActions: ['ApprovalController:addRequest'],
6788
}) as TokensControllerMessenger;
@@ -93,7 +114,7 @@ describe('TokensController', () => {
93114
},
94115
getERC20TokenName: sinon.stub(),
95116
getNetworkClientById: sinon.stub() as any,
96-
messenger,
117+
messenger: tokensControllerMessenger,
97118
});
98119
});
99120

@@ -1094,6 +1115,7 @@ describe('TokensController', () => {
10941115
image: 'image',
10951116
name: undefined,
10961117
};
1118+
10971119
createEthersStub = stubCreateEthers(tokensController, false);
10981120
});
10991121

@@ -1365,6 +1387,62 @@ describe('TokensController', () => {
13651387

13661388
generateRandomIdStub.mockRestore();
13671389
});
1390+
1391+
it('stores multiple tokens from a batched watchAsset confirmation screen correctly when user confirms', async function () {
1392+
const generateRandomIdStub = jest
1393+
.spyOn(tokensController, '_generateRandomId')
1394+
.mockImplementationOnce(() => requestId)
1395+
.mockImplementationOnce(() => '67890');
1396+
1397+
const acceptedRequest = new Promise<void>((resolve) => {
1398+
tokensController.subscribe((state) => {
1399+
if (
1400+
state.allTokens?.[ChainId.mainnet]?.[interactingAddress].length ===
1401+
2
1402+
) {
1403+
resolve();
1404+
}
1405+
});
1406+
});
1407+
1408+
const anotherAsset = {
1409+
address: '0x000000000000000000000000000000000000ABcD',
1410+
decimals: 18,
1411+
symbol: 'TEST',
1412+
image: 'image2',
1413+
name: undefined,
1414+
};
1415+
1416+
tokensController.watchAsset({ asset, type, interactingAddress });
1417+
tokensController.watchAsset({
1418+
asset: anotherAsset,
1419+
type,
1420+
interactingAddress,
1421+
});
1422+
1423+
await approvalController.accept(requestId);
1424+
await approvalController.accept('67890');
1425+
await acceptedRequest;
1426+
1427+
expect(
1428+
tokensController.state.allTokens[ChainId.mainnet][interactingAddress],
1429+
).toHaveLength(2);
1430+
expect(
1431+
tokensController.state.allTokens[ChainId.mainnet][interactingAddress],
1432+
).toStrictEqual([
1433+
{
1434+
isERC721: false,
1435+
aggregators: [],
1436+
...asset,
1437+
},
1438+
{
1439+
isERC721: false,
1440+
aggregators: [],
1441+
...anotherAsset,
1442+
},
1443+
]);
1444+
generateRandomIdStub.mockRestore();
1445+
});
13681446
});
13691447

13701448
describe('onPreferencesStateChange', function () {

packages/assets-controllers/src/TokensController.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,8 +299,9 @@ export class TokensController extends BaseController<
299299
interactingAddress?: string;
300300
networkClientId?: NetworkClientId;
301301
}): Promise<Token[]> {
302-
const { allTokens, allIgnoredTokens, allDetectedTokens } = this.state;
303302
const { chainId, selectedAddress } = this.config;
303+
const releaseLock = await this.mutex.acquire();
304+
const { allTokens, allIgnoredTokens, allDetectedTokens } = this.state;
304305
let currentChainId = chainId;
305306
if (networkClientId) {
306307
currentChainId =
@@ -309,7 +310,6 @@ export class TokensController extends BaseController<
309310

310311
const accountAddress = interactingAddress || selectedAddress;
311312
const isInteractingWithWalletAccount = accountAddress === selectedAddress;
312-
const releaseLock = await this.mutex.acquire();
313313

314314
try {
315315
address = toChecksumHexAddress(address);
@@ -321,8 +321,10 @@ export class TokensController extends BaseController<
321321
const newTokens: Token[] = [...tokens];
322322
const [isERC721, tokenMetadata] = await Promise.all([
323323
this._detectIsERC721(address, networkClientId),
324+
// TODO parameterize the token metadata fetch by networkClientId
324325
this.fetchTokenMetadata(address),
325326
]);
327+
// TODO remove this once this method is fully parameterized by networkClientId
326328
if (!networkClientId && currentChainId !== this.config.chainId) {
327329
throw new Error(
328330
'TokensController Error: Switched networks while adding token',

0 commit comments

Comments
 (0)