feat: migrate token balance migration tests to Anvil multichain harness#2109
Open
valera-grinenko-ai wants to merge 18 commits intomatter-labs:draft-v31from
Open
feat: migrate token balance migration tests to Anvil multichain harness#2109valera-grinenko-ai wants to merge 18 commits intomatter-labs:draft-v31from
valera-grinenko-ai wants to merge 18 commits intomatter-labs:draft-v31from
Conversation
bf3061a to
a05fd83
Compare
kelemeno
reviewed
Apr 6, 2026
| /// facet to process chain commitment data, which needs an empty priority tree | ||
| /// (i.e. a running sequencer). This function allows Anvil-based tests to simulate | ||
| /// the migration number change without a sequencer. | ||
| function setMigrationNumberForTesting(uint256 _chainId, uint256 _migrationNumber) external onlyUpgrader { |
Contributor
There was a problem hiding this comment.
its better to have a L2ChainAssetHandlerDev which inherits L2ChainAssetHandler and adds this function
kelemeno
reviewed
Apr 6, 2026
| // ── Interop attribute selectors (ERC-7786) ───────────────────── | ||
|
|
||
| // keccak256("interopCallValue(uint256)")[0:4] | ||
| export const INTEROP_CALL_VALUE_SELECTOR = "0x54b16529"; |
Contributor
There was a problem hiding this comment.
probably better to get them from the interfaces somhow
kelemeno
reviewed
Apr 6, 2026
| * L1 governance Forge script that generates a priority request to the GW chain. | ||
| * On Anvil we impersonate the L2ChainAssetHandler to call the bridgehub directly. | ||
| */ | ||
| export async function setGWBridgehubSettlementLayer(params: { |
Contributor
There was a problem hiding this comment.
can we just call that forge script instead? probably better to have everything done in the prod manner
a760807 to
f88dd7a
Compare
d195a22 to
195ab51
Compare
…ness Port all 27 interop-b integration tests from zksync-era into the era-contracts Anvil multichain testing framework (specs 07, 08, 09). Bundles (spec 07): single/multi/mixed direct and indirect call bundles, source-chain balance deductions, destination-chain balance deltas. Messages (spec 08): sendMessage API for base token and ERC20 transfers, with send-side and receive-side balance verification. Unbundle (spec 09): progressive unbundling, bundle/call status tracking, 7 negative-path revert checks, cross-chain verify+unbundle meta-bundle. DummyInteropRecipient contracts are deployed at test runtime using embedded creation bytecode. No changes to pre-generated chain states. Token balance migration tests will follow in a separate PR. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…interop tests Add a 5th L2 chain (chain 14) with a custom ERC20 base token to enable the 2 previously-skipped interop message tests (matter-labs#13, matter-labs#17 from source): - "sends base token to a chain with a different base token" (ETH goes through AssetRouter indirect call path) - "receives base token from sending chain as bridged ERC20" (ETH arrives as a wrapped ERC20 on the custom-base-token chain) Infrastructure changes: - AnvilChainConfig gains optional baseToken field - DeploymentRunner deploys TestnetERC20Token on L1 for custom-base-token chains - Gateway setup uses per-chain baseTokenAssetId for TBM (not hardcoded ETH) - Interop chain registrar validates registration without assuming ETH base token All 27 interop-b tests now fully covered — 0 skipped. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Regenerated v0.29.0 pre-generated chain states from the kl/generate-v29-state branch with chain 14 added. Chain 14 uses a custom ERC20 base token, matching its v0.31.0 configuration. This ensures the v29→v31 upgrade test covers all 5 L2 chains including the custom-base-token chain. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The v0.29.0 states were generated from kl/generate-v29-state which has v29 AdminFacet code (2-arg upgradeChainFromVersion, selector 0xfc57565f), but l1-deployment.toml set latest_protocol_version to 0x1f00000000 (v31). This caused DefaultChainUpgrade to use the 3-arg selector (0x3b6d7534) which isn't registered in the v29 AdminFacet, reverting with "F". Fix: set latest_protocol_version to 0x1d00000000 (v29) on the generation branch and regenerate all chain states including chain 14. Now the upgrade script correctly uses the 2-arg selector path for v29→v31 upgrades. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…m artifacts - Add trailing newline to chain-14.toml - Replace magic number 3 with callStarters.length in unbundle spec - Derive ERC-7786 attribute selectors from IERC7786Attributes ABI instead of hardcoding hex values; unify with token-transfer.ts which had a third approach (inline keccak256) - Load DummyInteropRecipient bytecode from forge artifacts via getCreationBytecode() instead of embedding 800 chars of hex - Add IERC7786Attributes and DummyInteropRecipient to ARTIFACTS registry Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Module organization: - Split 488-line interop-bundle-helper.ts into 3 focused modules: erc7930.ts (address encoding), interop-helpers.ts (RPC wrappers), balance-helpers.ts (assertions and balance utilities) - Deduplicate event extraction in token-transfer.ts — now uses sendInteropBundle()/executeBundle() instead of hand-rolled copies ERC-7930/7786 encoding: - Rewrite ERC-7930 encoding with string concatenation for clarity, matching temp-sdk.ts style from zksync-era - Use IERC7786Attributes.encodeFunctionData() for attribute encoding instead of manual selector+abiCoder; remove selector constants - Document why callStarters[].to uses chain-less encoding Test structure: - Make all tests self-contained (send + execute + verify per it() block) - Use exact balance assertions with gas cost from tx receipt - Extract assertion helpers: expectNativeSpend, expectBalanceDelta, expectRevert (with optional reason matching) - Standardize naming: interopFee, sourceTokenAddress, sourceAssetId - Rename helpers for accuracy: approveTokenForNtv, BalanceSnapshot, captureBalance Test coverage: - Add custom→era cross-base-token interop message test - Add replay protection test (executeBundle twice reverts) - Add executionAddress enforcement test (wrong sender reverts) - Add zero-call bundle test (documents protocol accepts empty bundles) - Add excess msg.value rejection test Code cleanup: - Remove dead code: FAILING_CALL_CONTRACT, extractBundleHash - Remove duplicated encodeEvmChain/encodeEvmAddress from token-transfer - Unify SendBundleResult/SendMessageResult into InteropSendResult - Fix cross-base-token tests to use before/after deltas not absolutes - Make destTokenAddress local per-test instead of shared mutable state Infrastructure hardening: - Fix Anvil process leak on failure in setup-and-dump-state.ts - Add missing port 4054 to cleanup.sh fallback list - Set FOUNDRY_PROFILE=anvil-interop in upgrade test - Replace deprecated fs.rmdirSync with fs.rmSync - Scope prettier to chain-states/ directory only - Improve storage slot override TODO tracking - Document chain-14.toml base_token_addr derivation - Fix baseToken JSDoc in AnvilChainConfig type Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The interop helpers load the IERC7786Attributes interface at module init time to derive ERC-7786 attribute selectors via encodeFunctionData. CI integration tests run with --no-compile and rely on zkstack-out/ for ABIs. Without this file, the import fails with ENOENT. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The CI integration-test job runs without forge build (no out/ directory). DummyInteropRecipient bytecode must be embedded since there is no compiled artifact available at runtime. This was the original design before the review refactored it to use getCreationBytecode(). Also remove DummyInteropRecipient from the ARTIFACTS registry and the unused getCreationBytecode import. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The check-zkstack-out CI job regenerates zkstack-out/ from scratch and compares against committed files. IERC7786Attributes was added to zkstack-out/ manually but not registered in the copy script, causing a mismatch. Add it to REQUIRED_CONTRACTS so it's generated consistently. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Set executionAddress = ANVIL_DEFAULT_ACCOUNT_ADDR and unbundlerAddress = ANVIL_ACCOUNT2_ADDR in sendAndPrepareBundle. This properly tests role separation — the executor and unbundler are now different accounts. Update all unbundleBundle calls to use ANVIL_ACCOUNT2_PRIVATE_KEY as the correct unbundler signer. The "wrong unbundler" test now uses the default account (the executor, not the unbundler). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add randomBigNumber(min, max) helper and replace fixed constants with per-test random amounts in all three interop specs. Each test now generates its own amount within a safe range, so tests don't pass only for specific hardcoded values. Ranges are kept small (gwei-scale for native, reasonable for ERC20) to avoid balance exhaustion while still being large enough to detect. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DummyInteropRecipient bytecode: - Store the full forge artifact (with bytecode) in zkstack-out/ so CI integration tests can load it without forge build - Update artifact loader to handle both formats: raw ABI arrays (existing zkstack-out convention) and full forge artifacts (with bytecode field) - Add DummyInteropRecipient to ARTIFACTS registry and copy-to-zkstack-out - Remove embedded hex bytecode from interop-helpers.ts expectRevert: - Now checks both the JS error message and on-chain revert data (error.error.data / error.data) for the expected reason string - Failure messages include excerpts of both message and data for debugging Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rebase onto latest draft-v31 (62de149) which regenerated v0.31.0 states due to the protocol ops tool PR (matter-labs#2097). Regenerated v0.31.0 states from scratch with chain 14 included. Chain 14's base_token_addr changed from 0xA4899D3... to 0x86A2EE8... due to upstream deploy nonce changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…verse flow Adds L2ChainAssetHandler.setMigrationNumberForTesting (onlyUpgrader) for reverse TBM testing on Anvil, plus 20-test TBM spec covering: - Pre-migration state (4 tests) - Forward TBM to gateway (8 tests) - Reverse TBM from gateway (8 tests) Updates zkstack-out, selectors, and AllContractsHashes for the contract change. Regenerated pre-generated chain states. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
a083757 to
3721720
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Port the complete Token Balance Migration (TBM) and cross-base-token interop tests from
zksync-erato the Anvil multichain testing framework. Stacked on #2108 (interop-b migration).All original tests are fully covered — 0 skipped.
Changes from base PR (#2108)
setMigrationNumberForTesting(test-only,onlyUpgrader)Contract change
Added
L2ChainAssetHandler.setMigrationNumberForTesting(uint256, uint256)— a test-only function gated byonlyUpgrader, following the existing pattern fromGWAssetTracker.setLegacySharedBridgeAddressForLocalTesting. This enables reverse TBM testing on Anvil where the production path (bridgeBurn→forwardedBridgeBurn) requires an empty priority tree (i.e. a running sequencer).Infrastructure: Chain 14 with custom base token
Added chain 14 (GW-settled, port 4054) with a custom ERC20 base token deployed on L1 during state generation. This enables the 2 cross-base-token interop message tests that were previously skipped because all chains shared ETH.
Key infrastructure changes:
AnvilChainConfiggains optionalbaseTokenfield ("custom"triggers ERC20 deploy)DeploymentRunnerdeploysTestnetERC20Tokenon L1 for custom-base-token chainsbaseTokenAssetIdfor TBM (generalized from hardcoded ETH)Test coverage
Spec 08 — Interop Messages (6 tests, was 4)
Spec 10 — TBM Lifecycle (20 tests)
Pre-migration (4): migration=0, NotInGatewayMode, execute/unbundle reverts on L1
Forward TBM (8): migration numbers, balance conservation, full TBM flow, idempotency, false assetId, non-migrated mismatch, unregistered chain
Reverse TBM (8): deposit populates GW balance, settlement layer change, bridgehub update, migration number set to 2,
initiateGatewayToL1MigrationOnGatewaydrains balance, GWAT=2, idempotency, L2 pending confirmationTest migration mapping — Interop Messages (source → target, all 18 tests)
Source:
core/tests/ts-integration/tests/interop-b-messages.test.tsTarget:
l1-contracts/test/anvil-interop/test/hardhat/08-interop-messages.spec.tsCan send cross chain messages(base token)sends a base token messageCan send cross chain messages(native ERC20)sends a native ERC20 token messageCan send cross chain messages(interop1 base token)sends base token to a chain with a different base tokenCan send cross chain messages(bridged ERC20)sends a bridged ERC20 token messageCan receive a message sending a base tokenreceives a message sending a base tokenCan receive a message sending a native ERC20 tokenreceives a message sending a native ERC20 tokenCan receive a message sending the base token from the sending chainreceives base token from sending chain as bridged ERC20Can receive a message sending a bridged tokenreceives a message sending a bridged tokenTest migration mapping — TBM (source → target, all 28 tests)
Source:
core/tests/highlevel-test-tools/tests/token-balance-migration.test.tsTarget:
l1-contracts/test/anvil-interop/test/hardhat/10-token-balance-migration.spec.tsCorrectly assigns chain token balancesL1AT migration number is 0 for direct-settled chain (ETH)Cannot initiate interop before migrating to gatewaycannot initiate interop before migrating to gateway (NotInGatewayMode)Can migrate both chains to GatewayCan deposit a token to the chain after migrating to gatewayL1 deposit to GW-settled chain populates GW chain balanceCannot initiate interop to non registered chainscannot initiate interop to non-registered chainsCan initiate token balance migration to Gatewaycan run TBM for test token (full initiate + finalize + relay flow)Cannot initiate interop for non migrated tokensnon-migrated tokens have assetMigrationNumber mismatchCannot withdraw tokens that have not been migratednon-migrated tokens have assetMigrationNumber mismatchCan finalize pending withdrawals after migrating to gatewayCannot initiate migration for a false assetIdcannot initiate migration for a false assetIdCan migrate token balances to gatewaycan run TBM for test token (full initiate + finalize + relay flow)Can withdraw tokens after migrating token balances to gatewayCorrectly assigns chain token balances after migratingL1AT and GWAT migration numbers >= 1+match for test token after TBMCan migrate the second chain from gatewaycan update GW L2Bridgehub settlement layer mapping+can set GW chain migration number to 2Can initiate interop to chains migrated from gatewayCan migrate the chain from gatewaycan change L2 settlement layer from GW back to L1setSettlementLayerViaBootloaderCannot execute interop bundle when settling on L1cannot execute interop bundle when settling on L1Cannot unbundle interop bundle when settling on L1cannot unbundle interop bundle when settling on L1Can withdraw tokens from the chainCan initiate token balance migration from Gatewaycan initiate reverse TBM on GW (drains chainBalance)initiateGatewayToL1MigrationOnGatewayCan deposit a token after migrating from gatewayL1 deposit to GW-settled chain populates GW chain balanceCannot finalize pending withdrawals before finalizing TBMreverse TBM is idempotent (re-running for drained asset reverts)Can migrate token balances to L1GWAT assetMigrationNumber is 2 after reverse TBMCorrectly assigns chain token balances after migrating to L1GWAT assetMigrationNumber is 2+L2 ETH assetMigrationNumber is still 1Can finalize pending withdrawals after migrating from gatewayNew tests (not in source)
sum of GW per-chain balances <= L1 GW chain balance (conservation)Files changed
contracts/core/chain-asset-handler/L2ChainAssetHandler.solsetMigrationNumberForTesting(test-only)scripts/copy-to-zkstack-out.tsAllContractsHashes.jsonselectorssetMigrationNumberForTestingzkstack-out/L2ChainAssetHandler.sol/test/anvil-interop/config/anvil-config.json"baseToken": "custom"test/anvil-interop/config/chain-14.tomltest/anvil-interop/src/core/types.tsbaseTokenfield on AnvilChainConfig,customBaseTokenson DeploymentStatetest/anvil-interop/src/deployment-runner.tstest/anvil-interop/src/deployers/gateway-setup.tstest/anvil-interop/src/deployers/interop-chain-registrar.tstest/anvil-interop/setup-and-dump-state.tstest/anvil-interop/test/hardhat/08-interop-messages.spec.tstest/anvil-interop/test/hardhat/10-token-balance-migration.spec.tstest/anvil-interop/src/helpers/harness-shims.tssetGWChainMigrationNumber,setGWBridgehubSettlementLayertest/anvil-interop/src/helpers/token-balance-migration-helper.tsmigrateTokenBalanceFromGWhelpertest/anvil-interop/src/core/const.tsSERVICE_TRANSACTION_SENDERconstanttest/anvil-interop/chain-states/v0.31.0/*.jsonTest plan
out/directory (simulates CI environment)calculate-hashes --check-onlypasses (foundry-zksync v0.1.5, Node 20)selectors --checkpasses🤖 Generated with Claude Code