Skip to content

perf(assets-controller): add hand-rolled aggregate3 codecs - #11078

Merged
Prithpal-Sooriya merged 5 commits into
mainfrom
feature/add-multicall-encodi-3af
Oct 28, 2026
Merged

Prithpal-Sooriya merged 5 commits into
mainfrom
feature/add-multicall-encodi-3af

Conversation

@Prithpal-Sooriya

@Prithpal-Sooriya Prithpal-Sooriya commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Explanation

Multicall3 aggregate3 balance batches currently go through the generic Ethers ABI coder. This adds fixed-layout encode and decode functions for that calldata shape and uses them on the balance hot path, following the approach in #11010. Initial research found this to be ~44x faster.

The previous Ethers functions remain exported as encodeAggregate3WithEthers, decodeAggregate3ResponseWithEthers, and decodeUint256WithEthers. Table-driven cases and 100 deterministic pseudo-random batches check that the new codecs match those Ethers results byte for byte, including empty, partial-word, and multiword call data.

https://www.loom.com/share/c696e7903eba4e46ae75e2b8984b0230

References

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

Test plan

  • yarn workspace @metamask/assets-controller exec vitest run --coverage.enabled=false src/data-sources/evm-rpc-services/clients/MulticallClient.test.ts (56 tests)
  • yarn oxlint on the changed MulticallClient files
  • yarn workspace @metamask/assets-controller run changelog:validate
  • Confirm the hand-rolled and Ethers codecs stay byte-identical for the pseudo-random cases in CI

Note

Medium Risk
Changes how on-chain balance batch calldata is built and parsed on every RPC refresh; parity tests reduce but do not eliminate risk from a hand-maintained ABI layout diverging from Ethers.

Overview
Multicall balance batches no longer encode/decode Multicall3 aggregate3 through the generic Ethers ABI coder on the hot path. A new aggregate3Codec module writes and reads the fixed aggregate3 layout directly (encodeAggregate3, decodeAggregate3Response, decodeUint256), and MulticallClient now calls those helpers when building and parsing balance multicalls.

The previous Ethers implementations remain as encodeAggregate3WithEthers, decodeAggregate3ResponseWithEthers, and decodeUint256WithEthers for reference and tests. aggregate3Codec.test.ts asserts byte-for-byte parity with Ethers (including 100 random batches and 300-call encode/decode cases). Package exports for encodeAggregate3 / decodeAggregate3Response now come from the codec module. An architecture decision doc and changelog entry document motivation (mobile encode/decode cost) and maintenance tradeoffs.

Reviewed by Cursor Bugbot for commit 08db40a. Bugbot is set up for automated code reviews on this repo. Configure here.

@Prithpal-Sooriya
Prithpal-Sooriya requested a review from a team as a code owner October 26, 2026 11:01
@Prithpal-Sooriya
Prithpal-Sooriya requested a review from a team as a code owner October 26, 2026 11:02
@Prithpal-Sooriya Prithpal-Sooriya added the area-performance Issues relating to slowness of app, cpu usage, and/or blank screens. label Oct 26, 2026
Encode and decode balance-batch aggregate3 calls without the Ethers ABI coder, and keep the Ethers codecs so tests can require the same bytes.

Co-authored-by: Cursor <cursoragent@cursor.com>
Prithpal-Sooriya and others added 4 commits October 26, 2026 21:20
…-encodi-3af

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	packages/assets-controller/CHANGELOG.md
The production batch cap is 300, and the random parity test only builds four calls.

Co-authored-by: Cursor <cursoragent@cursor.com>
Main released 20.0.0, so the aggregate3 entry has to stay above that version. The codec tests now only compare their output with Ethers.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Prithpal-Sooriya
Prithpal-Sooriya added this pull request to the merge queue Oct 28, 2026

@juanmigdr juanmigdr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

Merged via the queue into main with commit 5299dc5 Oct 28, 2026
52 checks passed
@Prithpal-Sooriya
Prithpal-Sooriya deleted the feature/add-multicall-encodi-3af branch October 28, 2026 16:03
Prithpal-Sooriya added a commit to Prithpal-Sooriya/metamask-mobile that referenced this pull request Nov 2, 2026
…38486)

<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
<!--
Please submit this PR as a draft initially.

Do not mark it as "Ready for review" until this PR meets the canonical
Definition of Ready For Review in `docs/readme/ready-for-review.md`.

In short: the template must be materially complete (not just section
titles
present), all status checks must be currently passing, and the only
expected
follow-up commits must be reviewer-driven.
-->
<!--
mms-check directive vocabulary — read by
.github/scripts/shared/pr-template-checks.ts
at module load to build the validation plan. Directives are invisible in
rendered
markdown and must NOT be removed or edited without updating the
validator registry.

  type=text           Section must contain non-placeholder prose.
  type=changelog      Section must have a valid CHANGELOG entry: line.
type=issue-link Section must have a Fixes:/Closes:/Refs: line with a
value.
type=manual-testing Section must have real testing steps or an explicit
N/A.
type=screenshot Section must have evidence (image/URL) or an explicit
N/A.
type=checklist Section must have all checkboxes consciously checked.
required=true|false Whether a missing/invalid section runs the validator
at all.
blocking=true|false Whether a failure of this check fails the CI
workflow.
Default: false — failures are shown as warnings in the sticky
                      comment but do not block the PR.

Sections without a directive are checked for structural presence only.
-->

## **Description**

<!-- mms-check: type=text required=true -->

Bumps `@metamask/assets-controller` from `^20.0.0` to `^20.0.2`.

`20.0.2` ([core#11078](MetaMask/core#11078))
keeps the 20.0.0 API. The only product change is a fix: balance batches
use hand-rolled Multicall3 `aggregate3` codecs, and the Ethers codecs
stay in place for a byte-identical comparison. `20.0.1` is included on
the way up. It moves the controller onto `@metamask/assets-controllers`
`^113.0.0`, `@metamask/transaction-controller` `^72.3.0`, and
`@metamask/permission-controller` `^14.1.0`.

Mobile already calls `AssetsController.syncAssetsPrices` on main, so
this PR does not change call sites. Companion ranges move so Yarn
resolves a single `@metamask/assets-controller@20.0.2`:

- `@metamask/bridge-controller` `^83.1.1` → `^83.1.2` (depends on
assets-controller `^20.0.1`)
- `@metamask/transaction-controller` `^72.2.1` → `^72.3.0`, and the
resolution pin `72.2.1` → `72.3.0`
- `@metamask/transaction-pay-controller` `^30.3.0` → `^30.3.1`

## **Changelog**

<!-- mms-check: type=changelog required=true blocking=true -->

CHANGELOG entry: null

## **Related issues**

<!-- mms-check: type=issue-link required=true -->

Refs: N/A — dependency bump with no linked issue. Follow-up to MetaMask#38308
and [core#11078](MetaMask/core#11078).

## **Manual testing steps**

<!-- mms-check: type=manual-testing required=true -->

N/A — no screen or copy change. `20.0.2` only replaces the Multicall3
balance-batch codec inside the controller, and the call sites already
use `syncAssetsPrices`.

## **Screenshots/Recordings**

<!-- mms-check: type=screenshot required=true -->

N/A — no user-visible UI change.

### **Before**

N/A

### **After**

N/A

## **Pre-merge author checklist**

<!-- mms-check: type=checklist required=true -->

- [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
- [x] 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.

#### Performance checks (if applicable)

- [x] I've tested on Android
  - Ideally on a mid-range device; emulator is acceptable
- [x] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [x] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example

For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).

## **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.

<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a
href="https://cursor.com/agents/bc-b8869cb8-6f26-4143-9bcb-78ef352a2bbc?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-b8869cb8-6f26-4143-9bcb-78ef352a2bbc&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Pulls in transaction-controller 72.3.0, which changes how type-4
smart-account upgrades are estimated and surfaced in activity, alongside
assets-controller balance-batch encoding changes.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
e03cd81. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
LabinatorSolutions pushed a commit to LabinatorSolutions/metamask-extension that referenced this pull request Nov 3, 2026
…47489)

<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
<!--
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**

Bumps `@metamask/assets-controller` from `^20.0.0` to `^20.0.2`.

`20.0.2` ([core#11078](MetaMask/core#11078))
keeps the 20.0.0 API. Balance batches use hand-rolled Multicall3
`aggregate3` codecs, and the Ethers codecs stay in place for a
byte-identical comparison. `20.0.1` is included on the way up and moves
the package onto `@metamask/assets-controllers` `^113.0.0`,
`@metamask/transaction-controller` `^72.3.0`, and
`@metamask/permission-controller` `^14.1.0`.

Companion ranges move so Yarn resolves a single
`@metamask/assets-controller@20.0.2`:

- `@metamask/assets-controllers` `^112.0.2` → `^113.0.0`.
`NftDetectionController` now requires `removeNft`, so initialization
forwards `nftController().removeNft`.
- `@metamask/bridge-controller` `^83.1.1` → `^83.1.2`
- `@metamask/transaction-pay-controller` `^30.3.0` → `^30.3.1`

LavaMoat policies pick up the permission-controller dedupe (`immer` is
now `@metamask/permission-controller>immer`). The 20.0.0 call-site
migration is already on main.

## **Changelog**

CHANGELOG entry: null

## **Related issues**

Refs: N/A — dependency bump with no linked issue. Follow-up to MetaMask#47400
and [core#11078](MetaMask/core#11078).

## **Manual testing steps**

1. Build and load this branch, then unlock a wallet that holds tokens on
Ethereum Mainnet.
2. Open the home asset list and confirm symbols, balances, and fiat
prices still appear.
3. Confirm NFT detection still starts when the preference is enabled.

<!--
## **Screenshots/Recordings**

### **Before**

### **After**
-->

## **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/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
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/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.

<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a
href="https://cursor.com/agents/bc-b8869cb8-6f26-4143-9bcb-78ef352a2bbc?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-b8869cb8-6f26-4143-9bcb-78ef352a2bbc&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches asset balance/NFT-related controller packages and LavaMoat
sandbox permissions; behavior changes live in upstream packages rather
than extension code in this diff.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
da12433. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Naz-Ovh pushed a commit to 0x-fork/metamask-core that referenced this pull request Nov 4, 2026
## Explanation

Release 1375.0.0, bumping `@metamask/assets-controller` from 20.0.1 to
20.0.2 (patch).

- Fixed: hand-rolled Multicall3 `aggregate3` codecs for balance batches
(MetaMask#11078)
- `@metamask/bridge-controller` and
`@metamask/transaction-pay-controller` only have their
`@metamask/assets-controller` dependency range bumped to `^20.0.2`; they
are intentionally not released.

## Checklist

- [x] `yarn changelog:validate` passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> This PR is release bookkeeping only; the balance-batch codec behavior
ships via the already-merged assets-controller 20.0.2 release, which
touches on-chain balance reads but is classified as a fix with parity
checks against Ethers.
> 
> **Overview**
> **Release 1375.0.0** cuts a patch release of
**`@metamask/assets-controller`** at **20.0.2** (monorepo root version
**1374.0.0 → 1375.0.0**). The new changelog entry documents the shipped
fix from [MetaMask#11078](MetaMask#11078): balance
batches use **hand-rolled Multicall3 `aggregate3` codecs**, with Ethers
codecs kept for byte-identical comparison.
> 
> **`@metamask/bridge-controller`** and
**`@metamask/transaction-pay-controller`** only update their
**`@metamask/assets-controller`** dependency to **`^20.0.2`** and adjust
changelogs; they are **not** version-bumped in this PR. **`yarn.lock`**
is refreshed accordingly.
> 
> There are **no application source changes** in this diff—versioning,
changelog, dependency ranges, and lockfile only.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
880c066. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Naz-Ovh pushed a commit to 0x-fork/metamask-core that referenced this pull request Nov 4, 2026
## Explanation

Re-lands the release that was reverted in MetaMask#11187 (the revert happened
because CI on main was broken; the CI issue has since been fixed via
MetaMask#11191, and Release/1375.0.0 was subsequently shipped by another team in
MetaMask#11192, so this release is now **Release/1376.0.0**).

To avoid cutting two separate releases, this PR also folds in the
`@metamask/profile-controller` patch release that was staged in MetaMask#11197
(that PR is now redundant and closed in favor of this one).

Two packages are released:

**`@metamask/assets-controller` 20.0.2 (patch)** — 20.0.2 was never
published to npm, so this re-release is safe.

- **Fixed:** hand-rolled Multicall3 `aggregate3` codecs for balance
batches (MetaMask#11078)

**`@metamask/profile-controller` 1.2.1 (patch)**

- **Added:** `clearState` method on `ProfileController` (MetaMask#11177)
- **Changed:** bump `@metamask/utils` from `^12.0.0` to `^12.0.1`
(MetaMask#11107)
- **Fixed:** `ProfileService.disconnectX` now calls `DELETE
profiles/x/account` instead of the non-existent `DELETE
profiles/{profileId}/x` endpoint, since the linked X account is resolved
server-side from the verified bearer token (MetaMask#11195)

All functional changes above are already merged on `main`; this diff
contains only version bumps and changelog finalization (plus a merge of
`main` to pick up MetaMask#11195).

- `@metamask/bridge-controller` and
`@metamask/transaction-pay-controller` only have their
`@metamask/assets-controller` dependency range bumped to `^20.0.2`; they
are intentionally not released.

## Checklist

- [x] `yarn changelog:validate` passes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-performance Issues relating to slowness of app, cpu usage, and/or blank screens.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants