Asset Transfers
A guide for sending L2 -> L2 asset transfers with ZKsync Connect.
Choose between using Hardhat 3 + viem, Hardhat 3 + ethers, or with a Foundry test.
- Create a new project folder
mkdir hardhat-example cd hardhat-example
- Initialize a new Hardhat 3 project with Node Test Runner and Viem.
npx hardhat --init
- Install the
zksync-jsnpm package.npm install -D @matterlabs/zksync-js - Install OpenZeppelin Contracts.
npm install -D @openzeppelin/contracts - Configure the
hardhat.config.tsfile with the three local chains setup in the local setup, a rich wallet, andignition.requiredConfirmationsset to1.ignition: { requiredConfirmations: 1, }, networks: { localZKsyncOSL1: { type: 'http', chainType: 'l1', url: 'http://localhost:8545', accounts: ['0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110'], }, localZKsyncOSChain1: { type: 'http', chainType: 'generic', url: 'http://localhost:3050', accounts: ['0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110'], }, localZKsyncOSChain2: { type: 'http', chainType: 'generic', url: 'http://localhost:3051', accounts: ['0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110'], }, }, - Create a new file in the
contractsfolder calledInteropToken.sol.touch contracts/InteropToken.sol - Copy and paste the contract below into the
InteropToken.solfile.// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; contract InteropToken is ERC20, Ownable, ERC20Burnable { constructor(string memory name, string memory symbol) ERC20(name, symbol) Ownable(msg.sender) { _mint(msg.sender, 100 * 10 ** decimals()); } function mint(address to, uint256 amount) public onlyOwner { _mint(to, amount); } }
Path 1: Deploy the token on L1
- Create a new file in the
scriptsfolder calledinterop-asset-transfer.ts.touch scripts/interop-asset-transfer.ts - Copy and paste the script below into the
interop-asset-transfer.tsfile. - Run the script.
npx hardhat run scripts/interop-asset-transfer.ts
This script deploys the token on L1, bridges it to chain 6565, and then transfers it to chain 6566 with interop.
Path 2: Deploy the token on L2
- Create two new files in the
scriptsfolder calledinterop-asset-transfer.tsandinterop-asset-migration.ts.touch scripts/interop-asset-transfer.ts touch scripts/interop-asset-migration.ts - Create a new file in the
ignition/modulesfolder calledInteropToken.ts.touch ignition/modules/InteropToken.ts - Copy and paste the Ignition module below into
InteropToken.ts.import { buildModule } from '@nomicfoundation/hardhat-ignition/modules'; export default buildModule('InteropToken', (m) => { const interopToken = m.contract('InteropToken', ['Interop Token', 'ITK']); return { interopToken }; }); - Compile and deploy the token contract on chain
6565.npx hardhat compilenpx hardhat ignition deploy ignition/modules/InteropToken.ts --network localZKsyncOSChain1 - Save the deployed token address as an environment variable.
export INTEROP_TOKEN_ADDRESS=0x...
- Copy and paste the Gateway migration script below into
scripts/interop-asset-migration.ts.
- Run the migration script.
npx hardhat run scripts/interop-asset-migration.ts
- Copy and paste the script below into the
interop-asset-transfer.tsfile.
- Run the script.
npx hardhat run scripts/interop-asset-transfer.ts
You should see output similar to this:
Using InteropToken on localZKsyncOSChain1 at: 0x...
SourceBalance: 100000000000000000000n
✅ Created interop transaction.
✅ Bundle is finalized on source; root available on destination.
Finalize result: {
bundleHash: '0x...',
dstExecTxHash: '0x...'
}
Chain 6565 balance after interop: 99999999999999000000
Mapped token on chain 6566 after interop: 0x...
Chain 6566 balance after interop: 1000000