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 are available from the GitHub releases page. Currently due to active development itโ€™s recommended to use the latest nightly from the releases page.

Using Foundryup-zksync

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

Open your terminal and run the following command from the repository:

git clone https://github.com/matter-labs/foundry-zksync.git
cd foundry-zksync/foundryup-zksync
./install.sh

This will install Foundryup-zksync, then simply follow the instructions on-screen, which will make the foundryup-zksync command available in your CLI.

Running foundryup-zksync by itself will install the latest (nightly) precompiled binaries: forge and cast. See foundryup-zksync --help for more options, like installing from a specific version or commit.

โ„น๏ธ 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 will need the Rust compiler and Cargo, the Rust package manager. The easiest way to install both is with rustup.rs.

Foundry-ZKsync generally only supports building on 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.

On Windows, you will also need a recent version of Visual Studio, installed with the โ€œDesktop Development With C++โ€ Workloads option.

Building

You can either use the different Foundryup-ZKsync flags:

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

Or, by using a single Cargo 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

Installing for CI in Github Action

The latest binaries for the appropriate architecture can be downloaded from the release page:

steps:
  - name: download binaries
    run: |
      wget -qc https://github.com/matter-labs/foundry-zksync/releases/download/nightly/foundry_nightly_linux_amd64.tar.gz -O - | tar -xz
      ./forge -V && ./cast -V

Using Foundry with Docker

โ„น๏ธ Note

No prebuilt images are available for docker yet.

First Steps with Foundry-ZKsync

This section provides an overview of the forge command line tool. We demonstrate how to create a new project, compile, and test it.

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

$ forge init hello_foundry

Letโ€™s check out what forge generated for us:

$ cd hello_foundry
$ tree . -d -L 1
.
โ”œโ”€โ”€ lib
โ”œโ”€โ”€ script
โ”œโ”€โ”€ src
โ””โ”€โ”€ test

5 directories

We can build the project with forge build --zksync:

$ forge build --zksync

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

And run the tests with forge test --zksync:

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

No files changed, compilation skipped

Ran 2 tests for test/Counter.t.sol:CounterTest
[PASS] testFuzz_SetNumber(uint256) (runs: 256, ฮผ: 8709, ~: 8709)
[PASS] test_Increment() (gas: 8675)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.30s (2.30s CPU time)

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

๐Ÿ’ก Tip

You can always print help for any subcommand (or their subcommands) by adding --help at the end.

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

5 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 2.67s
Compiler run successful!

And run the tests:

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

No files changed, compilation skipped

Ran 2 tests for test/Counter.t.sol:CounterTest
[PASS] testFuzz_SetNumber(uint256) (runs: 256, ฮผ: 8709, ~: 8709)
[PASS] test_Increment() (gas: 8675)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.30s (2.30s CPU time)

Ran 1 test suite in 2.30s (2.30s 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 4.76s
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 950.13ms
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, ฮผ: 8878, ~: 8878)
[PASS] test_Example() (gas: 11628)
Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 4.51s (4.51s CPU time)

Ran 1 test suite in 4.51s (4.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.6tNZz4eEvo/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

4 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 "@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).

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" }

Remapping Dependencies

The remapping of a dependency is performed automatically, Soldeer is adding the dependency into the remappings.txt.

E.g.

@openzeppelin-contracts-5.0.2=dependencies/@openzeppelin-contracts-5.0.2
@uniswap-universal-router-1.6.0=dependencies/@uniswap-universal-router-1.6.0
@prb-math-4.0.2=dependencies/@prb-math-4.0.2
@forge-std-1.8.1=dependencies/forge-std-1.8.1

These remappings mean:

  • To import from forge-std, we would write: import "forge-std-1.8.1/Contract.sol";
  • To import from @openzeppelin-contracts, we would write: import "@openzeppelin-contracts-5.0.2/Contract.sol";

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

To remove a dependency, you have to manually delete it from the dependencies directory and from the [dependencies] tag.

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 true

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 true

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

9 directories, 10 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

Limitations

See Compilation Limitations.

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
    }
    

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();
    }
}

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 CheatCodes {
    /// Existing forge cheatcodes
    ...

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

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.

Examples

/// contract LeetContract {
///     function leet() public {
///         // do something
///     }
/// }

vm.zkVm(true);
new LeetContract(); // deployed in zkEVM
vm.zkVm(false);
new LeetContract(); // deployed in EVM

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โŒ Not 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โŒ Not SupportedChecks the verification status of a contract on a blockchain explorer like Etherscan, confirming that it has been successfully verified.
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.26
Solc 0.8.26 finished in 643.85ms
Compiler run successful!

No files changed, compilation skipped

Ran 2 tests for test/Counter.t.sol:CounterTest
[PASS] testFuzz_SetNumber(uint256) (runs: 256, ฮผ: 8709, ~: 8709)
[PASS] test_Increment() (gas: 8675)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.30s (2.30s CPU time)

Ran 1 test suite in 2.30s (2.30s 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 838.97ms
Compiler run successful!

No files changed, compilation skipped

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.12ms (1.22ms CPU time)

Ran 1 test suite in 5.20ms (1.12ms 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 793.42ms
Compiler run successful!

No files changed, compilation skipped

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

Ran 1 test suite in 3.71ms (524.91ยต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 "forge-std/Test.sol";

Letโ€™s examine a basic test:

pragma solidity 0.8.10;

import "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. 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 "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 855.72ms
Compiler run successful!

No files changed, compilation skipped

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

Ran 1 test suite in 26.66ms (25.86ms 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: 8067)
[PASS] test_IncrementAsOwner() (gas: 6239)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 27.72ms (21.18ms CPU time)

Ran 1 test suite in 28.78ms (27.72ms 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: 8067)
Traces:
  [8067] 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496::testFail_IncrementAsNotOwner()
    โ”œโ”€ [0] VM::prank(0x0000000000000000000000000000000000000000)
    โ”‚   โ””โ”€ โ† [Return] 
    โ”œโ”€ [0] 0xF9E9ba9Ed9B96AB918c74B21dD0f1D5f2ac38a30::increment()
    โ”‚   โ””โ”€ โ† [Revert] Unauthorized()
    โ””โ”€ โ† [Revert] Unauthorized()

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

Ran 1 test suite in 17.86ms (16.93ms 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: 6239)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 25.80ms (13.66ms CPU time)

Ran 1 test suite in 26.65ms (25.80ms 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 "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 "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 "forge-std/Vm.sol";
import "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 "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.

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 "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 882.26ms
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 1 files with zksolc and solc 0.8.10
zksolc and solc 0.8.10 finished in 4.32s
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-09-04T13:36:33.830393Z ERROR foundry_zksync_core::vm::tracer: 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: 12349)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 22.37ms (9.84ms CPU time)

Ran 1 test suite in 23.53ms (22.37ms 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 803.35ms
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 4.37s
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-09-04T13:36:39.849766Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:39.858634Z ERROR foundry_zksync_core::vm::tracer: 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=0x29facca70000000000000000000000000000000011f88d5007acc7732f5149f83c18544e args=[23887431709642595303355446118883808334 [2.388e37]]] testFuzz_Withdraw(uint256) (runs: 1, ฮผ: 12406, ~: 12406)
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 29.56ms (18.29ms CPU time)

Ran 1 test suite in 30.50ms (29.56ms 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 789.85ms
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 4.46s
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-09-04T13:36:45.934590Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:45.943793Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:45.952954Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:45.962310Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:45.971784Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:45.981297Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:45.991752Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.001179Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.010511Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.019894Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.029096Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.038409Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.047597Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.056733Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.066012Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.075277Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.084517Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.093895Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.103296Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.112734Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.122184Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.131721Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.141096Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.150290Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.159721Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.169072Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.178249Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.187928Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.197382Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.206672Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.216031Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.225520Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.234989Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.244532Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.253909Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.263352Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.273026Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.282466Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.291931Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.301590Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.310879Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.320470Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.330571Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.340637Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.349964Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.359448Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.368876Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.378234Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.387756Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.397131Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.408643Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.418837Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.428215Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.437615Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.447084Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.456647Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.465915Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.475476Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.485069Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.494519Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.503993Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.513694Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.523622Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.533254Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.542779Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.552414Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.561638Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.571055Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.580449Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.590016Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.599246Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.608727Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.618173Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.627407Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.636799Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.646028Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.655652Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.664906Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.674419Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.683891Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.693160Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.702631Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.711719Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.720966Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.730219Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.739524Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.748704Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.758427Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.767993Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.777320Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.786954Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.796481Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.806035Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.815419Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.824733Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.834271Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.843712Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.853195Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.862478Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.872464Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.881876Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.891389Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.900875Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.910457Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.919993Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.929459Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.938834Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.948018Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.957543Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.967059Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.976592Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.986223Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:46.995617Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.005034Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.014516Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.024036Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.033229Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.042397Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.051772Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.061146Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.070476Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.079750Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.089342Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.098412Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.107771Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.116978Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.126189Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.135475Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.144662Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.154008Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.163463Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.173049Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.182263Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.191766Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.201408Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.210591Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.220122Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.229352Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.238818Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.248152Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.257555Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.266800Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.276162Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.285801Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.295178Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.304676Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.313998Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.323976Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.333901Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.343360Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.352824Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.362055Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.371929Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.381515Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.391284Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.400674Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.410118Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.420041Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.429274Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.438705Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.448279Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.457784Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.467256Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.476687Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.486379Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.495726Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.505179Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.514646Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.524615Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.534334Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.543752Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.553472Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.562802Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.572765Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.582320Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.591855Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.601249Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.610664Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.619998Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.629307Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.638653Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.648081Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.657570Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.667002Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.676678Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.686118Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.695482Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.704693Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.713837Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.723306Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.732539Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.741905Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.751277Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.760489Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.769762Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.779107Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.788425Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.797707Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.807081Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.816253Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.825523Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.834825Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.844099Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.853450Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.862921Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.872311Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.881762Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.891169Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.900381Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.909579Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.918855Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.928329Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.937820Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.947159Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.956414Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.965686Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.975391Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.984454Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:47.993664Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.003070Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.012611Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.022253Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.031714Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.040996Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.050437Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.059971Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.069095Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.078248Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.087718Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.097202Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.106467Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.115844Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.125300Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.134275Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.143585Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.152787Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.162354Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.171619Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.180731Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.190085Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.199353Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.208757Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.217691Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.226753Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.235893Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.245093Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.254464Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.263917Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.273199Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.282648Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.291883Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.301465Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.310667Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.320023Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.330812Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.345197Z ERROR foundry_zksync_core::vm::tracer: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-09-04T13:36:48.357410Z ERROR foundry_zksync_core::vm::tracer: 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, ฮผ: 12404, ~: 12509)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.44s (2.43s CPU time)

Ran 1 test suite in 2.45s (2.44s 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 "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 "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 Etherscan, Sourcify or Blockscout, if the network is supported, by passing --verify.

$ forge create --zksync \
    --rpc-url <your_rpc_url> \
    --constructor-args "ForgeUSD" "FUSD" 18 1000000000000000000000 \
    --private-key <your_private_key> \
    --etherscan-api-key <your_etherscan_api_key> \
    --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. Note that for Etherscan ETHERSCAN_API_KEY must be set.

If you are verifying an already deployed contract, read on.

You can verify a contract on 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-id 11155111 \
    --num-of-optimizations 1000000 \
    --watch \
    --constructor-args $(cast abi-encode "constructor(string,string,uint256,uint256)" "ForgeUSD" "FUSD" 18 1000000000000000000000) \
    --etherscan-api-key <your_etherscan_api_key> \
    --compiler-version v0.8.10+commit.fc410830 \
    <the_contract_address> \
    src/MyToken.sol:MyToken 

Submitted contract for verification:
                Response: `OK`
                GUID: `a6yrbjp5prvakia6bqp5qdacczyfhkyi5j1r6qbds1js41ak1a`
                url: https://sepolia.etherscan.io//address/0x6a54โ€ฆ3a4c#code

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-id 11155111 <GUID> <your_etherscan_api_key>
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 two different tools for this job, but they may be merged in the future:

  • Gas reports: Gas reports give you an overview of how much Forge thinks the individual functions in your contracts will consume in gas.
  • Gas snapshots: Gas snapshots give you an overview of how much each test consumes in gas.

Gas reports and gas 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 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 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 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 

Debugger

๐Ÿšจ Important

Debugger in not officially supported for ZKsync.

Forge ships with an interactive debugger.

The debugger is accessible on forge debug and on forge test.

Using forge test:

$ forge test --debug $FUNC

Where $FUNC is the signature of the function you want to debug. For example:

$ forge test --debug "testSomething()"

If you have multiple contracts with the same function name, you need to limit the matching functions down to only one case using --match-path and --match-contract.

If the matching test is a fuzz test, the debugger will open the first failing fuzz scenario, or the last successful one, whichever comes first.

Using forge debug:

$ forge debug --debug $FILE --sig $FUNC

Where $FILE is the path to the contract you want to debug, and $FUNC is the signature of the function you want to debug. For example:

$ forge debug --debug src/SomeContract.sol --sig "myFunc(uint256,string)" 123 "hello"

You can also specify raw calldata using --sig instead of a function signature.

If your source file contains more than one contract, specify the contract you want to debug using the --target-contract flag.

Debugger layout

An image of the debugger UI

When the debugger is run, you are presented with a terminal divided into four quadrants:

  • Quadrant 1: The opcodes in the debugging session, with the current opcode highlighted. Additionally, the address of the current account, the program counter and the accumulated gas usage is also displayed
  • Quadrant 2: The current stack, as well as the size of the stack
  • Quadrant 3: The source view
  • Quadrant 4: The current memory of the EVM

As you step through your code, you will notice that the words in the stack and memory sometimes change color.

For the memory:

  • Red words are about to be written to by the current opcode
  • Green words were written to by the previous opcode
  • Cyan words are being read by the current opcode

For the stack, cyan words are either being read or popped by the current opcode.

โš ๏ธ Note

In most test frameworks, the first test assertion to fail is the one reported. In foundry, the last test assertion to fail (that comes from DSTest or cheatcodes) is the one to be reported.

General

  • q: Quit the debugger
  • h: Show help
  • 0-9 + k: Step a number of times backwards (alternatively scroll up with your mouse)
  • 0-9 + j: Step a number of times forwards (alternatively scroll down with your mouse)
  • g: Move to the beginning of the transaction
  • G: Move to the end of the transaction
  • c: Move to the previous call-type instruction (i.e. CALL, STATICCALL, DELEGATECALL, and CALLCODE).
  • C: Move to the next call-type instruction
  • a: Move to the previous JUMP or JUMPI instruction
  • s: Move to the next JUMPDEST instruction
  • โ€™ + a-z: Move to <char> breakpoint set by a vm.breakpoint cheatcode
  • Ctrl + j: Scroll the memory view down
  • Ctrl + k: Scroll the memory view up
  • m: Show memory as UTF8
  • J: Scroll the stack view down
  • K: Scroll the stack view up
  • t: Show labels on the stack to see what items the current op will consume

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
3171140341090319403991872060 [3.171e27]

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

Anvil is a local testnet node shipped with Foundry. You can use it for testing your contracts from frontends or for interacting over RPC.

Officially we do not support anvil for ZKsync related operations.

An alternative to anvil is current recommended in ZKsync Era In-Memory Node

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.