Skip to content

Commit 281cb40

Browse files
authored
feat: replace viem with manual ABI encoding (#7839)
## Explanation The `MulticallClient` in `@metamask/assets-controller` previously used `viem` for ABI encoding and decoding of Multicall3 contract calls. This introduced a heavy dependency (~500KB+ bundled) for functionality that could be implemented with minimal code. This PR replaces `viem` with manual ABI encoding/decoding: - Added low-level hex manipulation helpers (`leftPad32`, `encodeUint256`, `encodeAddress`, etc.) - Implemented manual encoding for `balanceOf(address)`, `getEthBalance(address)`, and `aggregate3((address,bool,bytes)[])` function calls - Implemented manual decoding for `aggregate3` responses (`(bool,bytes)[]`) and `uint256` return values - Updated test helpers to also use manual encoding instead of `viem` This significantly reduces the package's dependency footprint while maintaining the same functionality. ## References - Continues work from #7677 (MulticallClient introduction) - Related to #7831 (isEnabled feature in same package) ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes the ABI encoding/decoding used for Multicall3 batching and balance parsing, which can impact token/native balance fetching if calldata or return-data handling differs. Test coverage was updated/expanded, but this still touches core RPC interaction paths. > > **Overview** > **Replaces `viem` with `@ethersproject/abi` for Multicall3 ABI work.** `MulticallClient` now uses `ethers` `Interface`/`defaultAbiCoder` to encode `balanceOf`, `getEthBalance`, and `aggregate3`, and to decode `aggregate3` responses. > > Updates uint256 decoding to treat empty/short return data as zero, adjusts tests to build mock responses via `ethers` encoding, and adds a round-trip test that `encodeAggregate3` calldata can be decoded. Removes `viem` from `@metamask/assets-controller` dependencies (and lockfile transitive deps) and notes the change in the changelog. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit ae46ab6. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 372d21e commit 281cb40

File tree

5 files changed

+96
-190
lines changed

5 files changed

+96
-190
lines changed

packages/assets-controller/CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616

1717
- Narrow `AssetsControllerState` types from `Json` to semantic types: `assetsMetadata``AssetMetadata`, `assetsBalance``AssetBalance`, `assetsPrice``AssetPrice`, `assetPreferences``AssetPreferences`, `customAssets``Caip19AssetId[]` ([#7777](https://github.com/MetaMask/core/pull/7777))
1818

19+
- Replace `viem` dependency with `@ethersproject/abi` for ABI encoding/decoding in `MulticallClient` ([#7839](https://github.com/MetaMask/core/pull/7839))
20+
1921
## [0.1.0]
2022

2123
### Added
@@ -27,7 +29,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2729
- Add batch utilities (`divideIntoBatches`, `reduceInBatchesSerially`) for processing arrays in batches ([#7677](https://github.com/MetaMask/core/pull/7677))
2830
- Add `TokenDetector` service for detecting ERC-20 tokens with non-zero balances on a chain ([#7683](https://github.com/MetaMask/core/pull/7683))
2931
- Add `BalanceFetcher` service for fetching token balances for user's imported/detected tokens ([#7684](https://github.com/MetaMask/core/pull/7684))
30-
- Add `viem` dependency for ABI encoding/decoding in MulticallClient
3132
- Add configurable polling intervals for `RpcDataSource` via `RpcDataSourceConfig` in `initDataSources` ([#7709](https://github.com/MetaMask/core/pull/7709))
3233
- Add comprehensive unit tests for data sources (`AccountsApiDataSource`, `BackendWebsocketDataSource`, `PriceDataSource`, `TokenDataSource`, `SnapDataSource`), `DetectionMiddleware`, and `AssetsController` ([#7714](https://github.com/MetaMask/core/pull/7714))
3334

@@ -38,7 +39,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3839
- Store native token metadata (type, symbol, name, decimals) in `assetsMetadata` derived from `NetworkController` chain status ([#7752](https://github.com/MetaMask/core/pull/7752))
3940
- `AccountsApiDataSource` now includes `assetsMetadata` in response with token info from V5 API ([#7752](https://github.com/MetaMask/core/pull/7752))
4041
- Bump `@metamask/keyring-controller` from `^25.0.0` to `^25.1.0` ([#7713](https://github.com/MetaMask/core/pull/7713))
41-
- Refactor `MulticallClient` to use viem for ABI encoding/decoding instead of manual implementation
4242
- Refactor `RpcDataSource` to delegate polling to `BalanceFetcher` and `TokenDetector` services ([#7709](https://github.com/MetaMask/core/pull/7709))
4343
- Refactor `BalanceFetcher` and `TokenDetector` to extend `StaticIntervalPollingControllerOnly` for independent polling management ([#7709](https://github.com/MetaMask/core/pull/7709))
4444

packages/assets-controller/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
},
5050
"dependencies": {
5151
"@ethereumjs/util": "^9.1.0",
52+
"@ethersproject/abi": "^5.7.0",
5253
"@ethersproject/providers": "^5.7.0",
5354
"@metamask/account-tree-controller": "^4.0.0",
5455
"@metamask/base-controller": "^9.0.0",
@@ -68,8 +69,7 @@
6869
"@metamask/utils": "^11.9.0",
6970
"async-mutex": "^0.5.0",
7071
"bignumber.js": "^9.1.2",
71-
"lodash": "^4.17.21",
72-
"viem": "^2.44.4"
72+
"lodash": "^4.17.21"
7373
},
7474
"devDependencies": {
7575
"@metamask/auto-changelog": "^3.4.4",

packages/assets-controller/src/data-sources/evm-rpc-services/clients/MulticallClient.test.ts

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import { defaultAbiCoder, Interface } from '@ethersproject/abi';
12
import type { Hex } from '@metamask/utils';
2-
import { encodeAbiParameters, parseAbiParameters } from 'viem';
33

44
import {
55
decodeAggregate3Response,
@@ -36,39 +36,37 @@ const MAINNET_CHAIN_ID: ChainId = '0x1' as ChainId;
3636
const UNSUPPORTED_CHAIN_ID: ChainId = '0xfffff' as ChainId;
3737

3838
// =============================================================================
39-
// HELPER FUNCTIONS
39+
// ABI ENCODING HELPERS FOR TESTS
4040
// =============================================================================
4141

4242
/**
43-
* Build a mock aggregate3 response using viem's ABI encoding.
44-
* Each result is (success: bool, returnData: bytes).
43+
* Build a mock aggregate3 response using ethers ABI encoding.
44+
* Encodes (bool success, bytes returnData)[]
4545
*
4646
* @param results - Array of result objects with success flag and optional balance
4747
* @returns The encoded aggregate3 response as hex
4848
*/
4949
function buildMockAggregate3Response(
5050
results: { success: boolean; balance?: string }[],
5151
): `0x${string}` {
52-
// Encode each result's returnData (balance as uint256)
53-
const resultsWithEncodedData = results.map((result) => {
54-
let returnData: `0x${string}` = '0x';
52+
// Build the results array with encoded returnData
53+
const encodedResults = results.map((result) => {
54+
let returnData: string = '0x';
5555
if (result.balance !== undefined) {
56-
// Encode the balance as a uint256
57-
returnData = encodeAbiParameters(parseAbiParameters('uint256'), [
58-
BigInt(result.balance),
59-
]);
56+
// Encode balance as uint256
57+
returnData = defaultAbiCoder.encode(['uint256'], [result.balance]);
6058
}
6159
return {
6260
success: result.success,
6361
returnData,
6462
};
6563
});
6664

67-
// Encode the full aggregate3 response: (bool success, bytes returnData)[]
68-
return encodeAbiParameters(
69-
parseAbiParameters('(bool success, bytes returnData)[]'),
70-
[resultsWithEncodedData],
71-
);
65+
// Encode the full response: (bool success, bytes returnData)[]
66+
return defaultAbiCoder.encode(
67+
['(bool success, bytes returnData)[]'],
68+
[encodedResults],
69+
) as `0x${string}`;
7270
}
7371

7472
// =============================================================================
@@ -687,6 +685,47 @@ describe('encodeAggregate3', () => {
687685
expect(result).toMatch(/^0x82ad56cb/u);
688686
expect(typeof result).toBe('string');
689687
});
688+
689+
it('should produce valid ABI-encoded calldata that can be decoded', () => {
690+
// Test with multiple calls to verify encoding is correct
691+
const calls = [
692+
{
693+
target: '0x1111111111111111111111111111111111111111' as Address,
694+
allowFailure: true,
695+
callData: '0xabcd' as Hex,
696+
},
697+
{
698+
target: '0x2222222222222222222222222222222222222222' as Address,
699+
allowFailure: false,
700+
callData: '0x1234567890' as Hex,
701+
},
702+
];
703+
704+
const result = encodeAggregate3(calls);
705+
706+
// Verify selector is correct (aggregate3)
707+
expect(result).toMatch(/^0x82ad56cb/u);
708+
709+
// Decode the calldata using the same interface to verify it's valid
710+
const multicall3Abi = new Interface([
711+
'function aggregate3((address target, bool allowFailure, bytes callData)[] calls) payable returns ((bool success, bytes returnData)[])',
712+
]);
713+
714+
const decoded = multicall3Abi.decodeFunctionData('aggregate3', result);
715+
const decodedCalls = decoded[0] as {
716+
target: string;
717+
allowFailure: boolean;
718+
callData: string;
719+
}[];
720+
721+
expect(decodedCalls).toHaveLength(2);
722+
expect(decodedCalls[0].target.toLowerCase()).toBe(calls[0].target);
723+
expect(decodedCalls[0].allowFailure).toBe(true);
724+
expect(decodedCalls[0].callData.toLowerCase()).toBe(calls[0].callData);
725+
expect(decodedCalls[1].target.toLowerCase()).toBe(calls[1].target);
726+
expect(decodedCalls[1].allowFailure).toBe(false);
727+
expect(decodedCalls[1].callData.toLowerCase()).toBe(calls[1].callData);
728+
});
690729
});
691730

692731
describe('decodeAggregate3Response', () => {

packages/assets-controller/src/data-sources/evm-rpc-services/clients/MulticallClient.ts

Lines changed: 33 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1+
import { Interface } from '@ethersproject/abi';
12
import type { Hex } from '@metamask/utils';
2-
import type { Abi } from 'viem';
3-
import { decodeFunctionResult, encodeFunctionData } from 'viem';
43

54
import type {
65
Address,
@@ -55,7 +54,7 @@ const MULTICALL3_ABI = [
5554
inputs: [{ name: 'addr', type: 'address' }],
5655
outputs: [{ name: 'balance', type: 'uint256' }],
5756
},
58-
] as const satisfies Abi;
57+
];
5958

6059
/**
6160
* ERC-20 ABI (subset for balanceOf).
@@ -68,7 +67,13 @@ const ERC20_ABI = [
6867
inputs: [{ name: 'account', type: 'address' }],
6968
outputs: [{ name: 'balance', type: 'uint256' }],
7069
},
71-
] as const satisfies Abi;
70+
];
71+
72+
/**
73+
* Interface instances for ABI encoding/decoding.
74+
*/
75+
const multicall3Interface = new Interface(MULTICALL3_ABI);
76+
const erc20Interface = new Interface(ERC20_ABI);
7277

7378
// =============================================================================
7479
// CONSTANTS
@@ -363,7 +368,7 @@ const MULTICALL3_ADDRESS_BY_CHAIN: Record<Hex, Hex> = {
363368
};
364369

365370
// =============================================================================
366-
// ENCODING/DECODING UTILITIES (using viem)
371+
// ENCODING/DECODING UTILITIES (using @ethersproject/abi)
367372
// =============================================================================
368373

369374
/**
@@ -373,11 +378,9 @@ const MULTICALL3_ADDRESS_BY_CHAIN: Record<Hex, Hex> = {
373378
* @returns The encoded call data.
374379
*/
375380
function encodeBalanceOf(accountAddress: Address): Hex {
376-
return encodeFunctionData({
377-
abi: ERC20_ABI,
378-
functionName: 'balanceOf',
379-
args: [accountAddress],
380-
});
381+
return erc20Interface.encodeFunctionData('balanceOf', [
382+
accountAddress,
383+
]) as Hex;
381384
}
382385

383386
/**
@@ -387,11 +390,9 @@ function encodeBalanceOf(accountAddress: Address): Hex {
387390
* @returns The encoded call data.
388391
*/
389392
function encodeGetEthBalance(accountAddress: Address): Hex {
390-
return encodeFunctionData({
391-
abi: MULTICALL3_ABI,
392-
functionName: 'getEthBalance',
393-
args: [accountAddress],
394-
});
393+
return multicall3Interface.encodeFunctionData('getEthBalance', [
394+
accountAddress,
395+
]) as Hex;
395396
}
396397

397398
/**
@@ -403,17 +404,13 @@ function encodeGetEthBalance(accountAddress: Address): Hex {
403404
export function encodeAggregate3(
404405
calls: readonly { target: Address; allowFailure: boolean; callData: Hex }[],
405406
): Hex {
406-
return encodeFunctionData({
407-
abi: MULTICALL3_ABI,
408-
functionName: 'aggregate3',
409-
args: [
410-
calls.map((call) => ({
411-
target: call.target,
412-
allowFailure: call.allowFailure,
413-
callData: call.callData,
414-
})),
415-
],
416-
});
407+
return multicall3Interface.encodeFunctionData('aggregate3', [
408+
calls.map((call) => ({
409+
target: call.target,
410+
allowFailure: call.allowFailure,
411+
callData: call.callData,
412+
})),
413+
]) as Hex;
417414
}
418415

419416
/**
@@ -428,16 +425,12 @@ export function decodeAggregate3Response(
428425
data: Hex,
429426
callCount: number,
430427
): { success: boolean; returnData: Hex }[] {
431-
const decoded = decodeFunctionResult({
432-
abi: MULTICALL3_ABI,
433-
functionName: 'aggregate3',
434-
data,
435-
});
436-
437-
// decoded is an array of { success: boolean, returnData: `0x${string}` }
438-
const results = decoded as readonly {
428+
const decoded = multicall3Interface.decodeFunctionResult('aggregate3', data);
429+
430+
// decoded[0] is the array of (success, returnData) tuples
431+
const results = decoded[0] as readonly {
439432
success: boolean;
440-
returnData: Hex;
433+
returnData: string;
441434
}[];
442435

443436
if (results.length !== callCount) {
@@ -446,7 +439,7 @@ export function decodeAggregate3Response(
446439

447440
return results.map((result) => ({
448441
success: result.success,
449-
returnData: result.returnData,
442+
returnData: result.returnData as Hex,
450443
}));
451444
}
452445

@@ -457,13 +450,12 @@ export function decodeAggregate3Response(
457450
* @returns The decoded balance as a string.
458451
*/
459452
function decodeUint256(data: Hex): string {
460-
const hexData = data.slice(2);
461-
if (hexData.length === 0) {
453+
if (data === '0x' || data.length < 66) {
454+
// Empty or invalid data; treat as 0
462455
return '0';
463456
}
464-
// Take first 64 chars (32 bytes) for uint256
465-
const normalizedHex = hexData.length > 64 ? hexData.slice(0, 64) : hexData;
466-
return BigInt(`0x${normalizedHex}`).toString();
457+
const decoded = erc20Interface.decodeFunctionResult('balanceOf', data);
458+
return decoded[0].toString();
467459
}
468460

469461
// =============================================================================

0 commit comments

Comments
 (0)