diff --git a/code/l1-l2-tutorial/README.md b/code/l1-l2-tutorial/README.md new file mode 100644 index 00000000..c382de18 --- /dev/null +++ b/code/l1-l2-tutorial/README.md @@ -0,0 +1,107 @@ +# 🔓 L1 Access Key → L2 Vault Unlocker + +This tutorial demonstrates how to send an L1 transaction that triggers a state change on an L2 ZKsync contract using the **Bridgehub**. We'll deploy an +**AccessKey** contract on a local `anvil` node (L1) and a **Vault* +contract on a local `anvil-zksync` node (L2). The L1 contract acts as an access gate, unlocking the vault on L2. + +--- + +## ✨ What You'll Learn + +- How to deploy smart contracts on both L1 and L2 locally using `anvil-zksync` +- How to send L1 → L2 messages + +--- + +## 🔧 Setup + +1. **Install Foundry-ZKsync** + + Install `foundry-zksync` which ships with `anvil-zksync` [here](https://foundry-book.zksync.io/getting-started/installation#using-foundryup-zksync). + +2. **Install dependencies** + + Navigate to `l1-access`: + + ```bash + forge soldeer install + ``` + + Navigate to `l2-vault`: + + ```bash + forge soldeer install + ``` + +3. **Configure `.env`** + + ```env + PRIVATE_KEY=0x... + ACCESS_KEY_ADDRESS=0x... # deployed L1 AccessKey contract + VAULT_ADDRESS=0x... # deployed L2 Vault contract + BRIDGE_HUB_ADDRESS=0x... # ZKsync Bridgehub on L1 + L2_CHAIN_ID=260 # e.g. anvil-zksync chainID (Default 260) + ``` + + Run: + + ```bash + source .env + ``` + + To fetch the BridgeHub address: + + ```bash + curl --request POST \ + --url http://localhost:8011 \ + --header 'Content-Type: application/json' \ + --data '{ + "jsonrpc": "2.0", + "id": 1, + "method": "zks_getBridgehubContract", + "params": [] + }' + ``` + +--- + +## 🚀 Deploy + +### 1. Deploy AccessKey (L1) + +```bash +forge script script/DeployAccessKey.s.sol:DeployAccessKey \ + --rpc-url anvil-zksync-l1 \ + --broadcast \ + --private-key $PRIVATE_KEY +``` + +### 2. Deploy Vault (L2) + +Make sure to alias the AccessKey address when deploying to L2: + +```bash +forge script script/DeployVault.s.sol:DeployVault \ + --rpc-url anvil-zksync-l2 \ + --broadcast \ + --private-key $PRIVATE_KEY +``` + +--- + +## 🔄 Trigger L2 Call from L1 + +This script sends the L1→L2 message via Bridgehub: + +```bash +forge script script/UnlockVaultFromL1.s.sol:UnlockVaultFromL1 \ + --rpc-url anvil-zksync-l1 \ + --broadcast \ + --private-key $PRIVATE_KEY +``` + +It: + +- Encodes the `unlock()` call to L2 +- Estimates base cost using `l2TransactionBaseCost` +- Sends the message via `AccessKey.unlockVaultOnL2()` using Bridgehub diff --git a/code/l1-l2-tutorial/l1-access/.env-example b/code/l1-l2-tutorial/l1-access/.env-example new file mode 100644 index 00000000..1b6d6c87 --- /dev/null +++ b/code/l1-l2-tutorial/l1-access/.env-example @@ -0,0 +1,9 @@ +PRIVATE_KEY= +ACCESS_KEY_ADDRESS=0x700b6A60ce7EaaEA56F065753d8dcB9653dbAD35 +VAULT_ADDRESS=0xF9099bBDcc3Dd9d3DcBe1Be3d60883e6F630c3ca +BRIDGE_HUB_ADDRESS=0xce2e9d3977d271d274f1d8c65254895771a33ff5 +L2_CHAIN_ID=260 + +# Optional +#L2_GAS_LIMIT= +#L2_PUBDATA_BYTES_LIMIT= diff --git a/code/l1-l2-tutorial/l1-access/.gitignore b/code/l1-l2-tutorial/l1-access/.gitignore new file mode 100644 index 00000000..49862551 --- /dev/null +++ b/code/l1-l2-tutorial/l1-access/.gitignore @@ -0,0 +1,8 @@ +cache/ +out/ +.vscode +.idea +broadcast/ +zkout/ +.env +dependencies/* \ No newline at end of file diff --git a/code/l1-l2-tutorial/l1-access/foundry.toml b/code/l1-l2-tutorial/l1-access/foundry.toml new file mode 100644 index 00000000..5e5e75a9 --- /dev/null +++ b/code/l1-l2-tutorial/l1-access/foundry.toml @@ -0,0 +1,13 @@ +[profile.default] +src = "src" +out = "out" +libs = ["dependencies"] + +[dependencies] +forge-std = "1.9.6" +"@zksync-contracts" = "0.0.1" + +[rpc_endpoints] +anvil-zksync-l1="http://localhost:8012" + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/code/l1-l2-tutorial/l1-access/remappings.txt b/code/l1-l2-tutorial/l1-access/remappings.txt new file mode 100644 index 00000000..50e3f6fc --- /dev/null +++ b/code/l1-l2-tutorial/l1-access/remappings.txt @@ -0,0 +1,2 @@ +@zksync-contracts/=dependencies/@zksync-contracts-0.0.1/contracts/ +forge-std/=dependencies/forge-std-1.9.6/ diff --git a/code/l1-l2-tutorial/l1-access/script/DeployAccessKey.s.sol b/code/l1-l2-tutorial/l1-access/script/DeployAccessKey.s.sol new file mode 100644 index 00000000..67581b4e --- /dev/null +++ b/code/l1-l2-tutorial/l1-access/script/DeployAccessKey.s.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import "forge-std/src/Script.sol"; +import "../src/AccessKey.sol"; + +/// @notice Deploys the AccessKey contract to L1 +contract DeployAccessKey is Script { + function run() external { + vm.startBroadcast(); + + AccessKey accessKey = new AccessKey(); + + console2.log("AccessKey deployed at:", address(accessKey)); + + vm.stopBroadcast(); + } +} diff --git a/code/l1-l2-tutorial/l1-access/script/UnlockVaultFromL1.s.sol b/code/l1-l2-tutorial/l1-access/script/UnlockVaultFromL1.s.sol new file mode 100644 index 00000000..d4a6c095 --- /dev/null +++ b/code/l1-l2-tutorial/l1-access/script/UnlockVaultFromL1.s.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import "forge-std/src/Script.sol"; +import "forge-std/src/console.sol"; +import "../src/AccessKey.sol"; +import "@zksync-contracts/l1-contracts/bridgehub/IBridgehub.sol"; + +interface IVault { + function unlock() external; +} + +/// @title UnlockVaultFromL1 +/// @notice This script unlocks the vault on L2 by sending a cross-chain message from L1 +/// @dev The script uses the AccessKey contract to send the message to the BridgeHub +/// @dev Should not be used in production +contract UnlockVaultFromL1 is Script { + function run() external { + // Load env vars + address accessKeyAddress = vm.envAddress("ACCESS_KEY_ADDRESS"); + address vaultAddress = vm.envAddress("VAULT_ADDRESS"); + address bridgeHubAddress = vm.envAddress("BRIDGE_HUB_ADDRESS"); + uint256 chainId = vm.envUint("L2_CHAIN_ID"); + uint256 gasLimit = vm.envOr("L2_GAS_LIMIT", uint256(350_000)); + uint256 gasPerPubdataByteLimit = vm.envOr("L2_PUBDATA_BYTE_LIMIT", uint256(800)); + + vm.startBroadcast(); + + // Encode the calldata for the L2 vault's unlock() function + bytes memory unlockData = abi.encodeWithSelector(IVault.unlock.selector); + + // Get base cost from the BridgeHub contract + IBridgehub bridge = IBridgehub(bridgeHubAddress); + uint256 gasPrice = tx.gasprice; + + uint256 baseCost = bridge.l2TransactionBaseCost( + chainId, + gasPrice, + gasLimit, + gasPerPubdataByteLimit + ); + + // Call AccessKey to send the cross-chain message + AccessKey(accessKeyAddress).unlockVaultOnL2{ value: baseCost }( + chainId, + bridgeHubAddress, + vaultAddress, + unlockData, + gasLimit, + gasPerPubdataByteLimit, + baseCost + ); + + vm.stopBroadcast(); + } +} diff --git a/code/l1-l2-tutorial/l1-access/soldeer.lock b/code/l1-l2-tutorial/l1-access/soldeer.lock new file mode 100644 index 00000000..285a97c8 --- /dev/null +++ b/code/l1-l2-tutorial/l1-access/soldeer.lock @@ -0,0 +1,13 @@ +[[dependencies]] +name = "@zksync-contracts" +version = "0.0.1" +url = "https://soldeer-revisions.s3.amazonaws.com/@zksync-contracts/0_0_1_21-02-2025_14:54:41_v2-testnet-contracts.zip" +checksum = "4436901ebe1e2f4c5248e0314451731db85437694ee2a1fb168ccd2d9acd7419" +integrity = "2fdb134b27489797615361ffd0707c3f758c369eab25322784e9ab8513687455" + +[[dependencies]] +name = "forge-std" +version = "1.9.6" +url = "https://soldeer-revisions.s3.amazonaws.com/forge-std/1_9_6_01-02-2025_20:49:10_forge-std-1.9.zip" +checksum = "55f341818321b3f925161a72fd0dcd62e4a0a4b66785a7a932bf2bfaf96fb9d1" +integrity = "e9ecdc364d152157431e5df5aa041ffddbe9bb1c1ad81634b1e72df9e23814e8" diff --git a/code/l1-l2-tutorial/l1-access/src/AccessKey.sol b/code/l1-l2-tutorial/l1-access/src/AccessKey.sol new file mode 100644 index 00000000..be51303c --- /dev/null +++ b/code/l1-l2-tutorial/l1-access/src/AccessKey.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import { IBridgehub, L2TransactionRequestDirect } from "@zksync-contracts/l1-contracts/bridgehub/IBridgehub.sol"; + +contract AccessKey { + address public owner; + + constructor() { + owner = msg.sender; + } + + function unlockVaultOnL2( + uint256 chainId, + address bridgeHubAddress, + address vaultAddress, + bytes memory data, + uint256 gasLimit, + uint256 gasPerPubdataByteLimit, + uint256 cost + ) external payable { + require(msg.sender == owner, "Only owner can unlock"); + + IBridgehub bridgeHub = IBridgehub(bridgeHubAddress); + + // Construct the L2 transaction request struct + L2TransactionRequestDirect memory request = L2TransactionRequestDirect({ + chainId: chainId, + mintValue: msg.value, + l2Contract: vaultAddress, + l2Value: 0, + l2Calldata: data, + l2GasLimit: gasLimit, + l2GasPerPubdataByteLimit: gasPerPubdataByteLimit, + factoryDeps: new bytes[](0), + refundRecipient: msg.sender + }); + + // Make the cross-chain transaction call + bridgeHub.requestL2TransactionDirect{ value: msg.value }(request); + } +} diff --git a/code/l1-l2-tutorial/l2-vault/.env-example b/code/l1-l2-tutorial/l2-vault/.env-example new file mode 100644 index 00000000..f81ca5ee --- /dev/null +++ b/code/l1-l2-tutorial/l2-vault/.env-example @@ -0,0 +1,2 @@ +PRIVATE_KEY= +ACCESS_KEY_ADDRESS=0x700b6A60ce7EaaEA56F065753d8dcB9653dbAD35 diff --git a/code/l1-l2-tutorial/l2-vault/.gitignore b/code/l1-l2-tutorial/l2-vault/.gitignore new file mode 100644 index 00000000..49862551 --- /dev/null +++ b/code/l1-l2-tutorial/l2-vault/.gitignore @@ -0,0 +1,8 @@ +cache/ +out/ +.vscode +.idea +broadcast/ +zkout/ +.env +dependencies/* \ No newline at end of file diff --git a/code/l1-l2-tutorial/l2-vault/foundry.toml b/code/l1-l2-tutorial/l2-vault/foundry.toml new file mode 100644 index 00000000..86829374 --- /dev/null +++ b/code/l1-l2-tutorial/l2-vault/foundry.toml @@ -0,0 +1,17 @@ +[profile.default] +src = "src" +out = "out" +libs = ["dependencies"] + +[profile.default.zksync] +compile = true +startup = true +enable_eravm_extensions = true +suppressed_warnings = ["assemblycreate"] + +[dependencies] +forge-std = "1.9.6" +"@zksync-contracts" = "0.0.1" + +[rpc_endpoints] +anvil-zksync-l2="http://localhost:8011" diff --git a/code/l1-l2-tutorial/l2-vault/remappings.txt b/code/l1-l2-tutorial/l2-vault/remappings.txt new file mode 100644 index 00000000..83d7c3f7 --- /dev/null +++ b/code/l1-l2-tutorial/l2-vault/remappings.txt @@ -0,0 +1,2 @@ +@zksync-contracts-0.0.1/=dependencies/@zksync-contracts-0.0.1/ +forge-std/=dependencies/forge-std-1.9.6/ diff --git a/code/l1-l2-tutorial/l2-vault/script/DeployVault.s.sol b/code/l1-l2-tutorial/l2-vault/script/DeployVault.s.sol new file mode 100644 index 00000000..74a66175 --- /dev/null +++ b/code/l1-l2-tutorial/l2-vault/script/DeployVault.s.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import "forge-std/src/Script.sol"; +import "../src/Vault.sol"; + +contract DeployVault is Script { + function run() external { + // Replace the address below with the actual AccessKey address you copied after deploying it. + address l1AccessKey = 0x700b6A60ce7EaaEA56F065753d8dcB9653dbAD35; + + // Apply L1-to-L2 aliasing + address aliasedAccessKey = address(uint160(l1AccessKey) + uint160(0x1111000000000000000000000000000000001111)); + + vm.startBroadcast(); + new Vault(aliasedAccessKey); + vm.stopBroadcast(); + } +} diff --git a/code/l1-l2-tutorial/l2-vault/soldeer.lock b/code/l1-l2-tutorial/l2-vault/soldeer.lock new file mode 100644 index 00000000..285a97c8 --- /dev/null +++ b/code/l1-l2-tutorial/l2-vault/soldeer.lock @@ -0,0 +1,13 @@ +[[dependencies]] +name = "@zksync-contracts" +version = "0.0.1" +url = "https://soldeer-revisions.s3.amazonaws.com/@zksync-contracts/0_0_1_21-02-2025_14:54:41_v2-testnet-contracts.zip" +checksum = "4436901ebe1e2f4c5248e0314451731db85437694ee2a1fb168ccd2d9acd7419" +integrity = "2fdb134b27489797615361ffd0707c3f758c369eab25322784e9ab8513687455" + +[[dependencies]] +name = "forge-std" +version = "1.9.6" +url = "https://soldeer-revisions.s3.amazonaws.com/forge-std/1_9_6_01-02-2025_20:49:10_forge-std-1.9.zip" +checksum = "55f341818321b3f925161a72fd0dcd62e4a0a4b66785a7a932bf2bfaf96fb9d1" +integrity = "e9ecdc364d152157431e5df5aa041ffddbe9bb1c1ad81634b1e72df9e23814e8" diff --git a/code/l1-l2-tutorial/l2-vault/src/Vault.sol b/code/l1-l2-tutorial/l2-vault/src/Vault.sol new file mode 100644 index 00000000..e8e86207 --- /dev/null +++ b/code/l1-l2-tutorial/l2-vault/src/Vault.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +contract Vault { + address public accessKey; + bool public isUnlocked; + + constructor(address _accessKey) { + accessKey = _accessKey; + isUnlocked = false; + } + + function unlock() public { + require(msg.sender == accessKey, "Unauthorized caller"); + isUnlocked = true; + } +} diff --git a/content/tutorials/anvil-zksync-guide/10.index.md b/content/tutorials/anvil-zksync-guide/10.index.md new file mode 100644 index 00000000..19271c36 --- /dev/null +++ b/content/tutorials/anvil-zksync-guide/10.index.md @@ -0,0 +1,61 @@ +--- +title: Using Local Anvil-ZKsync for L1-L2 Testing and Debugging +description: Learn how to deploy and test cross-chain interactions locally with Anvil-ZKsync, enabling realistic L1-L2 communication for rapid iteration and debugging. +--- + +## Prerequisites + +- This tutorial uses Solidity. You should have some experience with the language. +- You must have [anvil](https://book.getfoundry.sh/getting-started/installation) installed. +- You must have [Foundry-ZKsync](https://foundry-book.zksync.io/getting-started/installation#using-foundryup-zksync) installed. + +## What You Will Deploy + +- An L1 contract (`AccessKey`) that sends cross-chain messages. +- An L2 contract (`Vault`) that responds to L1-initiated calls. + +## Why Use L1-L2 Local Testing? + +Local testing with Anvil-ZKsync offers a fast, controlled environment for developing and debugging cross-chain applications. It allows you to: + +- Simulate L1-L2 interactions without the cost of public testnets. +- Debug system log production and protocol upgrade flows. + +## Introduction to L1-L2 Anvil-ZKsync Environment + +Anvil-ZKsync provides a local environment mimicing ZKsync's L1-L2 setup: + +- **Local L1 with Anvil:** Anvil serves as your Ethereum-like L1 environment, fully supporting priority transactions and protocol upgrade flows. +- **Local L2 with Anvil-ZKsync:** L2 is replicated with a precomputed state and commitment values, which allows you to test L1-L2 message +passing and log generation. +- **Limitations:** The current setup does not support L1-L2 forking. + +## How It Will Work + +Here's the updated step-by-step "How it will work" section with the Anvil-ZKsync environment setup as step 1: + +--- + +## How it will work + +1. **Set Up the Anvil-ZKsync Environment:** + First, you'll spin up a local environment using Anvil-ZKsync that makes use of `anvil` as the L1. + +2. **Deploy the AccessKey Contract on L1:** + With your local environment running, you will deploy a simple AccessKey contract on L1. This + contract is responsible for sending cross-chain messages to L2. + +3. **Deploy the Vault Contract on L2:** + Next, you'll deploy a Vault contract on the L2 side. This contract includes an `unlock()` function that can only be triggered by a valid + L1-initiated message. + +4. **Send an L1→L2 Message:** + Using a Foundry script, you'll send a cross-chain message from the AccessKey contract on L1. This message is routed via the Bridgehub and + is intended to invoke the Vault contract’s `unlock()` function on L2. + +5. **Prove Message Inclusion on L2:** + After sending the message, you will verify its inclusion in an L2 batch by calling a proof function (such as `proveL2MessageInclusion`). + This step demonstrates how to generate and validate Merkle proofs for cross-chain messages. + +6. **Debug and Validate:** + Finally, you will inspect gas estimations, system logs. This allows you to debug the transaction flow and confirm that all components interact as expected. diff --git a/content/tutorials/anvil-zksync-guide/20.deploying-the-contracts.md b/content/tutorials/anvil-zksync-guide/20.deploying-the-contracts.md new file mode 100644 index 00000000..001860a4 --- /dev/null +++ b/content/tutorials/anvil-zksync-guide/20.deploying-the-contracts.md @@ -0,0 +1,281 @@ +--- +title: Testing L1-L2 transactions using Anvil-ZKsync +description: Learn how to build and deploy L1 and L2 contracts, test them locally, and initiate cross-chain transactions using the latest anvil-zksync features + +--- + +### **Setting Up Your Project** + +In this guide, we will set up a **local L1-L2 environment** using `anvil-zksync`, then create and deploy an **L1** contract (`AccessKey`) +and an **L2** contract (`Vault`). By the end of this +section, you will have a development environment ready for **contract development, testing, and deployment** on ZKsync. + +## **1. Setting Up the L1-L2 Local Environment** + +`anvil-zksync` is included with Foundry-ZKsync and provides a local environment +for testing L1-L2 contract interactions on ZKsync. You have two main ways to start the local network: + +1. **Spawn a Local L1 Node** + + ```bash + anvil-zksync --spawn-l1 + # or specify a port: + anvil-zksync --spawn-l1 9000 + ``` + + - This command launches a local L2 node along with an embedded local L1 node. + - By default, the L1 node runs on port **8012** unless you specify another port. + +2. **Connect to an External L1 Node** + - Start your own Anvil L1 node with unlimited request size: + + ```bash + anvil --no-request-size-limit + ``` + + - Then run `anvil-zksync` pointing to that L1 node: + + ```bash + anvil-zksync --external-l1 http://localhost:8545 + ``` + + - Note that the `--spawn-l1` and `--external-l1` flags are **mutually exclusive**; you can only use one at a time. + +### **Starting the Local Node** + +A quick way to get started is simply: + +```bash +anvil-zksync -vv --spawn-l1 +``` + +- **`-vv`**: Enables user EraVM logs, which can be helpful for debugging. +- **`--spawn-l1`**: Spawns a local L1 node alongside the L2 environment. + +When you run this command, you’ll see logs indicating your local L2 node is running, along with a list of prefunded accounts +(e.g., addresses with 10,000 ETH). You can use these accounts to deploy contracts and make transactions **without** using real funds. + +With this local environment set up, you are ready to build, deploy, and test your L1-L2 contracts! + +## **Project Initialization** + +To get started, **clone the template repository** into a new project folder called `l1-l2-template`: + +```sh +git clone git@github.com:dutterbutter/l1-l2-template.git +l1-l2-template +``` + +This template includes the **basic Foundry setup** needed to begin development. Here’s what you’ll find inside: + +### **What’s Missing?** + +The template does not yet include: + +✅ L1-L2 Interaction script + +We’ll add these components within this tutorial. + +## **Installing Dependencies** + +To implement initiate L1-L2 transactions, we need a access to the Bridgehub contract: + +- **`@zksync-contracts`** – Provides interfaces for interacting with ZKsync-specific smart contracts. + +### **Installing Dependencies with Soldeer** + +For dependency management, we will use [Soldeer](https://soldeer.xyz/), a **dedicated Solidity package manager**. If you +prefer, you can install dependencies via Git submodules using `forge install matter-labs/v2-testnet-contracts@beta`. + +However, for this guide, we will use **Soldeer** to install our required packages: + +```sh +cd l1-l2-template +forge soldeer install +``` + +### **What This Command Does:** + +- Creates a `dependencies/` folder to store the installed packages. +- Installs `@zksync-contracts`, and `forge-zksync-std`. +- Generates a `remappings.txt` file for clean import paths, simplifying contract development. + +With our project set up and dependencies installed, we are now ready to **deploy our contracts on our local environment**! 🚀 + +### Contract Code + +Next, let’s review our L1AccessKey contract called L1AccessKey.sol in the l1-access directory. This contract +demonstrates how to send transactions from L1 to L2 (for example, to unlock a vault on zkSync's L2) by interacting with the Bridgehub contract. + +::drop-panel +::panel{label="L1AccessKey.sol"} + +```solidity [l1-access/src/L1AccessKey.sol] +:code-import{filePath="l1-l2-tutorial/l1-access/src/L1AccessKey.sol"} +``` + +:: +:: + +### Understanding the `L1AccessKey` Contract + +Let's break down the `L1AccessKey` contract and explain how it works. + +### **Contract Overview** + +The L1AccessKey contract is designed to: + +1. Store an owner address on L1. +2. Allow only that owner to initiate a cross-chain transaction to L2. +3. Use `IBridgehub` interface to send a direct transaction request to an L2 contract. + +## **Key Components** + +1. `owner`: Stores the address of the account (or contract) that deployed and manages access to this contract. +2. `constructor()`: Sets the deploying account as the owner. +3. `unlockVaultOnL2(...)`: The primary function that uses `Bridgehub` to send a transaction to L2. + +### **Functions** + +#### `unlockVaultOnL2` + +```solidity +function unlockVaultOnL2( + uint256 chainId, + address bridgeHubAddress, + address vaultAddress, + bytes memory data, + uint256 gasLimit, + uint256 gasPerPubdataByteLimit, + uint256 cost +) external payable { +``` + +**How it works:** + +1. **Ownership Check:** Ensures only the contract owner can initiate L2 calls. +2. **Create a Bridgehub Instance:** We instantiate `IBridgehub` using the address passed in. +3. **Construct L2 Transaction:** We build an `L2TransactionRequestDirect` struct, specifying all the details +needed for the L2 call (destination L2 contract, calldata, gas limits, etc.). +4. **Send the Transaction:** We call `bridgeHub.requestL2TransactionDirect` and provide `msg.value` as ETH to pay for bridging costs on L2. + +> Note: `msg.value` (the native tokens sent with the transaction) is forwarded to `requestL2TransactionDirect`, +covering the cost of sending this transaction cross-chain. + +## Deploying Contracts + +Let’s review the `DeployAccessKey.s.sol` deployment script in the `script/` directory. This script shows how to +deploy our L1 contract so it can be used to trigger L2 calls via `Bridgehub`. + +```solidity [script/DeployAccessKey.s.sol] +:code-import{filePath="l1-l2-tutorial/l1-access/script/DeployAccessKey.s.sol"} +``` + +### Understanding the `DeployAccessKey.s.sol` Deployment Script + +This script uses Foundry’s Script functionality to broadcast transactions to the network and deploy the `AccessKey` contract (our L1AccessKey in practice). + +#### **Deploy** + +```solidity +AccessKey accessKey = new AccessKey(); +console2.log("AccessKey deployed at:", address(accessKey)); +``` + +- **Purpose:** Instantiates and deploys our `AccessKey` contract to the L1 network. +- **Result:** Outputs the deployed contract’s address, which we can then use for cross-chain interactions. + +### **Deploying to Local Anvil-Zksync Instance** + +With the local node running, navigate to the `l1-access/` directory to deploy your **L1** contract, `AccessKey.sol`, using the provided deployment +script in `script/`. + +```bash +forge script script/DeployAccessKey.s.sol:DeployAccessKey \ + --rpc-url anvil-zksync-l1 \ + --broadcast \ + --interactive 1 +``` + +**This script will**: + +1. Deploy the `AccessKey` contract to the **spawned L1 node** (default port **8012** if using `--spawn-l1`). + +After running the script, you’ll be prompted to enter a private key. Use one of the prefunded keys displayed when you launched `anvil-zksync`: + +```bash +Enter private key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +``` + +#### **Successful Script Execution Output** + +If the deployment is successful, you should see output similar to: + +```bash +✅ [Success] Hash: 0x06ed287a74d0adb673f05d1947bf4a1be95b7c58dbffebb3f8263b777ed50fd2 +Contract Address: 0x700b6A60ce7EaaEA56F065753d8dcB9653dbAD35 +Block: 87 +Paid: 0.001609861554230312 ETH (536687 gas * 2.999628376 gwei) +``` + +**Make sure to copy this newly deployed `AccessKey` contract address**, as we’ll need it to deploy our L2 Vault contract next. + +--- + +### **Deploying the L2 Vault** + +After deploying your **AccessKey** contract on L1, **copy its address**. Then open `l2-vault/script/DeployVault.s.sol` and update the following line +with your newly deployed address: + +```solidity +address l1AccessKey = 0x700b6A60ce7EaaEA56F065753d8dcB9653dbAD35; +``` + +With that done, navigate to the `l2-vault` directory and execute: + +```bash +forge script script/DeployVault.s.sol:DeployVault \ + --rpc-url anvil-zksync-l2 \ + --broadcast \ + --interactive 1 +``` + +**This script will**: + +1. **Deploy** the `Vault` contract to the **running L2 node**. +2. Prompt you for a private key; use one of the prefunded keys from your `anvil-zksync` session. + +```bash +Enter private key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +``` + +#### **Successful Script Execution Output** + +If the deployment is successful, you should see: + +```bash +##### 260 +✅ [Success] Hash: 0xeb449568fa013508041294de83ac4cf3b246629ab41648bbaadd4d5fa232b764 +Contract Address: 0xF9099bBDcc3Dd9d3DcBe1Be3d60883e6F630c3ca +Block: 5 +Paid: 0.00012705046125 ETH (2807745 gas * 0.04525 gwei) + +✅ Sequence #1 on 260 | Total Paid: 0.00012705046125 ETH (2807745 gas * avg 0.04525 gwei) +``` + +Congratulations! You now have: + +- `AccessKey` deployed on our local L1 node +- `Vault` deploy on on our local L2 node + +Next, let’s interact with the L2 contract from L1. + +--- + +### **Next Steps: Interacting with the L2 Contract from L1** + +Now that we have deployed both the **AccessKey** contract on **L1** and the **Vault** contract on **L2**, the next step is to demonstrate how +**we can interact** with the L2 contract from L1 by calling `unlockVaultOnL2(...)`. We’ll walk through how to: + +- **Call L2 contract from L1** and supply the correct parameters. +- **Review the anvil-zksync logs** to showcase what is happening. diff --git a/content/tutorials/anvil-zksync-guide/30.interacting-l1-l2.md b/content/tutorials/anvil-zksync-guide/30.interacting-l1-l2.md new file mode 100644 index 00000000..69cd6cd5 --- /dev/null +++ b/content/tutorials/anvil-zksync-guide/30.interacting-l1-l2.md @@ -0,0 +1,32 @@ +--- +title: +description: + +--- + +### **Sending L1-L2 Message** + +After running the command, you will be prompted to enter a private key. Select one from the list displayed when launching `anvil-zksync`: + +```bash +Enter private key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +``` + +### **What This Command Does** +✅ Calls + +--- + +## **Next Steps** + +🎯 **Verify Balance Changes** → After sending transactions and deploying contracts, check the paymaster's balance to confirm it covered the fees: + +```sh +cast balance 0xd9498989Fada9e78798F696B17Ab6B3b5Fe65FDF --rpc-url anvil-zksync | cast from-wei +``` + +📖 **Explore the ZKsync Paymaster Documentation** → Learn more about paymaster configurations and advanced usage in the +[ZKsync Paymaster Docs](https://docs.zksync.io/zksync-era/unique-features/paymaster). + +🚀 **Build on This** → Now that you’ve successfully deployed and interacted with a Gasless Paymaster, +consider extending its logic to include **custom validation rules, user whitelisting, or sponsorship models** for transactions! diff --git a/content/tutorials/anvil-zksync-guide/_dir.yml b/content/tutorials/anvil-zksync-guide/_dir.yml new file mode 100644 index 00000000..0a639bc9 --- /dev/null +++ b/content/tutorials/anvil-zksync-guide/_dir.yml @@ -0,0 +1,24 @@ +title: Local L1-L2 Messaging with Anvil-ZKsync +featured: true +authors: + - name: dustinbrickwood + url: https://github.com/dutterbutter + avatar: https://avatars.githubusercontent.com/u/29983536?v=4 +github_repo: https://github.com/ZKsync-Community-Hub +tags: + - anvil-zksync + - foundry-zksync + - L1-L2 communication + - smart contracts + - debugging +summary: Spin up a local environment to test L1→L2 interactions with Anvil-ZKsync. +description: Learn how to build, deploy, and test cross-chain contracts using Anvil-ZKsync. +what_you_will_learn: + - How to launch Anvil-ZKsync for cross-chain testing + - How to deploy contracts on both L1 and L2 locally + - How to send L1→L2 calls + - How to debug and use VM tracing locally +updated: 2025-03-25 +tools: + - Anvil-ZKsync + - Foundry-ZKsync