Foundry-ZKsync Book

Foundry-ZKsync is a smart contract development toolchain for ZKsync, built upon Foundry.

Foundry-ZKsync manages your dependencies, compiles your project, runs tests, deploys, and lets you interact with the chain from the command-line and via Solidity scripts.

⚠️ Alpha Stage: The project is in alpha, so you might encounter issues. For more information or reporting bugs, please visit the Foundry-ZKsync GitHub repository.

Sections

Getting Started

To get started with Foundry-ZKsync, install Foundry-ZKsync and set up your first project.

Projects

This section will give you an overview of how to create and work with existing projects.

ZKsync Specific

This section will give you an the necessary information to write and run tests for zkEVM.

Supported Commands Overview

This section has a comprehensive review of all the Foundry commands actually supported in the Foundry-ZKSync tool.

📖 Contributing

This book is actual work in progress as part of the global efforts to support Foundry in ZKSync. More information, sections and pages will be added as we the project progresses.

Feedback and contributions are welcome. You can contribute to this book on Foundry-ZKSync Book GitHub repository.


For general information about Foundry, see the Foundry Book.

Installation

Precompiled Binaries

Precompiled binaries can be downloaded from the GitHub releases page. For easier management, we recommend using Foundryup. Currently due to active development it’s recommended to use the latest nightly from the releases page.

Using Foundryup-zksync

Foundryup-zksync is the official installer for the Foundry-ZKsync toolchain. You can learn more about it here.

To install Foundryup-zksync, open your terminal and run the following command:

curl -L https://raw.githubusercontent.com/matter-labs/foundry-zksync/main/install-foundry-zksync | bash

This will install Foundryup-zksync. Simply follow the on-screen instructions, and the foundryup-zksync command will become available in your CLI.

Running foundryup-zksync automatically installs the latest nightly versions of the precompiled binaries, including forge and cast. Additionally, it fetches the most recent version of the precompiled binary anvil-zksync from the anvil-zksync releases.

For additional options, such as installing a specific version or commit, run foundryup-zksync --help.

ℹ️ Note

Currently only forge and cast are supported for ZKsync. Other commands retain their original behavior but may not work as intended.

ℹ️ Note

If you’re on Windows, you will need to install and use Git BASH or WSL, as your terminal, since Foundryup-zksync currently does not support Powershell or Cmd. Windows support is currently provided as best-effort.

Building from Source

Prerequisites

You’ll need the Rust compiler and Cargo, Rust’s package manager. The easiest way to install both is by using rustup.rs.

Foundry-ZKsync generally supports building only with the configured nightly Rust version. The presence of rust-toolchain file automatically downloads the correct nightly rust version when commands are run from the Foundry-ZKsync directory.

For Windows users, you’ll also need a recent version of Visual Studio, with the “Desktop Development With C++” workload installed.

Building

You can either use the different Foundryup-ZKsync flags:

foundryup-zksync --branch main
foundryup-zksync --path path/to/foundry-zksync

Alternatively, you can install via Cargo with the following command:

cargo install --git https://github.com/matter-labs/foundry-zksync --profile release --locked forge cast

Or, by manually building from a local copy of the Foundry-ZKsync repository:

# clone the repository
git clone https://github.com/matter-labs/foundry-zksync.git
cd foundry
# install Forge
cargo install --path ./crates/forge --profile release --force --locked
# install Cast
cargo install --path ./crates/cast --profile release --force --locked

CI Installation with GitHub Actions

The latest binaries for the appropriate architecture can be installed directly using the following GitHub Action:

steps:
    - name: Install Foundry-ZKsync
      uses: dutterbutter/foundry-zksync-toolchain@v1

For further details, visit the foundry-zksync-toolchain repository.

Using Foundry with Docker

ℹ️ Note

No prebuilt images are available for docker yet.

First Steps with Foundry-ZKsync

This section introduces the forge command-line tool. We will walk through creating a new project, compiling it, and running tests.

To start a new project with Foundry-ZKsync, use the forge init command:

$ forge init hello_foundry

Now, let’s explore the structure that forge has generated for us:

$ cd hello_foundry
$ tree . -d -L 1
.
├── lib
├── script
├── src
└── test

4 directories

You can compile the project using forge build --zksync:

$ forge build --zksync

Compiling 27 files with zksolc and solc 0.8.26
zksolc and solc 0.8.26 finished in 3.97s
Compiler run successful!

To run the tests, use the forge test --zksync command:

$ forge test --zksync
Compiling 25 files with Solc 0.8.27
Solc 0.8.27 finished in 902.81ms
Compiler run successful!

No files changed, compilation skipped

Ran 2 tests for test/Counter.t.sol:CounterTest
[PASS] testFuzz_SetNumber(uint256) (runs: 256, μ: 31210, ~: 31288)
[PASS] test_Increment() (gas: 31303)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 5.75ms (5.36ms CPU time)

Ran 1 test suite in 10.34ms (5.75ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)

💡 Tip

You can always view detailed help for any command or subcommand by appending --help to it.

For visual learners, be sure to check out these beginner tutorials.

Creating a New Project

To start a new project with Foundry-ZKsync, use forge init:

$ forge init hello_foundry

This creates a new directory hello_foundry from the default template. This also initializes a new git repository.

If you want to create a new project using a different template, you would pass the --template flag, like so:

$ forge init --template https://github.com/foundry-rs/forge-template hello_template

For now, let’s check what the default template looks like:

$ cd hello_foundry
$ tree . -d -L 1
.
├── lib
├── script
├── src
└── test

4 directories

The default template comes with one dependency installed: Forge Standard Library. This is the preferred testing library used for Foundry projects. Additionally, the template also comes with an empty starter contract and a simple test.

Let’s build the project:

$ forge build --zksync

Compiling 27 files with zksolc and solc 0.8.26
zksolc and solc 0.8.26 finished in 3.97s
Compiler run successful!

And run the tests:

$ forge test --zksync
Compiling 25 files with Solc 0.8.27
Solc 0.8.27 finished in 902.81ms
Compiler run successful!

No files changed, compilation skipped

Ran 2 tests for test/Counter.t.sol:CounterTest
[PASS] testFuzz_SetNumber(uint256) (runs: 256, μ: 31210, ~: 31288)
[PASS] test_Increment() (gas: 31303)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 5.75ms (5.36ms CPU time)

Ran 1 test suite in 10.34ms (5.75ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)

You’ll notice that two new directories have popped up: out, zkout and cache.

The out directory contains your EVM contract artifact, such as the ABI, the zkout directory contains the zkEVM contract artifacts, while the cache is used by forge to only recompile what is necessary.

Working on an Existing Project

Foundry makes developing with existing projects have no overhead.

For this example, we will use PaulRBerg’s foundry-template.

First, clone the project and run forge install inside the project directory.

$ git clone https://github.com/PaulRBerg/foundry-template
$ cd foundry-template 
$ forge install
$ bun install # install Solhint, Prettier, and other Node.js deps

We run forge install to install the submodule dependencies that are in the project.

To build, use forge build:

$ forge build --zksync

Compiling 28 files with zksolc and solc 0.8.25
zksolc and solc 0.8.25 finished in 6.15s
Compiler run successful!

And to test, use forge test:

$ forge test --zksync
Compiling 25 files with Solc 0.8.25
Solc 0.8.25 finished in 925.41ms
Compiler run successful!

No files changed, compilation skipped

Ran 3 tests for test/Foo.t.sol:FooTest
[PASS] testFork_Example() (gas: 3779)
[PASS] testFuzz_Example(uint256) (runs: 1000, μ: 213166, ~: 213166)
[PASS] test_Example() (gas: 215916)
Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 6.51s (6.51s CPU time)

Ran 1 test suite in 6.52s (6.51s CPU time): 3 tests passed, 0 failed, 0 skipped (3 total tests)

Clone a Verified Contract on Chain

To clone an on-chain verified contract as a Forge project, use forge clone, say WETH9 on Ethereum mainnet:

$ forge clone 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 WETH9

This creates a new directory WETH9, configures it as a foundry project and clones all the source code of the contract into it. This also initializes a new git repository.

Downloading the source code of 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 from Etherscan...
Initializing /home/zhan4987/WETH9...
Installing forge-std in /home/zhan4987/WETH9/lib/forge-std (url: Some("https://github.com/foundry-rs/forge-std"), tag: None)
Cloning into '/home/zhan4987/WETH9/lib/forge-std'...
remote: Enumerating objects: 2243, done.
remote: Counting objects: 100% (2238/2238), done.
remote: Compressing objects: 100% (778/778), done.
remote: Total 2243 (delta 1489), reused 2097 (delta 1391), pack-reused 5
Receiving objects: 100% (2243/2243), 649.07 KiB | 8.89 MiB/s, done.
Resolving deltas: 100% (1489/1489), done.
    Installed forge-std v1.8.1
    Initialized forge project
Collecting the creation information of 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 from Etherscan...
Waiting for 5 seconds to avoid rate limit...
[⠊] Compiling...
[⠒] Compiling 1 files with 0.4.19
[⠢] Solc 0.4.19 finished in 9.50ms
Compiler run successful!

The cloned Forge project comes with an additional .clone.meta metadata file besides those ordinary files that a normal Forge project has.

Let’s see what the .clone.meta file looks like:

{
  "path": "src/Contract.sol",
  "targetContract": "WETH9",
  "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
  "chainId": 1,
  "creationTransaction": "0xb95343413e459a0f97461812111254163ae53467855c0d73e0f1e7c5b8442fa3",
  "deployer": "0x4f26ffbe5f04ed43630fdc30a87638d53d0b0876",
  "constructorArguments": "0x",
  "storageLayout": {
    "storage": [],
    "types": {}
  }
}

clone.meta is a compact JSON data file that contains the information of the on-chain contract instance, e.g., contract address, constructor arguments, etc. More details of the metadata can be found in the reference.

Dependencies

Forge manages dependencies using git submodules by default, which means that it works with any GitHub repository that contains smart contracts.

Adding a dependency

To add a dependency, run forge install:

$ forge install transmissions11/solmate
Installing solmate in /tmp/tmp.ECnnuozGYk/deps/lib/solmate (url: Some("https://github.com/transmissions11/solmate"), tag: None)
    Installed solmate

This pulls the solmate library, stages the .gitmodules file in git and makes a commit with the message “Installed solmate”.

If we now check the lib folder:

$ tree lib -L 1
lib
├── forge-std
├── solmate
└── weird-erc20

3 directories, 0 files

We can see that Forge installed solmate!

By default, forge install installs the latest master branch version. If you want to install a specific tag or commit, you can do it like so:

$ forge install transmissions11/solmate@v7

Remapping dependencies

Forge can remap dependencies to make them easier to import. Forge will automatically try to deduce some remappings for you:

$ forge remappings
ds-test/=lib/solmate/lib/ds-test/src/
forge-std/=lib/forge-std/src/
solmate/=lib/solmate/src/
weird-erc20/=lib/weird-erc20/src/

These remappings mean:

  • To import from forge-std we would write: import "forge-std/Contract.sol";
  • To import from ds-test we would write: import "ds-test/Contract.sol";
  • To import from solmate we would write: import "solmate/Contract.sol";
  • To import from weird-erc20 we would write: import "weird-erc20/Contract.sol";

You can customize these remappings by creating a remappings.txt file in the root of your project.

Let’s create a remapping called solmate-utils that points to the utils folder in the solmate repository!

@solmate-utils/=lib/solmate/src/utils/

You can also set remappings in foundry.toml.

remappings = [
    "@solmate-utils/=lib/solmate/src/utils/",
]

Now we can import any of the contracts in src/utils of the solmate repository like so:

import {LibString} from "@solmate-utils/LibString.sol";

Updating dependencies

You can update a specific dependency to the latest commit on the version you have specified using forge update <dep>. For example, if we wanted to pull the latest commit from our previously installed master-version of solmate, we would run:

$ forge update lib/solmate

Alternatively, you can do this for all dependencies at once by just running forge update.

Removing dependencies

You can remove dependencies using forge remove <deps>..., where <deps> is either the full path to the dependency or just the name. For example, to remove solmate both of these commands are equivalent:

$ forge remove solmate
# ... is equivalent to ...
$ forge remove lib/solmate

Hardhat compatibility

Forge also supports Hardhat-style projects where dependencies are npm packages (stored in node_modules) and contracts are stored in contracts as opposed to src.

To enable Hardhat compatibility mode pass the --hh flag.

Soldeer as a Package Manager

As explained here, Foundry has been using git submodules to handle dependencies up until now.

The need for a native package manager started to emerge as projects became more complex.

A new approach has been in the making, soldeer.xyz, which is a Solidity native dependency manager built in Rust and open sourced (check the repository https://github.com/mario-eth/soldeer).

Initialize a new project

If you’re using Soldeer for the first time in a new Foundry project, you can use the init command to install a fresh instance of Soldeer, complete with the necessary configurations and the latest version of forge-std.

forge soldeer init

Adding a Dependency

Add a Dependency Stored in the Central Repository

To add a dependency, you can visit soldeer.xyz and search for the dependency you want to add (e.g., openzeppelin 5.0.2).

image

Then just run the forge command:

forge soldeer install @openzeppelin-contracts~5.0.2

This will download the dependency from the central repository and install it into a dependencies directory.

Soldeer can manage two types of dependency configuration: using soldeer.toml or embedded in the foundry.toml. In order to work with Foundry, you have to define the [dependencies] config in the foundry.toml. This will tell the soldeer CLI to define the installed dependencies there. E.g.

# Full reference https://github.com/foundry-rs/foundry/tree/master/crates/config

[profile.default]
auto_detect_solc = false 
bytecode_hash = "none" 
fuzz = { runs = 1_000 } 
libs = ["dependencies"] # <= This is important to be added
gas_reports = ["*"] 

[dependencies] # <= Dependencies will be added under this config
"@openzeppelin-contracts" = { version = "5.0.2" }
"@uniswap-universal-router" = { version = "1.6.0" }
"@prb-math" = { version = "4.0.2" }
forge-std = { version = "1.8.1" }

If the central repository does not have a certain dependency, you can install it by providing a zip archive link.

E.g.

forge soldeer install @custom-dependency~1.0.0 https://my-website.com/custom-dependency-1-0-0.zip

The above command will try to download the dependency from the provided link and install it as a normal dependency. For this, you will see in the config an additional field called path.

E.g.

[dependencies]
"@custom-dependency" = { version = "1.0.0", path = "https://my-website.com/custom-dependency-1-0-0.zip" }

Add a Dependency Stored in GIT

If you choose to use Git as a source for your dependencies — though we generally discourage this, since Git isn’t designed to be a dependency manager — you can provide the Git repository link as an additional argument. Soldeer will then automatically handle the installation using a Git subprocess. For example:

forge soldeer install forge-std~1.9.2 https://github.com/foundry-rs/forge-std.git

If you want to use a specific revision, branch, or tag, you can do so by appending the following arguments to the command: --rev/--tag/--branch

e.g.

forge soldeer install forge-std~1.9.2 https://github.com/foundry-rs/forge-std.git --rev 4695fac44b2934aaa6d7150e2eaf0256fdc566a7

Updating Dependencies

Because Soldeer specifies the dependencies in a config file (foundry or soldeer toml), sharing a dependency configuration within the team is much easier.

For example, having this Foundry config file in a git repository, one can pull the repository and then run forge soldeer update. This command will automatically install all the dependencies specified under the [dependencies] tag.

# Full reference https://github.com/foundry-rs/foundry/tree/master/crates/config

[profile.default]
auto_detect_solc = false 
bytecode_hash = "none" 
fuzz = { runs = 1_000 } 
libs = ["dependencies"] # <= This is important to be added
gas_reports = ["*"]

[dependencies] # <= Dependencies will be added under this config
"@openzeppelin-contracts" = { version = "5.0.2" }
"@uniswap-universal-router" = { version = "1.6.0" }
"@prb-math" = { version = "4.0.2" }
forge-std = { version = "1.8.1" }

Removing Dependencies

You can use forge soldeer uninstall DEPENDENCY.

Example: forge soldeer uninstall @openzeppelin-contracts. This will action will remove:

  • the config entry
  • the dependencies artifacts
  • the soldeer.lock entry
  • the remappings entry (txt or config remapping)

Additionally you can manually remove a dependency by just removing the artifacts: dependency files, config entry, remappings entry.

Remappings

The remappings are now fully configurable, the config TOML file (foundry.toml) accepts a [soldeer] field with the following options

[soldeer]
# whether soldeer manages remappings
remappings_generate = true

# whether soldeer re-generates all remappings when installing, updating or uninstalling deps
remappings_regenerate = false

# whether to suffix the remapping with the version: `name-a.b.c`
remappings_version = true

# a prefix to add to the remappings ("@" would give `@name`)
remappings_prefix = ""

# where to store the remappings ("txt" for `remappings.txt` or "config" for `foundry.toml`)
# ignored when `soldeer.toml` is used as config (uses `remappings.txt`)
remappings_location = "txt"

Installing dependencies of dependencies aka sub-dependencies

Whenever you install a dependency, that dependency might have other dependencies that need to be installed as well. Currently, you can handle this by either specifying the recursive_deps field as a configuration entry in the config file or by passing the --recursive-deps argument when running the install or update command. This will ensure that all necessary sub-dependencies are automatically pulled in. e.g.

[soldeer]
recursive_deps = true

Pushing a New Version to the Central Repository

Soldeer acts like npmjs/crates.io, encouraging all developers to publish their projects to the central repository.

To do that, you have to go to soldeer.xyz, create an account, verify it, then

image

Just add a new project

image

After the project is created, you can go into your project source and:

  • Create a .soldeerignore file that acts as a .gitignore to exclude files that aren’t needed. The .gitignore file is also respected.
  • Run forge soldeer login to log into your account.
  • Run forge soldeer push my-project~1.0.0 in your terminal in the directory that you want to push to the central repository associated with the project my-project at version 1.0.0.

If you want to push a specific directory and not the current directory your terminal is in, you can use forge soldeer push my-project~1.0.0 /path/to/directory.

Warning ⚠️

You are at risk to push sensitive files to the central repository that then can be seen by everyone. Make sure to exclude sensitive files in the .soldeerignore file. Furthermore, we’ve implemented a warning that it will be triggered if you try to push a project that contains any .dot files/directories. If you want to skip this warning, you can just use

forge soldeer push my-project~1.0.0 --skip-warnings

Dry-run

In case you want to simulate what would happen if you push a version, you can use the --dry-run flag. This will create a zip file that you can inspect before pushing it to the central repository.

forge soldeer push my-project~1.0.0 --dry-run

Login Data

By default, Soldeer saves the login token in the ~/.soldeer/.soldeer_login file, which is used to push files to the central repository. If you prefer to save the token in a different location, you can set the environment variable SOLDEER_LOGIN_FILE.

Warning ⚠️

  • Once a project is created, it cannot be deleted.
  • Once a version is pushed, it cannot be deleted.
  • You cannot push the same version twice.
  • The project name in the command that you run in the terminal must match the project name that you created on the Soldeer website.
  • We encourage everyone to use version pinning when importing them into the contracts, this will help with securing your code by knowing exactly what version of a dependency you are using. Furthermore, it will help security researchers in their work.
  • Make sure you delete this zip file before pushing the version if you run dry-run. e.g. instead of using import '@openzeppelin-contracts/token/ERC20.sol' you should do import '@openzeppelin-contracts-5.0.2/token/ERC20.sol'

What happens if a certain package is not present in the central repository?

  • If a certain package is not present in the central repository, you can open an issue in the Soldeer Repository and the team will look into adding it.
  • If you have a package that you want to use and it is not present in the central repository, you can push it to the central repository by following the steps above.

Project Layout

Forge is flexible on how you structure your project. By default, the structure is:

.
├── foundry.toml
├── lib
│   └── forge-std
│       ├── foundry.toml
│       ├── LICENSE-APACHE
│       ├── LICENSE-MIT
│       ├── package.json
│       ├── README.md
│       ├── scripts
│       ├── src
│       └── test
├── README.md
├── script
│   └── Counter.s.sol
├── src
│   └── Counter.sol
└── test
    └── Counter.t.sol

8 directories, 11 files
  • You can configure Foundry’s behavior using foundry.toml.
  • Remappings are specified in remappings.txt.
  • The default directory for contracts is src/.
  • The default directory for tests is test/, where any contract with a function that starts with test is considered to be a test.
  • Dependencies are stored as git submodules in lib/.

You can configure where Forge looks for both dependencies and contracts using the --lib-paths and --contracts flags respectively. Alternatively you can configure it in foundry.toml.

Combined with remappings, this gives you the flexibility needed to support the project structure of other toolchains such as Hardhat and Truffle.

For automatic Hardhat support you can also pass the --hh flag, which sets the following flags: --lib-paths node_modules --contracts contracts.

ZKSync Specifics

Execution Overview

A forge test begins its execution on the EVM, hence the need to compile solc artifacts. During test execution, the test can switchover to ZKsync context in multiple ways.

The following operations are performed during the switchover:

  1. All persisted_accounts storages are migrated to ZKsync storage.
  2. Any EVM bytecode deployed under the migrated account is replaced by its zksolc variant.
  3. Solidity globals such as block.number, address.balance on the test level (which executes in EVM context) return ZKsync values.
  4. The original EVM context (block environment) is preserved for a switch back from the ZKsync context.

Switching to ZKsync

Switching over to ZKsync context can be achieved in the following ways:

CLI Flags

In general, the shorthand --zksync flag compiles the sources for zksolc and does the switchover to ZKsync context on test execution. The flag is a shorthand alias for enabling the following flags:

  • --zk-startup - performs ZKsync switchover on test startup
  • --zk-compile - compiles the sources for zksolc

Forking

If during test execution, forking cheatcodes such as vm.selectFork or vm.createSelectFork are used to fork over to a ZKsync network, the execution switches to ZKsync context. The rpc endpoint is tested for zks_L1ChainId method, and if it exists, the rpc url is deemed to be a ZKsync-compatible endpoint.

Similarly, if the selected fork url is not a ZKsync endpoint, then the test execution is set to EVM context.

Cheatcode Override

A custom cheatcode vm.zkVm is provided to switch the test execution to ZKsync mode manually. Passing a value of true enables ZKsync mode, whereas false switches it back to EVM mode.

ℹ️ Note

Using --zksync is equivalent to having vm.zkVm(true) as the first statement in a test.

ZKSync mode

When a test is running in ZKsync mode, any CREATE or CALL instructions that are encountered within the scope of the test (which runs on EVM) are intercepted and simulated in zkEVM. For example, in the following scenario:

contract MyContract {
    function getNumber() public returns (uint256) {
        return 42;
    }
}

contract FooTest is Test {
    function testFoo() public {
        vm.roll(10);                                // EVM
        vm.assertEq(10, block.number);              // EVM
        MyContract testContract = new MyContract(); // zkEVM
        uint256 number = testContract.getNumber();  // zkEVM
        vm.assertEq(42, number);                    // EVM
    }
}

Upon running testFoo() with --zksync, it is initially run in Foundry’s EVM context. However, due to the presence of --zksync flag, the storage switchover to ZKsync context is performed immediately upon its execution.

The cheatcode vm.roll(10) is then intercepted within EVM, as are all cheatcodes, but the operation is applied on ZKsync storage. Similarly, the statement block.number also returns the ZKsync storage value.

Once we encounter new BlockEnv() which is a CREATE operation, we intercept this within the EVM, and execute it on the zkEVM instead, returning the result. Similarly, blockEnv.getBlockNumber(), which is a CALL operation, is executed on the zkEVM, and the result (here: 42) is stored in the variable.

It is to be noted that any nested instructions from the above calls will always be executed within the zkEVM, since the parent CREATE or CALL were dispatched to the zkEVM.

ℹ️ Note

Only CREATE and CALL operations are executed on the zkEVM from the test scope. However, once they are dispatched to zkEVM, any internal code will always be executed in zkEVM, where we do not support cheatcodes. There can not be any references to vm within the code that is executed in zkEVM. This is undefined behavior.

Compilation Overview

zksolc is the compiler used by ZKsync to convert solidity code to zkEVM-compatible bytecode. It uses the same input format as solc but the output bytecodes and their respective hashes. Internally it uses a custom-compiled solc

Dual Compilation

To allow switching back and forth between EVM and zkEVM as defined in the Execution Overview, we compile the same contract with solc and zksolc. This dual-compiled contract can then be freely translated between both environments as needed. As such every contract in Foundry ZKsync always has two bytecodes attached - EVM bytecode and zkEVM bytecode, which are not equivalent.

Limitations

See Compilation Limitations.

Configuration Overview

Foundry-ZKsync adds additional configuration options that can be specified in the foundry.toml.

These include the following:

Limitations

In the process of migrating contracts written for EVM to work on zkEVM, notable changes are necessary. These changes are primarily due to the fundamental incompatibility between EVM and zkEVM and as such cannot be ignored or circumvented in anyway. These constraints are usually enforced by the zkEVM’s bootloader, and can lead to panics if ignored.

Here we enlist the common limitations and their mitigation strategies, if any.

General Limitations

These limitations apply at all times when working within the ZKsync context.

Reserved Address Range

On zkEVM, address in the range [0..2^16-1] are reserved for kernel space. As such using these addresses, even for mocking, within a test may lead to undefined behavior. It is therefor recommended, to start the user address range from 65536 onwards.

contract FooTest is Test {
    function testFoo() public {
        vm.mockCall(
            address(0),     // invalid
            abi.encodeWithSelector(bytes4(keccak256("number()"))),
            abi.encode(5)
        );

        vm.mockCall(
            address(65536),     // valid
            abi.encodeWithSelector(bytes4(keccak256("number()"))),
            abi.encode(5)
        );
    }
}

Additionally during fuzz-testing, these addresses must be ignored. This can be done via either vm.assume(address(value) >= 65536) assertion, or by settings no_zksync_reserved_addresses = true in fuzz configuration.

Origin Address

While foundry allows mocking the tx.origin address as normal, zkEVM will fail all calls to it. As such the following code with not work:

library IFooBar {
    function number() return (uint8);
}

contract FooTest is Test {
    function testFoo() public {
        address target = tx.origin;

        vm.mockCall(
            address(target),     // invalid
            abi.encodeWithSelector(bytes4(keccak256("number()"))),
            abi.encode(5)
        );

        IFooBar(target).number() // will fail
    }
}

Bytecode Constraints

zkEVM asserts a bytecode to be valid if it satisfies the following constraints:

  • Has its length in bytes divisible by 32 (i.e. 32-byte words).
  • Has a length of less than 2^16 words.
  • Has an odd length in words.
contract FooTest is Test {
    function testFoo() public {
        // invalid, word-size of 1 byte
        vm.etch(address(65536), hex"00");

        // invalid, even number of words
        vm.etch(
            address(65536), 
            hex"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
        );

        // valid, 32-byte word, odd number of words
        vm.etch(
            address(65536), 
            hex"0000000000000000000000000000000000000000000000000000000000000000"
        );

    }
}

Bytecode Hash

Bytecode hashes output by zksolc are fundamentally different from the hash obtained via solc. The most glaring difference is the first (most-significant) byte denotes the version of the format, which at present is 1. This leads to all zksolc bytecode hashes to begin with 1, whereas solc bytecodes are merely the keccak hash of the bytecode.

Any code making assumptions about bytecode hashes around EVM-scope, would need to be migrated to accommodate for ZKsync’s bytecode hashes.

Address Derivation

zkEVM uses a different CREATE and CREATE2 address derivation strategy compared to EVM. This can lead to issues for tests that have the CREATE2 addresses hard-coded for EVM. These tests would therefore need to be updated to reflect the ZKsync derived addresses.

function create2Address(sender: Address, bytecodeHash: BytesLike, salt: BytesLike, input: BytesLike) {
  const prefix = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("zksyncCreate2"));
  const inputHash = ethers.utils.keccak256(input);
  const addressBytes = ethers.utils.keccak256(ethers.utils.concat([prefix, ethers.utils.zeroPad(sender, 32), salt, bytecodeHash, inputHash])).slice(26);
  return ethers.utils.getAddress(addressBytes);
}

function createAddress(sender: Address, senderNonce: BigNumberish) {
  const prefix = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("zksyncCreate"));
  const addressBytes = ethers.utils
    .keccak256(ethers.utils.concat([prefix, ethers.utils.zeroPad(sender, 32), ethers.utils.zeroPad(ethers.utils.hexlify(senderNonce), 32)]))
    .slice(26);

  return ethers.utils.getAddress(addressBytes);
}

Accessing Contract Bytecode and Hash

zkEVM does not allow obtaining bytecodes from address.code or computing their respective hashes, which will be raised as an error during compilation. This is particularly useful when computing CREATE2 addresses.

To circumvent this limitation, it is recommended to use the FFI functionality of cheatcodes:

contract Calculator {
    function add(uint8 a, uint8 b) return (uint8) {
        return a+b;
    }
}

contract FooTest is Test {
    function testFoo() public {
        string memory artifact = vm.readFile(
            "zkout/FooTest.sol/Calculator.json"
        );
        bytes32 bytecodeHash = vm.parseJsonBytes32(artifact, ".hash");
        bytes32 salt = 0x0000000000000000000000000000000000000001;
        
        ISystemContractDeployer deployer = ISystemContractDeployer(
            address(0x0000000000000000000000000000000000008006)
        );
        address addr = deployer.getNewAddressCreate2(
            address(this),
            salt,
            bytecodeHash,
            ""
        );
    }
}

Note, that this requires adding read permissions in foundry.toml:

[profile.default]
...
fs_permissions = [{ access = "read", path = "./zkout/FooTest.sol/Calculator.json"}]

Compilation Limitations

These limitations apply to zksolc compilation of source contracts.

Contract Bytecode Access

Contract bytecode cannot be accessed on zkEVM architecture, therefore EXTCODECOPY always produces a compile-time error with zksolc. As such using address(..).code in a solidity contract will produce a compile-time error.

contract FooBar {
    function number() return (uint8) {
        return 10;
    }
}

contract FooTest is Test {
    function testFoo() public {
        FooBar target = new FooBar();
        address(target).code;   // will fail at compile-time
    }
}

See here on how to circumvent this issue.

Contract Size Limit

zksolc currently limits the number of instructions to 2^16 that are compiled for a contract. As such for large contracts, the compilation will fail with the error:

Error: assembly-to-bytecode conversion: assembly parse error Label DEFAULT_UNWIND was tried to be used
for either PC or constant at offset 65947 that is more than 65535 addressable space

Solution

There are three possible solutions to address this issue:

  1. Compilation with --zk-force-evmla=true:

    You can attempt to compile the contract using ZKsync’s EVM legacy architecture by adding the --zk-force-evmla=true flag. This can sometimes bypass the contract size limit by compiling in a different mode.

    Example command:

    forge build --zk-force-evmla=true --zksync
    
  2. Compilation with --zk-fallback-oz=true:

    If the contract size still exceeds the limit, try compiling with optimization level -Oz by using the --zk-fallback-oz=true flag. This tells the compiler to fall back to -Oz optimization when the bytecode is too large, potentially reducing the contract size further.

    Example command:

    forge build --zk-fallback-oz=true --zksync
    
  3. Split the Contract into Smaller Units

    If neither of the above flags resolves the issue, the contract must be refactored into smaller, modular contracts. This involves separating your logic into different contracts and using contract inheritance or external contract calls to maintain functionality.

    Before (single large contract):

    contract LargeContract {
        function largeFunction1() public { /* complex logic */ }
        function largeFunction2() public { /* complex logic */ }
        // Additional large functions and state variables...
    }
    

    After (multiple smaller contracts):

    contract ContractPart1 {
        function part1Function() public { /* logic from largeFunction1 */ }
    }
    contract ContractPart2 {
        function part2Function() public { /* logic from largeFunction2 */ }
    }
    contract MainContract is ContractPart1, ContractPart2 {
        // Logic to combine the functionalities of both parts
    }
    

Non inlinable libraries

Compiling contracts without linking non-inlinable libraries is currently not supported. Libraries need to be deployed before building contracts using them.

When building the contracts, the addresses need to be passed using the libraries config which contains a list of CONTRACT_PATH:ADDRESS mappings.

on foundry.toml:

libraries = [
    "src/MyLibrary.sol:MyLibrary:0xfD88CeE74f7D78697775aBDAE53f9Da1559728E4"
]

as a cli flag:

forge build --zksync --libraries src/MyLibrary.sol:MyLibrary:0xfD88CeE74f7D78697775aBDAE53f9Da1559728E4

For more information please refer to official docs.

Listing missing libraries

To scan missing non-inlinable libraries, you can build the project using the --zk-detect-missing-libraries flag. This will give a list of the libraries that need to be deployed and their addresses provided via the libraries option for the contracts to compile. Metadata about the libraries will be saved in .zksolc-libraries-cache/missing_library_dependencies.json.

Broadcast Limitations

These limitations apply when using cast to broadcast transactions.

No Batch Support

Batching is currently not supported on ZKsync networks, as such, any batched transactions may not be executed in order. This can often lead to failures as in the following case:

contract Calculator {
    function add(uint8 a, uint8 b) return (uint8) {
        return a+b;
    }
}

contract FooScript is Script {
    function run() public {
        vm.startBroadcast();
        Calculator calc = new Calculator();     // tx1
        uint8 sum = calc.add(1, 2);             // tx2
        vm.assertEqual(3, sum);
        vm.stopBroadcast();
    }
}
forge script script/FooTest.s.sol:FooScript ... --zksync --rpc-url https://sepolia.era.zksync.dev --broadcast 

Here the recorded transactions tx1 and tx2 would be batched as a single transaction with appropriate nonces. However, upon broadcasting to a ZKsync network, tx2 may be executed before tx1 which would cause a revert.

To circumvent this, the --slow flag may be used to sequentially send the transactions to the rpc endpoint, which keeps them in-order.

forge script script/FooTest.s.sol:FooScript ... --zksync --rpc-url https://sepolia.era.zksync.dev --broadcast --slow

Emitted Events

zkEVM in addition to user events, emits its own system events, like Transfer, Withdrawl, ContractCreated, etc. These events are not printed as part of traces, as currently it’s not trivial to match emitted events with zkEVM traces.

These system events can be observed via setting the RUST_LOG env variable:

RUST_LOG=foundry_zksync_core::vm::inspect=info,era_test_node::formatter=info forge test --zksync
==== 2 events
EthToken System Contract                  
  Topics:
    0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
    0x0000000000000000000000001804c8ab1f12e6bbf3894d4083f33e07309d1f38
    0x0000000000000000000000000000000000000000000000000000000000008001
  Data (Hex): 0x00000000000000000000000000000000000000000000000003dfd24000000000

  ...

Issues with expectEmit

This can often come as a surprise to users, who have the following test structure in place:

contract Number {
    uint256 accesses;
    function one() public returns (uint8) {
        accesses++;
        return 1;
    }
    function two() public returns (uint8) {
        accesses++;
        return 2;
    }
    function three() public returns (uint8) {
        accesses++;
        return 3;
    }
}

contract Calculator {
    event Added(uint8 indexed sum);
    function add(uint8 a, uint8 b) public returns (uint8) {
        uint8 sum = a + b;
        emit Added(sum);
        return sum;
    }
}

contract FooTest is Test {
    event Added(uint8 indexed sum);
    
    function testFoo() public {
        Number num = new Number();

        // We emit the event we expect to see.
        vm.expectEmit();
        emit Added(num.three());    // num.three() will emit zkEVM events

        Calculator calc = new Calculator();
        calc.add(num.one(), num.two());
    }
}

This test would currently fail as the non-static call to num.three() when setting up vm.expectEmit().

If run with RUST_LOG enabled as specified above, the following output will be observed:

┌──────────────────────────┐
│   VM EXECUTION RESULTS   │
└──────────────────────────┘
Cycles Used:          6703
Computation Gas Used: 106816
Contracts Used:       26
════════════════════════════
=== Console Logs: 
=== Calls: 
Call(Normal) Account Code Storage                                         4de2e468    4227857424
  Call(Normal) System context                                               02fa5779    4227853014
    ...
  Call(Normal) Bootloader utilities                                         ebe4a3d7    4227834933
    ...
  Call(Normal) 0x1804c8ab1f12e6bbf3894d4083f33e07309d1f38                   202bcce7    78705333
    Call(Normal) Nonce Holder                                                 e1239cd8    77474754
      ...
    Call(Normal) EthToken System Contract                                     9cc7f708    77468328
      Call(Normal) Keccak                                               00000000    76257342
  Call(Normal) Nonce Holder                                                 6ee1dc20    78694182
    Call(Normal) Keccak                                               00000000    77464044
  Call(Normal) EthToken System Contract                                     9cc7f708    78692796
    Call(Normal) Keccak                                               00000000    77462658
  Call(Normal) 0x1804c8ab1f12e6bbf3894d4083f33e07309d1f38                   e2f318e3    78691095
    Call(Normal) Msg Value System Contract                            0x    77460741
      Call(Normal) EthToken System Contract                                     579952fc    76249719
        ...
        Call(Normal) Event writer                                                 00000000    75052593
      Call(Mimic) bootloader                                           0x    76243293
  Call(Normal) EthToken System Contract                                     9cc7f708    78682086
    Call(Normal) Keccak                                               00000000    77452137
  Call(Normal) EthToken System Contract                                     579952fc    78680889
    ...
    Call(Normal) Event writer                                                 00000000    77449239
  Call(Normal) Known code storage                                           e516761e    78656571
  Call(Normal) Account Code Storage                                         4de2e468    78654114
  Call(Normal) System context                                               a851ae78    78653421
  Call(Normal) 0x1804c8ab1f12e6bbf3894d4083f33e07309d1f38                   df9c1589    78652161
    Call(Normal) 0xf9e9ba9ed9b96ab918c74b21dd0f1d5f2ac38a30                   45caa117    77422023
  Call(Normal) System context                                               a851ae78    4227757947
  ...
==== 3 events
EthToken System Contract                  
  Topics:
    0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
    0x0000000000000000000000001804c8ab1f12e6bbf3894d4083f33e07309d1f38
    0x0000000000000000000000000000000000000000000000000000000000008001
  Data (Hex): 0x00000000000000000000000000000000000000000000000003dfd24000000000

EthToken System Contract                  
  Topics:
    0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
    0x0000000000000000000000000000000000000000000000000000000000008001
    0x0000000000000000000000001804c8ab1f12e6bbf3894d4083f33e07309d1f38
  Data (Hex): 0x00000000000000000000000000000000000000000000000003dfd24000000000

EthToken System Contract                  
  Topics:
    0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
    0x0000000000000000000000000000000000000000000000000000000000008001
    0x0000000000000000000000001804c8ab1f12e6bbf3894d4083f33e07309d1f38
  Data (String): 

zk vm decoded result 0000000000000000000000000000000000000000000000000000000000000003

Here we observe that 3 events were emitted when we called num.three() in zkEVM. These correspond to the Transfer(address indexed from, address indexed to, uint256 value) event, which denotes change of L2 ETH. As a result the vm.expectEmit will register the first event emitted, and try to match the next two events, which will fail and so will the test with:

[FAIL. Reason: log != expected log] testFoo() (gas: 35515)
Traces:
  [35515] 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496::testFoo()
    ├─ [0] → new <unknown>@0xF9E9ba9Ed9B96AB918c74B21dD0f1D5f2ac38a30
    │   └─ ← [Return] 32 bytes of code
    ├─ [0] VM::expectEmit()
    │   └─ ← [Return] 
    ├─ [0] 0xF9E9ba9Ed9B96AB918c74B21dD0f1D5f2ac38a30::three()
    │   └─ ← [Return] 3
    └─ ← [Revert] log != expected log

To avoid such a scenario, it’s recommended to explicitly emit the events for expectEmit, with hard-coded values.

Trace Limitations

zkEVM traces are attached to the EVM traces that are printed with -vvvv.

  • The events emitted from within the zkEVM will not show on traces. See events in zkEVM.
  • The system call traces from within the zkEVM’s bootloader are currently ignored in order to simplify the trace output.
  • Executing each CREATE or CALL in its own zkEVM has additional bootloader gas costs, which may sometimes not be accounted in the traces. The ignored bootloader system calls, have a heuristic in-place to sum up their gas usage to the nearest non-system parent call, but this may also not add up accurately.

These system traces can be observed via setting the RUST_LOG env variable:

RUST_LOG=foundry_zksync_core::vm::inspect=info,era_test_node::formatter=info forge test --zksync
┌──────────────────────────┐
│   VM EXECUTION RESULTS   │
└──────────────────────────┘
Cycles Used:          6703
Computation Gas Used: 106816
Contracts Used:       26
════════════════════════════
=== Console Logs: 
=== Calls: 
Call(Normal) Account Code Storage                                         4de2e468    4227857424
  Call(Normal) System context                                               02fa5779    4227853014
    ...
  Call(Normal) Bootloader utilities                                         ebe4a3d7    4227834933
    ...
  Call(Normal) 0x1804c8ab1f12e6bbf3894d4083f33e07309d1f38                   202bcce7    78705333
    Call(Normal) Nonce Holder                                                 e1239cd8    77474754
      ...
    Call(Normal) EthToken System Contract                                     9cc7f708    77468328
      Call(Normal) Keccak                                               00000000    76257342
  Call(Normal) Nonce Holder                                                 6ee1dc20    78694182
    Call(Normal) Keccak                                               00000000    77464044
  Call(Normal) EthToken System Contract                                     9cc7f708    78692796
    Call(Normal) Keccak                                               00000000    77462658
  Call(Normal) 0x1804c8ab1f12e6bbf3894d4083f33e07309d1f38                   e2f318e3    78691095
    Call(Normal) Msg Value System Contract                            0x    77460741
      Call(Normal) EthToken System Contract                                     579952fc    76249719
        ...
        Call(Normal) Event writer                                                 00000000    75052593
      Call(Mimic) bootloader                                           0x    76243293
  Call(Normal) EthToken System Contract                                     9cc7f708    78682086
    Call(Normal) Keccak                                               00000000    77452137
  Call(Normal) EthToken System Contract                                     579952fc    78680889
    ...
    Call(Normal) Event writer                                                 00000000    77449239
  Call(Normal) Known code storage                                           e516761e    78656571
  Call(Normal) Account Code Storage                                         4de2e468    78654114
  Call(Normal) System context                                               a851ae78    78653421
  Call(Normal) 0x1804c8ab1f12e6bbf3894d4083f33e07309d1f38                   df9c1589    78652161
    Call(Normal) 0xf9e9ba9ed9b96ab918c74b21dd0f1d5f2ac38a30                   45caa117    77422023
  Call(Normal) System context                                               a851ae78    4227757947
  ...

Combined Traces

Foundry ZKsync will combine the traces from within the zkEVM into the EVM traces, that foundry displays. Running the following test with forge test --zksync -vvvv, yields the displayed trace:

contract InnerNumber {
    event Value(uint8);

    function innerFive() public returns (uint8) {
        emit Value(5);
        return 5;
    }
}

contract Number {
    function five() public returns (uint8) {
        InnerNumber num = new InnerNumber();
        return num.innerFive();
    }
}

contract Adder {
    function add() public returns (uint8) {
        Number num = new Number();
        return num.five() + num.five();
    }
}

contract FooTest is Test {
    function testFoo() public {
        Adder adder = new Adder();
        uint8 value = adder.add();
        assert(value == 10);
        console.log(value);
    }
}
[PASS] testFoo() (gas: 35807)
Logs:
  10

Traces:
  [35807] ZkTraceTest::testZkTraceOutputDuringCall()
    ├─ [0] → new Adder@0xF9E9ba9Ed9B96AB918c74B21dD0f1D5f2ac38a30
    │   └─ ← [Return] 2976 bytes of code
    ├─ [0] Adder::add()
    │   ├─ [127] → new Number@0xf232f12E115391c535FD519B00efADf042fc8Be5
    │   │   └─ ← [Return] 2272 bytes of code
    │   ├─ [91190] Number::five()
    │   │   ├─ [91] → new InnerNumber@0xEd570f3F91621894E001DF0fB70BfbD123D3c8AD
    │   │   │   └─ ← [Return] 736 bytes of code
    │   │   ├─ [889] InnerNumber::innerFive()
    │   │   │   └─ ← [Return] 5
    │   │   └─ ← [Return] 5
    │   ├─ [74776] Number::five()
    │   │   ├─ [91] → new InnerNumber@0xAbceAEaC3d3a2ac3Dcffd7A60Ca00A3fAC9490cA
    │   │   │   └─ ← [Return] 736 bytes of code
    │   │   ├─ [889] InnerNumber::innerFive()
    │   │   │   └─ ← [Return] 5
    │   │   └─ ← [Return] 5
    │   └─ ← [Return] 10
    ├─ [0] console::log(10) [staticcall]
    │   └─ ← [Stop] 
    └─ ← [Stop] 

Cheatcode Limitations

As outlined in the Execution Overview, due to the nature of how transactions are executed in zkEVM, cheatcode support is limited to the root level of an executing test. That is, all cheatcode access must happen outside of any CREATE or CALL that is dispatched to the zkEVM.

Therefore, the following are valid cheatcode accesses:

contract MyContract {
    function getNumber() public returns (uint256) {
        return 42;
    }
}

contract FooTest is Test {
    function testFoo_1() public {
        vm.roll(10);                    // valid
        vm.assertEq(10, block.number);
    }

    function testFoo_2() public {
        vm.roll(10);                    // valid
        new MyContract();
    }

    function testFoo_3() public {
        vm.roll(10);                    // valid
        MyContract testContract = new MyContract();
        testContract.getNumber();
    }
}

And consequently, since libraries do not lead to a CREATE or a CALL, they can be used with cheatcodes:

library MyLibrary {
    function setBlockNumber(value uint256) public {
        vm.roll(value);                 // valid
    }
}

contract FooTest is Test {
    function testFoo_1() public {
        vm.roll(10);                    // valid
        vm.assertEq(10, block.number);
        MyLibrary.setBlockNumber(20);
        vm.assertEq(10, block.number);
    }
}

However, the following situations will lead to undefined behavior (or not work at all), as the cheatcodes are not supported within the zkEVM:

contract MyContract {
    constructor() {
        vm.roll(20);                    // invalid
    }

    function getNumber() public returns (uint256) {
        vm.roll(20);                    // invalid
        return 42;
    }
}

contract FooTest is Test {
    function testFoo_1() public {
        vm.roll(10);                    // valid
        MyContract testContract = new MyContract();
        testContract.getNumber();
    }
}

Forge-ZKsync Standard Library

`forge-std` exports the most common constructs that allow users to write tests. However, in Foundry ZKsync, we’ve added some new additions in the form of cheatcodes (or anything we deem useful in the future). To allow users to access these interfaces, `forge-zksync-std` is provided as an add-on to `forge-std`.

Installation

forge install Moonsong-Labs/forge-zksync-std

Usage

In the absence of forge-zksync-std, the new cheatcodes are only accessible via low-level calls:

import {Test} from "forge-std/Test.sol";

contract FooTest is Test {
    function testZkTraceOutputDuringCreate() public {
        vm.startPrank(address(65536));                                             // normal foundry cheatcodes
        new Contract1();

        (bool success,) = address(vm).call(abi.encodeWithSignature("zkVmSkip()")); // additional foundry-zksync cheatcodes
        require(success, "zkVmSkip() call failed");
        new Contract2();
    }
}

However with the TextExt interface, the new cheatcodes can be accessed via vmExt property directly. The usual foundry cheatcodes are still available under the vm property.

import {Test} from "forge-std/Test.sol";
import {TestExt} from "forge-zksync-std/TestExt.sol";

contract FooTest is Test, TestExt {
    function testZkTraceOutputDuringCreate() public {
        vm.startPrank(address(65536));  // normal foundry cheatcodes
        new Contract1();

        vmExt.zkVmSkip();               // additional foundry-zksync cheatcodes
        new Contract2();
    }
}

This approach ensures that the existing tests need not be modified to use a completely different package than foundry/forge-std yet still allow for the additional ZKsync functionality to be included when necessary.

Additional Cheatcodes

In addition to the existing Cheatcodes, there are additional cheatcodes to help within the ZKsync context.

Cheatcodes Interface

This is the extended Solidity interface for all ZKsync specific cheatcodes present in Forge.

interface CheatCodesExt {
    /// Registers bytecodes for ZK-VM for transact/call and create instructions.
    function zkRegisterContract(
        string calldata name,
        bytes32 evmBytecodeHash,
        bytes calldata evmDeployedBytecode,
        bytes calldata evmBytecode,
        bytes32 zkBytecodeHash,
        bytes calldata zkDeployedBytecode
    ) external pure;

    /// Enables/Disables use ZK-VM usage for transact/call and create instructions.
    function zkVm(bool enable) external pure;

    /// When running in zkEVM context, skips the next CREATE or CALL, executing it on the EVM instead.
    /// All `CREATE`s executed within this skip, will automatically have `CALL`s to their target addresses
    /// executed in the EVM, and need not be marked with this cheatcode at every usage location.
    function zkVmSkip() external pure;

    /// Enables the use of a paymaster for the next transaction.
    function zkUsePaymaster(address paymaster, bytes calldata paymaster_input) external pure;
}

Usage

Refer to the forge-zksync-std section on how to access these cheatcodes in your tests.

zkRegisterContract

Signature

function zkRegisterContract(
    string calldata name,
    bytes32 evmBytecodeHash,
    bytes calldata evmDeployedBytecode,
    bytes calldata evmBytecode,
    bytes32 zkBytecodeHash,
    bytes calldata zkDeployedBytecode
) external pure;

Description

Registers bytecodes for ZK-VM for transact/call and create instructions.

This is especially useful if there are certain contracts already deployed on-chain (EVM or ZKsync). Since we compile with both solc and zksolc as defined in the Dual Compilation section, if there’s an already existing EVM bytecode that must be translated into its zkEVM counterpart, we need to define it with this cheatcode.

Such an operation must be carried out separately where the source of the pre-deployed contract must be obtained and compiled with zksolc. The artifact json will contain the zkBytecodeHash and zkDeployedBytecode parameters. The process is similar for obtaining EVM parameters with solc - evmBytecodeHash, evmDeployedBytecode, evmBytecode.

The name parameter must be unique if possible and not clash with locally existing contracts.

Examples

// LeetContract is pre-deployed on EVM on address(65536)

/// interface ILeetContract {
///     function leet() public {
///         // do something
///     }
/// }

vmExt.zkVm(true);
ILeetContract(address(65536)).leet();       // fails, as the contract was not found locally so not migrated to zkEVM


vmExt.zkRegisterContract("LeetContract", 0x111.., 0x222.., 0x333..., 0x444..., 0x555...); // register LeetContract for migration
vmExt.zkVm(true);
ILeetContract(address(65536)).leet();       // succeeds, as the contract was registered via cheatcode, so migrated to zkEVM

zkVm

Signature

function zkVm(bool enable) external pure;

Description

Enables/Disables ZKsync context for transact/call and create instructions within a test or script execution.

Switching VMs is a intensive process that translates the entire storage back-and-forth between EVM and zkEVM, and as such must be used sparingly in a test to switch between contexts.

See Execution Overview for further details.

See zkVmSkip for a one-off and much simpler operation.

Examples

/// contract LeetContract {
///     constructor(uint8 value) public {
///         // do something
///     }
/// }

vmExt.zkVm(true);
new LeetContract(1); // deployed in zkEVM
new LeetContract(2); // deployed in zkEVM

vmExt.zkVm(false);
new LeetContract(3); // deployed in EVM
new LeetContract(4); // deployed in EVM

zkVmSkip

Signature

function zkVmSkip() external pure;

Description

When running in zkEVM context, skips the next CREATE or CALL, executing it on the EVM instead. All CREATEs executed within this skip, will automatically have CALLs to their target addresses executed in the EVM, and need not be marked with this cheatcode at every usage location.

Skipping the next operation in zkEVM does not involve migrating storages as is done for zkVm cheatcode.

Examples

/// contract LeetContract {
///     constructor(uint8 value) public {
///         // do something
///     }
/// }

vmExt.zkVm(true);
new LeetContract(1); // deployed in zkEVM

vmExt.zkVmSkip();
new LeetContract(2); // deployed in EVM

new LeetContract(3); // deployed in zkEVM

Any contract deployed within a skip is remembered as such, so adding zkVmSkip to all of its calls is not necessary:

/// contract LeetContract {
///     constructor(uint8 value) public {
///         // do something
///     }
///     
///     function sayLeet() public {
///         // do something
///     }
/// }

contract FooTest is Test, TestExt {
    LeetContract leet1;
    LeetContract leet2;

    function setUp() public {
        leet1 = new LeetContract(1); // deployed in zkEVM

        vmExt.zkVmSkip();
        leet2 = new LeetContract(2); // deployed in EVM
    }

    function testAutomaticLeetDetection() public {
        leet1.sayLeet();            // executed in zkEVM
        
        leet2.sayLeet();            // automatically executed in EVM
    }

    function testManualLeetDetection() public {
        leet1.sayLeet();            // executed in zkEVM
        
        vmExt.zkVmSkip();           // redundant here, as it is
        leet2.sayLeet();            // automatically executed in EVM
    }
}

zkUsePaymaster

Signature

function zkUsePaymaster(address paymaster, bytes calldata input) external pure;

Description

This cheatcode enables the use of a paymaster for the next transaction in the contract. The parameters specify the address of the paymaster and the pre-encoded data to be passed to the paymaster. The paymaster should be deployed before using this cheatcode.

Examples

import {TestExt} from "forge-zksync-std/TestExt.sol";

contract Test is TestExt {
    function test_zkUsePaymaster() public {
        MyPaymaster paymaster = new MyPaymaster();
        // Encode paymaster input
        bytes memory paymaster_encoded_input = abi.encodeWithSelector(
            bytes4(keccak256("general(bytes)")),
            bytes("0x")
        );
        vmExt.zkUsePaymaster(address(paymaster), paymaster_encoded_input);
    }
}

The paymaster flow depends on the type of paymaster used. Here’s an example of the simplest usage of a ‘general’ paymaster in Foundry:

  1. Write a custom paymaster:
contract MyPaymaster is IPaymaster {
    modifier onlyBootloader() {
        require(msg.sender == BOOTLOADER_FORMAL_ADDRESS, "Only bootloader can call this method");
        _;
    }

    constructor() payable {}

    function validateAndPayForPaymasterTransaction(bytes32, bytes32, Transaction calldata _transaction)
        external
        payable
        onlyBootloader
        returns (bytes4 magic, bytes memory context)
    {
        // Always accept the transaction
        magic = PAYMASTER_VALIDATION_SUCCESS_MAGIC;

        // Pay for the transaction fee
        uint256 requiredETH = _transaction.gasLimit * _transaction.maxFeePerGas;
        (bool success,) = payable(BOOTLOADER_FORMAL_ADDRESS).call{value: requiredETH}("");
        require(success, "Failed to transfer tx fee to the bootloader");
    }

    function postTransaction( 
        bytes calldata _context,
        Transaction calldata _transaction,
        bytes32,
        bytes32,
        ExecutionResult _txResult,
        uint256 _maxRefundedGas
    ) external payable override onlyBootloader {}

    receive() external payable {}
}
  1. Deploy the paymaster:

You can deploy the paymaster either in a test or script:

MyPaymaster paymaster = new MyPaymaster();

Or using the forge create command:

forge create ./src/MyPaymaster.sol:MyPaymaster --rpc-url {RPC_URL} --private-key {PRIVATE_KEY} --zksync
  1. Use the cheatcode to set the paymaster for the next transaction:
vmExt.zkUsePaymaster(address(paymaster), abi.encodeWithSelector(
    bytes4(keccak256("general(bytes)")),
    bytes("0x")
));

For more examples, see the Foundry ZkSync Paymaster Tests.

Also, see the ZKsync Paymaster Documentation for more information.

zkUseFactoryDep

Signature

function zkUseFactoryDep(string calldata name) external pure;

Description

Marks a given contract as a factory dependency only for the next CREATE or CALL, unmarking it afterwards, similar to prank.

This cheatcode is useful when deploying contracts through factories that do not directly depend on a given contract, as it allows explicitly marking this type of contract as a factory dependency, enabling the factory to deploy the contract. More information on factory dependencies can be found in the official ZKsync docs.

Examples

contract Deployer {
    // Factory does not directly depend on TwoUserMultisig, so we need to mark it explicitly
    // as a factory dependency to allow deployment through the factory
    // Deploy the factory
    Factory factory = new Factory(multisigBytecodeHash);

    // Mark the bytecode as a factory dependency
    vmExt.zkUseFactoryDep("TwoUserMultisig");

    // Deploy the account using the factory
    factory.deployAccount(multisigBytecodeHash);
}

Gas Overview

Gas reported back to the EVM

Foundry has an isolate mode for the EVM where all CALL/CREATE operations at the root level of a test (i.e: with depth 1) will be intercepted and treated as independent transactions. This allows for accounting of the actual transaction gas and include for example the fixed 21000 gas cost charged to the user.

Running in zkEVM mode is anologous to running in isolate mode but using the zkEVM instead. Every CALL/CREATE will be intercepted, a transaction representing the operation built, and finally a VM with that transaction in the bootloader’s heap will be spawned and run in order to simulate the execution of that transaction. The gas used reported back to the EVM, and hence the one seen on traces and gas-reports, is what would be charged to the user for submitting that transaction. That value differs from the computational cost of running the called contract code and includes:

  1. Intrinsic costs: Overhead charged on each transaction.
  2. Validation costs: Gas spent on transaction validation. May vary depending on the account making the transaction. See Account Abstraction docs.
  3. Execution costs: Gas spent on marking factory deps and executing the transaction.
  4. Pubdata costs: Gas spent on publishing pubdata, is influenced by the gasPerPubdata network value.

More info about ZKSync Era’s fee model can be found here.

Transaction/Network values that impact gas cost

The gas cost mentioned above is influenced by transaction and network values. The values are set when running the VM in the following way:

  1. Transaction Params:
  • max_fee_per_gas: will be gas price of the root evm transaction (e.g: when running tests, the value of --gas-price option is used) with a minimum value of 0.26GWei, which is the base fee used in some test environments/networks.
  • gas_limit: The sender remaining balance capped to a max of 2^31 - 1. Note that, no matter the gas limit, the vm caps how much gas a single transaction can use to MAX_GAS_PER_TRANSACTION, currently set to 80_000_000.
  1. Network Params:
  • fair_l2_gas_price: set to the minumum of max_fee_per_gas and the base fee of the root evm transaction (e.g: when running tests, the value of the --base-fee option).
  • l1_gas_price: set to the same as fair_l2_gas_price, with a minimum value of 1000.

Deriving relevant transaction gas values

From the params above we can get all gas related values used in the transaction:

  • fair_pubdata_price: l1_gas_price * L1_GAS_PER_PUBDATA_BYTE.
  • baseFee: Maximum value between fair_l2_gas_price and (fair_pubdata_price / MAX_L2_GAS_PER_PUBDATA).
  • gasPerPubdata: fairPubdataPrice / baseFee.

L1_GAS_PER_PUBDATA_BYTE and MAX_L2_GAS_PER_PUBDATA are system constants currently set to 17 and 50_000 respectively.

Paymaster Overview

Paymasters in the ZKsync ecosystem represent a groundbreaking approach to handling transaction fees. They are special accounts designed to subsidize transaction costs for other accounts, potentially making certain transactions free for end-users. This feature is particularly useful for dApp developers looking to improve their platform’s accessibility and user experience by covering transaction fees on behalf of their users.

How Paymasters Work

Paymasters are smart contracts that implement the IPaymaster interface. They are designed to be used in conjunction with the ZKsync network’s transaction processing mechanism. When a transaction is sent from an account, the paymaster is specified in the transaction’s metadata. The paymaster is then responsible for paying the transaction fee and any other costs associated with the transaction.

How to interact with a Paymaster using Foundry

As of right now, the ways to interact with a paymaster contract using Foundry are using cast send, forge create or with the use of the zkUsePaymaster cheatcode.

Using cast send

cast send signs and publishes a transaction. The documentation can be found here.

To pair this with a paymaster contract, you need to specify the paymaster address and the encoded paymaster input in the command.

The flags for this are:

--zk-paymaster-address
    The address where the paymaster contract is deployed.

--zk-paymaster-input
    The encoded input for the paymaster contract. This depends on the paymaster contract implementation.

To encode the paymaster input, you can use the cast calldata command which can be found here.

cast send 0xdb8bA5F5DfB1636361d2fE851d7D3ed93acfc487 "increment()" --rpc-url https://sepolia.era.zksync.dev --private-key <your-private-key> --zk-paymaster-address 0x3cB2b87D10Ac01736A65688F3e0Fb1b070B3eeA3 --zk-paymaster-input $(cast calldata "approvalBased(address,uint256,bytes)" 0x31c43ac5e6A0fe62954B9056441b0A214722516e 1000000000000000000 "0x")

Using forge create

forge create is a command-line tool for deploying smart contracts using the Foundry framework. The documentation can be found here.

To deploy a paymaster contract using forge create, you need to specify the paymaster contract address in the command.

The flags for this are:

--zk-paymaster-address
    The address where the paymaster contract is deployed.

--zk-paymaster-input
    The encoded input for the paymaster contract. This depends on the paymaster contract implementation.

To encode the paymaster input, you can use the cast calldata command also which can be found here.

forge create Greeter.sol:Greeter --rpc-url "https://sepolia.era.zksync.dev" --private-key <your-private-key> --zksync --zk-paymaster-address 0x3cB2b87D10Ac01736A65688F3e0Fb1b070B3eeA3 --zk-paymaster-input $(cast calldata "approvalBased(address,uint256,bytes)" 0x31c43ac5e6A0fe62954B9056441b0A214722516e 1 "0x")

Also, see the ZKsync Paymaster Documentation for more information.

ZKsync specific examples

Using the zkUsePaymaster Cheatcode in General Flow Paymaster Contracts

This example covers the use of a general flow paymaster contract. For this example we will use the GaslessPaymaster contract from the paymaster example repository here.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@matterlabs/era-contracts/interfaces/IPaymaster.sol";
import "@matterlabs/era-contracts/interfaces/IPaymasterFlow.sol";
import "@matterlabs/era-contracts/Constants.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/// @author Matter Labs
/// @notice This contract does not include any validations other than using the paymaster general flow.
contract GaslessPaymaster is IPaymaster, Ownable {
    constructor() Ownable(msg.sender) {}

    modifier onlyBootloader() {
        require(
            msg.sender == BOOTLOADER_FORMAL_ADDRESS,
            "Only bootloader can call this method"
        );
        // Continue execution if called from the bootloader.
        _;
    }

    function validateAndPayForPaymasterTransaction(
        bytes32,
        bytes32,
        Transaction calldata _transaction
    )
        external
        payable
        onlyBootloader
        returns (bytes4 magic, bytes memory context)
    {
        // By default we consider the transaction as accepted.
        magic = PAYMASTER_VALIDATION_SUCCESS_MAGIC;
        require(
            _transaction.paymasterInput.length >= 4,
            "The standard paymaster input must be at least 4 bytes long"
        );

        bytes4 paymasterInputSelector = bytes4(
            _transaction.paymasterInput[0:4]
        );
        if (paymasterInputSelector == IPaymasterFlow.general.selector) {
            // Note, that while the minimal amount of ETH needed is tx.gasPrice * tx.gasLimit,
            // neither paymaster nor account are allowed to access this context variable.
            uint256 requiredETH = _transaction.gasLimit *
                _transaction.maxFeePerGas;
            // The bootloader never returns any data, so it can safely be ignored here.
            (bool success, ) = payable(BOOTLOADER_FORMAL_ADDRESS).call{
                value: requiredETH
            }("");
            require(
                success,
                "Failed to transfer tx fee to the Bootloader. Paymaster balance might not be enough."
            );
        } else {
            revert("Unsupported paymaster flow in paymasterParams.");
        }
    }

    function postTransaction(
        bytes calldata _context,
        Transaction calldata _transaction,
        bytes32,
        bytes32,
        ExecutionResult _txResult,
        uint256 _maxRefundedGas
    ) external payable override onlyBootloader {
        // Refunds are not supported yet.
    }

    function withdraw(address payable _to) external onlyOwner {
        // send paymaster funds to the owner
        uint256 balance = address(this).balance;
        (bool success, ) = _to.call{value: balance}("");
        require(success, "Failed to withdraw funds from paymaster.");
    }

    receive() external payable {}
}

This contract is a general flow paymaster, which means that it can pay for any account. To be able to use this paymaster we need to first deploy it in the intended network.

For this example we will deploy it in the era-test-node and then use the zkUsePaymaster cheatcode to pay for a transaction using a script.

pragma solidity ^0.8.0;

import {Script} from "forge-std/Script.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "../src/GeneralPaymaster.sol";
// We need to import the TestExt to use the zkUsePaymaster cheatcode
// as this is a ZKsync specific cheatcode
import "../src/Counter.sol";
import {TestExt} from "forge-zksync-std/TestExt.sol";

contract PaymasterUsageScript is Script, TestExt {
    Counter public counter;

    function run() public {
        vm.startBroadcast();

        GaslessPaymaster paymaster = new GaslessPaymaster();

        // fund the paymaster
        address(paymaster).call{value: 0.05 ether}("");

        bytes memory paymaster_encoded_input = abi.encodeWithSelector(
            bytes4(keccak256("general(bytes)")),
            bytes("0x")
        );
        vmExt.zkUsePaymaster(
            address(paymaster),
            paymaster_encoded_input
        );

        counter = new Counter();

        vm.stopBroadcast();
    }
}

The key part of this script is encoding the paymaster call with the general(bytes) selector and then using the zkUsePaymaster cheatcode to pay for the transaction. This will vary depending on the paymaster contract that you are using.

        // This is the encoding for the GaslessPaymaster
        bytes memory paymaster_encoded_input = abi.encodeWithSelector(
            bytes4(keccak256("general(bytes)")),
            bytes("0x")
        );

        // Using the encoded parameters to call the zkUsePaymaster cheatcode
        vmExt.zkUsePaymaster(
            address(paymaster),
            paymaster_encoded_input
        );

After calling the zkUsePaymaster cheatcode, the paymaster will pay for the following transaction.

Using the zkUsePaymaster Cheatcode in Approval-Based Paymaster Contracts

This example covers the use of an approval-based paymaster contract. The paymaster contract used is the testnet paymaster of zkSync documented here.

Steps Overview

  1. Setup and Initialization
    • Create a custom ERC20 token contract.
    • Deploy the ERC20 contract.
    • Mint tokens to the address using the paymaster.
  2. Approval and Paymaster Preparation
    • Create a paymaster contract.
    • Encode the paymaster call with required parameters.
    • Use the zkUsePaymaster cheatcode.

Step-by-Step

Let’s start by deploying the ERC20 contract and minting some tokens to the account that is using the paymaster. The idea behind the approval-based paymaster is that users transfer ERC20 tokens to the paymaster, which in turn pays for the transaction.

This is the code for the ERC20 contract:

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyERC20 is ERC20 {
    constructor() ERC20("SPITTE", "SPT") {}

    function mint(address to, uint256 amount) public {
        _mint(to, amount);
    }
}

Now, in the script we are going to run, we deploy the contract and mint some tokens to the account that is using the paymaster:

import {Script} from "forge-std/Script.sol";
import {console2} from "../lib/forge-std/src/console2.sol";
// We need to import the TestExt to use the zkUsePaymaster cheatcode
// as this is a ZKsync specific cheatcode
import {TestExt} from "forge-zksync-std/TestExt.sol";

contract PaymasterApprovalScript is Script, TestExt {
    function run() external {
        vm.startBroadcast();

        MyERC20 erc20 = new MyERC20();
        erc20.mint(address(tx.origin), 10);

        vm.stopBroadcast();
    }
}

Next, we prepare the encoded input for the paymaster:

// Encode the paymaster input
bytes memory paymaster_encoded_input = abi.encodeWithSelector(
    bytes4(keccak256("approvalBased(address,uint256,bytes)")), 
    address(erc20),  // ERC20 token address
    uint256(1 ether), // Approval amount
    bytes("0x")       // Additional data (empty in this case)
);

Here, we are encoding the paymaster input with the approvalBased method signature and the required parameters. The second parameter is the address of the recently deployed ERC20 contract, the third parameter is the amount of tokens that the paymaster consumes from the user to pay for the transaction, and the last one is empty bytes in this case.

With the encoded input prepared, we can now use the zkUsePaymaster cheatcode to prepare the next call to be executed using the paymaster:

// Using zkUsePaymaster with the encoded input
vmExt.zkUsePaymaster(address(0x3cB2b87D10Ac01736A65688F3e0Fb1b070B3eeA3), paymaster_encoded_input);

Counter counter = new Counter();
counter.increment();

The counter.increment() call will be executed using the paymaster that we set up in the encoded input.

Complete code

Below is the full code for the PaymasterTestScript demonstrating all steps:

pragma solidity ^0.8.0;

import {Script} from "forge-std/Script.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
// We need to import the TestExt to use the zkUsePaymaster cheatcode
// as this is a ZKsync specific cheatcode
import {TestExt} from "forge-zksync-std/TestExt.sol";


contract PaymasterTestScript is Script, TestExt {
    function run() external {
        vm.startBroadcast();

        // Deploy the ERC20 contract
        MyERC20 erc20 = new MyERC20();

        // Mint some tokens 
        erc20.mint(address(tx.origin), 10);

        // Encode the paymaster input
        bytes memory paymaster_encoded_input = abi.encodeWithSelector(
            bytes4(keccak256("approvalBased(address,uint256,bytes)")), // Function selector
            address(erc20), // ERC20 address
            uint256(1 ether), // The uint256 value
            bytes("0x") // Empty bytes "0x"
        );

        // Create a new Counter contract
        Counter counter = new Counter();

        // Use the zkUsePaymaster cheatcode to prepare the next call to be executed using the paymaster
        vm.zkUsePaymaster(address(0x3cB2b87D10Ac01736A65688F3e0Fb1b070B3eeA3), paymaster_encoded_input);

        // Increment the counter
        counter.increment();

        vm.stopBroadcast();
    }
}

contract Counter {
    uint256 public count = 0;

    function increment() public {
        count++;
    }

    function getCount() public view returns (uint256) {
        return count;
    }
}

contract MyERC20 is ERC20 {
    constructor() ERC20("SPITTE", "SPT") {}

    function mint(address to, uint256 amount) public {
        _mint(to, amount);
    }
}

Using a Ledger device

This example covers the use of a Ledger device to interact with ZKsync network.

Steps Overview

  1. Enable blind signing in the Ethereum App.
  2. Deploy a custom Paymaster contract using forge create --ledger.
  3. Fund the paymaster using cast send --ledger.
  4. Deploy a Counter contract using the paymaster with forge script --ledger

Note that the steps are showcasing different usages of --ledger, and not necessarily what are best the best practices for executing a similar flow.

Step-by-Step

Enable blind signing in the Ethereum App

To proceed with the usage of the device, one must first enable “Blind Signing” in the Ethereum App. To do so, open the app on the device, navigate to “Settings”, and then to “Blind signing”. Toggle the option so that it is “Enabled”.

Custom paymaster deployment with forge create

We will be deploying the MyPaymaster contract introduced in zkUsePaymaster cheatcode:

forge create ./src/MyPaymaster.sol:MyPaymaster --rpc-url {RPC_URL} --ledger --zksync

Proceed on your device to sign the transaction.

Take note of the resulting deployment address displayed on your terminal, henceforth referred to as $PAYMASTER_ADDRESS.

Paymaster funding with cast send

To ensure paymaster has sufficient funds to pay for our transactions, we’ll be funding it with some base token:

cast send $PAYMASTER_ADDRESS --value 0.1ether --rpc-url $RPC_URL --ledger

Again, proceed to sign the transaction on your device.

Contract deployment with forge script

We will be deploying a Counter contract using the paymaster, from within a script using:

vmExt.zkUsePaymaster(vm.envAddress("PAYMASTER_ADDRESS"),
                  abi.encodeWithSelector(
                       bytes4(keccak256("general(bytes)")),
                       bytes("0x")
                  ));
Counter cnt = new Counter();

We can now execute script using forge script --ledger:

forge script ./scripts/Counter.s.sol --zksync --rpc-url $RPC_URL --broadcast --slow --ledger

In this case, proceed to sign on your device the single broadcastable transaction generated for the deployment. When broadcasting multiple transactions, a proportionate number of signatures is necessary.

Complete code

The following is the full script and source code referenced in this document:

import {Script} from "forge-std/Script.sol";
import {TestExt} from "forge-zksync-std/TestExt.sol";

contract Counter {
    uint256 public count = 0;

    function increment() public {
        count++;
    }

    function getCount() public view returns (uint256) {
        return count;
    }
}

contract CounterScript is Script, TestExt {
    function setUp() public {}

    function run() public {
        vm.startBroadcast();

        vmExt.zkUsePaymaster(vm.envAddress("PAYMASTER_ADDRESS"),
                          abi.encodeWithSelector(
                              bytes4(keccak256("general(bytes)")),
                              bytes("0x")
                          ));
        Counter cnt = new Counter();

        vm.stopBroadcast();
    }
}

Deploying a multisig smart account

This example covers the configuration and deployment of a multisig smart account.

Steps Overview

  1. Specify the owners of the multisig account
  2. Execute the deployment script

Contracts

For this example, we will use 3 contracts:

  • AAFactory - A factory contract that will be used to deploy the multisig account.
  • TwoUserMultisig - A multisig account with 2 owners.
  • DeployMultisig - A script to deploy the multisig account through the factory.

AAFactory

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "@era-contracts/Constants.sol";
import "@era-contracts/libraries/SystemContractsCaller.sol";

contract AAFactory {
    bytes32 public aaBytecodeHash;

    constructor(bytes32 _aaBytecodeHash) {
        aaBytecodeHash = _aaBytecodeHash;
    }

    function deployAccount(
        bytes32 salt,
        address owner1,
        address owner2
    ) external returns (address accountAddress) {
        (bool success, bytes memory returnData) = SystemContractsCaller
            .systemCallWithReturndata(
                uint32(gasleft()),
                address(DEPLOYER_SYSTEM_CONTRACT),
                uint128(0),
                abi.encodeCall(
                    DEPLOYER_SYSTEM_CONTRACT.create2Account,
                    (
                        salt,
                        aaBytecodeHash,
                        abi.encode(owner1, owner2),
                        IContractDeployer.AccountAbstractionVersion.Version1
                    )
                )
            );
        require(success, "Deployment failed");

        (accountAddress) = abi.decode(returnData, (address));
    }
}

TwoUserMultisig

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "@era-contracts/interfaces/IAccount.sol";
import "@era-contracts/libraries/TransactionHelper.sol";
import "@era-contracts/Constants.sol";
import "@era-contracts/libraries/SystemContractsCaller.sol";

import "@openzeppelin/contracts/interfaces/IERC1271.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract TwoUserMultisig is IAccount, IERC1271 {
    // to get transaction hash
    using TransactionHelper for Transaction;

    // state variables for account owners
    address public owner1;
    address public owner2;

    bytes4 constant EIP1271_SUCCESS_RETURN_VALUE = 0x1626ba7e;

    modifier onlyBootloader() {
        require(
            msg.sender == BOOTLOADER_FORMAL_ADDRESS,
            "Only bootloader can call this function"
        );
        // Continue execution if called from the bootloader.
        _;
    }

    constructor(address _owner1, address _owner2) {
        owner1 = _owner1;
        owner2 = _owner2;
    }

    function validateTransaction(
        bytes32,
        bytes32 _suggestedSignedHash,
        Transaction calldata _transaction
    ) external payable override onlyBootloader returns (bytes4 magic) {
        return _validateTransaction(_suggestedSignedHash, _transaction);
    }

    function _validateTransaction(
        bytes32 _suggestedSignedHash,
        Transaction calldata _transaction
    ) internal returns (bytes4 magic) {
        // Incrementing the nonce of the account.
        // Note, that reserved[0] by convention is currently equal to the nonce passed in the transaction
        SystemContractsCaller.systemCallWithPropagatedRevert(
            uint32(gasleft()),
            address(NONCE_HOLDER_SYSTEM_CONTRACT),
            0,
            abi.encodeCall(
                INonceHolder.incrementMinNonceIfEquals,
                (_transaction.nonce)
            )
        );

        bytes32 txHash;
        // While the suggested signed hash is usually provided, it is generally
        // not recommended to rely on it to be present, since in the future
        // there may be tx types with no suggested signed hash.
        if (_suggestedSignedHash == bytes32(0)) {
            txHash = _transaction.encodeHash();
        } else {
            txHash = _suggestedSignedHash;
        }

        // The fact there is enough balance for the account
        // should be checked explicitly to prevent user paying for fee for a
        // transaction that wouldn't be included on Ethereum.
        uint256 totalRequiredBalance = _transaction.totalRequiredBalance();
        require(
            totalRequiredBalance <= address(this).balance,
            "Not enough balance for fee + value"
        );

        if (
            isValidSignature(txHash, _transaction.signature) ==
            EIP1271_SUCCESS_RETURN_VALUE
        ) {
            magic = ACCOUNT_VALIDATION_SUCCESS_MAGIC;
        } else {
            magic = bytes4(0);
        }
    }

    function executeTransaction(
        bytes32,
        bytes32,
        Transaction calldata _transaction
    ) external payable override onlyBootloader {
        _executeTransaction(_transaction);
    }

    function _executeTransaction(Transaction calldata _transaction) internal {
        address to = address(uint160(_transaction.to));
        uint128 value = Utils.safeCastToU128(_transaction.value);
        bytes memory data = _transaction.data;

        if (to == address(DEPLOYER_SYSTEM_CONTRACT)) {
            uint32 gas = Utils.safeCastToU32(gasleft());

            // Note, that the deployer contract can only be called
            // with a "systemCall" flag.
            SystemContractsCaller.systemCallWithPropagatedRevert(
                gas,
                to,
                value,
                data
            );
        } else {
            bool success;
            assembly {
                success := call(
                    gas(),
                    to,
                    value,
                    add(data, 0x20),
                    mload(data),
                    0,
                    0
                )
            }
            require(success);
        }
    }

    function executeTransactionFromOutside(
        Transaction calldata _transaction
    ) external payable {
        bytes4 magic = _validateTransaction(bytes32(0), _transaction);
        require(magic == ACCOUNT_VALIDATION_SUCCESS_MAGIC, "NOT VALIDATED");

        _executeTransaction(_transaction);
    }

    function isValidSignature(
        bytes32 _hash,
        bytes memory _signature
    ) public view override returns (bytes4 magic) {
        magic = EIP1271_SUCCESS_RETURN_VALUE;

        if (_signature.length != 130) {
            // Signature is invalid anyway, but we need to proceed with the signature verification as usual
            // in order for the fee estimation to work correctly
            _signature = new bytes(130);

            // Making sure that the signatures look like a valid ECDSA signature and are not rejected rightaway
            // while skipping the main verification process.
            _signature[64] = bytes1(uint8(27));
            _signature[129] = bytes1(uint8(27));
        }

        (
            bytes memory signature1,
            bytes memory signature2
        ) = extractECDSASignature(_signature);

        if (
            !checkValidECDSASignatureFormat(signature1) ||
            !checkValidECDSASignatureFormat(signature2)
        ) {
            magic = bytes4(0);
        }

        address recoveredAddr1 = ECDSA.recover(_hash, signature1);
        address recoveredAddr2 = ECDSA.recover(_hash, signature2);

        // Note, that we should abstain from using the require here in order to allow for fee estimation to work
        if (recoveredAddr1 != owner1 || recoveredAddr2 != owner2) {
            magic = bytes4(0);
        }
    }

    // This function verifies that the ECDSA signature is both in correct format and non-malleable
    function checkValidECDSASignatureFormat(
        bytes memory _signature
    ) internal pure returns (bool) {
        if (_signature.length != 65) {
            return false;
        }

        uint8 v;
        bytes32 r;
        bytes32 s;
        // Signature loading code
        // we jump 32 (0x20) as the first slot of bytes contains the length
        // we jump 65 (0x41) per signature
        // for v we load 32 bytes ending with v (the first 31 come from s) then apply a mask
        assembly {
            r := mload(add(_signature, 0x20))
            s := mload(add(_signature, 0x40))
            v := and(mload(add(_signature, 0x41)), 0xff)
        }
        if (v != 27 && v != 28) {
            return false;
        }

        // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
        // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
        // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most
        // signatures from current libraries generate a unique signature with an s-value in the lower half order.
        //
        // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
        // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
        // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
        // these malleable signatures as well.
        if (
            uint256(s) >
            0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0
        ) {
            return false;
        }

        return true;
    }

    function extractECDSASignature(
        bytes memory _fullSignature
    ) internal pure returns (bytes memory signature1, bytes memory signature2) {
        require(_fullSignature.length == 130, "Invalid length");

        signature1 = new bytes(65);
        signature2 = new bytes(65);

        // Copying the first signature. Note, that we need an offset of 0x20
        // since it is where the length of the `_fullSignature` is stored
        assembly {
            let r := mload(add(_fullSignature, 0x20))
            let s := mload(add(_fullSignature, 0x40))
            let v := and(mload(add(_fullSignature, 0x41)), 0xff)

            mstore(add(signature1, 0x20), r)
            mstore(add(signature1, 0x40), s)
            mstore8(add(signature1, 0x60), v)
        }

        // Copying the second signature.
        assembly {
            let r := mload(add(_fullSignature, 0x61))
            let s := mload(add(_fullSignature, 0x81))
            let v := and(mload(add(_fullSignature, 0x82)), 0xff)

            mstore(add(signature2, 0x20), r)
            mstore(add(signature2, 0x40), s)
            mstore8(add(signature2, 0x60), v)
        }
    }

    function payForTransaction(
        bytes32,
        bytes32,
        Transaction calldata _transaction
    ) external payable override onlyBootloader {
        bool success = _transaction.payToTheBootloader();
        require(success, "Failed to pay the fee to the operator");
    }

    function prepareForPaymaster(
        bytes32, // _txHash
        bytes32, // _suggestedSignedHash
        Transaction calldata _transaction
    ) external payable override onlyBootloader {
        _transaction.processPaymasterInput();
    }

    fallback() external {
        // fallback of default account shouldn't be called by bootloader under no circumstances
        assert(msg.sender != BOOTLOADER_FORMAL_ADDRESS);

        // If the contract is called directly, behave like an EOA
    }

    receive() external payable {
        // If the contract is called directly, behave like an EOA.
        // Note, that is okay if the bootloader sends funds with no calldata as it may be used for refunds/operator payments
    }
}

DeployMultisig

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "forge-std/Script.sol";
import "@era-contracts/libraries/SystemContractsCaller.sol";
import {Create2Factory} from "@era-contracts/Create2Factory.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "../src/AAFactory.sol";
import "../src/TwoUserMultisig.sol";

contract DeployMultisig is Script {
    function run() external {
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");

        // Owners for the multisig account
        // Can be random
        address owner1 = vm.envAddress("OWNER_1");
        address owner2 = vm.envAddress("OWNER_2");

        // Read artifact file and get the bytecode hash
        string memory artifact = vm.readFile(
            "zkout/TwoUserMultisig.sol/TwoUserMultisig.json"
        );
        bytes32 multisigBytecodeHash = vm.parseJsonBytes32(artifact, ".hash");
        console.log("Bytecode hash: ");
        console.logBytes32(multisigBytecodeHash);
        bytes32 salt = "1234";

        vm.startBroadcast(deployerPrivateKey);
        AAFactory factory = new AAFactory(multisigBytecodeHash);
        console.log("Factory deployed at: ", address(factory));

        // Mark the bytecode as a factory dependency
        vmExt.zkUseFactoryDep("TwoUserMultisig");

        factory.deployAccount(salt, owner1, owner2);
        vm.stopBroadcast();
    }
}

Running the script

forge script ./script/DeployMultisig.s.sol:DeployMultisig --rpc-url <RPC_URL> --private-key <PRIVATE_KEY> --broadcast --via-ir --zk-enable-eravm-extensions --zksync

For the complete source code, visit the minimal account abstraction multisig repository.

Foundry-ZKSync Supported Commands

This is a comprehensive review of all the Foundry commands actually supported in the actual stage of development.

🔄 Last update: September 12, 2024

CommandStatusDescription
forge bind✅ SupportedGenerates type-safe bindings for Solidity contracts, which can be used in other programming languages like Go.
forge bind-json✅ SupportedSimilar to forge bind, but generates bindings directly from JSON ABI files.
forge build✅ SupportedCompiles Solidity contracts and generates build artifacts, such as ABI and bytecode files.
forge clone✅ SupportedClones an existing project from a Git repository, setting up a new Foundry project.
forge completions✅ SupportedGenerates shell completion scripts for forge, enhancing command-line usability.
forge config✅ SupportedDisplays or modifies the configuration settings for a Foundry project.
forge create✅ SupportedDeploys a Solidity contract to a blockchain network, handling the transaction and deployment process.
forge doc✅ SupportedGenerates documentation for Solidity contracts, extracting comments and annotations into a readable format.
forge flatten✅ SupportedFlattens a Solidity contract and its dependencies into a single file, useful for verification or analysis.
forge coverage❌ Not SupportedRuns tests and generates a code coverage report, showing how much of the code is covered by tests.
forge debug❌ Not SupportedDebugs a transaction on a local fork or a live network, allowing you to step through the execution.
forge cache clean✅ SupportedClears the local cache, removing stored build artifacts and other cached data.
forge cache ls✅ SupportedLists the contents of the local cache, including build artifacts and other data.
forge clean✅ SupportedRemoves build artifacts and resets the project’s build state.
forge eip712✅ SupportedGenerates EIP-712 typed data structures for Solidity contracts, used for off-chain signing and verification.
forge fmt✅ SupportedFormats Solidity source code according to a standard style guide, ensuring consistency.
forge geiger✅ SupportedAnalyzes a Solidity project for unsafe or potentially insecure code patterns, helping to improve security.
forge generate✅ SupportedAutomatically generates Solidity code or tests based on specified templates or patterns.
forge generate test✅ SupportedGenerates boilerplate test files for Solidity contracts, speeding up the testing process.
forge generate-fig-spec✅ SupportedGenerates a Fig spec for Forge, which can be used to create command-line autocomplete functionality.
forge init✅ SupportedInitializes a new Foundry project, creating the necessary directories and configuration files.
forge inspect❌ Not SupportedInspects the details of a Solidity contract, such as ABI, bytecode, and other metadata.
forge install✅ SupportedInstalls dependencies from the Foundry package manager, adding them to the project.
forge remappings✅ SupportedManages remappings for Solidity imports, allowing for custom paths or package names.
forge remove✅ SupportedRemoves a dependency from the project, cleaning up any related files or configuration.
forge script✅ SupportedExecutes Solidity scripts, which can be used for tasks like deploying contracts or interacting with the blockchain.
forge selectors✅ SupportedExtracts and manages function selectors from Solidity contracts, used for interacting with contracts.
forge selectors collision✅ SupportedDetects and reports any selector collisions in Solidity contracts, preventing potential conflicts.
forge selectors upload✅ SupportedUploads function selectors to a specified registry, making them available for use in other projects.
forge selectors list✅ SupportedLists all function selectors in a Solidity contract, providing an overview of its interface.
forge snapshot✅ SupportedCreates a snapshot of the current state of tests, which can be used to check for regressions.
forge soldeer install✅ SupportedInstalls a specific version of Soldeer, ensuring compatibility with the project.
forge soldeer update✅ SupportedUpdates the Soldeer installation to the latest version, applying any necessary patches or improvements.
forge soldeer login✅ SupportedLogs into the Soldeer service, providing authentication for managing dependencies and projects.
forge soldeer push✅ SupportedPushes changes to a Soldeer project, syncing them with the remote repository or service.
forge soldeer version-dry-run✅ SupportedTests a version update of Soldeer without actually applying the changes, useful for checking compatibility.
forge test✅ SupportedRuns unit tests for Solidity contracts, with options for gas reporting, fuzzing, and more.
forge tree✅ SupportedDisplays the dependency tree of the project, showing how contracts and libraries are interconnected.
forge update✅ SupportedUpdates the project’s dependencies to their latest versions, ensuring everything is up-to-date.
forge verify-bytecode❌ Not SupportedVerifies that a deployed contract’s bytecode matches the expected source code, ensuring it hasn’t been tampered with.
forge verify-check✅ SupportedChecks the contract’s verification status on either the ZKsync block explorer (using --verifier) or Etherscan, confirming successful verification.
forge verify-contract✅ SupportedVerifies a deployed contract on Etherscan, ensuring it matches the source code.
cast 4byte✅ SupportedFetches function signatures from the 4byte.directory by their selector.
cast 4byte-decode✅ SupportedDecodes a given 4-byte selector into its associated function signature.
cast 4byte-event✅ SupportedFetches event signatures from the 4byte.directory by their selector.
cast abi-decode✅ SupportedDecodes ABI-encoded data into a human-readable format.
cast abi-encode✅ SupportedEncodes data into ABI format for function calls and transactions.
cast access-list❌ Not SupportedGenerates an access list for a transaction, which can be used to optimize gas usage.
cast address-zero✅ SupportedOutputs the zero address (0x0000000000000000000000000000000000000000).
cast admin✅ SupportedReturns the admin of a specified proxy contract.
cast age✅ SupportedCalculates the age of a block in seconds.
cast balance✅ SupportedRetrieves the balance of an address in wei or ether.
cast base-fee✅ SupportedFetches the base fee of the latest block, useful for estimating gas costs.
cast bind (DEPRECATED)✅ SupportedGenerates Go bindings for Solidity contracts, similar to forge bind.
cast block✅ SupportedRetrieves detailed information about a specific block on the blockchain.
cast block-number✅ SupportedReturns the current block number of the Ethereum blockchain.
cast call✅ SupportedExecutes a read-only (constant) call to a smart contract.
cast call –create❌ Not SupportedCalls a contract and creates a new contract in the same transaction.
cast calldata✅ SupportedEncodes function call data for a contract, which can be used in transactions.
cast calldata-decode✅ SupportedDecodes encoded calldata back into its original arguments.
cast chain❌ Not SupportedDisplays information about the current Ethereum chain, including its name and ID.
cast chain-id✅ SupportedReturns the chain ID of the Ethereum network, which is used for transaction signing.
cast client✅ SupportedFetches information about the connected Ethereum client, such as its version.
cast code✅ SupportedRetrieves the bytecode of a contract deployed at a specific address.
cast codesize✅ SupportedReturns the size of the bytecode at a specific address, in bytes.
cast completions✅ SupportedGenerates shell completions for cast, improving command-line usability.
cast compute-address✅ SupportedComputes the Ethereum address for a contract deployed by a specific account.
cast concat-hex✅ SupportedConcatenates multiple hexadecimal values into a single hex string.
cast create2✅ SupportedComputes the address of a contract deployed using the CREATE2 opcode.
cast decode-eof✅ SupportedDecodes Ethereum Object Format (EOF) bytecode, used in Ethereum contracts.
cast decode-transaction✅ SupportedDecodes the data and parameters of a raw transaction.
cast disassemble❌ Not SupportedDisassembles contract bytecode into readable EVM assembly instructions.
cast estimate❌ Not SupportedEstimates the gas cost of executing a transaction on the blockchain.
cast estimate –create❌ Not SupportedEstimates the gas cost for deploying a contract with a creation transaction.
cast etherscan-source✅ SupportedFetches and displays the verified source code of a contract from Etherscan.
cast find-block✅ SupportedFinds a block based on a given timestamp, returning the block number.
cast format-bytes32-string✅ SupportedConverts a string into a bytes32 format for Solidity.
cast from-bin✅ SupportedDecodes binary-encoded data into a human-readable format.
cast from-fixed-point✅ SupportedConverts a fixed-point number into a human-readable string.
cast from-rlp✅ SupportedDecodes RLP-encoded data, commonly used in Ethereum transactions.
cast from-utf8✅ SupportedConverts a UTF-8 string to a hex-encoded representation.
cast from-wei✅ SupportedConverts a value from wei (the smallest unit of ether) to ether.
cast gas-price✅ SupportedFetches the current gas price on the Ethereum network.
cast generate-fig-spec✅ SupportedGenerates a Fig spec for Cast, which can be used for command-line autocomplete functionality.
cast hash-message (DEPRECATED)✅ SupportedHashes a message using Ethereum’s eth_sign method, preparing it for signing.
cast hash-zero✅ SupportedReturns the hash of an empty string (0x000…000) using Keccak-256.
cast implementation✅ SupportedReturns the implementation address of a specified proxy contract.
cast index❌ Not SupportedFetches the indexed logs of an event from the blockchain, useful for querying historical data.
cast index-erc7201✅ SupportedFetches the logs of an ERC-7201 compliant event from the blockchain
cast interface❌ Not SupportedGenerates a Solidity interface from a deployed contract’s ABI.
cast keccak✅ SupportedComputes the Keccak-256 hash of the provided input data.
cast logs✅ SupportedFetches logs and events from the blockchain, based on specified filters.
cast lookup-address✅ SupportedFetches the ENS name associated with a given Ethereum address, if any.
cast max-int✅ SupportedOutputs the maximum value for a signed 256-bit integer.
cast max-uint✅ SupportedOutputs the maximum value for an unsigned 256-bit integer.
cast min-int✅ SupportedOutputs the minimum value for a signed 256-bit integer.
cast mktx✅ SupportedCreates a transaction object without sending it, useful for offline signing.
cast mktx –create❌ Not SupportedCreates a transaction that deploys a contract, without sending it.
cast namehash✅ SupportedComputes the ENS namehash for a given domain name.
cast nonce✅ SupportedRetrieves the nonce of an Ethereum address, useful for determining transaction order.
cast parse-bytes32-address✅ SupportedParses a bytes32 value into an Ethereum address.
cast parse-bytes32-string✅ SupportedParses a bytes32 value into a human-readable string.
cast pretty-calldata✅ SupportedFormats calldata in a human-readable manner.
cast proof❌ Not SupportedRetrieves and displays a Merkle proof for a specific storage slot or account.
cast publish✅ SupportedPublishes a smart contract’s ABI to Etherscan.
cast receipt✅ SupportedFetches and displays the receipt of a transaction, including gas used and status.
cast resolve-name✅ SupportedResolves an ENS name to its associated Ethereum address.
cast rpc✅ SupportedSends a raw JSON-RPC request to an Ethereum node, allowing low-level interaction.
cast run❌ Not SupportedRuns a script file, such as a .js or .ts file, with access to Cast functions.
cast selectors❌ Not SupportedFetches the function selectors for a given contract or ABI.
cast send✅ SupportedSends a transaction to the blockchain, including smart contract interactions.
cast send –create❌ Not SupportedSends a transaction that creates a new contract on the blockchain.
cast shl✅ SupportedPerforms a bitwise left shift on the provided input.
cast shr✅ SupportedPerforms a bitwise right shift on the provided input.
cast sig✅ SupportedOutputs the Keccak-256 hash of a function signature.
cast sig-event✅ SupportedOutputs the Keccak-256 hash of an event signature.
cast storage✅ SupportedFetches and displays the raw storage value of a contract at a specific slot.
cast to-ascii✅ SupportedConverts a hexadecimal string to an ASCII string.
cast to-base✅ SupportedConverts a number to a different base (e.g., from decimal to hexadecimal).
cast to-bytes32✅ SupportedConverts input data to a bytes32 format.
cast to-check-sum-address✅ SupportedConverts an Ethereum address to a checksummed format, which includes capital letters for error detection.
cast to-dec✅ SupportedConverts input data to a decimal number.
cast to-fixed-point✅ SupportedConverts input data to a fixed-point number representation.
cast to-hex✅ SupportedConverts input data to a hexadecimal format.
cast to-hexdata✅ SupportedConverts input data to hex-encoded binary data.
cast to-int256✅ SupportedConverts input data to a signed 256-bit integer.
cast to-rlp✅ SupportedEncodes input data in Recursive Length Prefix (RLP) format.
cast to-uint256✅ SupportedConverts input data to an unsigned 256-bit integer.
cast to-unit✅ SupportedConverts ether or wei into different units, like gwei or finney.
cast to-utf8✅ SupportedConverts a hexadecimal string to a UTF-8 encoded string.
cast to-wei✅ SupportedConverts ether or other units into wei, the smallest unit of ether.
cast tx✅ SupportedFetches and displays details of a specific Ethereum transaction.
cast upload-signature✅ SupportedUploads a function or event signature to the 4byte.directory.
cast wallet✅ SupportedA suite of wallet-related commands, allowing you to manage Ethereum wallets, create new ones, sign transactions, and more.
cast wallet new✅ SupportedGenerates a new Ethereum wallet with a private key and address.
cast wallet new-mnemonic✅ SupportedCreates a new wallet using a mnemonic phrase, which can be used to recover the wallet later.
cast wallet vanity✅ SupportedGenerates a new wallet with a custom, vanity address (e.g., one that starts with specific characters).
cast wallet address✅ SupportedOutputs the Ethereum address associated with a given private key.
cast wallet sign✅ SupportedSigns a message or transaction using the private key of a specified wallet.
cast wallet sign-auth (DEPRECATED?)✅ SupportedSigns an authorization message with a private key, often used in authentication workflows.
cast wallet verify✅ SupportedVerifies a signed message, confirming that it was signed by the holder of the private key associated with a specific address.
cast wallet import✅ SupportedImports an Ethereum wallet using a private key or mnemonic phrase.
cast wallet list✅ SupportedLists all wallets stored in a specific keystore.
cast wallet private-key✅ SupportedOutputs the private key associated with a given wallet, provided proper authentication.
cast wallet decrypt-keystore✅ SupportedDecrypts a keystore file to retrieve the private key, requiring the correct password.
anvil✅ SupportedA local Ethereum node implementation, similar to Ganache, that can be used for testing and development.
anvil completions✅ SupportedGenerates shell completions for anvil, useful for auto-completing commands in the terminal.
anvil generate-fig-spec✅ SupportedGenerates a Fig autocomplete spec for anvil, providing interactive command suggestions.
chisel✅ SupportedA tool used to interact with and modify smart contracts, providing operations like loading, listing, and clearing caches of tools.
chisel list✅ SupportedLists all available chisel tools or operations that can be applied to smart contracts.
chisel load✅ SupportedLoads a specific chisel tool or operation, making it ready for use on a smart contract.
chisel view✅ SupportedDisplays the details or configuration of a loaded chisel tool or operation.
chisel clear-cache✅ SupportedClears the cache of chisel tools or operations, forcing a reload or update.

Overview of Forge

Forge is a command-line tool that ships with Foundry. Forge tests, builds, and deploys your smart contracts.

Tests

Forge can run your tests with the forge test command. All tests are written in Solidity.

Forge will look for the tests anywhere in your source directory. Any contract with a function that starts with test is considered to be a test. Usually, tests will be placed in test/ by convention and end with .t.sol.

Here’s an example of running forge test in a freshly created project, that only has the default test:

$ forge test --zksync
Compiling 25 files with Solc 0.8.27
Solc 0.8.27 finished in 902.81ms
Compiler run successful!

No files changed, compilation skipped

Ran 2 tests for test/Counter.t.sol:CounterTest
[PASS] testFuzz_SetNumber(uint256) (runs: 256, μ: 31210, ~: 31288)
[PASS] test_Increment() (gas: 31303)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 5.75ms (5.36ms CPU time)

Ran 1 test suite in 10.34ms (5.75ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)

You can also run specific tests by passing a filter:

$ forge test --zksync --match-contract ComplicatedContractTest --match-test test_Deposit
Compiling 24 files with Solc 0.8.10
Solc 0.8.10 finished in 1.14s
Compiler run successful!

Compiling 24 files with zksolc and solc 0.8.10
zksolc and solc 0.8.10 finished in 5.54s
Compiler run successful!

Ran 2 tests for test/ComplicatedContract.t.sol:ComplicatedContractTest
[PASS] test_DepositERC20() (gas: 102193)
[PASS] test_DepositETH() (gas: 61414)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.28ms (1.39ms CPU time)

Ran 1 test suite in 5.27ms (1.28ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)

This will run the tests in the ComplicatedContractTest test contract with testDeposit in the name. Inverse versions of these flags also exist (--no-match-contract and --no-match-test).

You can run tests in filenames that match a glob pattern with --match-path.

$ forge test --zksync --match-path test/ContractB.t.sol
Compiling 1 files with Solc 0.8.10
Solc 0.8.10 finished in 1.08s
Compiler run successful!

Compiling 1 files with zksolc and solc 0.8.10
zksolc and solc 0.8.10 finished in 5.17s
Compiler run successful!

Ran 1 test for test/ContractB.t.sol:ContractBTest
[PASS] testExample() (gas: 257)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 397.78µs (61.04µs CPU time)

Ran 1 test suite in 4.86ms (397.78µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

The inverse of the --match-path flag is --no-match-path.

Logs and traces

The default behavior for forge test is to only display a summary of passing and failing tests. You can control this behavior by increasing the verbosity (using the -v flag). Each level of verbosity adds more information:

  • Level 2 (-vv): Logs emitted during tests are also displayed. That includes assertion errors from tests, showing information such as expected vs actual.
  • Level 3 (-vvv): Stack traces for failing tests are also displayed.
  • Level 4 (-vvvv): Stack traces for all tests are displayed, and setup traces for failing tests are displayed.
  • Level 5 (-vvvvv): Stack traces and setup traces are always displayed.

Watch mode

Forge can re-run your tests when you make changes to your files using forge test --watch.

By default, only changed test files are re-run. If you want to re-run all tests on a change, you can use forge test --zksync --watch --run-all.

Writing Tests

Tests are written in Solidity. If the test function reverts, the test fails, otherwise it passes.

Let’s go over the most common way of writing tests, using the Forge Standard Library’s Test contract, which is the preferred way of writing tests with Forge.

In this section, we’ll go over the basics using the functions from the Forge Std’s Test contract, which is itself a superset of DSTest. You will learn how to use more advanced stuff from the Forge Standard Library soon.

DSTest provides basic logging and assertion functionality. To get access to the functions, import forge-std/Test.sol and inherit from Test in your test contract:

import {Test} from "forge-std/Test.sol";

Let’s examine a basic test:

pragma solidity 0.8.10;

import {Test} from "forge-std/Test.sol";

contract ContractBTest is Test {
    uint256 testNumber;

    function setUp() public {
        testNumber = 42;
    }

    function test_NumberIs42() public {
        assertEq(testNumber, 42);
    }

    function testFail_Subtract43() public {
        testNumber -= 43;
    }
}

Forge uses the following keywords in tests:

  • setUp: An optional function invoked before each test case is run.
    function setUp() public {
        testNumber = 42;
    }
  • test: Functions prefixed with test are run as a test case.
    function test_NumberIs42() public {
        assertEq(testNumber, 42);
    }
  • testFail: The inverse of the test prefix - if the function does not revert, the test fails.
    function testFail_Subtract43() public {
        testNumber -= 43;
    }

A good practice is to use the pattern test_Revert[If|When]_Condition in combination with the expectRevert cheatcode (cheatcodes are explained in greater detail in the following section). Also, other testing practices can be found in the Tutorials section.

Note: To use stdError constants (like arithmeticError in the example below), make sure to import StdError.sol:

import {stdError} from "forge-std/StdError.sol";

Now, instead of using testFail, you know exactly what reverted and with which error:

    function test_CannotSubtract43() public {
        vm.expectRevert(stdError.arithmeticError);
        testNumber -= 43;
    }

Tests are deployed to 0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84. If you deploy a contract within your test, then 0xb4c...7e84 will be its deployer. If the contract deployed within a test gives special permissions to its deployer, such as Ownable.sol’s onlyOwner modifier, then the test contract 0xb4c...7e84 will have those permissions.

⚠️ Note

Test functions must have either external or public visibility. Functions declared as internal or private won’t be picked up by Forge, even if they are prefixed with test.

Before test setups

Unit and fuzz tests are stateless and are executed as single transactions, meaning that the state modified by a test won’t be available for a different one (instead, they’ll use the same state created by setUp call). It is possible to simulate multiple transactions in a single test, with a dependency tree, by implementing the beforeTestSetup function.

  • beforeTestSetup: Optional function that configures a set of transactions to be executed before test.
function beforeTestSetup(
    bytes4 testSelector
) public returns (bytes[] memory beforeTestCalldata)

where

  • bytes4 testSelector is the selector of the test for which transactions are applied
  • bytes[] memory beforeTestCalldata is an array of arbitrary calldata applied before test execution

💡 Tip

This setup can be used for chaining tests or for scenarios when a test needs certain transactions committed before test run (e.g. when using selfdestruct). The test fails if any of the configured transaction reverts.

For example, in contract below, testC is configured to use state modified by testA and setB(uint256) functions:

contract ContractTest is Test {
    uint256 a;
    uint256 b;

    function beforeTestSetup(
        bytes4 testSelector
    ) public pure returns (bytes[] memory beforeTestCalldata) {
        if (testSelector == this.testC.selector) {
            beforeTestCalldata = new bytes[](2);
            beforeTestCalldata[0] = abi.encodePacked(this.testA.selector);
            beforeTestCalldata[1] = abi.encodeWithSignature("setB(uint256)", 1);
        }
    }

    function testA() public {
        require(a == 0);
        a += 1;
    }

    function setB(uint256 value) public {
        b = value;
    }

    function testC() public {
        assertEq(a, 1);
        assertEq(b, 1);
    }
}

Shared setups

It is possible to use shared setups by creating helper abstract contracts and inheriting them in your test contracts:

abstract contract HelperContract {
    address constant IMPORTANT_ADDRESS = 0x543d...;
    SomeContract someContract;
    constructor() {...}
}

contract MyContractTest is Test, HelperContract {
    function setUp() public {
        someContract = new SomeContract(0, IMPORTANT_ADDRESS);
        ...
    }
}

contract MyOtherContractTest is Test, HelperContract {
    function setUp() public {
        someContract = new SomeContract(1000, IMPORTANT_ADDRESS);
        ...
    }
}

💡 Tip

Use the getCode cheatcode to deploy contracts with incompatible Solidity versions.

Cheatcodes

🚨 Important

See Cheatcode Limitations when using cheatcodes in ZKsync context.

Most of the time, simply testing your smart contracts outputs isn’t enough. To manipulate the state of the blockchain, as well as test for specific reverts and events, Foundry is shipped with a set of cheatcodes.

Cheatcodes allow you to change the block number, your identity, and more. They are invoked by calling specific functions on a specially designated address: 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D.

You can access cheatcodes easily via the vm instance available in Forge Standard Library’s Test contract. Forge Standard Library is explained in greater detail in the following section.

Let’s write a test for a smart contract that is only callable by its owner.

pragma solidity 0.8.10;

import {Test} from "forge-std/Test.sol";

error Unauthorized();

contract OwnerUpOnly {
    address public immutable owner;
    uint256 public count;

    constructor() {
        owner = msg.sender;
    }

    function increment() external {
        if (msg.sender != owner) {
            revert Unauthorized();
        }
        count++;
    }
}

contract OwnerUpOnlyTest is Test {
    OwnerUpOnly upOnly;

    function setUp() public {
        upOnly = new OwnerUpOnly();
    }

    function test_IncrementAsOwner() public {
        assertEq(upOnly.count(), 0);
        upOnly.increment();
        assertEq(upOnly.count(), 1);
    }
}

If we run forge test now, we will see that the test passes, since OwnerUpOnlyTest is the owner of OwnerUpOnly.

$ forge test
Compiling 24 files with Solc 0.8.10
Solc 0.8.10 finished in 801.67ms
Compiler run successful!

Compiling 24 files with zksolc and solc 0.8.10
zksolc and solc 0.8.10 finished in 5.98s
Compiler run successful!

Ran 1 test for test/OwnerUpOnly.t.sol:OwnerUpOnlyTest
[PASS] test_IncrementAsOwner() (gas: 734680)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 34.55ms (19.27ms CPU time)

Ran 1 test suite in 35.22ms (34.55ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Let’s make sure that someone who is definitely not the owner can’t increment the count:

contract OwnerUpOnlyTest is Test {
    OwnerUpOnly upOnly;

    // ...

    function testFail_IncrementAsNotOwner() public {
        vm.prank(address(0));
        upOnly.increment();
    }
}

If we run forge test now, we will see that all the test pass.

$ forge test
No files changed, compilation skipped

No files changed, compilation skipped

Ran 2 tests for test/OwnerUpOnly.t.sol:OwnerUpOnlyTest
[PASS] testFail_IncrementAsNotOwner() (gas: 216319)
[PASS] test_IncrementAsOwner() (gas: 734680)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 35.53ms (27.31ms CPU time)

Ran 1 test suite in 36.34ms (35.53ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)

The test passed because the prank cheatcode changed our identity to the zero address for the next call (upOnly.increment()). The test case passed since we used the testFail prefix, however, using testFail is considered an anti-pattern since it does not tell us anything about why upOnly.increment() reverted.

If we run the tests again with traces turned on, we can see that we reverted with the correct error message.

$ forge test -vvvv --match-test testFail_IncrementAsNotOwner
No files changed, compilation skipped

No files changed, compilation skipped

Ran 1 test for test/OwnerUpOnly.t.sol:OwnerUpOnlyTest
[PASS] testFail_IncrementAsNotOwner() (gas: 216319)
Traces:
  [216319] OwnerUpOnlyTest::testFail_IncrementAsNotOwner()
    ├─ [0] VM::prank(0x0000000000000000000000000000000000000000)
    │   └─ ← [Return] 
    ├─ [208252] OwnerUpOnly::increment()
    │   └─ ← [Revert] Unauthorized()
    └─ ← [Revert] Unauthorized()

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 21.84ms (6.47ms CPU time)

Ran 1 test suite in 22.73ms (21.84ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

To be sure in the future, let’s make sure that we reverted because we are not the owner using the expectRevert cheatcode:

contract OwnerUpOnlyTest is Test {
    OwnerUpOnly upOnly;

    // ...

    // Notice that we replaced `testFail` with `test`
    function test_RevertWhen_CallerIsNotOwner() public {
        vm.expectRevert(Unauthorized.selector);
        vm.prank(address(0));
        upOnly.increment();
    }
}

If we run forge test one last time, we see that the test still passes, but this time we are sure that it will always fail if we revert for any other reason.

$ forge test
No files changed, compilation skipped

No files changed, compilation skipped

Ran 1 test for test/OwnerUpOnly.t.sol:OwnerUpOnlyTest
[PASS] test_IncrementAsOwner() (gas: 734680)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 34.49ms (19.06ms CPU time)

Ran 1 test suite in 35.33ms (34.49ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Another cheatcode that is perhaps not so intuitive is the expectEmit function. Before looking at expectEmit, we need to understand what an event is.

Events are inheritable members of contracts. When you emit an event, the arguments are stored on the blockchain. The indexed attribute can be added to a maximum of three parameters of an event to form a data structure known as a “topic.” Topics allow users to search for events on the blockchain.

pragma solidity 0.8.10;

import {Test} from "forge-std/Test.sol";

contract EmitContractTest is Test {
    event Transfer(address indexed from, address indexed to, uint256 amount);

    function test_ExpectEmit() public {
        ExpectEmit emitter = new ExpectEmit();
        // Check that topic 1, topic 2, and data are the same as the following emitted event.
        // Checking topic 3 here doesn't matter, because `Transfer` only has 2 indexed topics.
        vm.expectEmit(true, true, false, true);
        // The event we expect
        emit Transfer(address(this), address(1337), 1337);
        // The event we get
        emitter.t();
    }

    function test_ExpectEmit_DoNotCheckData() public {
        ExpectEmit emitter = new ExpectEmit();
        // Check topic 1 and topic 2, but do not check data
        vm.expectEmit(true, true, false, false);
        // The event we expect
        emit Transfer(address(this), address(1337), 1338);
        // The event we get
        emitter.t();
    }
}

contract ExpectEmit {
    event Transfer(address indexed from, address indexed to, uint256 amount);

    function t() public {
        emit Transfer(msg.sender, address(1337), 1337);
    }
}

When we call vm.expectEmit(true, true, false, true);, we want to check the 1st and 2nd indexed topic for the next event.

The expected Transfer event in test_ExpectEmit() means we are expecting that from is address(this), and to is address(1337). This is compared against the event emitted from emitter.t().

In other words, we are checking that the first topic from emitter.t() is equal to address(this). The 3rd argument in expectEmit is set to false because there is no need to check the third topic in the Transfer event, since there are only two. It does not matter even if we set to true.

The 4th argument in expectEmit is set to true, which means that we want to check “non-indexed topics”, also known as data.

For example, we want the data from the expected event in test_ExpectEmit - which is amount - to equal to the data in the actual emitted event. In other words, we are asserting that amount emitted by emitter.t() is equal to 1337. If the fourth argument in expectEmit was set to false, we would not check amount.

In other words, test_ExpectEmit_DoNotCheckData is a valid test case, even though the amounts differ, since we do not check the data.


📚 Reference

See the Cheatcodes Reference for a complete overview of all the available cheatcodes.

Forge Standard Library Overview

Forge Standard Library (Forge Std for short) is a collection of helpful contracts that make writing tests easier, faster, and more user-friendly.

Using Forge Std is the preferred way of writing tests with Foundry.

It provides all the essential functionality you need to get started writing tests:

  • Vm.sol: Up-to-date cheatcodes interface
  • console.sol and console2.sol: Hardhat-style logging functionality
  • Script.sol: Basic utilities for Solidity scripting
  • Test.sol: A superset of DSTest containing standard libraries, a cheatcodes instance (vm), and Hardhat console

Simply import Test.sol and inherit from Test in your test contract:

import {Test} from "forge-std/Test.sol";

contract ContractTest is Test { ...

Now, you can:

// Access Hevm via the `vm` instance
vm.startPrank(alice);

// Assert and log using Dappsys Test
assertEq(dai.balanceOf(alice), 10000e18);

// Log with the Hardhat `console` (`console2`)
console.log(alice.balance);

// Use anything from the Forge Std std-libraries
deal(address(dai), alice, 10000e18);

To import the Vm interface or the console library individually:

import {Vm} from "forge-std/Vm.sol";
import {console} from "forge-std/console.sol";

Note: console2.sol contains patches to console.sol that allows Forge to decode traces for calls to the console, but it is not compatible with Hardhat.

import {console2} from "forge-std/console2.sol";

Standard libraries

Forge Std currently consists of six standard libraries.

Std Logs

Std Logs expand upon the logging events from the DSTest library.

Std Assertions

Std Assertions expand upon the assertion functions from the DSTest library.

Std Cheats

Std Cheats are wrappers around Forge cheatcodes that make them safer to use and improve the DX.

You can access Std Cheats by simply calling them inside your test contract, as you would any other internal function:

// set up a prank as Alice with 100 ETH balance
hoax(alice, 100 ether);

Std Errors

Std Errors provide wrappers around common internal Solidity errors and reverts.

Std Errors are most useful in combination with the expectRevert cheatcode, as you do not need to remember the internal Solidity panic codes yourself. Note that you have to access them through stdError, as this is a library.

// expect an arithmetic error on the next call (e.g. underflow)
vm.expectRevert(stdError.arithmeticError);

Std Storage

Std Storage makes manipulating contract storage easy. It can find and write to the storage slot(s) associated with a particular variable.

The Test contract already provides a StdStorage instance stdstore through which you can access any std-storage functionality. Note that you must add using stdStorage for StdStorage in your test contract first.

// find the variable `score` in the contract `game`
// and change its value to 10
stdstore
    .target(address(game))
    .sig(game.score.selector)
    .checked_write(10);

Std Math

Std Math is a library with useful mathematical functions that are not provided in Solidity.

Note that you have to access them through stdMath, as this is a library.

// get the absolute value of -10
uint256 ten = stdMath.abs(-10)

📚 Reference

See the Forge Standard Library Reference for a complete overview of Forge Standard Library.

Forge ZKsync Standard Library Overview

Forge ZKsync Standard Library is an addition to the Forge Standard Library that adds further collection of helpful contracts that make writing tests easier, faster, and more user-friendly for the ZKsync ecosystem.

Refer to this section for more details.

Understanding Traces

Forge can produce traces either for failing tests (-vvv) or all tests (-vvvv).

Traces follow the same general format:

  [<Gas Usage>] <Contract>::<Function>(<Parameters>)
    ├─ [<Gas Usage>] <Contract>::<Function>(<Parameters>)
    │   └─ ← <Return Value>
    └─ ← <Return Value>

Each trace can have many more subtraces, each denoting a call to a contract and a return value.

If your terminal supports color, the traces will also come with a variety of colors:

  • Green: For calls that do not revert
  • Red: For reverting calls
  • Blue: For calls to cheat codes
  • Cyan: For emitted logs
  • Yellow: For contract deployments

The gas usage (marked in square brackets) is for the entirety of the function call. You may notice, however, that sometimes the gas usage of one trace does not exactly match the gas usage of all its subtraces:

  [24661] OwnerUpOnlyTest::testIncrementAsOwner()
    ├─ [2262] OwnerUpOnly::count()
    │   └─ ← 0
    ├─ [20398] OwnerUpOnly::increment()
    │   └─ ← ()
    ├─ [262] OwnerUpOnly::count()
    │   └─ ← 1
    └─ ← ()

The gas unaccounted for is due to some extra operations happening between calls, such as arithmetic and store reads/writes.

Forge will try to decode as many signatures and values as possible, but sometimes this is not possible. In these cases, the traces will appear like so:

  [<Gas Usage>] <Address>::<Calldata>
    └─ ← <Return Data>

Some traces might be harder to grasp at first glance. These include:

  • The OOG shorthand stands for “Out Of Gas”.
  • The acronym EOF stands for “Ethereum Object Format”, which introduces an extensible and versioned container format for EVM bytecode. For more information, read here.
  • NotActivated means the feature or opcode is not activated. Some versions of the EVM only support certain opcodes. You may need to use a more recent version usign the --evm_version flag. For example, the PUSH0 opcode is only available since the Shanghai hardfork.
  • InvalidFEOpcode means that an undefined bytecode value has been encountered during execution. The EVM catches the unknown bytecode and returns the INVALID opcode instead, of value 0xFE. You can find out more here.

For a deeper insight into the various traces, you can explore the revm source code.

ZKsync Limitations

In addition to the above anomalies of incorrect gas and un-decodable traces, there are additional caveats within the ZKsync context:

  • The events emitted from within the zkEVM will not show on traces. See events in zkEVM.
  • The system call traces from within the zkEVM’s bootloader are currently ignored in order to simplify the trace output.
  • Executing each CREATE or CALL in its own zkEVM has additional bootloader gas costs, which may sometimes not be accounted in the traces. The ignored bootloader system calls, have a heuristic in-place to sum up their gas usage to the nearest non-system parent call, but this may also not add up accurately.

Fork Testing

Forge supports testing in a forked environment with two different approaches:

Which approach to use? Forking mode affords running an entire test suite against a specific forked environment, while forking cheatcodes provide more flexibility and expressiveness to work with multiple forks in your tests. Your particular use case and testing strategy will help inform which approach to use.

Note that ZKsync context will be set accordingly based on the fork url, so the --zksync flag need not be passed.

Forking Mode

To run all tests in a forked environment, such as a forked Ethereum mainnet, pass an RPC URL via the --fork-url flag:

forge test --fork-url <your_rpc_url>

The following values are changed to reflect those of the chain at the moment of forking:

It is possible to specify a block from which to fork with --fork-block-number:

forge test --fork-url <your_rpc_url> --fork-block-number 1

Forking is especially useful when you need to interact with existing contracts. You may choose to do integration testing this way, as if you were on an actual network.

Caching

If both --fork-url and --fork-block-number are specified, then data for that block is cached for future test runs.

The data is cached in ~/.foundry/cache/rpc/<chain name>/<block number>. To clear the cache, simply remove the directory or run forge clean (removes all build artifacts and cache directories).

It is also possible to ignore the cache entirely by passing --no-storage-caching, or with foundry.toml by configuring no_storage_caching and rpc_storage_caching.

Improved traces

Forge supports identifying contracts in a forked environment with Etherscan.

To use this feature, pass the Etherscan API key via the --etherscan-api-key flag:

forge test --fork-url <your_rpc_url> --etherscan-api-key <your_etherscan_api_key>

Alternatively, you can set the ETHERSCAN_API_KEY environment variable.

Forking Cheatcodes

Forking cheatcodes allow you to enter forking mode programmatically in your Solidity test code. Instead of configuring forking mode via forge CLI arguments, these cheatcodes allow you to use forking mode on a test-by-test basis and work with multiple forks in your tests. Each fork is identified via its own unique uint256 identifier.

Usage

Important to keep in mind that all test functions are isolated, meaning each test function is executed with a copy of the state after setUp and is executed in its own stand-alone EVM.

Therefore forks created during setUp are available in tests. The code example below uses createFork to create two forks, but does not select one initially. Each fork is identified with a unique identifier (uint256 forkId), which is assigned when it is first created.

Enabling a specific fork is done via passing that forkId to selectFork.

createSelectFork is a one-liner for createFork plus selectFork.

There can only be one fork active at a time, and the identifier for the currently active fork can be retrieved via activeFork.

Similar to roll, you can set block.number of a fork with rollFork.

To understand what happens when a fork is selected, it is important to know how the forking mode works in general:

Each fork is a standalone EVM, i.e. all forks use completely independent storage. The only exception is the state of the msg.sender and the test contract itself, which are persistent across fork swaps. In other words all changes that are made while fork A is active (selectFork(A)) are only recorded in fork A’s storage and are not available if another fork is selected. However, changes recorded in the test contract itself (variables) are still available because the test contract is a persistent account.

The selectFork cheatcode sets the remote section with the fork’s data source, however the local memory remains persistent across fork swaps. This also means selectFork can be called at all times with any fork, to set the remote data source. However, it is important to keep in mind the above rules for read/write access always apply, meaning writes are persistent across fork swaps.

Examples

Create and Select Forks
contract ForkTest is Test {
    // the identifiers of the forks
    uint256 mainnetFork;
    uint256 optimismFork;

    //Access variables from .env file via vm.envString("varname")
    //Replace ALCHEMY_KEY by your alchemy key or Etherscan key, change RPC url if need
    //inside your .env file e.g:
    //MAINNET_RPC_URL = 'https://eth-mainnet.g.alchemy.com/v2/ALCHEMY_KEY'
    //string MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL");
    //string OPTIMISM_RPC_URL = vm.envString("OPTIMISM_RPC_URL");

    // create two _different_ forks during setup
    function setUp() public {
        mainnetFork = vm.createFork(MAINNET_RPC_URL);
        optimismFork = vm.createFork(OPTIMISM_RPC_URL);
    }

    // demonstrate fork ids are unique
    function testForkIdDiffer() public {
        assert(mainnetFork != optimismFork);
    }

    // select a specific fork
    function testCanSelectFork() public {
        // select the fork
        vm.selectFork(mainnetFork);
        assertEq(vm.activeFork(), mainnetFork);

        // from here on data is fetched from the `mainnetFork` if the EVM requests it and written to the storage of `mainnetFork`
    }

    // manage multiple forks in the same test
    function testCanSwitchForks() public {
        vm.selectFork(mainnetFork);
        assertEq(vm.activeFork(), mainnetFork);

        vm.selectFork(optimismFork);
        assertEq(vm.activeFork(), optimismFork);
    }

    // forks can be created at all times
    function testCanCreateAndSelectForkInOneStep() public {
        // creates a new fork and also selects it
        uint256 anotherFork = vm.createSelectFork(MAINNET_RPC_URL);
        assertEq(vm.activeFork(), anotherFork);
    }

    // set `block.number` of a fork
    function testCanSetForkBlockNumber() public {
        vm.selectFork(mainnetFork);
        vm.rollFork(1_337_000);

        assertEq(block.number, 1_337_000);
    }
}
Separated and persistent storage

As mentioned each fork is essentially an independent EVM with separated storage.

Only the accounts of msg.sender and the test contract (ForkTest) are persistent when forks are selected. But any account can be turned into a persistent account: makePersistent.

An account that is persistent is unique, i.e. it exists on all forks

contract ForkTest is Test {
    // the identifiers of the forks
    uint256 mainnetFork;
    uint256 optimismFork;

    //Access variables from .env file via vm.envString("varname")
    //Replace ALCHEMY_KEY by your alchemy key or Etherscan key, change RPC url if need
    //inside your .env file e.g:
    //MAINNET_RPC_URL = 'https://eth-mainnet.g.alchemy.com/v2/ALCHEMY_KEY'
    //string MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL");
    //string OPTIMISM_RPC_URL = vm.envString("OPTIMISM_RPC_URL");

    // create two _different_ forks during setup
    function setUp() public {
        mainnetFork = vm.createFork(MAINNET_RPC_URL);
        optimismFork = vm.createFork(OPTIMISM_RPC_URL);
    }

    // creates a new contract while a fork is active
    function testCreateContract() public {
        vm.selectFork(mainnetFork);
        assertEq(vm.activeFork(), mainnetFork);

        // the new contract is written to `mainnetFork`'s storage
        SimpleStorageContract simple = new SimpleStorageContract();

        // and can be used as normal
        simple.set(100);
        assertEq(simple.value(), 100);

        // after switching to another contract we still know `address(simple)` but the contract only lives in `mainnetFork`
        vm.selectFork(optimismFork);

        /* this call will therefore revert because `simple` now points to a contract that does not exist on the active fork
        * it will produce following revert message:
        *
        * "Contract 0xCe71065D4017F316EC606Fe4422e11eB2c47c246 does not exist on active fork with id `1`
        *       But exists on non active forks: `[0]`"
        */
        simple.value();
    }

     // creates a new _persistent_ contract while a fork is active
     function testCreatePersistentContract() public {
        vm.selectFork(mainnetFork);
        SimpleStorageContract simple = new SimpleStorageContract();
        simple.set(100);
        assertEq(simple.value(), 100);

        // mark the contract as persistent so it is also available when other forks are active
        vm.makePersistent(address(simple));
        assert(vm.isPersistent(address(simple)));

        vm.selectFork(optimismFork);
        assert(vm.isPersistent(address(simple)));

        // This will succeed because the contract is now also available on the `optimismFork`
        assertEq(simple.value(), 100);
     }
}

contract SimpleStorageContract {
    uint256 public value;

    function set(uint256 _value) public {
        value = _value;
    }
}

For more details and examples, see the forking cheatcodes reference.

Replaying Failures

Forge supports incrementally replaying last test run failures by persisting them on the disk.

Rerun failures

The --rerun option can be used to omit successful tests and replay recorded failures only:

forge test --rerun

The failed tests are written in ~/.foundry/cache/test-failures file. This file is updated each time forge test is performed, so it reflects failures from the last run.

Fuzz tests failures

Forge saves all fuzz tests counterexamples and replays them before new test campaigns are started (This is done in order to ensure there is no regression introduced). Fuzz tests failures encountered in several runs are by default persisted in ~/.foundry/cache/fuzz/failures file. The file content is not replaced by subsequent test runs, but new records are added to existing entries.

The default file used to persist and rerun fuzz test failures from can be changed in foundry.toml:

[fuzz]
failure_persist_file="/tests/failures.txt"

or by using inline config

/// forge-config: default.fuzz.failure-persist-file = /tests/failures.txt

Invariant tests failures

Failures from invariant tests are saved and replayed before new test campaigns are started, similar with fuzz tests. The difference is that the failed sequences are persisted in individual files, with specific ~/.foundry/cache/invariant/failures/{TEST_SUITE_NAME}/{INVARIANT_NAME} default path. Content of this file is replaced only when a different counterexample is found.

The default directory to persist invariant test failures can be changed in foundry.toml:

[invariant]
failure_persist_dir="/tests/dir"

or by using inline config

/// forge-config: default.invariant.failure-persist-dir = /tests/dir

Remove persisted failures

To ignore saved failures and start a clean test campaign, simply remove the persisted files or run forge clean (removes all build artifacts and cache directories).

Advanced Testing

Forge comes with a number of advanced testing methods:

In the future, Forge will also support these:

Each chapter dives into what problem the testing methods solve, and how to apply them to your own project.

Fuzz Testing

Forge supports property based testing.

Property-based testing is a way of testing general behaviors as opposed to isolated scenarios.

Let’s examine what that means by writing a unit test, finding the general property we are testing for, and converting it to a property-based test instead:

pragma solidity 0.8.10;

import {Test} from "forge-std/Test.sol";

contract Safe {
    receive() external payable {}

    function withdraw() external {
        payable(msg.sender).call{value: address(this).balance}("");
    }
}

contract SafeTest is Test {
    Safe safe;

    // Needed so the test contract itself can receive ether
    // when withdrawing
    receive() external payable {}

    function setUp() public {
        safe = new Safe();
    }

    function test_Withdraw() public {
        payable(address(safe)).call{value: 1 ether}("");
        uint256 preBalance = address(this).balance;
        safe.withdraw();
        uint256 postBalance = address(this).balance;
        assertEq(preBalance + 1 ether, postBalance);
    }
}

Running the test, we see it passes:

$ forge test --zksync
Compiling 24 files with Solc 0.8.10
Solc 0.8.10 finished in 1.11s
Compiler run successful with warnings:
Warning (9302): Return value of low-level calls not used.
  --> test/Safe.t.sol:11:9:
   |
11 |         payable(msg.sender).call{value: address(this).balance}("");
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Warning (9302): Return value of low-level calls not used.
  --> test/Safe.t.sol:27:9:
   |
27 |         payable(address(safe)).call{value: 1 ether}("");
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^


Compiling 24 files with zksolc and solc 0.8.10
zksolc and solc 0.8.10 finished in 5.81s
Compiler run successful with warnings:
Warning (9302)
Warning: Return value of low-level calls not used.
  --> test/Safe.t.sol:11:9:
   |
11 |         payable(msg.sender).call{value: address(this).balance}("");
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Warning (9302)
Warning: Return value of low-level calls not used.
  --> test/Safe.t.sol:27:9:
   |
27 |         payable(address(safe)).call{value: 1 ether}("");
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

2024-10-06T00:06:07.481112Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""

Ran 1 test for test/Safe.t.sol:SafeTest
[PASS] test_Withdraw() (gas: 1199829)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 28.85ms (13.44ms CPU time)

Ran 1 test suite in 29.66ms (28.85ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

This unit test does test that we can withdraw ether from our safe. However, who is to say that it works for all amounts, not just 1 ether?

The general property here is: given a safe balance, when we withdraw, we should get whatever is in the safe.

Forge will run any test that takes at least one parameter as a property-based test, so let’s rewrite:

contract SafeTest is Test {
    // ...

    function testFuzz_Withdraw(uint256 amount) public {
        payable(address(safe)).call{value: amount}("");
        uint256 preBalance = address(this).balance;
        safe.withdraw();
        uint256 postBalance = address(this).balance;
        assertEq(preBalance + amount, postBalance);
    }
}

If we run the test now, we can see that Forge runs the property-based test, but it fails for high values of amount:

$ forge test
Compiling 1 files with Solc 0.8.10
Solc 0.8.10 finished in 819.99ms
Compiler run successful with warnings:
Warning (9302): Return value of low-level calls not used.
  --> test/Safe.t.sol:11:9:
   |
11 |         payable(msg.sender).call{value: address(this).balance}("");
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Warning (9302): Return value of low-level calls not used.
  --> test/Safe.t.sol:30:9:
   |
30 |         payable(address(safe)).call{value: amount}("");
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^


Compiling 1 files with zksolc and solc 0.8.10
zksolc and solc 0.8.10 finished in 5.79s
Compiler run successful with warnings:
Warning (9302)
Warning: Return value of low-level calls not used.
  --> test/Safe.t.sol:11:9:
   |
11 |         payable(msg.sender).call{value: address(this).balance}("");
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Warning (9302)
Warning: Return value of low-level calls not used.
  --> test/Safe.t.sol:30:9:
   |
30 |         payable(address(safe)).call{value: amount}("");
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

2024-10-06T00:06:15.308856Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""

Ran 1 test for test/Safe.t.sol:SafeTest
[FAIL. Reason: assertion failed; counterexample: calldata=0x29facca700000000000000000000000000000000000001e35e0786a154ba56aa29228e12 args=[38296303183566758584479465508370 [3.829e31]]] testFuzz_Withdraw(uint256) (runs: 0, μ: 0, ~: 0)
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 29.00ms (13.46ms CPU time)

Ran 1 test suite in 29.74ms (29.00ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)

The default amount of ether that the test contract is given is 2**96 wei (as in DappTools), so we have to restrict the type of amount to uint96 to make sure we don’t try to send more than we have:

    function testFuzz_Withdraw(uint96 amount) public {

And now it passes:

$ forge test --zksync
Compiling 1 files with Solc 0.8.10
Solc 0.8.10 finished in 847.95ms
Compiler run successful with warnings:
Warning (9302): Return value of low-level calls not used.
  --> test/Safe.t.sol:11:9:
   |
11 |         payable(msg.sender).call{value: address(this).balance}("");
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Warning (9302): Return value of low-level calls not used.
  --> test/Safe.t.sol:29:9:
   |
29 |         payable(address(safe)).call{value: amount}("");
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^


Compiling 1 files with zksolc and solc 0.8.10
zksolc and solc 0.8.10 finished in 5.99s
Compiler run successful with warnings:
Warning (9302)
Warning: Return value of low-level calls not used.
  --> test/Safe.t.sol:11:9:
   |
11 |         payable(msg.sender).call{value: address(this).balance}("");
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Warning (9302)
Warning: Return value of low-level calls not used.
  --> test/Safe.t.sol:29:9:
   |
29 |         payable(address(safe)).call{value: amount}("");
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

2024-10-06T00:06:23.331938Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.345903Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.359428Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.372780Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.386122Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.399445Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.412821Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.426576Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.439887Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.453148Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.466502Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.479790Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.493327Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.508274Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.521597Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.534853Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.548315Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.561608Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.575000Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.588318Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.601598Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.614948Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.628305Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.641584Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.654867Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.668118Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.681380Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.694643Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.708046Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.721287Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.734602Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.747917Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.761136Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.774378Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.787596Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.800848Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.814072Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.827425Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.840706Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.854035Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.867305Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.880672Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.893991Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.907290Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.920613Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.933875Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.947108Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.960362Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.973694Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.986924Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.000161Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.013385Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.026762Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.040011Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.053185Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.066446Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.079639Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.093074Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.106308Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.119607Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.132847Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.146042Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.159229Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.172625Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.185998Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.199339Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.212739Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.226044Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.239326Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.252540Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.265805Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.279235Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.292659Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.306850Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.320262Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.333621Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.346983Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.360354Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.373691Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.387072Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.400302Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.413762Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.427217Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.440618Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.453915Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.467234Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.480611Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.494411Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.507939Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.521187Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.534513Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.547832Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.561113Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.574438Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.587761Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.601179Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.614612Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.627890Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.641183Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.654465Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.667758Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.681066Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.694356Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.707827Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.721143Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.734479Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.747757Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.761018Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.774276Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.787530Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.800799Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.814083Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.827371Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.840678Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.853984Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.867329Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.882303Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.896239Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.909740Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.923287Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.936861Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.950356Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.968098Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.988084Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.002207Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.015815Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.029436Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.043060Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.056630Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.070191Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.084092Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.097437Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.110758Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.124160Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.137051Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.150173Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.163572Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.176865Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.190256Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.203621Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.217100Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.230452Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.243789Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.257124Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.270453Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.283888Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.297215Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.310586Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.325137Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.338738Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.352459Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.365767Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.379083Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.392345Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.405695Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.419153Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.434180Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.447513Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.460802Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.474155Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.487444Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.500676Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.514007Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.527266Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.540562Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.553775Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.567087Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.580410Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.593703Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.607090Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.620294Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.633672Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.647031Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.660402Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.674061Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.687594Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.700859Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.715210Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.728717Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.742385Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.755831Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.769295Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.782724Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.796195Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.809492Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.822718Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.836027Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.849216Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.862508Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.875737Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.888952Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.902123Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.915368Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.928775Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.942060Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.955427Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.968683Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.981962Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.995160Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.008416Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.021826Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.035135Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.048334Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.061572Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.074915Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.088196Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.101535Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.114856Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.127698Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.140786Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.154128Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.167431Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.180673Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.193932Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.207174Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.220574Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.233890Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.247199Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.260473Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.273766Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.287069Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.300298Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.313551Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.326910Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.340148Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.353474Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.366865Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.380371Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.393685Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.406930Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.420321Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.433653Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.446937Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.460151Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.473379Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.486611Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.499907Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.513228Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.526535Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.539798Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.553094Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.566456Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.579731Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.592566Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.605634Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.619056Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.632333Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.645624Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.658878Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.672160Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.685420Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.698786Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.712119Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.725413Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.738752Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.751953Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.765158Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""

Ran 1 test for test/Safe.t.sol:SafeTest
[PASS] testFuzz_Withdraw(uint96) (runs: 257, μ: 1072652, ~: 995989)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.46s (3.45s CPU time)

Ran 1 test suite in 3.46s (3.46s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

You may want to exclude certain cases using the assume cheatcode. In those cases, fuzzer will discard the inputs and start a new fuzz run:

function testFuzz_Withdraw(uint96 amount) public {
    vm.assume(amount > 0.1 ether);
    // snip
}

There are different ways to run property-based tests, notably parametric testing and fuzzing. Forge only supports fuzzing.

Interpreting results

You might have noticed that fuzz tests are summarized a bit differently compared to unit tests:

  • “runs” refers to the amount of scenarios the fuzzer tested. By default, the fuzzer will generate 256 scenarios, but this and other test execution parameters can be setup by the user. Fuzzer configuration details are provided here.
  • “μ” (Greek letter mu) is the mean gas used across all fuzz runs
  • “~” (tilde) is the median gas used across all fuzz runs

Configuring fuzz test execution

Fuzz tests execution is governed by parameters that can be controlled by users via Forge configuration primitives. Configs can be applied globally or on a per-test basis. For details on this topic please refer to 📚 Global config and 📚 In-line config.

Fuzz test fixtures

Fuzz test fixtures can be defined when you want to make sure a certain set of values is used as inputs for fuzzed parameters. These fixtures can be declared in tests as:

  • storage arrays prefixed with fixture and followed by param name to be fuzzed. For example, fixtures to be used when fuzzing parameter amount of type uint32 can be defined as
uint32[] public fixtureAmount = [1, 5, 555];
  • functions named with fixture prefix, followed by param name to be fuzzed. Function should return an (fixed size or dynamic) array of values to be used for fuzzing. For example, fixtures to be used when fuzzing parameter named owner of type address can be defined in a function with signature
function fixtureOwner() public returns (address[] memory)

If the type of value provided as a fixture is not the same type as the named parameter to be fuzzed then it is rejected and an error is raised.

An example where fixture could be used is to reproduce the DSChief vulnerability. Consider the 2 functions

    function etch(address yay) public returns (bytes32 slate) {
        bytes32 hash = keccak256(abi.encodePacked(yay));

        slates[hash] = yay;

        return hash;
    }

    function voteSlate(bytes32 slate) public {
        uint weight = deposits[msg.sender];
        subWeight(weight, votes[msg.sender]);
        votes[msg.sender] = slate;
        addWeight(weight, votes[msg.sender]);
    }

where the vulnerability can be reproduced by calling voteSlate before etch, with slate value being a hash of yay address. To make sure fuzzer includes in the same run a slate value derived from a yay address, following fixtures can be defined:

    address[] public fixtureYay = [
        makeAddr("yay1"),
        makeAddr("yay2"),
        makeAddr("yay3")
    ];

    bytes32[] public fixtureSlate = [
        keccak256(abi.encodePacked(makeAddr("yay1"))),
        keccak256(abi.encodePacked(makeAddr("yay2"))),
        keccak256(abi.encodePacked(makeAddr("yay3")))
    ];

Following image shows how fuzzer generates values with and without fixtures being declared: Fuzzer

Invariant Testing

Overview

Invariant testing allows for a set of invariant expressions to be tested against randomized sequences of pre-defined function calls from pre-defined contracts. After each function call is performed, all defined invariants are asserted.

Invariant testing is a powerful tool to expose incorrect logic in protocols. Due to the fact that function call sequences are randomized and have fuzzed inputs, invariant testing can expose false assumptions and incorrect logic in edge cases and highly complex protocol states.

Invariant testing campaigns have two dimensions, runs and depth:

  • runs: Number of times that a sequence of function calls is generated and run.
  • depth: Number of function calls made in a given run. Invariants are asserted after each function call is made. If a function call reverts, the depth counter still increments.

ℹ️ Note

When implementing invariant tests is important to be aware that for each invariant_* function a different EVM executor is created, therefore invariants are not asserted against same EVM state. This means that if invariant_A() and invariant_B() functions are defined then invariant_B() won’t be asserted against EVM state of invariant_A() (and the other way around).

If you want to assert all invariants at the same time then they can be grouped and run on multiple jobs. For example, assert all invariants using two jobs can be implemented as:

function invariant_job1() public {
   assertInvariants();
}

function invariant_job2() public {
   assertInvariants();
}

function assertInvariants() internal {
   assertEq(val1, val2);
   assertEq(val3, val4);
}

These and other invariant configuration aspects are explained here.

Similar to how standard tests are run in Foundry by prefixing a function name with test, invariant tests are denoted by prefixing the function name with invariant (e.g., function invariant_A()).

afterInvariant() function is called at the end of each invariant run (if declared), allowing post campaign processing. This function can be used for logging campaign metrics (e.g. how many times a selector was called) and post fuzz campaign testing (e.g. close out all positions and assert all funds are able to exit the system).

Configuring invariant test execution

Invariant tests execution is governed by parameters that can be controlled by users via Forge configuration primitives. Configs can be applied globally or on a per-test basis. For details on this topic please refer to 📚 Global config and 📚 In-line config.

Defining Invariants

Invariants are conditions expressions that should always hold true over the course of a fuzzing campaign. A good invariant testing suite should have as many invariants as possible, and can have different testing suites for different protocol states.

Examples of invariants are:

  • “The xy=k formula always holds” for Uniswap
  • “The sum of all user balances is equal to the total supply” for an ERC-20 token.

There are different ways to assert invariants, as outlined in the table below:

TypeExplanationExample
Direct assertions Query a protocol smart contract and assert values are as expected.
assertGe(
    token.totalAssets(),
    token.totalSupply()
)
Ghost variable assertions Query a protocol smart contract and compare it against a value that has been persisted in the test environment (ghost variable).
assertEq(
    token.totalSupply(),
    sumBalanceOf
)
Deoptimizing (Naive implementation assertions) Query a protocol smart contract and compare it against a naive and typically highly gas-inefficient implementation of the same desired logic.
assertEq(
    pool.outstandingInterest(),
    test.naiveInterest()
)

Conditional Invariants

Invariants must hold over the course of a given fuzzing campaign, but that doesn’t mean they must hold true in every situation. There is the possibility for certain invariants to be introduced/removed in a given scenario (e.g., during a liquidation).

It is not recommended to introduce conditional logic into invariant assertions because they have the possibility of introducing false positives because of an incorrect code path. For example:

function invariant_example() external {
    if (protocolCondition) return;

    assertEq(val1, val2);
}

In this situation, if protocolCondition == true, the invariant is not asserted at all. Sometimes this can be desired behavior, but it can cause issues if the protocolCondition is true for the whole fuzzing campaign unexpectedly, or there is a logic error in the condition itself. For this reason its better to try and define an alternative invariant for that condition as well, for example:

function invariant_example() external {
    if (protocolCondition) {
        assertLe(val1, val2);
        return;
    };

    assertEq(val1, val2);
}

Another approach to handle different invariants across protocol states is to utilize dedicated invariant testing contracts for different scenarios. These scenarios can be bootstrapped using the setUp function, but it is more powerful to leverage invariant targets to govern the fuzzer to behave in a way that will only yield certain results (e.g., avoid liquidations).

Invariant Targets

Target Contracts: The set of contracts that will be called over the course of a given invariant test fuzzing campaign. This set of contracts defaults to all contracts that were deployed in the setUp function, but can be customized to allow for more advanced invariant testing.

Target Senders: The invariant test fuzzer picks values for msg.sender at random when performing fuzz campaigns to simulate multiple actors in a system by default. If desired, the set of senders can be customized in the setUp function.

Target Interfaces: The set of addresses and their project identifiers that are not deployed during setUp but fuzzed in a forked environment (E.g. [(0x1, ["IERC20"]), (0x2, ("IOwnable"))]). This enables targeting of delegate proxies and contracts deployed with create or create2.

Target Selectors: The set of function selectors that are used by the fuzzer for invariant testing. These can be used to use a subset of functions within a given target contract.

Target Artifacts: The desired ABI to be used for a given contract. These can be used for proxy contract configurations.

Target Artifact Selectors: The desired subset of function selectors to be used within a given ABI to be used for a given contract. These can be used for proxy contract configurations.

Priorities for the invariant fuzzer in the cases of target clashes are:

targetInterfaces | targetSelectors > excludeSelectors | targetArtifactSelectors > excludeContracts | excludeArtifacts > targetContracts | targetArtifacts

Function Call Probability Distribution

Functions from these contracts will be called at random (with a uniformly distributed probability) with fuzzed inputs.

For example:

targetContract1:
├─ function1: 20%
└─ function2: 20%

targetContract2:
├─ function1: 20%
├─ function2: 20%
└─ function3: 20%

This is something to be mindful of when designing target contracts, as target contracts with less functions will have each function called more often due to this probability distribution.

Invariant Test Helper Functions

Invariant test helper functions are included in forge-std to allow for configurable invariant test setup. The helper functions are outlined below:

FunctionDescription
excludeContract(address newExcludedContract_)Adds a given address to the _excludedContracts array. This set of contracts is explicitly excluded from the target contracts.
excludeSelector(FuzzSelector memory newExcludedSelector_)Adds a given FuzzSelector to the _excludedSelectors array. This set of FuzzSelectors is explicitly excluded from the target contract selectors.
excludeSender(address newExcludedSender_)Adds a given address to the _excludedSenders array. This set of addresses is explicitly excluded from the target senders.
excludeArtifact(string memory newExcludedArtifact_)Adds a given string to the _excludedArtifacts array. This set of strings is explicitly excluded from the target artifacts.
targetArtifact(string memory newTargetedArtifact_)Adds a given string to the _targetedArtifacts array. This set of strings is used for the target artifacts.
targetArtifactSelector(FuzzArtifactSelector memory newTargetedArtifactSelector_)Adds a given FuzzArtifactSelector to the _targetedArtifactSelectors array. This set of FuzzArtifactSelectors is used for the target artifact selectors.
targetContract(address newTargetedContract_)Adds a given address to the _targetedContracts array. This set of addresses is used for the target contracts. This array overwrites the set of contracts that was deployed during the setUp.
targetSelector(FuzzSelector memory newTargetedSelector_)Adds a given FuzzSelector to the _targetedSelectors array. This set of FuzzSelectors is used for the target contract selectors.
targetSender(address newTargetedSender_)Adds a given address to the _targetedSenders array. This set of addresses is used for the target senders.
targetInterface(FuzzInterface memory newTargetedInterface_)Adds a given FuzzInterface to the _targetedInterfaces array. This set of FuzzInterface extends the contracts and selectors to fuzz and enables targeting of addresses that are not deployed during setUp such as when fuzzing in a forked environment. Also enables targeting of delegate proxies and contracts deployed with create or create2.

Target Contract Setup

Target contracts can be set up using the following three methods:

  1. Contracts that are manually added to the targetContracts array are added to the set of target contracts.
  2. Contracts that are deployed in the setUp function are automatically added to the set of target contracts (only works if no contracts have been manually added using option 1).
  3. Contracts that are deployed in the setUp can be removed from the target contracts if they are added to the excludeContracts array.

Open Testing

The default configuration for target contracts is set to all contracts that are deployed during the setup. For smaller modules and more arithmetic contracts, this works well. For example:

contract ExampleContract1 {

    uint256 public val1;
    uint256 public val2;
    uint256 public val3;

    function addToA(uint256 amount) external {
        val1 += amount;
        val3 += amount;
    }

    function addToB(uint256 amount) external {
        val2 += amount;
        val3 += amount;
    }

}

This contract could be deployed and tested using the default target contract pattern:

contract InvariantExample1 is Test {

    ExampleContract1 foo;

    function setUp() external {
        foo = new ExampleContract1();
    }

    function invariant_A() external {
        assertEq(foo.val1() + foo.val2(), foo.val3());
    }

    function invariant_B() external {
        assertGe(foo.val1() + foo.val2(), foo.val3());
    }

}

This setup will call foo.addToA() and foo.addToB() with a 50%-50% probability distribution with fuzzed inputs. Inevitably, the inputs will start to cause overflows and the function calls will start reverting. Since the default configuration in invariant testing is fail_on_revert = false, this will not cause the tests to fail. The invariants will hold throughout the rest of the fuzzing campaign and the result is that the test will pass. The output will look something like this:

[PASS] invariant_A() (runs: 50, calls: 10000, reverts: 5533)
[PASS] invariant_B() (runs: 50, calls: 10000, reverts: 5533)

Handler-Based Testing

For more complex and integrated protocols, more sophisticated target contract usage is required to achieve the desired results. To illustrate how Handlers can be leveraged, the following contract will be used (an ERC-4626 based contract that accepts deposits of another ERC-20 token):

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;

interface IERC20Like {

    function balanceOf(address owner_) external view returns (uint256 balance_);

    function transferFrom(
        address owner_,
        address recipient_,
        uint256 amount_
    ) external returns (bool success_);

}

contract Basic4626Deposit {

    /**********************************************************************************************/
    /*** Storage                                                                                ***/
    /**********************************************************************************************/

    address public immutable asset;

    string public name;
    string public symbol;

    uint8 public immutable decimals;

    uint256 public totalSupply;

    mapping(address => uint256) public balanceOf;

    /**********************************************************************************************/
    /*** Constructor                                                                            ***/
    /**********************************************************************************************/

    constructor(address asset_, string memory name_, string memory symbol_, uint8 decimals_) {
        asset    = asset_;
        name     = name_;
        symbol   = symbol_;
        decimals = decimals_;
    }

    /**********************************************************************************************/
    /*** External Functions                                                                     ***/
    /**********************************************************************************************/

    function deposit(uint256 assets_, address receiver_) external returns (uint256 shares_) {
        shares_ = convertToShares(assets_);

        require(receiver_ != address(0), "ZERO_RECEIVER");
        require(shares_   != uint256(0), "ZERO_SHARES");
        require(assets_   != uint256(0), "ZERO_ASSETS");

        totalSupply += shares_;

        // Cannot overflow because totalSupply would first overflow in the statement above.
        unchecked { balanceOf[receiver_] += shares_; }

        require(
            IERC20Like(asset).transferFrom(msg.sender, address(this), assets_),
            "TRANSFER_FROM"
        );
    }

    function transfer(address recipient_, uint256 amount_) external returns (bool success_) {
        balanceOf[msg.sender] -= amount_;

        // Cannot overflow because minting prevents overflow of totalSupply,
        // and sum of user balances == totalSupply.
        unchecked { balanceOf[recipient_] += amount_; }

        return true;
    }

    /**********************************************************************************************/
    /*** Public View Functions                                                                  ***/
    /**********************************************************************************************/

    function convertToShares(uint256 assets_) public view returns (uint256 shares_) {
        uint256 supply_ = totalSupply;  // Cache to stack.

        shares_ = supply_ == 0 ? assets_ : (assets_ * supply_) / totalAssets();
    }

    function totalAssets() public view returns (uint256 assets_) {
        assets_ = IERC20Like(asset).balanceOf(address(this));
    }

}

Handler Functions

This contract’s deposit function requires that the caller has a non-zero balance of the ERC-20 asset. In the Open invariant testing approach, deposit() and transfer() would be called with a 50-50% distribution, but they would revert on every call. This would cause the invariant tests to “pass”, but in reality no state was manipulated in the desired contract at all. This is where target contracts can be leveraged. When a contract requires some additional logic in order to function properly, it can be added in a dedicated contract called a Handler.

function deposit(uint256 assets) public virtual {
    asset.mint(address(this), assets);

    asset.approve(address(token), assets);

    uint256 shares = token.deposit(assets, address(this));
}

This contract will provide the necessary setup before a function call is made in order to ensure it is successful.

Building on this concept, Handlers can be used to develop more sophisticated invariant tests. With Open invariant testing, the tests run as shown in the diagram below, with random sequences of function calls being made to the protocol contracts directly with fuzzed parameters. This will cause reverts for more complex systems as outlined above.

Blank diagram

By manually adding all Handler contracts to the targetContracts array, all function calls made to protocol contracts can be made in a way that is governed by the Handler to ensure successful calls. This is outlined in the diagram below.

Invariant Diagrams - Page 2

With this layer between the fuzzer and the protocol, more powerful testing can be achieved.

Handler Ghost Variables

Within Handlers, “ghost variables” can be tracked across multiple function calls to add additional information for invariant tests. A good example of this is summing all of the shares that each LP owns after depositing into the ERC-4626 token as shown above, and using that in the invariant (totalSupply == sumBalanceOf).

function deposit(uint256 assets) public virtual {
    asset.mint(address(this), assets);

    asset.approve(address(token), assets);

    uint256 shares = token.deposit(assets, address(this));

    sumBalanceOf += shares;
}

Function-Level Assertions

Another benefit is the ability to perform assertions on function calls as they are happening. An example is asserting the ERC-20 balance of the LP has decremented by assets during the deposit function call, as well as their LP token balance incrementing by shares. In this way, handler functions are similar to fuzz tests because they can take in fuzzed inputs, perform state changes, and assert before/after state.

function deposit(uint256 assets) public virtual {
    asset.mint(address(this), assets);

    asset.approve(address(token), assets);

    uint256 beforeBalance = asset.balanceOf(address(this));

    uint256 shares = token.deposit(assets, address(this));

    assertEq(asset.balanceOf(address(this)), beforeBalance - assets);

    sumBalanceOf += shares;
}

Bounded/Unbounded Functions

In addition, with Handlers, input parameters can be bounded to reasonable expected values such that fail_on_revert in foundry.toml can be set to true. This can be accomplished using the bound() helper function from forge-std. This ensures that every function call that is being made by the fuzzer must be successful against the protocol in order to get tests to pass. This is very useful for visibility and confidence that the protocol is being tested in the desired way.

function deposit(uint256 assets) external {
    assets = bound(assets, 0, 1e30);

    asset.mint(address(this), assets);

    asset.approve(address(token), assets);

    uint256 beforeBalance = asset.balanceOf(address(this));

    uint256 shares = token.deposit(assets, address(this));

    assertEq(asset.balanceOf(address(this)), beforeBalance - assets);

    sumBalanceOf += shares;
}

This can also be accomplished by inheriting non-bounded functions from dedicated “unbounded” Handler contracts that can be used for fail_on_revert = false testing. This type of testing is also useful since it can expose issues in assumptions made with bound function usage.

// Unbounded
function deposit(uint256 assets) public virtual {
    asset.mint(address(this), assets);

    asset.approve(address(token), assets);

    uint256 beforeBalance = asset.balanceOf(address(this));

    uint256 shares = token.deposit(assets, address(this));

    assertEq(asset.balanceOf(address(this)), beforeBalance - assets);

    sumBalanceOf += shares;
}
// Bounded
function deposit(uint256 assets) external {
    assets = bound(assets, 0, 1e30);

    super.deposit(assets);
}

Actor Management

In the function calls above, it can be seen that address(this) is the sole depositor in the ERC-4626 contract, which is not a realistic representation of its intended use. By leveraging the prank cheatcodes in forge-std, each Handler can manage a set of actors and use them to perform the same function call from different msg.sender addresses. This can be accomplished using the following modifier:

address[] public actors;

address internal currentActor;

modifier useActor(uint256 actorIndexSeed) {
    currentActor = actors[bound(actorIndexSeed, 0, actors.length - 1)];
    vm.startPrank(currentActor);
    _;
    vm.stopPrank();
}

Using multiple actors allows for more granular ghost variable usage as well. This is demonstrated in the functions below:

// Unbounded
function deposit(
    uint256 assets,
    uint256 actorIndexSeed
) public virtual useActor(actorIndexSeed) {
    asset.mint(currentActor, assets);

    asset.approve(address(token), assets);

    uint256 beforeBalance = asset.balanceOf(address(this));

    uint256 shares = token.deposit(assets, address(this));

    assertEq(asset.balanceOf(address(this)), beforeBalance - assets);

    sumBalanceOf += shares;

    sumDeposits[currentActor] += assets
}
// Bounded
function deposit(uint256 assets, uint256 actorIndexSeed) external {
    assets = bound(assets, 0, 1e30);

    super.deposit(assets, actorIndexSeed);
}

Differential Testing

Forge can be used for differential testing and differential fuzzing. You can even test against non-EVM executables using the ffi cheatcode.

Background

Differential testing cross references multiple implementations of the same function by comparing each one’s output. Imagine we have a function specification F(X), and two implementations of that specification: f1(X) and f2(X). We expect f1(x) == f2(x) for all x that exist in an appropriate input space. If f1(x) != f2(x), we know that at least one function is incorrectly implementing F(X). This process of testing for equality and identifying discrepancies is the core of differential testing.

Differential fuzzing is an extension of differential testing. Differential fuzzing programmatically generates many values of x to find discrepancies and edge cases that manually chosen inputs might not reveal.

Note: the == operator in this case can be a custom definition of equality. For example, if testing floating point implementations, you might use approximate equality with a certain tolerance.

Some real life uses of this type of testing include:

  • Comparing upgraded implementations to their predecessors
  • Testing code against known reference implementations
  • Confirming compatibility with third party tools and dependencies

Below are some examples of how Forge is used for differential testing.

Primer: The ffi cheatcode

ffi allows you to execute an arbitrary shell command and capture the output. Here’s a mock example:

import {Test} from "forge-std/Test.sol";

contract TestContract is Test {

    function testMyFFI () public {
        string[] memory cmds = new string[](2);
        cmds[0] = "cat";
        cmds[1] = "address.txt"; // assume contains abi-encoded address.
        bytes memory result = vm.ffi(cmds);
        address loadedAddress = abi.decode(result, (address));
        // Do something with the address
        // ...
    }
}

An address has previously been written to address.txt, and we read it in using the FFI cheatcode. This data can now be used throughout your test contract.

Example: Differential Testing Merkle Tree Implementations

Merkle Trees are a cryptographic commitment scheme frequently used in blockchain applications. Their popularity has led to a number of different implementations of Merkle Tree generators, provers, and verifiers. Merkle roots and proofs are often generated using a language like JavaScript or Python, while proof verification usually occurs on-chain in Solidity.

Murky is a complete implementation of Merkle roots, proofs, and verification in Solidity. Its test suite includes differential tests against OpenZeppelin’s Merkle proof library, as well as root generation tests against a reference JavaScript implementation. These tests are powered by Foundry’s fuzzing and ffi capabilities.

Differential fuzzing against a reference TypeScript implementation

Using the ffi cheatcode, Murky tests its own Merkle root implementation against a TypeScript implementation using data provided by Forge’s fuzzer:

function testMerkleRootMatchesJSImplementationFuzzed(bytes32[] memory leaves) public {
    vm.assume(leaves.length > 1);
    bytes memory packed = abi.encodePacked(leaves);
    string[] memory runJsInputs = new string[](8);

    // Build ffi command string
    runJsInputs[0] = 'npm';
    runJsInputs[1] = '--prefix';
    runJsInputs[2] = 'differential_testing/scripts/';
    runJsInputs[3] = '--silent';
    runJsInputs[4] = 'run';
    runJsInputs[5] = 'generate-root-cli';
    runJsInputs[6] = leaves.length.toString();
    runJsInputs[7] = packed.toHexString();

    // Run command and capture output
    bytes memory jsResult = vm.ffi(runJsInputs);
    bytes32 jsGeneratedRoot = abi.decode(jsResult, (bytes32));

    // Calculate root using Murky
    bytes32 murkyGeneratedRoot = m.getRoot(leaves);
    assertEq(murkyGeneratedRoot, jsGeneratedRoot);
}

Note: see Strings2.sol in the Murky Repo for the library that enables (bytes memory).toHexString()

Forge runs npm --prefix differential_testing/scripts/ --silent run generate-root-cli {numLeaves} {hexEncodedLeaves}. This calculates the Merkle root for the input data using the reference JavaScript implementation. The script prints the root to stdout, and that printout is captured as bytes in the return value of vm.ffi().

The test then calculates the root using the Solidity implementation.

Finally, the test asserts that the both roots are exactly equal. If they are not equal, the test fails.

Differential fuzzing against OpenZeppelin’s Merkle Proof Library

You may want to use differential testing against another Solidity implementation. In that case, ffi is not needed. Instead, the reference implementation is imported directly into the test.

import {MerkleProof} from "openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol";
//...
function testCompatibilityOpenZeppelinProver(bytes32[] memory _data, uint256 node) public {
    vm.assume(_data.length > 1);
    vm.assume(node < _data.length);
    bytes32 root = m.getRoot(_data);
    bytes32[] memory proof = m.getProof(_data, node);
    bytes32 valueToProve = _data[node];
    bool murkyVerified = m.verifyProof(root, proof, valueToProve);
    bool ozVerified = MerkleProof.verify(proof, root, valueToProve);
    assertTrue(murkyVerified == ozVerified);
}

Differential testing against a known edge case

Differential tests are not always fuzzed – they are also useful for testing known edge cases. In the case of the Murky codebase, the initial implementation of the log2ceil function did not work for certain arrays whose lengths were close to a power of 2 (like 129). As a safety check, a test is always run against an array of this length and compared to the TypeScript implementation. You can see the full test here.

Standardized Testing against reference data

FFI is also useful for injecting reproducible, standardized data into the testing environment. In the Murky library, this is used as a benchmark for gas snapshotting (see forge snapshot).

bytes32[100] data;
uint256[8] leaves = [4, 8, 15, 16, 23, 42, 69, 88];

function setUp() public {
    string[] memory inputs = new string[](2);
    inputs[0] = "cat";
    inputs[1] = "src/test/standard_data/StandardInput.txt";
    bytes memory result =  vm.ffi(inputs);
    data = abi.decode(result, (bytes32[100]));
    m = new Merkle();
}

function testMerkleGenerateProofStandard() public view {
    bytes32[] memory _data = _getData();
    for (uint i = 0; i < leaves.length; ++i) {
        m.getProof(_data, leaves[i]);
    }
}

src/test/standard_data/StandardInput.txt is a text file that contains an encoded bytes32[100] array. It’s generated outside of the test and can be used in any language’s Web3 SDK. It looks something like:

0xf910ccaa307836354233316666386231414464306335333243453944383735313..423532

The standardized testing contract reads in the file using ffi. It decodes the data into an array and then, in this example, generates proofs for 8 different leaves. Because the data is constant and standard, we can meaningfully measure gas and performance improvements using this test.

Of course, one could just hardcode the array into the test! But that makes it much harder to do consistent testing across contracts, implementations, etc.

Example: Differential Testing Gradual Dutch Auctions

The reference implementation for Paradigm’s Gradual Dutch Auction mechanism contains a number of differential, fuzzed tests. It is an excellent repository to further explore differential testing using ffi.

Reference Repositories

If you have another repository that would serve as a reference, please contribute it!

Deploying

Forge can deploy smart contracts to a given network with the forge create command.

Forge CLI can deploy only one contract at a time.

For deploying and verifying multiple smart contracts in one go, Forge’s Solidity scripting would be the more efficient approach.

To deploy a contract, you must provide a RPC URL (env: ETH_RPC_URL) and the private key of the account that will deploy the contract.

To deploy MyContract to a network:

$ forge create --zksync --rpc-url <your_rpc_url> --private-key <your_private_key> src/MyContract.sol:MyContract
compiling...
success.
Deployer: 0xa735b3c25f...
Deployed to: 0x4054415432...
Transaction hash: 0x6b4e0ff93a...

Solidity files may contain multiple contracts. :MyContract above specifies which contract to deploy from the src/MyContract.sol file.

Use the --constructor-args flag to pass arguments to the constructor:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import {ERC20} from "solmate/tokens/ERC20.sol";

contract MyToken is ERC20 {
    constructor(
        string memory name,
        string memory symbol,
        uint8 decimals,
        uint256 initialSupply
    ) ERC20(name, symbol, decimals) {
        _mint(msg.sender, initialSupply);
    }
}

Additionally, we can tell Forge to verify our contract on ZKsync Block Explorer, Etherscan, Sourcify or Blockscout, if the network is supported, by passing --verify.

It is recommended to make use of ZKsync Block Explorer by specifying --verifier zksync and using the verification URL (e.g. ZKsync Sepolia https://explorer.sepolia.era.zksync.dev/contract_verification).

$ forge create --zksync \
    --rpc-url <your_rpc_url> \
    --constructor-args "ForgeUSD" "FUSD" 18 1000000000000000000000 \
    --private-key <your_private_key> \
    --verifier zksync  \
    --verifier-url https://explorer.sepolia.era.zksync.dev/contract_verification \
    --verify \
    src/MyToken.sol:MyToken

Verifying a pre-existing contract

It is recommended to use the --verify flag with forge create to automatically verify the contract on explorer after a deployment using --verifier zksync to target ZKsync Block Explorer instance.

If you are verifying an already deployed contract, read on.

You can verify a contract on ZKsync Block Explorer, Etherscan, Sourcify, oklink or Blockscout with the forge verify-contract command.

You must provide:

  • the contract address
  • the contract name or the path to the contract <path>:<contractname>
  • your Etherscan API key (env: ETHERSCAN_API_KEY) (if verifying on Etherscan).

Moreover, you may need to provide:

  • the constructor arguments in the ABI-encoded format, if there are any
  • compiler version used for build, with 8 hex digits from the commit version prefix (the commit will usually not be a nightly build). It is auto-detected if not specified.
  • the number of optimizations, if the Solidity optimizer was activated. It is auto-detected if not specified.
  • the chain ID, if the contract is not on Ethereum Mainnet

Let’s say you want to verify MyToken (see above). You set the number of optimizations to 1 million, compiled it with v0.8.10, and deployed it, as shown above, to the Sepolia testnet (chain ID: 11155111). Note that --num-of-optimizations will default to 0 if not set on verification, while it defaults to 200 if not set on deployment, so make sure you pass --num-of-optimizations 200 if you left the default compilation settings.

Here’s how to verify it:

forge verify-contract \
    --zksync \
    --chain zksync-testnet \
    --num-of-optimizations 1000000 \
    --watch \
    --verifier zksync  \
    --verifier-url https://explorer.sepolia.era.zksync.dev/contract_verification \
    --constructor-args $(cast abi-encode "constructor(string,string,uint256,uint256)" "ForgeUSD" "FUSD" 18 1000000000000000000000) \
    <the_contract_address> \
    src/MyToken.sol:MyToken 

Submitting verification for [src/MyToken.sol:MyToken] at address 0x21d6dffe4B406c59E80CD62b4cB1763363c8a040.
Verification submitted successfully. Verification ID: 27574
Checking verification status for ID: 27574 using verifier: ZKsync at URL: https://explorer.sepolia.era.zksync.dev/contract_verification
Verification was successful.

It is recommended to use the --watch flag along with verify-contract command in order to poll for the verification result.

If the --watch flag was not supplied, you can check the verification status with the forge verify-check command:

$ forge verify-check --zksync --chain zksync-testnet <verificationId> --verifier zksync
Contract successfully verified.

💡 Tip

Use Cast’s abi-encode to ABI-encode arguments.

In this example, we ran cast abi-encode "constructor(string,string,uint8,uint256)" "ForgeUSD" "FUSD" 18 1000000000000000000000 to ABI-encode the arguments.


Troubleshooting

missing hex prefix ("0x") for hex string

Make sure the private key string begins with 0x.

EIP-1559 not activated

EIP-1559 is not supported or not activated on the RPC server. Pass the --legacy flag to use legacy transactions instead of the EIP-1559 ones. If you do development in a local environment, you can use Hardhat instead of Ganache.

Failed to parse tokens

Make sure the passed arguments are of correct type.

Signature error

Make sure the private key is correct.

Compiler version commit for verify

If you want to check the exact commit you are running locally, try: ~/.svm/0.x.y/solc-0.x.y --version where x and y are major and minor version numbers respectively. The output of this will be something like:

solc, the solidity compiler commandline interface
Version: 0.8.12+commit.f00d7308.Darwin.appleclang

Note: You cannot just paste the entire string “0.8.12+commit.f00d7308.Darwin.appleclang” as the argument for the compiler-version. But you can use the 8 hex digits of the commit to look up exactly what you should copy and paste from compiler version.

Known Issues

Verifying Contracts With Ambiguous Import Paths

Forge passes source directories (src, lib, test etc) as --include-path arguments to the compiler. This means that given the following project tree

|- src
|-- folder
|--- Contract.sol
|--- IContract.sol

it is possible to import IContract inside the Contract.sol using folder/IContract.sol import path.

Etherscan is not able to recompile such sources. Consider changing the imports to use relative import path.

Verifying Contracts With No Bytecode Hash

Currently, it’s not possible to verify contracts on Etherscan with bytecode_hash set to none. Click here to learn more about how metadata hash is used for source code verification.

Gas Tracking

🚨 Important

Gas tracking may not be entirely accurate in the ZKsync context. This is mostly due to the additional overhead to executing each CREATE or CALL in its own zkEVM which has additional bootloader gas costs.

Forge can help you estimate how much gas your contract will consume.

Currently, Forge ships with three different tools for this job:

  • Gas reports: Gas reports give you an overview of how much Forge thinks the individual functions in your contracts will consume in gas.
  • Gas function snapshots: Gas function snapshots give you an overview of how much each test function consumes in gas.
  • Gas section snapshots: Gas section snapshots give you the ability to capture gas usage over arbitrary sections inside of test functions. This also tracks internal gas usage. You can access this by using the snapshotGas* cheatcodes inside your tests.

Gas reports, gas function snapshots and gas section snapshots differ in some ways:

  • Gas reports use tracing to figure out gas costs for individual contract calls.
    This gives more granular insight, at the cost of speed.
  • Gas function snapshots have more built-in tools, such as diffs and exporting the results to a file. Snapshots are not as granular as gas reports, but they are faster to generate.
  • Gas section snapshots provides the most granular way to capture gas usage. Every captured gas snapshot is written to a file in a snapshots directory. By default these snapshots are grouped by the contract name of the test.

Gas Reports

🚨 Important

Gas reports may not be entirely accurate in the ZKsync context. This is mostly due to the additional overhead to executing each CREATE or CALL in its own zkEVM which has additional bootloader gas costs.

Forge can produce gas reports for your contracts. You can configure which contracts output gas reports via the gas_reports field in foundry.toml.

To produce reports for specific contracts:

gas_reports = ["MyContract", "MyContractFactory"]

To produce reports for all contracts:

gas_reports = ["*"]

To generate gas reports, run forge test --gas-report.

You can also use it in combination with other subcommands, such as forge test --match-test testBurn --gas-report, to generate only a gas report relevant to this test.

Example output:

╭───────────────────────┬─────────────────┬────────┬────────┬────────┬─────────╮
│ MockERC1155 contract  ┆                 ┆        ┆        ┆        ┆         │
╞═══════════════════════╪═════════════════╪════════╪════════╪════════╪═════════╡
│ Deployment Cost       ┆ Deployment Size ┆        ┆        ┆        ┆         │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ 1082720               ┆ 5440            ┆        ┆        ┆        ┆         │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ Function Name         ┆ min             ┆ avg    ┆ median ┆ max    ┆ # calls │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ balanceOf             ┆ 596             ┆ 596    ┆ 596    ┆ 596    ┆ 44      │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ balanceOfBatch        ┆ 2363            ┆ 4005   ┆ 4005   ┆ 5647   ┆ 2       │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ batchBurn             ┆ 2126            ┆ 5560   ┆ 2584   ┆ 11970  ┆ 3       │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ batchMint             ┆ 2444            ┆ 135299 ┆ 125081 ┆ 438531 ┆ 18      │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ burn                  ┆ 814             ┆ 2117   ┆ 2117   ┆ 3421   ┆ 2       │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ isApprovedForAll      ┆ 749             ┆ 749    ┆ 749    ┆ 749    ┆ 1       │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ mint                  ┆ 26039           ┆ 31943  ┆ 27685  ┆ 118859 ┆ 22      │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ safeBatchTransferFrom ┆ 2561            ┆ 137750 ┆ 126910 ┆ 461304 ┆ 8       │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ safeTransferFrom      ┆ 1335            ┆ 34505  ┆ 28103  ┆ 139557 ┆ 9       │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ setApprovalForAll     ┆ 24485           ┆ 24485  ┆ 24485  ┆ 24485  ┆ 12      │
╰───────────────────────┴─────────────────┴────────┴────────┴────────┴─────────╯

╭───────────────────────┬─────────────────┬────────┬────────┬────────┬─────────╮
│ Example contract      ┆                 ┆        ┆        ┆        ┆         │
╞═══════════════════════╪═════════════════╪════════╪════════╪════════╪═════════╡
│ Deployment Cost       ┆ Deployment Size ┆        ┆        ┆        ┆         │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ 1082720               ┆ 5440            ┆        ┆        ┆        ┆         │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ Function Name         ┆ min             ┆ avg    ┆ median ┆ max    ┆ # calls │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ foo                   ┆ 596             ┆ 596    ┆ 596    ┆ 596    ┆ 44      │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ bar                   ┆ 2363            ┆ 4005   ┆ 4005   ┆ 5647   ┆ 2       │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ baz                   ┆ 2126            ┆ 5560   ┆ 2584   ┆ 11970  ┆ 3       │
╰───────────────────────┴─────────────────┴────────┴────────┴────────┴─────────╯

You can also ignore contracts via the gas_reports_ignore field in foundry.toml:

gas_reports_ignore = ["Example"]

This would change the output to:

╭───────────────────────┬─────────────────┬────────┬────────┬────────┬─────────╮
│ MockERC1155 contract  ┆                 ┆        ┆        ┆        ┆         │
╞═══════════════════════╪═════════════════╪════════╪════════╪════════╪═════════╡
│ Deployment Cost       ┆ Deployment Size ┆        ┆        ┆        ┆         │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ 1082720               ┆ 5440            ┆        ┆        ┆        ┆         │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ Function Name         ┆ min             ┆ avg    ┆ median ┆ max    ┆ # calls │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ balanceOf             ┆ 596             ┆ 596    ┆ 596    ┆ 596    ┆ 44      │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ balanceOfBatch        ┆ 2363            ┆ 4005   ┆ 4005   ┆ 5647   ┆ 2       │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ batchBurn             ┆ 2126            ┆ 5560   ┆ 2584   ┆ 11970  ┆ 3       │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ batchMint             ┆ 2444            ┆ 135299 ┆ 125081 ┆ 438531 ┆ 18      │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ burn                  ┆ 814             ┆ 2117   ┆ 2117   ┆ 3421   ┆ 2       │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ isApprovedForAll      ┆ 749             ┆ 749    ┆ 749    ┆ 749    ┆ 1       │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ mint                  ┆ 26039           ┆ 31943  ┆ 27685  ┆ 118859 ┆ 22      │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ safeBatchTransferFrom ┆ 2561            ┆ 137750 ┆ 126910 ┆ 461304 ┆ 8       │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ safeTransferFrom      ┆ 1335            ┆ 34505  ┆ 28103  ┆ 139557 ┆ 9       │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ setApprovalForAll     ┆ 24485           ┆ 24485  ┆ 24485  ┆ 24485  ┆ 12      │
╰───────────────────────┴─────────────────┴────────┴────────┴────────┴─────────╯

Gas Function Snapshots

🚨 Important

Gas snapshots may not be entirely accurate in the ZKsync context. This is mostly due to the > additional overhead to executing each CREATE or CALL in its own zkEVM which has additional bootloader gas costs.

Forge can generate gas snapshots for all your test functions. This can be useful to get a general feel for how much gas your contract will consume, or to compare gas usage before and after various optimizations.

To generate the gas snapshot, run forge snapshot.

This will generate a file called .gas-snapshot by default with all your tests and their respective gas usage.

$ forge snapshot
$ cat .gas-snapshot

ERC20Test:testApprove() (gas: 31162)
ERC20Test:testBurn() (gas: 59875)
ERC20Test:testFailTransferFromInsufficientAllowance() (gas: 81034)
ERC20Test:testFailTransferFromInsufficientBalance() (gas: 81662)
ERC20Test:testFailTransferInsufficientBalance() (gas: 52882)
ERC20Test:testInfiniteApproveTransferFrom() (gas: 90167)
ERC20Test:testMetadata() (gas: 14606)
ERC20Test:testMint() (gas: 53830)
ERC20Test:testTransfer() (gas: 60473)
ERC20Test:testTransferFrom() (gas: 84152)

Filtering

If you would like to specify a different output file, run forge snapshot --snap <FILE_NAME>.

You can also sort the results by gas usage. Use the --asc option to sort the results in ascending order and --desc to sort the results in descending order.

Finally, you can also specify a min/max gas threshold for all your tests. To only include results above a threshold, you can use the --min <VALUE> option. In the same way, to only include results under a threshold, you can use the --max <VALUE> option.

Keep in mind that the changes will be made in the snapshot file, and not in the snapshot being displayed on your screen.

You can also use it in combination with the filters for forge test, such as forge snapshot --match-path contracts/test/ERC721.t.sol to generate a gas snapshot relevant to this test contract.

Comparing gas usage

If you would like to compare the current snapshot file with your latest changes, you can use the --diff or --check options.

--diff will compare against the snapshot and display changes from the snapshot.

It can also optionally take a file name (--diff <FILE_NAME>), with the default being .gas-snapshot.

For example:

$ forge snapshot --diff .gas-snapshot2

Running 10 tests for src/test/ERC20.t.sol:ERC20Test
[PASS] testApprove() (gas: 31162)
[PASS] testBurn() (gas: 59875)
[PASS] testFailTransferFromInsufficientAllowance() (gas: 81034)
[PASS] testFailTransferFromInsufficientBalance() (gas: 81662)
[PASS] testFailTransferInsufficientBalance() (gas: 52882)
[PASS] testInfiniteApproveTransferFrom() (gas: 90167)
[PASS] testMetadata() (gas: 14606)
[PASS] testMint() (gas: 53830)
[PASS] testTransfer() (gas: 60473)
[PASS] testTransferFrom() (gas: 84152)
Test result: ok. 10 passed; 0 failed; finished in 2.86ms
testBurn() (gas: 0 (0.000%))
testFailTransferFromInsufficientAllowance() (gas: 0 (0.000%))
testFailTransferFromInsufficientBalance() (gas: 0 (0.000%))
testFailTransferInsufficientBalance() (gas: 0 (0.000%))
testInfiniteApproveTransferFrom() (gas: 0 (0.000%))
testMetadata() (gas: 0 (0.000%))
testMint() (gas: 0 (0.000%))
testTransfer() (gas: 0 (0.000%))
testTransferFrom() (gas: 0 (0.000%))
testApprove() (gas: -8 (-0.000%))
Overall gas change: -8 (-0.000%)

--check will compare a snapshot with an existing snapshot file and display all the differences, if any. You can change the file to compare against by providing a different file name: --check <FILE_NAME>.

For example:

$ forge snapshot --check .gas-snapshot2

Running 10 tests for src/test/ERC20.t.sol:ERC20Test
[PASS] testApprove() (gas: 31162)
[PASS] testBurn() (gas: 59875)
[PASS] testFailTransferFromInsufficientAllowance() (gas: 81034)
[PASS] testFailTransferFromInsufficientBalance() (gas: 81662)
[PASS] testFailTransferInsufficientBalance() (gas: 52882)
[PASS] testInfiniteApproveTransferFrom() (gas: 90167)
[PASS] testMetadata() (gas: 14606)
[PASS] testMint() (gas: 53830)
[PASS] testTransfer() (gas: 60473)
[PASS] testTransferFrom() (gas: 84152)
Test result: ok. 10 passed; 0 failed; finished in 2.47ms
Diff in "ERC20Test::testApprove()": consumed "(gas: 31162)" gas, expected "(gas: 31170)" gas 

Gas Section Snapshots

🚨 Important

Gas snapshots may not be entirely accurate in the ZKsync context. This is mostly due to the > additional overhead to executing each CREATE or CALL in its own zkEVM which has additional bootloader gas costs.

Forge can capture gas snapshots over arbitrary sections inside of your test functions. This can be useful to get a granular measurement of how much gas your logic is consuming as both external calls and internal gas usage are measured.

Instead of running a command like forge snapshot or forge test --gas-report, you use the snapshotGas cheatcodes in your tests to capture gas usage as follows:

snapshotGas cheatcodes

Signature

/// Start a snapshot capture of the current gas usage by name.
/// The group name is derived from the contract name.
function startSnapshotGas(string calldata name) external;

/// Start a snapshot capture of the current gas usage by name in a group.
function startSnapshotGas(string calldata group, string calldata name) external;

/// Stop the snapshot capture of the current gas by latest snapshot name, capturing the gas used since the start.
function stopSnapshotGas() external returns (uint256 gasUsed);

/// Stop the snapshot capture of the current gas usage by name, capturing the gas used since the start.
/// The group name is derived from the contract name.
function stopSnapshotGas(string calldata name) external returns (uint256 gasUsed);

/// Stop the snapshot capture of the current gas usage by name in a group, capturing the gas used since the start.
function stopSnapshotGas(string calldata group, string calldata name) external returns (uint256 gasUsed);

/// Snapshot capture an arbitrary numerical value by name.
/// The group name is derived from the contract name.
function snapshotValue(string calldata name, uint256 value) external;

/// Snapshot capture an arbitrary numerical value by name in a group.
function snapshotValue(string calldata group, string calldata name, uint256 value) external;

/// Snapshot capture the gas usage of the last call by name from the callee perspective.
function snapshotGasLastCall(string calldata name) external returns (uint256 gasUsed);

/// Snapshot capture the gas usage of the last call by name in a group from the callee perspective.
function snapshotGasLastCall(string calldata group, string calldata name) external returns (uint256 gasUsed);

Description

snapshotGas* cheatcodes allow you to capture gas usage in your tests. This can be useful to track how much gas your logic is consuming. You can capture the gas usage of the last call by name, capture an arbitrary numerical value by name, or start and stop a snapshot capture of the current gas usage by name.

In order to strictly compare gas usage across test runs, set the FORGE_SNAPSHOT_CHECK environment variable to true before running your tests. This will compare the gas usage of your tests against the last snapshot and fail if the gas usage has changed. By default the snapshots directory will be newly created and its contents removed before each test run to ensure no stale data is present.

It is intended that the snapshots directory created when using the snapshotGas* cheatcodes is checked into version control. This allows you to track changes in gas usage over time and compare gas usage during code reviews.

When running forge clean the snapshots directory will be deleted.

Examples

Capturing the gas usage of a section of code that calls an external contract:

contract SnapshotGasTest is Test {
    uint256 public slot0;

    Flare public flare;

    function setUp() public {
        flare = new Flare();
    }

    function testSnapshotGas() public {
        vm.startSnapshotGas("externalA");
        flare.run(256);
        uint256 gasUsed = vm.stopSnapshotGas();
    }
}

Capturing the gas usage of multiple sections of code that modify the internal state:

contract SnapshotGasTest is Test {
    uint256 public slot0;


    /// Writes to `snapshots/SnapshotGasTest.json` group with name `internalA`, `internalB`, and `internalC`.
    function testSnapshotGas() public {
        vm.startSnapshotGas("internalA");
        slot0 = 1;
        vm.stopSnapshotGas();

        vm.startSnapshotGas("internalB");
        slot0 = 2;
        vm.stopSnapshotGas();

        vm.startSnapshotGas("internalC");
        slot0 = 0;
        vm.stopSnapshotGas();
    }
}

Capturing the gas usage of a section of code that modifies both the internal state and calls an external contract:

contract SnapshotGasTest is Test {
    uint256 public slot0;
    Flare public flare;

    function setUp() public {
        flare = new Flare();
    }

    /// Writes to `snapshots/SnapshotGasTest.json` group with name `combinedA`.
    function testSnapshotGas() public {
        vm.startSnapshotGas("combinedA");
        flare.run(256);
        slot0 = 1;
        vm.stopSnapshotGas();
    }
}

Capturing an arbitrary numerical value (such as the bytecode size of a contract):

```solidity
contract SnapshotGasTest is Test {
    uint256 public slot0;

    /// Writes to `snapshots/SnapshotGasTest.json` group with name `valueA`, `valueB`, and `valueC`.
    function testSnapshotValue() public {
        uint256 a = 123;
        uint256 b = 456;
        uint256 c = 789;

        vm.snapshotValue("valueA", a);
        vm.snapshotValue("valueB", b);
        vm.snapshotValue("valueC", c);
    }
}

Capturing the gas usage of the last call from the callee perspective:

contract SnapshotGasTest is Test {
    Flare public flare;

    function setUp() public {
        flare = new Flare();
    }

    /// Writes to `snapshots/SnapshotGasTest.json` group with name `lastCallA`.
    function testSnapshotGasLastCall() public {
        flare.run(1);
        vm.snapshotGasLastCall("lastCallA");
    }
}

For each of the above examples you can also use the group variant of the cheatcodes to group the snapshots together in a custom group.

contract SnapshotGasTest is Test {
    uint256 public slot0;

    /// Writes to `snapshots/CustomGroup.json` group with name `internalA`, `internalB`, and `internalC`.
    function testSnapshotGas() public {
        vm.startSnapshotGas("CustomGroup", "internalA");
        slot0 = 1;
        vm.stopSnapshotGas();

        vm.startSnapshotGas("CustomGroup", "internalB");
        slot0 = 2;
        vm.stopSnapshotGas();

        vm.startSnapshotGas("CustomGroup", "internalC");
        slot0 = 0;
        vm.stopSnapshotGas();
    }
}

Overview of Cast

Cast is Foundry’s command-line tool for performing Ethereum RPC calls. You can make smart contract calls, send transactions, or retrieve any type of chain data - all from your command-line!

How to use Cast

To use Cast, run the cast command followed by a subcommand:

$ cast <subcommand>

Examples

Let’s use cast to retrieve the total supply of the DAI token:

$ cast call 0x6b175474e89094c44da98b954eedeac495271d0f "totalSupply()(uint256)" --rpc-url https://eth-mainnet.alchemyapi.io/v2/Lc7oIGYeL_QvInzI0Wiu_pOZZDEKBrdf
3110058793252544024496112199 [3.11e27]

cast also provides many convenient subcommands, such as for decoding calldata:

$ cast 4byte-decode 0x1F1F897F676d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e7
1) "fulfillRandomness(bytes32,uint256)"
0x676d000000000000000000000000000000000000000000000000000000000000
999

You can also use cast to send arbitrary messages. Here’s an example of sending a message between two Anvil accounts.

$ cast send --private-key <Your Private Key> 0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc $(cast from-utf8 "hello world") --rpc-url http://127.0.0.1:8545/

📚 Reference

See the cast Reference for a complete overview of all the available subcommands.

Overview of Anvil-ZKsync

Anvil-ZKsync is a local testnet node shipped with Foundry-ZKsync. You can use it for testing your contracts from frontends or for interacting over RPC.

Anvil-ZKsync is part of the Foundry-ZKsync suite and is installed alongside forge, and cast. If you haven’t installed Foundry-ZKsync yet, see Foundry-ZKsync installation.

Note: If you have an older version of Foundry-ZKsync installed, you’ll need to re-install foundryup-zksync in order for Anvil-ZKsync to be downloaded.

How to use Anvil-ZKsync

To use Anvil-ZKsync, simply type anvil-zksync. You should see a list of accounts and private keys available for use, as well as the address and port that the node is listening on.

Anvil-ZKsync is highly configurable. You can run anvil-zksync -h to see all the configuration options.

Some basic options are:

#  Number of dev accounts to generate and configure. [default: 10]
anvil-zksync -a, --accounts <ACCOUNTS>

# Port number to listen on. [default: 8545]
anvil-zksync  -p, --port <PORT>

📚 Reference

See the anvil-zksync Reference for in depth information on Anvil-ZKsync and its capabilities.

Overview of Chisel

Chisel is an advanced Solidity REPL shipped with Foundry. It can be used to quickly test the behavior of Solidity snippets on a local or forked network.

Officially we do not support chisel for ZKsync related operations.

Configuring with foundry.toml

Forge can be configured using a configuration file called foundry.toml, which is placed in the root of your project.

Configuration can be namespaced by profiles. The default profile is named default, from which all other profiles inherit. You are free to customize the default profile, and add as many new profiles as you need.

Additionally, you can create a global foundry.toml in your home directory.

Let’s take a look at a configuration file that contains two profiles: the default profile, which always enables the optimizer, as well as a CI profile, that always displays traces:

[profile.default]
optimizer = true
optimizer_runs = 20_000

[profile.ci]
verbosity = 4

When running forge, you can specify the profile to use using the FOUNDRY_PROFILE environment variable.

Standalone sections

Besides the profile sections, the configuration file can also contain standalone sections ([fmt], [fuzz], [invariant] etc). By default, each standalone section belongs to the default profile. i.e. [fmt] is equivalent to [profile.default.fmt].

To configure the standalone section for different profiles other than default, use syntax [profile.<profile name>.<standalone>]. i.e. [profile.ci.fuzz].


📚 Reference

See the foundry.toml Reference for a complete overview of what you can configure.

Creating an NFT with Solmate

This tutorial will walk you through creating an OpenSea compatible NFT with Foundry ZKsync and Solmate. A full implementation of this tutorial can be found here.

This tutorial is for illustrative purposes only and provided on an as-is basis. The tutorial is not audited nor fully tested. No code in this tutorial should be used in a production environment.

Create project and install dependencies

Start by setting up a Foundry project following the steps outlined in the Getting started section. We will also install Solmate for their ERC721 implementation, as well as some OpenZeppelin utility libraries. Install the dependencies by running the following commands from the root of your project:

forge install transmissions11/solmate Openzeppelin/openzeppelin-contracts

These dependencies will be added as git submodules to your project.

Implement a basic NFT

Next, we will remove the boilerplate contracts found in src/Counter.sol, test/Counter.t.sol, and script/Counter.s.sol. After that, create a new file in the src/ directory named NFT.sol and replace its content with the following code:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;

import {ERC721} from "solmate/tokens/ERC721.sol";
import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol";

contract NFT is ERC721 {
    uint256 public currentTokenId;

    constructor(
        string memory _name,
        string memory _symbol
    ) ERC721(_name, _symbol) {}

    function mintTo(address recipient) public payable returns (uint256) {
        uint256 newItemId = ++currentTokenId;
        _safeMint(recipient, newItemId);
        return newItemId;
    }

    function tokenURI(uint256 id) public view virtual override returns (string memory) {
        return Strings.toString(id);
    }
}

Let’s take a look at this very basic implementation of an NFT. We start by importing two contracts from our git submodules. We import solmate’s gas optimized implementation of the ERC721 standard which our NFT contract will inherit from. Our constructor takes the _name and _symbol arguments for our NFT and passes them on to the constructor of the parent ERC721 implementation. Lastly we implement the mintTo function which allows anyone to mint an NFT. This function increments the currentTokenId and makes use of the _safeMint function of our parent contract.

Compile & deploy with forge

To compile the NFT contract run forge build --zksync. You might experience a build failure due to wrong mapping:

Error:
Compiler run failed
error[6275]: ParserError: Source "lib/openzeppelin-contracts/contracts/contracts/utils/Strings.sol" not found: File not found. Searched the following locations: "/PATH/TO/REPO".
 --> src/NFT.sol:5:1:
  |
5 | import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol";
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

this can be fixed by setting up the correct remapping. Create a file remappings.txt in your project and add the line

openzeppelin-contracts/=lib/openzeppelin-contracts/

(You can find out more on remappings in the dependencies documentation.

By default the compiler output will be in the zkout directory. To deploy our compiled contract with Forge we have to set environment variables for the RPC endpoint and the private key we want to use to deploy.

Set your environment variables by running:

export RPC_URL=<Your RPC endpoint>
export PRIVATE_KEY=<Your wallets private key>

Once set, you can deploy your NFT with Forge by running the below command while adding the relevant constructor arguments to the NFT contract:

forge create NFT --rpc-url=$RPC_URL --private-key=$PRIVATE_KEY --constructor-args <name> <symbol> --zksync

If successfully deployed, you will see the deploying wallet’s address, the contract’s address as well as the transaction hash printed to your terminal.

Minting NFTs from your contract

Calling functions on your NFT contract is made simple with Cast, Foundry’s command-line tool for interacting with smart contracts, sending transactions, and getting chain data. Let’s have a look at how we can use it to mint NFTs from our NFT contract.

Given that you already set your RPC and private key env variables during deployment, mint an NFT from your contract by running:

cast send --rpc-url=$RPC_URL <contractAddress>  "mintTo(address)" <arg> --private-key=$PRIVATE_KEY

Well done! You just minted your first NFT from your contract. You can sanity check the owner of the NFT with currentTokenId equal to 1 by running the below cast call command. The address you provided above should be returned as the owner.

cast call --rpc-url=$RPC_URL --private-key=$PRIVATE_KEY <contractAddress> "ownerOf(uint256)" 1

Extending our NFT contract functionality and testing

Let’s extend our NFT by adding metadata to represent the content of our NFTs, as well as set a minting price, a maximum supply and the possibility to withdraw the collected proceeds from minting. To follow along you can replace your current NFT contract with the code snippet below:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;

import {ERC721} from "solmate/tokens/ERC721.sol";
import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol";
import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol";

error MintPriceNotPaid();
error MaxSupply();
error NonExistentTokenURI();
error WithdrawTransfer();

contract NFT is ERC721, Ownable {
    using Strings for uint256;

    string public baseURI;
    uint256 public currentTokenId;
    uint256 public constant TOTAL_SUPPLY = 10_000;
    uint256 public constant MINT_PRICE = 0.08 ether;

    constructor(
        string memory _name,
        string memory _symbol,
        string memory _baseURI
    ) ERC721(_name, _symbol) Ownable(msg.sender) {
        baseURI = _baseURI;
    }

    function mintTo(address recipient) public payable returns (uint256) {
        if (msg.value != MINT_PRICE) {
            revert MintPriceNotPaid();
        }
        uint256 newTokenId = currentTokenId + 1;
        if (newTokenId > TOTAL_SUPPLY) {
            revert MaxSupply();
        }
        currentTokenId = newTokenId;
        _safeMint(recipient, newTokenId);
        return newTokenId;
    }

    function tokenURI(uint256 tokenId)
        public
        view
        virtual
        override
        returns (string memory)
    {
        if (ownerOf(tokenId) == address(0)) {
            revert NonExistentTokenURI();
        }
        return
            bytes(baseURI).length > 0
                ? string(abi.encodePacked(baseURI, tokenId.toString()))
                : "";
    }

    function withdrawPayments(address payable payee) external onlyOwner {
        if (address(this).balance == 0) {
            revert WithdrawTransfer();
        }
        
        (bool success, ) = payable(payee).call{value: address(this).balance}("");
        require(success, "Transfer failed");
    }

    function _checkOwner() internal view override {
        require(msg.sender == owner(), "Ownable: caller is not the owner");
    }
}

Among other things, we have added metadata that can be queried from any front-end application like OpenSea, by calling the tokenURI method on our NFT contract.

Note: If you want to provide a real URL to the constructor at deployment, and host the metadata of this NFT contract please follow the steps outlined here.

Let’s test some of this added functionality to make sure it works as intended. Foundry offers an extremely fast EVM native testing framework through Forge.

Within your test folder create the test file NFT.t.sol. This file will contain all tests regarding the NFT’s mintTo method. Next, replace the existing boilerplate code with the below:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;

import {Test} from "forge-std/Test.sol";
import {NFT} from "../src/NFT.sol";

contract NFTTest is Test {
    using stdStorage for StdStorage;

    NFT private nft;

    function setUp() public {
        // Deploy NFT contract
        nft = new NFT("NFT_tutorial", "TUT", "baseUri");
    }

    function test_RevertMintWithoutValue() public {
        vm.expectRevert(MintPriceNotPaid.selector);
        // Make use of an address outside of the reserved address range
        nft.mintTo(address(65536));
    }

    function test_MintPricePaid() public {
        // Make use of an address outside of the reserved address range
        nft.mintTo{value: 0.08 ether}(address(68536));
    }

    function test_RevertMintMaxSupplyReached() public {
        uint256 slot = stdstore
            .target(address(nft))
            .sig("currentTokenId()")
            .find();
        bytes32 loc = bytes32(slot);
        bytes32 mockedCurrentTokenId = bytes32(abi.encode(10000));
        vm.store(address(nft), loc, mockedCurrentTokenId);
        vm.expectRevert(MaxSupply.selector);
        // Make use of an address outside of the reserved address range
        nft.mintTo{value: 0.08 ether}(address(65536));
    }

    function test_RevertMintToZeroAddress() public {
        vm.expectRevert("INVALID_RECIPIENT");
        nft.mintTo{value: 0.08 ether}(address(0));
    }

    function test_NewMintOwnerRegistered() public {
        // Make use of an address outside of the reserved address range
        nft.mintTo{value: 0.08 ether}(address(68536));
        uint256 slotOfNewOwner = stdstore
            .target(address(nft))
            .sig(nft.ownerOf.selector)
            .with_key(address(1))
            .find();

        uint160 ownerOfTokenIdOne = uint160(
            uint256(
                (vm.load(address(nft), bytes32(abi.encode(slotOfNewOwner))))
            )
        );
 
        assertEq(address(ownerOfTokenIdOne), address(68536));
    }

    function test_BalanceIncremented() public {
        // Make use of an address outside of the reserved address range
        nft.mintTo{value: 0.08 ether}(address(68536));
        uint256 slotBalance = stdstore
            .target(address(nft))
            .sig(nft.balanceOf.selector)
            .with_key(address(68536))
            .find();

        uint256 balanceFirstMint = uint256(
            vm.load(address(nft), bytes32(slotBalance))
        );
        assertEq(balanceFirstMint, 1);

        nft.mintTo{value: 0.08 ether}(address(68536));
        uint256 balanceSecondMint = uint256(
            vm.load(address(nft), bytes32(slotBalance))
        );
        assertEq(balanceSecondMint, 2);
    }

    function test_SafeContractReceiver() public {
        Receiver receiver = new Receiver();
        nft.mintTo{value: 0.08 ether}(address(receiver));
        uint256 slotBalance = stdstore
            .target(address(nft))
            .sig(nft.balanceOf.selector)
            .with_key(address(receiver))
            .find();

        uint256 balance = uint256(vm.load(address(nft), bytes32(slotBalance)));
        assertEq(balance, 1);
    }

    function test_RevertUnSafeContractReceiver() public {
        // Make use of an address outside of the reserved address range
        // Ensure bytecode is divisible by 32
        vm.etch(address(65538), bytes.concat(bytes("mock code"), new bytes(23)));
        vm.expectRevert(bytes(""));
        nft.mintTo{value: 0.08 ether}(address(65538));
    }

    function test_WithdrawalWorksAsOwner() public {
        // Mint an NFT, sending eth to the contract
        Receiver receiver = new Receiver();
        address payable payee = payable(address(65539));
        uint256 priorPayeeBalance = payee.balance;
        nft.mintTo{value: nft.MINT_PRICE()}(address(receiver));
        // Check that the balance of the contract is correct
        assertEq(address(nft).balance, nft.MINT_PRICE());
        uint256 nftBalance = address(nft).balance;
        // Withdraw the balance and assert it was transferred
        nft.withdrawPayments(payee);
        assertEq(payee.balance, priorPayeeBalance + nftBalance);
    }

    function test_WithdrawalFailsAsNotOwner() public {
        // Mint an NFT, sending eth to the contract
        Receiver receiver = new Receiver();
        nft.mintTo{value: nft.MINT_PRICE()}(address(receiver));
        // Check that the balance of the contract is correct
        assertEq(address(nft).balance, nft.MINT_PRICE());
        // Confirm that a non-owner cannot withdraw
        vm.expectRevert("Ownable: caller is not the owner");
        vm.startPrank(address(65540));
        nft.withdrawPayments(payable(address(65540)));
        vm.stopPrank();
    }
}

contract Receiver is ERC721TokenReceiver {
    function onERC721Received(
        address operator,
        address from,
        uint256 id,
        bytes calldata data
    ) external override returns (bytes4) {
        return this.onERC721Received.selector;
    }
}

The test suite is set up as a contract with a setUp method which runs before every individual test.

As you can see, Forge offers a number of cheatcodes to manipulate state to accommodate your testing scenario.

For example, our testFailMaxSupplyReached test checks that an attempt to mint fails when the max supply of NFT is reached. Thus, the currentTokenId of the NFT contract needs to be set to the max supply by using the store cheatcode which allows you to write data to your contracts storage slots. The storage slots you wish to write to can easily be found using the forge-std helper library. You can run the test with the following command:

forge test --zksync

If you want to put your Forge skills to practice, write tests for the remaining methods of our NFT contract. Feel free to PR them to nft-tutorial, where you will find the full implementation of this tutorial.

That’s it, I hope this will give you a good practical basis of how to get started with foundry. We think there is no better way to deeply understand solidity than writing your tests in solidity. You will also experience less context switching between javascript and solidity. Happy coding!

Deterministic deployment using CREATE2 on ZKsync

Introduction

Enshrined into the EVM as part of the Constantinople fork of 2019, CREATE2 is an opcode that started its journey as EIP-1014. CREATE2 allows you to deploy smart contracts to deterministic addresses, based on parameters controlled by the deployer. As a result, it’s often mentioned as enabling “counterfactual” deployments, where you can interact with an addresses that haven’t been created yet because CREATE2 guarantees known code can be placed at that address. This is in contrast to the CREATE opcode, where the address of the deployed contract is a function of the deployer’s nonce. With CREATE2, you can use the same deployer account to deploy contracts to the same address across multiple networks, even if the address has varying nonces.

ℹ️ Note This guide is intended to help understand CREATE2. In most use cases, you won’t need to write and use your own deployer, and can use an existing deterministic deployer (new MyContract{salt: salt}()).

In this tutorial, we will:

  1. Look at a CREATE2 factory implementation.
  2. Deploy the factory using the traditional deployment methods.
  3. Use this deployed factory to in turn deploy a simple counter contract at a deterministic address.
  4. Simulate this set of events by writing a simple test using Foundry ZKsync.

Prerequisites

  1. Some familiarity with Solidity and Foundry is required, and some familiarity with inline assembly is recommended. Refer to the official Solidity docs for a primer on inline assembly.

  2. Make sure you have Foundry ZKsync installed on your system.

  3. Initialize a new Foundry project.

  4. Install the ZKsync contracts by running the following command in your project directory:

    forge install matter-labs/era-contracts
    

CREATE2 Factory

Create a file named Create2ZK.sol Inside the src directory. Initialize a contract named Create2ZK like this:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Create2ZK {

    error Create2FailedDeployment();
}

The error is meant to enforce some sanity checks on the factory deployment, and revert the whole transaction when triggered. The Create2FailedDeployment() error triggers if the deployment fails for any reason.

ℹ️ Note

Please note that a CREATE2 deployment may fail due to a number of reasons. For example, if the bytecodeHash is invalid, or if a contract is already deployed at the computed address. Your deployment may also fail if your constructor reverts for any reason.

Next, create a function named deploy:

function deploy(bytes32 salt, bytes32 bytecodeHash, bytes calldata inputData) external payable returns (address addr) {
 
 }

This function takes 3 inputs:

  1. The salt used to calculate the final address. This can basically be any random value we want it to be.
  2. The bytecodeHash of the contract that we want to deploy.
  3. The inputData which are the constructor parameters of the contract.

The address of the newly deployed contract is the returned after a successful deploy.

ℹ️ Note

You can send ETH to a contract that is being deployed using CREATE2, but only if it has a payable constructor. If you try to send ETH to it without a payable constructor, the transaction will revert.

Next, we will call the create2 function from the ContractDeployer system contract on ZKsync. This can be done by calling SystemContractsCaller.systemCallWithReturndata to interact with system contracts:

To call the create2 function, we need to pass in 3 parameters:

    (bool success, bytes memory returnData) = SystemContractsCaller
        .systemCallWithReturndata(
            uint32(gasleft()),
            address(DEPLOYER_SYSTEM_CONTRACT),
            uint128(0),
            abi.encodeCall(
                DEPLOYER_SYSTEM_CONTRACT.create2,
                (
                    salt,
                    bytecodeHash,
                    inputData
                )
            )
        );
  1. The salt: This is used to differentiate contract deployments and ensure unique contract addresses. It is a key part of the deterministic address generation in CREATE2.
  2. The bytecodeHash: In ZKsync, contracts are deployed using the hash of the bytecode, not the bytecode itself.
  3. The inputData: This contains the constructor arguments for the contract being deployed. Similar to traditional contract deployment, this field passes the initialization data to the contract being deployed.

Alternatively, instead of writing your own deployment logic, you can leverage the CREATE2Factory.sol system contract, which simplifies calling the create2 method. In many cases, you won’t need to manually write a deployer function since you can use existing deterministic deployers, such as the CREATE2Factory.sol system contract, or deploy contracts directly using the new MyContract{salt: salt}() syntax.

Here’s an example of how you can use the CREATE2Factory.sol:

import {Create2Factory} from "era-contracts/system-contracts/contracts/Create2Factory.sol";

Create2Factory create2Factory = new Create2Factory();
address deployedAddress = create2Factory.create2(
    salt,
    bytecodeHash,
    abi.encode()
);

This method allows you to deploy a contract deterministically without having to write the deployment logic from scratch. It handles the create2 call and returns the address of the newly deployed contract.

This approach simplifies the deployment process by using a pre-built deployer contract, making it easier to manage and reuse your deployment logic across different projects.

Finally, if the deployment fails for any reason, you can handle it by reverting the transaction, similar to how you would handle failure in the EVM:

if (!success) {
    revert Create2FailedDeployment();
}

Computing the Contract Address on zkSync

Lastly, we will create a view function named computeAddress. This function should take in the salt, bytecodeHash, and constructorInput as parameters and return the address of the contract that would be deployed using the deploy function on ZKsync:

function computeAddress(
    address sender,
    bytes32 salt, 
    bytes32 bytecodeHash, 
    bytes32 constructorInputHash
) external view returns (address addr) {

 }

Inside the function, we’ll use the L2ContractHelper.computeCreate2Address method, which follows the address calculation logic specific to ZKsync:

import {L2ContractHelper} from "era-contracts/l2-contracts/contracts/L2ContractHelper.sol";

function computeAddress(
    address sender,
    bytes32 salt, 
    bytes32 bytecodeHash, 
    bytes32 constructorInputHash
) external view returns (address addr) {

    address computedAddress = L2ContractHelper.computeCreate2Address(
        sender,
        salt,
        bytecodeHash,
        constructorInputHash
    );
}

Here’s the breakdown of the parameters and logic used in ZKsync’s CREATE2 address calculation:

  1. Sender: This refers to the address of the contract (typically the factory contract) calling the create2 function.
  2. Salt: The salt is used to differentiate deployments and ensure unique contract addresses, just like in traditional CREATE2 usage.
  3. Bytecode Hash: In ZKsync, you must pass the hash of the contract bytecode. This hash must be known to the operator, as the actual bytecode is provided in the factory_deps field of the transaction. For more info on this refer to the docs here.
  4. Constructor Input Hash: ZKsync requires the constructor input (or initialization) data to be hashed using keccak256. This hash is then included in the address derivation formula.

The ZKsync-specific address derivation formula differs slightly from Ethereum’s traditional CREATE2:

bytes32 hash = keccak256(
    bytes.concat(
        CREATE2_PREFIX,               // zkSync-specific prefix
        bytes32(uint256(uint160(_sender))),  // Address of the contract deployer
        _salt,                         // Salt for the deployment
        _bytecodeHash,                 // Hash of the bytecode
        constructorInputHash           // Hash of the constructor input data
    )
);

ℹ️ Note

The prefix (CREATE2_PREFIX) is specific to ZKsync, helping avoid collisions with Ethereum’s CREATE2 opcode. The keccak256 function is used to compute the hash from these components, and the address is derived from this hash.

Finally, we will return the calculated address, ensuring it conforms to the ZKsync address derivation rules:

return address(uint160(uint256(hash)));

Formula Recap

The formula that ZKsync uses to calculate the contract address is:

keccak256(zksyncCreate2 ++ address ++ salt ++ keccak256(bytecode) ++ keccak256(constructorInput))[12:]
  • zksyncCreate2 is a ZKsync-specific prefix to avoid collisions.
  • address is the contract deployer’s address.
  • salt is the deployment salt.
  • keccak256(bytecode) is the hash of the contract bytecode.
  • keccak256(constructorInput) is the hash of the constructor data.

These values are concatenated and passed through keccak256 to produce a 32-byte hash, and the last 20 bytes are used as the deployed contract’s address.

ℹ️ Note

You can check out the complete code for this implementation here.

Testing our factory

Create a file named Create2ZK.t.sol inside the test directory. Initialize a contract named Create2ZKTest like this:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import {Test} from "forge-std/Test.sol";
import {Counter} from "../src/Counter.sol";
import {ZKCreate2} from "../src/Create2zk.sol";
import {ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT} from "era-contracts/system-contracts/contracts/Constants.sol";

contract Create2ZKTest is Test {

}

Initialize the following state variables and the setUp() function:

    Create2ZK internal create2ZK;
    Counter internal counter;

    function setUp() public {
        create2ZK = new Create2ZK();
        counter = new Counter();
    }

Deterministic Deployment Test

We’ll now create a function named testDeterministicDeployment() to do the following:

  1. Deploy a new instance of the ZKCreate2 contract.
  2. Allocate 100 ETH to the deployer address, using the vm.deal cheat code, and impersonate this address with the prank cheat code.
  3. Set up the salt and bytecodeHash parameters.
  4. Use the zkCreate2 contract to deploy the Counter contract at a deterministic address using the create2 system contract.
  5. Assert that the computed address is equal to the deployed address.
    function testDeterministicDeployment() public {
        address deployerAddress = address(create2ZK);
        
        // Deal 100 ETH to the deployer address
        vm.deal(deployerAddress, 100 ether);
        vm.startPrank(deployerAddress);

        // Set up salt and retrieve bytecode hash
        bytes32 salt = "12345";
        bytes32 bytecodeHash = ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT.getRawCodeHash(address(counter));

        // Compute the expected address using ZKsync's specific `CREATE2` logic
        address expectedAddress = zkCreate2.computeCreate2Address(
            deployerAddress,
            salt,
            bytecodeHash,
            keccak256(abi.encode()) // constructor input data hash
        );

        // Deploy the contract using the `ZKCreate2` contract
        address deployedAddress = create2ZK.deploy(
            salt,
            bytecodeHash,
            abi.encode() // constructor input data
        );

        vm.stopPrank();

        // Log the computed and deployed addresses for debugging
        console.log("Computed address:", expectedAddress);
        console.log("Deployed address:", deployedAddress);

        // Assert that the computed address matches the deployed address
        assertEq(deployedAddress, expectedAddress);
    }

Explanation

  • vm.deal: This cheat code allocates 100 ETH to the deployer address, allowing it to fund contract deployments.
  • vm.startPrank: This makes the deployer address impersonate the caller for all subsequent calls, so we simulate real-world deployment scenarios.
  • bytes32 salt: The salt is used to ensure the deployed contract has a deterministic address.
  • bytes32 bytecodeHash: We retrieve the bytecode hash of the Counter contract from the ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT to pass it to the ZKsync CREATE2 function.
  • abi.encode(): We use this to pass constructor input data, hashed using keccak256.
  • computeCreate2Address: This function computes the expected address based on ZKsync’s deterministic address calculation for CREATE2.
  • deploy: This deploys the contract using ZKsync’s ContractDeployer system contract.

Finally, we assert that the expected address matches the deployed address, ensuring that the contract was deployed deterministically.

Save all your files, and run the test using forge test --match-path test/Create2ZK.t.sol --zksync --enable-eravm-extensions -vvvv. Your test should pass without any errors.

Solidity Scripting

Introduction

Solidity scripting is a way to declaratively deploy contracts using Solidity, instead of using the more limiting and less user friendly forge create.

Solidity scripts are like the scripts you write when working with tools like Hardhat; what makes Solidity scripting different is that they are written in Solidity instead of JavaScript, and they are run on the fast Foundry backend, which provides dry-run capabilities.

High Level Overview

forge script does not work in a sync manner. First, it collects all transactions from the script, and only then does it broadcast them all. It can essentially be split into 4 phases:

  1. Local Simulation - The contract script is run in a local evm. If a rpc/fork url has been provided, it will execute the script in that context. Any external call (not static, not internal) from a vm.broadcast and/or vm.startBroadcast will be appended to a list.
  2. Onchain Simulation - Optional. If a rpc/fork url has been provided, then it will sequentially execute all the collected transactions from the previous phase here.
  3. Broadcasting - Optional. If the --broadcast flag is provided and the previous phases have succeeded, it will broadcast the transactions collected at step 1. and simulated at step 2.

Given this flow, it’s important to be aware that transactions whose behaviour can be influenced by external state/actors might have a different result than what was simulated on step 2. Eg. frontrunning.

Set Up

Let’s try to deploy the NFT contract made in the solmate tutorial with solidity scripting. First of all, we would need to create a new Foundry project via:

forge init solidity-scripting

Since the NFT contract from the solmate tutorial inherits both solmate and OpenZeppelin contracts, we’ll have to install them as dependencies by running:

# Enter the project
cd solidity-scripting

# Install Solmate and OpenZeppelin contracts as dependencies
forge install transmissions11/solmate Openzeppelin/openzeppelin-contracts@v5.0.1

Next, we have to delete the Counter.sol file in the src folder and create another file called NFT.sol. You can do this by running:

rm src/Counter.sol test/Counter.t.sol script/Counter.s.sol && touch src/NFT.sol && ls src

Once that’s done, you should open up your preferred code editor and copy the code below into the NFT.sol file.

// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.10;

import {ERC721} from "solmate/tokens/ERC721.sol";
import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol";
import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol";

error MintPriceNotPaid();
error MaxSupply();
error NonExistentTokenURI();
error WithdrawTransfer();

contract NFT is ERC721, Ownable {
    using Strings for uint256;

    string public baseURI;
    uint256 public currentTokenId;
    uint256 public constant TOTAL_SUPPLY = 10_000;
    uint256 public constant MINT_PRICE = 0.08 ether;

    constructor(
        string memory _name,
        string memory _symbol,
        string memory _baseURI
    ) ERC721(_name, _symbol) Ownable(msg.sender) {
        baseURI = _baseURI;
    }

    function mintTo(address recipient) public payable returns (uint256) {
        if (msg.value != MINT_PRICE) {
            revert MintPriceNotPaid();
        }
        uint256 newTokenId = ++currentTokenId;
        if (newTokenId > TOTAL_SUPPLY) {
            revert MaxSupply();
        }
        _safeMint(recipient, newTokenId);
        return newTokenId;
    }

    function tokenURI(uint256 tokenId)
        public
        view
        virtual
        override
        returns (string memory)
    {
        if (ownerOf(tokenId) == address(0)) {
            revert NonExistentTokenURI();
        }
        return
            bytes(baseURI).length > 0
                ? string(abi.encodePacked(baseURI, tokenId.toString()))
                : "";
    }

    function withdrawPayments(address payable payee) external onlyOwner {
        uint256 balance = address(this).balance;
        (bool transferTx, ) = payee.call{value: balance}("");
        if (!transferTx) {
            revert WithdrawTransfer();
        }
    }
}

Now, let’s try compiling our contract to make sure everything is in order.

forge build --zksync

If your output looks like this, the contracts successfully compiled. compile successful

Deploying our contract

We’re going to deploy the NFT contract to the ZKsync Sepolia testnet, but to do this we’ll need to configure Foundry ZKsync a bit, by setting things like a ZKsync Sepolia RPC URL, and the private key of an account that’s funded with ZKsync Sepolia Eth.

💡 Note: You can get some ZKsync Sepolia testnet ETH here .

Environment Configuration

Once you have all that create a .env file and add the variables. Foundry automatically loads in a .env file present in your project directory.

The .env file should follow this format:

ZKSYNC_SEPOLIA_RPC_URL=
PRIVATE_KEY=

We now need to edit the foundry.toml file. There should already be one in the root of the project.

Add the following lines to the end of the file:

[rpc_endpoints]
zksync-sepolia = "${ZKSYNC_SEPOLIA_RPC_URL}"

This creates a RPC alias for ZKsync Sepolia.

Writing the Script

Next, we have to create a folder and name it script and create a file in it called NFT.s.sol. This is where we will create the deployment script itself.

The contents of NFT.s.sol should look like this:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script} from "forge-std/Script.sol";
import {NFT} from "../src/NFT.sol";

contract MyScript is Script {
    function run() external {
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        vm.startBroadcast(deployerPrivateKey);

        NFT nft = new NFT("NFT_tutorial", "TUT", "baseUri");

        vm.stopBroadcast();
    }
}

Now let’s read through the code and figure out what it actually means and does.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

Remember even if it’s a script it still works like a smart contract, but is never deployed, so just like any other smart contract written in Solidity the pragma version has to be specified.

import {Script} from "forge-std/Script.sol";
import {NFT} from "../src/NFT.sol";

Just like we may import Forge Std to get testing utilities when writing tests, Forge Std also provides some scripting utilities that we import here.

The next line just imports the NFT contract.

contract MyScript is Script {

We have created a contract called MyScript and it inherits Script from Forge Std.

function run() external {

By default, scripts are executed by calling the function named run, our entrypoint.

uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");

This loads in the private key from our .env file. Note: you must be careful when exposing private keys in a .env file and loading them into programs. This is only recommended for use with non-privileged deployers or for local / test setups. For production setups please review the various wallet options that Foundry supports.

vm.startBroadcast(deployerPrivateKey);

This is a special cheatcode that records calls and contract creations made by our main script contract. We pass the deployerPrivateKey in order to instruct it to use that key for signing the transactions. Later, we will broadcast these transactions to deploy our NFT contract.

NFT nft = new NFT("NFT_tutorial", "TUT", "baseUri");

Here we have just created our NFT contract. Because we called vm.startBroadcast() before this line, the contract creation will be recorded by Forge, and as mentioned previously, we can broadcast the transaction to deploy the contract on-chain. The broadcast transaction logs will be stored in the broadcast directory by default. You can change the logs location by setting broadcast in your foundry.toml file.

The broadcasting sender is determined by checking the following in order:

  1. If --sender argument was provided, that address is used.
  2. If exactly one signer (e.g. private key, hardware wallet, keystore) is set, that signer is used.
  3. Otherwise, the default Foundry sender (0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38) is attempted to be used.

Now that you’re up to speed about what the script smart contract does, let’s run it.

You should have added the variables we mentioned earlier to the .env for this next part to work.

At the root of the project run:

# To load the variables in the .env file
source .env

# To deploy our contract
forge script --chain zksync-testnet script/NFT.s.sol:MyScript --rpc-url $ZKSYNC_SEPOLIA_RPC_URL --broadcast --zksync -vvvv

Forge is going to run our script and broadcast the transactions for us - this can take a little while, since Forge will also wait for the transaction receipts.

This confirms that you have successfully deployed the NFT contract to the ZKsync Sepolia testnet.

💡 Note: A full implementation of this tutorial can be found here and for further reading about solidity scripting, you can check out the forge script reference. This confirms that you have successfully deployed the NFT contract to the Sepolia testnet and have also verified it on Etherscan, all with one command.

Scripting with Arguments

Let’s enhance our script to accept arguments, making it more flexible and reusable. This approach allows us to deploy different NFT contracts with varying names, symbols, and base URIs without modifying the script each time. We’ll start by modifying the NFT.s.sol script:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script} from "forge-std/Script.sol";
import {NFT} from "../src/NFT.sol";

contract MyScript is Script {
    function run(
        string calldata _name,
        string calldata _symbol,
        string calldata _baseUri
    ) external {
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        vm.startBroadcast(deployerPrivateKey);

        NFT nft = new NFT(_name, _symbol, _baseUri);

        vm.stopBroadcast();
    }
}

At the root of the project run:

# To load the variables in the .env file
source .env

# To deploy and verify our contract
forge script --chain sepolia script/NFT.s.sol:MyScript "NFT tutorial" TUT baseUri --sig 'run(string,string,string)' --rpc-url $SEPOLIA_RPC_URL --broadcast --verify -vvvv

Let’s break down the additions to our command:

"NFT tutorial" TUT baseUri --sig 'run(string,string,string)'

  • "NFT tutorial" - is the first argument of the new run command - the name of the collection
  • TUT - is the second argument - the symbol of the collection
  • baseUri - is the third argument - the baseURI of the collection
  • --sig 'run(string,string,string)' - changes the signature of the function we want to call in the contract

Forge is going to run our script and broadcast the transactions using the parameters we specified on the command line. You should see an output similar to the previous section.

Deploying locally

You can deploy to Anvil, the local testnet, by configuring the port as the fork-url.

Here, we have two options in terms of accounts. We can either start anvil without any flags and use one of the private keys provided. Or, we can pass a mnemonic to anvil to use.

Using Anvil’s Default Accounts

First, start Anvil:

anvil

Update your .env file with a private key given to you by Anvil.

Then run the following script:

forge script script/NFT.s.sol:MyScript --fork-url http://localhost:8545 --broadcast

Using a Custom Mnemonic

Add the following line to your .env file and complete it with your mnemonic:

MNEMONIC=

It is expected that the PRIVATE_KEY environment variable we set earlier is one of the first 10 accounts in this mnemonic.

Start Anvil with the custom mnemonic:

source .env

anvil -m $MNEMONIC

Then run the following script:

forge script script/NFT.s.sol:MyScript --fork-url http://localhost:8545 --broadcast

💡 Note: A full implementation of this tutorial can be found here and for further reading about solidity scripting, you can check out the forge script reference.

References

anvil-zksync

NAME

anvil-zksync - Create a local testnet node for deploying and testing smart contracts. It can also be used to fork other ZK chains.

SYNOPSIS

anvil_zksync [options]

DESCRIPTION

Create a local testnet node for deploying and testing smart contracts. It can also be used to fork other ZK chains.

This section covers an extensive list of information about Supported Transport Layers, Supported RPC Methods, Anvil-ZKsync flags and their usages. You can run multiple flags at the same time.

Mining Modes

Mining modes refer to how frequent blocks are mined using anvil-zksync. By default, it automatically generates a new block as soon as a transaction is submitted.

You can change this setting to interval mining if you will, which means that a new block will be generated in a given period of time selected by the user. If you want to go for this type of mining, you can do it by adding the --block-time <block-time-in-seconds> flag, like in the following example.

# Produces a new block every 10 seconds
anvil-zksync --block-time 10

There’s also a third mining mode called never. In this case, it disables auto and interval mining, and mine on demand instead. You can do this by typing:

# Enables never mining mode
anvil-zksync --no-mining

Supported Transport Layers

HTTP and Websocket connections are supported. The server listens on port 8011 by default, but it can be changed by running the following command:

anvil-zksync --port <PORT>

Default CREATE2 Deployer

Anvil-ZKsync, when used without forking, includes the default CREATE2 deployer proxy at the address 0x0000000000000000000000000000000000010000.

This allows you to test CREATE2 deployments locally without forking.

Supported RPC Methods

ANVIL Namespace

The anvil_* namespace provides custom methods for advanced node manipulation and testing.


anvil_setMinGasPrice
Status: NOT IMPLEMENTED
Description: Set the minimum gas price for the node. Unsupported for ZKsync as it is only relevant for pre-EIP1559 chains.

anvil_setLoggingEnabled
Status: SUPPORTED
Description: Enables or disables logging.

anvil_snapshot
Status: SUPPORTED
Description: Snapshot the state of the blockchain at the current block.

anvil_revert
Status: SUPPORTED
Description: Revert the state of the blockchain to a previous snapshot.

anvil_setTime
Status: SUPPORTED
Description: Sets the internal clock time to the given timestamp.

anvil_increaseTime
Status: SUPPORTED
Description: Jump forward in time by the given amount of time, in seconds.

anvil_setNextBlockTimestamp
Status: SUPPORTED
Description: Sets the timestamp of the next block.

anvil_setBlockTimestampInterval
Status: SUPPORTED
Description: Sets a recurring interval for block timestamps in seconds.

anvil_removeBlockTimestampInterval
Status: SUPPORTED
Description: Removes the recurring block timestamp interval, if set.

anvil_getAutomine
Status: SUPPORTED
Description: Retrieves the current status of the automine feature.

anvil_setAutomine
Status: SUPPORTED
Description: Enables or disables the automine feature.

anvil_setIntervalMining
Status: SUPPORTED
Description: Configures the node to mine blocks at regular intervals specified in seconds.

anvil_autoImpersonateAccount
Status: SUPPORTED
Description: Sets auto impersonation status.

anvil_setNonce
Status: SUPPORTED
Description: Sets the nonce of an address.

anvil_impersonateAccount
Status: SUPPORTED
Description: Impersonate an account.

anvil_stopImpersonatingAccount
Status: SUPPORTED
Description: Stop impersonating an account after previously impersonating it.

anvil_reset
Status: SUPPORTED
Description: Resets the state of the network; cannot revert to past block numbers, unless they’re in a fork.

anvil_mine
Status: SUPPORTED
Description: Mines any number of blocks at once, in constant time.

anvil_setBalance
Status: SUPPORTED
Description: Modifies the balance of an account.

anvil_setCode
Status: SUPPORTED
Description: Sets the bytecode of a given account.

anvil_setStorageAt
Status: SUPPORTED
Description: Sets the storage value at a given key for a given account.


CONFIG Namespace

Configuration methods to adjust node settings dynamically.

config_getShowCalls
Status: SUPPORTED
Description: Gets the current value of show_calls that’s originally set with the --show-calls option.

config_getShowOutputs
Status: SUPPORTED
Description: Gets the current value of show_outputs that’s originally set with the --show-outputs option.

config_getCurrentTimestamp
Status: SUPPORTED
Description: Gets the value of current_timestamp for the node.

config_setResolveHashes
Status: SUPPORTED
Description: Updates resolve-hashes to call OpenChain for human-readable ABI names in call traces.

config_setShowCalls
Status: SUPPORTED
Description: Updates show_calls to print more detailed call traces.

config_setShowOutputs
Status: SUPPORTED
Description: Updates show_outputs to print calls outputs.

config_setShowStorageLogs
Status: SUPPORTED
Description: Updates show_storage_logs to print storage log reads/writes.

config_setShowVmDetails
Status: SUPPORTED
Description: Updates show_vm_details to print more detailed results from VM execution.

config_setShowGasDetails
Status: SUPPORTED
Description: Updates show_gas_details to print more details about gas estimation and usage.

config_setLogLevel
Status: SUPPORTED
Description: Sets the logging level for the node and only displays the node logs.

config_setLogging
Status: SUPPORTED
Description: Sets the fine-tuned logging levels for the node and any of its dependencies.


DEBUG Namespace

Debugging tools to trace and inspect transactions and blocks.

debug_traceCall
Status: SUPPORTED
Description: Performs a call and returns structured traces of the execution.

debug_traceTransaction
Status: SUPPORTED
Description: Returns a structured trace of the execution of the specified transaction.

debug_traceBlockByHash
Status: SUPPORTED
Description: Returns structured traces for operations within the block of the specified block hash.

debug_traceBlockByNumber
Status: SUPPORTED
Description: Returns structured traces for operations within the block of the specified block number.


ETH Namespace

Standard Ethereum JSON-RPC methods.

eth_accounts
Status: SUPPORTED
Description: Returns a list of addresses owned by the client.

eth_chainId
Status: SUPPORTED
Description: Returns the currently configured chain ID (default is 260).

eth_coinbase
Status: NOT IMPLEMENTED
Description: Returns the client coinbase address.

eth_estimateGas
Status: SUPPORTED
Description: Generates and returns an estimate of how much gas is necessary for the transaction to complete.

eth_feeHistory
Status: SUPPORTED
Description: Returns a collection of historical block gas data (hardcoded with gas price of 50_000_000).

eth_gasPrice
Status: SUPPORTED
Description: Returns the current price per gas in wei (hardcoded to 50_000_000).

eth_getBalance
Status: SUPPORTED
Description: Returns the balance of the account of given address.

eth_getBlockByHash
Status: SUPPORTED
Description: Returns information about a block by block hash.

eth_getBlockByNumber
Status: SUPPORTED
Description: Returns information about a block by block number.

eth_getBlockTransactionCountByHash
Status: SUPPORTED
Description: Number of transactions in a block matching the given block hash.

eth_getBlockTransactionCountByNumber
Status: SUPPORTED
Description: Number of transactions in a block matching the given block number.

eth_getCompilers
Status: NOT IMPLEMENTED
Description: Returns a list of available compilers.

eth_getProof
Status: NOT IMPLEMENTED
Description: Returns the account’s Merkle proof and storage values for specified storage keys.

eth_getStorageAt
Status: SUPPORTED
Description: Returns the value from a storage position at a given address.

eth_getTransactionByHash
Status: SUPPORTED
Description: Returns information about a transaction requested by transaction hash.

eth_getTransactionCount
Status: SUPPORTED
Description: Returns the number of transactions sent from an address.

eth_getTransactionByBlockHashAndIndex
Status: SUPPORTED
Description: Returns information about a transaction by block hash and transaction index.

eth_getTransactionByBlockNumberAndIndex
Status: SUPPORTED
Description: Returns information about a transaction by block number and transaction index.

eth_getTransactionReceipt
Status: SUPPORTED
Description: Returns the receipt of a transaction by transaction hash.

eth_getUncleByBlockHashAndIndex
Status: NOT IMPLEMENTED
Description: Returns information about an uncle of a block by hash and uncle index.

eth_getUncleByBlockNumberAndIndex
Status: NOT IMPLEMENTED
Description: Returns information about an uncle of a block by number and uncle index.

eth_getUncleCountByBlockHash
Status: NOT IMPLEMENTED
Description: Returns the number of uncles in a block matching the given block hash.

eth_getUncleCountByBlockNumber
Status: NOT IMPLEMENTED
Description: Returns the number of uncles in a block matching the given block number.

eth_getWork
Status: NOT IMPLEMENTED
Description: Returns mining-related data such as current block header pow-hash, seed hash, and target.

eth_hashrate
Status: NOT IMPLEMENTED
Description: Returns the number of hashes per second that the node is mining with.

eth_maxPriorityFeePerGas
Status: NOT IMPLEMENTED
Description: Returns a maxPriorityFeePerGas value suitable for quick transaction inclusion.

eth_mining
Status: NOT IMPLEMENTED
Description: Returns true if the client is actively mining new blocks.

eth_newBlockFilter
Status: SUPPORTED
Description: Creates a filter in the node to notify when a new block arrives.

eth_newFilter
Status: SUPPORTED
Description: Creates a filter object to notify when state changes (logs) occur.

eth_newPendingTransactionFilter
Status: SUPPORTED
Description: Creates a filter to notify when new pending transactions arrive.

eth_protocolVersion
Status: SUPPORTED
Description: Returns the current Ethereum protocol version.

eth_sendTransaction
Status: SUPPORTED
Description: Creates a new message call transaction or a contract creation.

eth_sendRawTransaction
Status: SUPPORTED
Description: Creates a new message call transaction or a contract creation for signed transactions.

eth_sign
Status: NOT IMPLEMENTED
Description: Calculates an Ethereum-specific signature.

eth_signTransaction
Status: NOT IMPLEMENTED
Description: Signs a transaction for later submission using eth_sendRawTransaction.

eth_signTypedData
Status: NOT IMPLEMENTED
Description: Identical to eth_signTypedData_v4.

eth_signTypedData_v4
Status: NOT IMPLEMENTED
Description: Returns a hex-encoded signature.

eth_submitHashrate
Status: NOT IMPLEMENTED
Description: Used for submitting mining hashrate.

eth_submitWork
Status: NOT IMPLEMENTED
Description: Used for submitting a proof-of-work solution.

eth_subscribe
Status: NOT IMPLEMENTED
Description: Starts a subscription to a particular event.

eth_syncing
Status: SUPPORTED
Description: Returns an object containing data about the sync status or false when not syncing.

eth_uninstallFilter
Status: SUPPORTED
Description: Uninstalls a filter with the given ID.

eth_unsubscribe
Status: NOT IMPLEMENTED
Description: Cancels a subscription to a particular event.


HARDHAT Namespace

Custom methods provided by Hardhat for advanced testing and node manipulation.

hardhat_addCompilationResult
Status: NOT IMPLEMENTED
Description: Add information about compiled contracts.

hardhat_dropTransaction
Status: NOT IMPLEMENTED
Description: Remove a transaction from the mempool.

hardhat_impersonateAccount
Status: SUPPORTED
Description: Impersonate an account.

hardhat_getAutomine
Status: PARTIALLY SUPPORTED
Description: Currently always returns true as anvil_zksync by default mines new blocks with each transaction.

hardhat_metadata
Status: NOT IMPLEMENTED
Description: Returns the metadata of the current network.

hardhat_mine
Status: SUPPORTED
Description: Mine any number of blocks at once, in constant time.

hardhat_reset
Status: PARTIALLY SUPPORTED
Description: Resets the state of the network; cannot revert to past block numbers, unless they’re in a fork.

hardhat_setBalance
Status: SUPPORTED
Description: Modifies the balance of an account.

hardhat_setCode
Status: SUPPORTED
Description: Sets the bytecode of a given account.

hardhat_setCoinbase
Status: NOT IMPLEMENTED
Description: Sets the coinbase address.

hardhat_setLoggingEnabled
Status: NOT IMPLEMENTED
Description: Enables or disables logging in Hardhat Network.

hardhat_setMinGasPrice
Status: NOT IMPLEMENTED
Description: Sets the minimum gas price.

hardhat_setNextBlockBaseFeePerGas
Status: NOT IMPLEMENTED
Description: Sets the base fee per gas for the next block.

hardhat_setPrevRandao
Status: NOT IMPLEMENTED
Description: Sets the PREVRANDAO value of the next block.

hardhat_setNonce
Status: SUPPORTED
Description: Sets the nonce of a given account.

hardhat_setStorageAt
Status: SUPPORTED
Description: Sets the storage value at a given key for a given account.

hardhat_stopImpersonatingAccount
Status: SUPPORTED
Description: Stop impersonating an account after previously impersonating it.


EVM Namespace

Ethereum Virtual Machine manipulation methods for testing and development.

evm_addAccount
Status: NOT IMPLEMENTED
Description: Adds any arbitrary account.

evm_increaseTime
Status: SUPPORTED
Description: Jump forward in time by the given amount of time, in seconds.

evm_mine
Status: SUPPORTED
Description: Force a single block to be mined.

evm_removeAccount
Status: NOT IMPLEMENTED
Description: Removes an account.

evm_revert
Status: SUPPORTED
Description: Revert the state of the blockchain to a previous snapshot.

evm_setAccountBalance
Status: NOT IMPLEMENTED
Description: Sets the given account’s balance to the specified WEI value.

evm_setAccountCode
Status: NOT IMPLEMENTED
Description: Sets the given account’s code to the specified data.

evm_setAccountNonce
Status: SUPPORTED
Description: Sets the given account’s nonce to the specified value.

evm_setAccountStorageAt
Status: NOT IMPLEMENTED
Description: Sets the given account’s storage slot to the specified data.

evm_setAutomine
Status: NOT IMPLEMENTED
Description: Enables or disables the automatic mining of new blocks with each transaction.

evm_setBlockGasLimit
Status: NOT IMPLEMENTED
Description: Sets the Block Gas Limit of the network.

evm_setIntervalMining
Status: NOT IMPLEMENTED
Description: Enables or disables automatic mining of blocks at regular intervals.

evm_setNextBlockTimestamp
Status: SUPPORTED
Description: Sets the timestamp of the next block.

evm_setTime
Status: SUPPORTED
Description: Sets the current timestamp for the node.

evm_snapshot
Status: SUPPORTED
Description: Snapshot the state of the blockchain at the current block.

evm_revert
Status: SUPPORTED
Description: Revert the state of the blockchain to a previous snapshot.


WEB3 Namespace

Standard Web3 methods.

web3_clientVersion
Status: SUPPORTED
Description: Returns the client version (e.g., zkSync/v2.0).


ZKS Namespace

ZkSync-specific methods for enhanced functionality on the zkRollup.

zks_estimateFee
Status: SUPPORTED
Description: Gets the fee estimation data for a given request.

zks_estimateGasL1ToL2
Status: NOT IMPLEMENTED
Description: Estimate of the gas required for an L1 to L2 transaction.

zks_getAllAccountBalances
Status: SUPPORTED
Description: Returns all balances for confirmed tokens given by an account address.

zks_getBridgeContracts
Status: SUPPORTED
Description: Returns L1/L2 addresses of default bridges.

zks_getBlockDetails
Status: SUPPORTED
Description: Returns additional zkSync-specific information about the L2 block.

zks_getBytecodeByHash
Status: NOT IMPLEMENTED
Description: Returns bytecode of a transaction given by its hash.

zks_getConfirmedTokens
Status: SUPPORTED
Description: Returns [address, symbol, name, and decimal] information of all tokens within a range of IDs.

zks_getBaseTokenL1Address
Status: SUPPORTED
Description: Returns the L1 base token address (hard-coded to 0x0000000000000000000000000000000000000001).

zks_getL1BatchBlockRange
Status: NOT IMPLEMENTED
Description: Returns the range of blocks contained within a batch given by batch number.

zks_getL1BatchDetails
Status: NOT IMPLEMENTED
Description: Returns data pertaining to a given batch.

zks_getL2ToL1LogProof
Status: NOT IMPLEMENTED
Description: Returns the proof for the corresponding L2 to L1 log.

zks_getL2ToL1MsgProof
Status: NOT IMPLEMENTED
Description: Returns the proof for the message sent via the L1Messenger system contract.

zks_getMainContract
Status: NOT IMPLEMENTED
Description: Returns the address of the zkSync Era contract.

zks_getRawBlockTransactions
Status: SUPPORTED
Description: Returns data of transactions in a block.

zks_getTestnetPaymaster
Status: NOT IMPLEMENTED
Description: Returns the address of the testnet paymaster.

zks_getTokenPrice
Status: SUPPORTED
Description: Gets the USD price of a token (ETH is hard-coded to 1_500, while some others are 1).

zks_getTransactionDetails
Status: SUPPORTED
Description: Returns data from a specific transaction given by the transaction hash.

zks_L1BatchNumber
Status: NOT IMPLEMENTED
Description: Returns the latest L1 batch number.

zks_L1ChainId
Status: IMPLEMENTED
Description: Returns the chain ID of the underlying L1.

OPTIONS

General Options

-h, --help      Print help information.

-V, --version      Print version information.

--offline      Run in offline mode (disables all network requests).

--health-check-endpoint      Enable health check endpoint. It will be available for GET requests at /health. The endpoint will return 200 OK if the node is healthy.

--config-out <OUT_FILE>      Writes output of era-test-node as JSON to the user-specified file.

Account Configuration

-a, --accounts <NUM>      Number of dev accounts to generate and configure. [default: 10]

--balance <NUM>      The balance of every dev account in Ether. [default: 10000]

-m, --mnemonic <MNEMONIC>      BIP39 mnemonic phrase used for generating accounts. Cannot be used if --mnemonic-random or --mnemonic-seed-unsafe are used.

--mnemonic-random [<MNEMONIC_RANDOM>]      Automatically generates a BIP39 mnemonic phrase and derives accounts from it. Cannot be used with other --mnemonic options. You can specify the number of words you want in the mnemonic. [default: 12]

--mnemonic-seed-unsafe <MNEMONIC_SEED>      Generates a BIP39 mnemonic phrase from a given seed. Cannot be used with other --mnemonic options. CAREFUL: This is NOT SAFE and should only be used for testing. Never use the private keys generated in production.

--derivation-path <DERIVATION_PATH>      Sets the derivation path of the child key to be derived. [default: m/44'/60'/0'/0/]

--auto-impersonate      Enables automatic impersonation on startup. This allows any transaction sender to be simulated as different accounts, which is useful for testing contract behavior.      [aliases: auto-unlock]

Network Options

--port <PORT>      Port to listen on. [default: 8011]

--host <IP_ADDR>      The IP address the server will listen on.      [env: ANVIL_ZKSYNC_IP_ADDR]      [default: 127.0.0.1]

--chain-id <CHAIN_ID>      Specify chain ID. [default: 260]

Debugging Options

-d, --debug-mode      Enable default settings for debugging contracts.

--show-calls <SHOW_CALLS>      Show call debug information.      [possible values: none, user, system, all]

--show-outputs <SHOW_OUTPUTS>      Show call output information.      [possible values: true, false]

--show-storage-logs <SHOW_STORAGE_LOGS>      Show storage log information.      [possible values: none, read, write, paid, all]

--show-vm-details <SHOW_VM_DETAILS>      Show VM details information.      [possible values: none, all]

--show-gas-details <SHOW_GAS_DETAILS>      Show gas details information.      [possible values: none, all]

--resolve-hashes <RESOLVE_HASHES>      If true, the tool will try to resolve ABI and topic names for better readability. May decrease performance.      [possible values: true, false]

Gas Configuration

--l1-gas-price <L1_GAS_PRICE>      Custom L1 gas price (in wei).

--l2-gas-price <L2_GAS_PRICE>      Custom L2 gas price (in wei).

--l1-pubdata-price <L1_PUBDATA_PRICE>      Custom L1 pubdata price (in wei).

--price-scale-factor <PRICE_SCALE_FACTOR>      Gas price estimation scale factor.

--limit-scale-factor <LIMIT_SCALE_FACTOR>      Gas limit estimation scale factor.

System Configuration

--override-bytecodes-dir <OVERRIDE_BYTECODES_DIR>      Directory to override bytecodes.

--dev-system-contracts <DEV_SYSTEM_CONTRACTS>      Option for system contracts (default: built-in).      [possible values: built-in, local, built-in-without-security]

--emulate-evm      Enables EVM emulation. Requires local system contracts.

Logging Configuration

--log <LOG>      Log level (default: info).      [possible values: trace, debug, info, warn, error]

--log-file-path <LOG_FILE_PATH>      Log file path. [default: anvil_zksync.log]

Cache Options

--cache <CACHE>      Cache type (none, memory, or disk). [default: disk]      [possible values: none, memory, disk]

--reset-cache <RESET_CACHE>      Reset the local disk cache.      [possible values: true, false]

--cache-dir <CACHE_DIR>      Cache directory location for disk cache. [default: .cache]

Commands

run      Starts a new empty local network.

fork      Starts a local network that is a fork of another network.

replay_tx      Starts a local network that is a fork of another network and replays a given transaction on it.

help      Print this message or the help of the given subcommand(s).

Command-Specific Options

fork Command

--fork-url <FORK_URL>      Network to fork from (e.g., http://XXX:YY, mainnet, sepolia-testnet).

--fork-block-number <BLOCK>      Fetch state from a specific block number over a remote endpoint.

--fork-transaction-hash <TRANSACTION>      Fetch state from a specific transaction hash over a remote endpoint.      See --fork-url.

replay_tx Command

<TX>      Transaction hash to replay.

Additional Options

--timestamp <NUM>      The timestamp of the genesis block.

--init <PATH>      Initialize the genesis block with the given genesis.json file.


Usage Examples:

  • Start a new empty local network:

    anvil-zksync run [OPTIONS]
    
  • Start a forked network:

    anvil-zksync fork [OPTIONS] --fork-url <FORK_URL>
    
  • Replay a transaction on a forked network:

    anvil-zksync replay_tx --fork-url <FORK_URL> <TX>
    
  • Display help information:

    anvil-zksync --help
    

Shell Completions

anvil_zksync completions shell

Generates a shell completions script for the given shell.

Supported shells are:

  • bash
  • elvish
  • fish
  • powershell
  • zsh

EXAMPLES

  1. Generate shell completions script for zsh:
    anvil-zksync completions zsh > $HOME/.oh-my-zsh/completions/_anvil_zksync
    

Using genesis.json

The genesis.json file in Anvil serves a similar purpose as in Geth, defining the network’s initial state, consensus rules, and preallocated accounts to ensure all nodes start consistently and maintain network integrity. All values, including balance, gas limit and such, are to be defined as hexadecimals.


GENESIS BLOCK FIELDS

  • hash: The hash of the block.
  • parent_hash: The hash of the parent block. All zeros for the genesis block since there is no parent.
  • block_number: The block number, with the genesis block being 0.
  • timestamp: The creation time of the genesis block in Unix time.
  • l1_batch_env: The environment configuration for the Layer 1 batch, containing:
    • previous_batch_hash:The hash of the previous batch. null for the first batch.

    • number: The batch number.

    • timestamp: The timestamp of the batch in Unix time.

    • fee_input: Details of the fee inputs:

      • PubdataIndependent: Contains independent fee parameters:
        • l1_gas_price: The gas price for Layer 1 transactions.
        • fair_l2_gas_price: The fair gas price for Layer 2 transactions.
        • fair_pubdata_price: The fair pubdata price.
    • fee_account: The address designated for fee collection. Defaults to 0x0000000000000000000000000000000000000000.

    • enforced_base_fee: The enforced base fee for transactions. null if not enforced.

    • first_l2_block: Details of the first Layer 2 block:

      • number: The block number.
      • timestamp: The timestamp of the block in Unix time.
      • prev_block_hash: The hash of the previous block. All zeros for the genesis block.
      • max_virtual_blocks_to_create: The maximum number of virtual blocks that can be created.
  • transactions: An array of transactions included in the block. Empty array for the genesis block.
  • gas_used: The total amount of gas used in the block.
  • logs_bloom: The bloom filter for logs in the block.

Example:

{
  "hash": null,
  "parent_hash": null,
  "block_number": 0,
  "timestamp": 1638316800,
  "l1_batch_env": {
    "previous_batch_hash": null,
    "number": 0,
    "timestamp": 1732653220,
    "fee_input": {
      "PubdataIndependent": {
        "l1_gas_price": 1,
        "fair_l2_gas_price": 2,
        "fair_pubdata_price": 1000000000000000
      }
    },
    "fee_account": "0x0000000000000000000000000000000000000000",
    "enforced_base_fee": null,
    "first_l2_block": {
      "number": 0,
      "timestamp": 1638316800,
      "prev_block_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
      "max_virtual_blocks_to_create": 0
    }
  },
  "transactions": [],
  "gas_used": null,
  "logs_bloom": null
}