Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions code/l1-l2-tutorial/README.md
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions code/l1-l2-tutorial/l1-access/.env-example
Original file line number Diff line number Diff line change
@@ -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=
8 changes: 8 additions & 0 deletions code/l1-l2-tutorial/l1-access/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
cache/
out/
.vscode
.idea
broadcast/
zkout/
.env
dependencies/*
13 changes: 13 additions & 0 deletions code/l1-l2-tutorial/l1-access/foundry.toml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions code/l1-l2-tutorial/l1-access/remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@zksync-contracts/=dependencies/@zksync-contracts-0.0.1/contracts/
forge-std/=dependencies/forge-std-1.9.6/
18 changes: 18 additions & 0 deletions code/l1-l2-tutorial/l1-access/script/DeployAccessKey.s.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}
56 changes: 56 additions & 0 deletions code/l1-l2-tutorial/l1-access/script/UnlockVaultFromL1.s.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}
13 changes: 13 additions & 0 deletions code/l1-l2-tutorial/l1-access/soldeer.lock
Original file line number Diff line number Diff line change
@@ -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"
42 changes: 42 additions & 0 deletions code/l1-l2-tutorial/l1-access/src/AccessKey.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 2 additions & 0 deletions code/l1-l2-tutorial/l2-vault/.env-example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
PRIVATE_KEY=
ACCESS_KEY_ADDRESS=0x700b6A60ce7EaaEA56F065753d8dcB9653dbAD35
8 changes: 8 additions & 0 deletions code/l1-l2-tutorial/l2-vault/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
cache/
out/
.vscode
.idea
broadcast/
zkout/
.env
dependencies/*
17 changes: 17 additions & 0 deletions code/l1-l2-tutorial/l2-vault/foundry.toml
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 2 additions & 0 deletions code/l1-l2-tutorial/l2-vault/remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@zksync-contracts-0.0.1/=dependencies/@zksync-contracts-0.0.1/
forge-std/=dependencies/forge-std-1.9.6/
19 changes: 19 additions & 0 deletions code/l1-l2-tutorial/l2-vault/script/DeployVault.s.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}
13 changes: 13 additions & 0 deletions code/l1-l2-tutorial/l2-vault/soldeer.lock
Original file line number Diff line number Diff line change
@@ -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"
17 changes: 17 additions & 0 deletions code/l1-l2-tutorial/l2-vault/src/Vault.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading
Loading