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

Dual Compilation

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

Limitations

See Compilation Limitations.

Configuration Overview

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

These include the following:

Limitations

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

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

General Limitations

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

Reserved Address Range

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

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

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

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

Origin Address

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

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

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

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

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

Bytecode Constraints

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

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

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

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

    }
}

Bytecode Hash

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

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

Address Derivation

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

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

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

  return ethers.utils.getAddress(addressBytes);
}

Accessing Contract Bytecode and Hash

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

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

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

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

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

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

Compilation Limitations

These limitations apply to zksolc compilation of source contracts.

Contract Bytecode Access

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

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

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

See here on how to circumvent this issue.

Contract Size Limit

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

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

Solution

There are three possible solutions to address this issue:

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

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

    Example command:

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

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

    Example command:

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

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

    Before (single large contract):

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

    After (multiple smaller contracts):

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

Broadcast Limitations

These limitations apply when using cast to broadcast transactions.

No Batch Support

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

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

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

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

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

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

Emitted Events

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

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

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

  ...

Issues with expectEmit

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

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

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

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

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

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

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

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

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

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

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

zk vm decoded result 0000000000000000000000000000000000000000000000000000000000000003

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

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

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

Trace Limitations

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

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

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

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

Combined Traces

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

contract InnerNumber {
    event Value(uint8);

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

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

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

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

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

Cheatcode Limitations

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

Therefore, the following are valid cheatcode accesses:

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

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

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

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

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

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

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

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

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

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

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

Forge-ZKsync Standard Library

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

Installation

forge install Moonsong-Labs/forge-zksync-std

Usage

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

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

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

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

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

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

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

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

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

Additional Cheatcodes

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

Cheatcodes Interface

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

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

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

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

Usage

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

zkRegisterContract

Signature

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

Description

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

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

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

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

Examples

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

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

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


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

zkVm

Signature

function zkVm(bool enable) external pure;

Description

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

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

See Execution Overview for further details.

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

Examples

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

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

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

zkVmSkip

Signature

function zkVmSkip() external pure;

Description

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

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

Examples

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

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

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

new LeetContract(3); // deployed in zkEVM

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

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

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

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

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

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

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

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.

Forge ZKsync Standard Library Overview

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

Refer to this section for more details.

Understanding Traces

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

Traces follow the same general format:

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

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

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

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

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

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

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

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

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

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

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

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

ZKsync Limitations

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

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

Fork Testing

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

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

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

Forking Mode

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

forge test --fork-url <your_rpc_url>

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

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

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

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

Caching

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

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

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

Improved traces

Forge supports identifying contracts in a forked environment with Etherscan.

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

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

Alternatively, you can set the ETHERSCAN_API_KEY environment variable.

Forking Cheatcodes

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

Usage

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

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

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

createSelectFork is a one-liner for createFork plus selectFork.

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

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

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

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

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

Examples

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

contract SimpleStorageContract {
    uint256 public value;

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

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

Replaying Failures

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

Rerun failures

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

forge test --rerun

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

Fuzz tests failures

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

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

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

or by using inline config

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

Invariant tests failures

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

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

[invariant]
failure_persist_dir="/tests/dir"

or by using inline config

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

Remove persisted failures

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

Advanced Testing

Forge comes with a number of advanced testing methods:

In the future, Forge will also support these:

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

Fuzz Testing

Forge supports property based testing.

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

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

pragma solidity 0.8.10;

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

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.

Configuring with foundry.toml

Forge can be configured using a configuration file called foundry.toml, which is placed in the root of your project.

Configuration can be namespaced by profiles. The default profile is named default, from which all other profiles inherit. You are free to customize the default profile, and add as many new profiles as you need.

Additionally, you can create a global foundry.toml in your home directory.

Let’s take a look at a configuration file that contains two profiles: the default profile, which always enables the optimizer, as well as a CI profile, that always displays traces:

[profile.default]
optimizer = true
optimizer_runs = 20_000

[profile.ci]
verbosity = 4

When running forge, you can specify the profile to use using the FOUNDRY_PROFILE environment variable.

Standalone sections

Besides the profile sections, the configuration file can also contain standalone sections ([fmt], [fuzz], [invariant] etc). By default, each standalone section belongs to the default profile. i.e. [fmt] is equivalent to [profile.default.fmt].

To configure the standalone section for different profiles other than default, use syntax [profile.<profile name>.<standalone>]. i.e. [profile.ci.fuzz].


📚 Reference

See the foundry.toml Reference for a complete overview of what you can configure.

References

Config Reference

Config Overview

Foundry’s configuration system allows you to configure its tools.

Profiles

Configuration can be arbitrarily namespaced into profiles. The default profile is named default, and all other profiles inherit values from this profile. Profiles are defined in the profile map.

To add a profile named local, you would add:

[profile.local]

You can select the profile to use by setting the FOUNDRY_PROFILE environment variable.

Global configuration

You can create a foundry.toml file in ~/.foundry folder to configure Foundry globally.

Environment variables

Configuration can be overridden with FOUNDRY_ and DAPP_ prefixed environment variables.

Exceptions are:

  • FOUNDRY_FFI, DAPP_FFI, DAPP_TEST_FFI
  • FOUNDRY_PROFILE
  • FOUNDRY_REMAPPINGS, DAPP_REMAPPINGS
  • FOUNDRY_LIBRARIES, DAPP_LIBRARIES
  • FOUNDRY_FS_PERMISSIONS, DAPP_FS_PERMISSIONS, DAPP_TEST_FS_PERMISSIONS
  • DAPP_TEST_CACHE
  • DAPP_TEST_FUZZ_RUNS
  • DAPP_TEST_FUZZ_DEPTH

Configuration format

Configuration files are written in the TOML format, with simple key-value pairs inside of sections.

This page describes each configuration key in detail. To see the default values, either refer to the specific key in this document, or see the default config.

Configuration keys

This section documents all configuration keys. All configuration keys must live under a profile, such as default.

Project

Configuration related to the project in general.

src
  • Type: string
  • Default: src
  • Environment: FOUNDRY_SRC or DAPP_SRC

The path to the contract sources relative to the root of the project.

test
  • Type: string
  • Default: test
  • Environment: FOUNDRY_TEST or DAPP_TEST

The path to the test contract sources relative to the root of the project.

script
  • Type: string
  • Default: script
  • Environment: FOUNDRY_SCRIPT or DAPP_SCRIPT

The path to the script contract sources relative to the root of the project.

out
  • Type: string
  • Default: out
  • Environment: FOUNDRY_OUT or DAPP_OUT

The path to put contract artifacts in, relative to the root of the project.

libs
  • Type: array of strings (paths)
  • Default: lib
  • Environment: FOUNDRY_LIBS or DAPP_LIBS

An array of paths that contain libraries, relative to the root of the project.

cache
  • Type: boolean
  • Default: true
  • Environment: FOUNDRY_CACHE or DAPP_CACHE

Whether or not to enable caching. If enabled, the result of compiling sources, tests, and dependencies, are cached in cache.

cache_path
  • Type: string
  • Default: cache
  • Environment: FOUNDRY_CACHE_PATH or DAPP_CACHE_PATH

The path to the cache, relative to the root of the project.

broadcast
  • Type: string
  • Default: broadcast

The path to the broadcast transaction logs, relative to the root of the project.

force
  • Type: boolean
  • Default: false
  • Environment: FOUNDRY_FORCE or DAPP_FORCE

Whether or not to perform a clean build, discarding the cache.

ZKsync Settings

Additional zksync settings can be defined on the profile of choice by specifying profile.<name>.zksync section.

For example to define the settings on profile.default:

[profile.default]
# ...Normal foundry settings...

[profile.default.zksync]
# ...ZKSync specific settings...
compile
  • Type: boolean
  • Default: false

Compile contracts for zkEVM.

startup
  • Type: boolean
  • Default: true (will only have effect if compile = true)

Requires compile = true to have an effect.
Enable ZKsync context on startup. This applies to tests, scripts, and any other commands that need to switch immediately to zkEVM on startup.

zksolc
  • Type: string
  • Default:

The zksolc version to use for compilation.

solc_path
  • Type: string
  • Default:

The solc path to use during zksolc compilation.

bytecode_hash
  • Type: boolean
  • Default: “none”

Whether to include the metadata hash for zksolc compiled bytecode.

fallback_oz
  • Type: boolean
  • Default: false

Allow compiler to recompile using optimizer_mode = 'z' if contracts won’t fit in the EraVM bytecode size limitations.

enable_eravm_extensions
  • Type: boolean
  • Default: false

Enable EraVM extensions (e.g. system-mode). This allows compilation of ZKsync-specific simulations.

force_evmla
  • Type: boolean
  • Default: false

Force compilation via EVMLA instead of Yul codegen pipeline.

avoid_contracts
  • Type: array of strings
  • Default: []

List of glob patterns to avoid compiling with zksolc.

optimizer
  • Type: boolean
  • Default: true

Enable optimizer on zksolc.

optimizer_mode
  • Type: string
  • Default: ‘3’

The zksolc optimizer mode (0 | 1 | 2 | 3 | s | z).

optimizer_details
  • Type: object
  • Default: {}

Allows specifying additional zksolc Optimizer Details.

Solidity compiler

Configuration related to the behavior of the Solidity compiler.

Sections

General

Configuration related to the behavior of the Solidity compiler.

remappings
  • Type: array of strings (remappings)
  • Default: none
  • Environment: FOUNDRY_REMAPPINGS or DAPP_REMAPPINGS

An array of remappings in the following format: <name>=<target>.

A remapping remaps Solidity imports to different directories. For example, the following remapping

@openzeppelin/=node_modules/@openzeppelin/openzeppelin-contracts/

with an import like

import "@openzeppelin/contracts/utils/Context.sol";

becomes

import "node_modules/@openzeppelin/openzeppelin-contracts/contracts/utils/Context.sol";
auto_detect_remappings
  • Type: boolean
  • Default: true
  • Environment: FOUNDRY_AUTO_DETECT_REMAPPINGS or DAPP_AUTO_DETECT_REMAPPINGS

If enabled, Foundry will automatically try auto-detect remappings by scanning the libs folder(s).

If set to false, only the remappings in foundry.toml and remappings.txt are used.

allow_paths
  • Type: array of strings (paths)
  • Default: none
  • Environment: FOUNDRY_ALLOW_PATHS or DAPP_ALLOW_PATHS

Tells solc to allow reading source files from additional directories. This is mainly relevant for complex workspaces managed by pnpm or similar.

See also solc allowed-paths

include_paths
  • Type: array of strings (paths)
  • Default: none
  • Environment: FOUNDRY_INCLUDE_PATHS or DAPP_INCLUDE_PATHS

Make an additional source directory available to the default import callback. Use this option if you want to import contracts whose location is not fixed in relation to your main source tree, e.g. third-party libraries installed using a package manager. Can be used multiple times. Can only be used if base path has a non-empty value.

See also solc path resolution

libraries
  • Type: array of strings (libraries)
  • Default: none
  • Environment: FOUNDRY_LIBRARIES or DAPP_LIBRARIES

An array of libraries to link against in the following format: <file>:<lib>:<address>, for example: src/MyLibrary.sol:MyLibrary:0xfD88CeE74f7D78697775aBDAE53f9Da1559728E4.

solc_version
  • Type: string (semver)
  • Default: none
  • Environment: FOUNDRY_SOLC_VERSION or DAPP_SOLC_VERSION

If specified, overrides the auto-detection system (more below) and uses a single Solidity compiler version for the project.

Only strict versions are supported (i.e. 0.8.11 is valid, but ^0.8.0 is not).

auto_detect_solc
  • Type: boolean
  • Default: true
  • Environment: FOUNDRY_AUTO_DETECT_SOLC or DAPP_AUTO_DETECT_SOLC

If enabled, Foundry will automatically try to resolve appropriate Solidity compiler versions to compile your project.

This key is ignored if solc_version is set.

offline
  • Type: boolean
  • Default: false
  • Environment: FOUNDRY_OFFLINE or DAPP_OFFLINE

If enabled, Foundry will not attempt to download any missing solc versions.

If both offline and auto-detect-solc are set to true, the required version(s) of solc will be auto detected but any missing versions will not be installed.

ignored_warnings_from
  • Type: array of strings (file paths)
  • Default: none
  • Environment: FOUNDRY_IGNORED_WARNINGS_FROM OR DAPP_IGNORED_WARNINGS_FROM

An array of file paths from which warnings should be ignored during the compulation process. This is useful when you have a specific directories of files that produce known warning and you wish to suppress these warnings without affecting others.

Each entry in the array should be a path to a directory or a specific file. For Example:

ignored_warnings_from = ["path/to/warnings/file1.sol", "path/to/warnings/file2.sol"]

This configuration will cause the compiler to ignore any warnings that originate from the specified paths.

ignored_error_codes
  • Type: array of integers/strings
  • Default: none for source, SPDX license identifiers and contract size for tests
  • Environment: FOUNDRY_IGNORED_ERROR_CODES or DAPP_IGNORED_ERROR_CODES

An array of Solidity compiler error codes to ignore during build, such as warnings.

Valid values are:

  • license: 1878
  • code-size: 5574
  • func-mutability: 2018
  • unused-var: 2072
  • unused-param: 5667
  • unused-return: 9302
  • virtual-interfaces: 5815
  • missing-receive-ether: 3628
  • shadowing: 2519
  • same-varname: 8760
  • unnamed-return: 6321
  • unreachable: 5740
  • pragma-solidity: 3420
  • constructor-visibility: 2462
  • init-code-size: 3860
  • transient-storage: 2394
  • too-many-warnings: 4591
deny_warnings
  • Type: boolean
  • Default: false
  • Environment: FOUNDRY_DENY_WARNINGS or DAPP_DENY_WARNINGS

If enabled, Foundry will treat Solidity compiler warnings as errors, stopping artifacts from being written to disk.

evm_version
  • Type: string
  • Default: paris
  • Environment: FOUNDRY_EVM_VERSION or DAPP_EVM_VERSION

The EVM version to use during tests. The value must be an EVM hardfork name, such as london, byzantium, etc.

revert_strings
  • Type: string
  • Default: default
  • Environment: FOUNDRY_REVERT_STRINGS or DAPP_REVERT_STRINGS

Possible values are:

  • default does not inject compiler-generated revert strings and keeps user-supplied ones.
  • strip removes all revert strings (if possible, i.e. if literals are used) keeping side-effects.
  • debug injects strings for compiler-generated internal reverts, implemented for ABI encoders V1 and V2 for now.
  • verboseDebug even appends further information to user-supplied revert strings (not yet implemented).
extra_output_files
  • Type: array of strings
  • Default: none
  • Environment: N/A

Extra output from the Solidity compiler that should be written to files in the artifacts directory.

Valid values are:

  • metadata: Written as a metadata.json file in the artifacts directory
  • ir: Written as a .ir file in the artifacts directory
  • irOptimized: Written as a .iropt file in the artifacts directory
  • ewasm: Written as a .ewasm file in the artifacts directory
  • evm.assembly: Written as a .asm file in the artifacts directory
extra_output
  • Type: array of strings
  • Default: see below
  • Environment: N/A

Extra output to include in the contract’s artifact.

The following values are always set, since they’re required by Forge:

extra_output = [
  "abi",
  "evm.bytecode",
  "evm.deployedBytecode",
  "evm.methodIdentifiers",
]

For a list of valid values, see the [Solidity docs][output-desc].

bytecode_hash
  • Type: string
  • Default: ipfs
  • Environment: FOUNDRY_BYTECODE_HASH or DAPP_BYTECODE_HASH

Determines the hash method for the metadata hash that is appended to the bytecode.

Valid values are:

  • ipfs (default)
  • bzzr1
  • none
sparse_mode
  • Type: boolean
  • Default: false
  • Environment: FOUNDRY_SPARSE_MODE or DAPP_SPARSE_MODE

Enables sparse mode for builds.

Optimizer

Configuration related to the Solidity optimizer.

optimizer
  • Type: boolean
  • Default: true
  • Environment: FOUNDRY_OPTIMIZER or DAPP_OPTIMIZER

Whether or not to enable the Solidity optimizer.

optimizer_runs
  • Type: integer
  • Default: 200
  • Environment: FOUNDRY_OPTIMIZER_RUNS or DAPP_OPTIMIZER_RUNS

The amount of optimizer runs to perform.

via_ir
  • Type: boolean
  • Default: false
  • Environment: FOUNDRY_VIA_IR or DAPP_VIA_IR

If set to true, changes compilation pipeline to go through the new IR optimizer.

use_literal_content
  • Type: boolean
  • Default: false

If set to true, changes compilation to only use literal content and not URLs.

[optimizer_details]

The optimizer details section is used to tweak how the Solidity optimizer behaves. There are several configurable values in this section (each of them are booleans):

  • peephole
  • inliner
  • jumpdestRemover
  • orderLiterals
  • deduplicate
  • cse
  • constantOptimizer
  • yul

Refer to the Solidity compiler input description for the default values.

[optimizer_details.yul_details]

The Yul details subsection of the optimizer details section is used to tweak how the new IR optimizer behaves. There are two configuration values:

  • stack_allocation: Tries to improve the allocation of stack slots by freeing them up earlier.
  • optimizer_steps: Selects the optimizer steps to be applied.

Refer to the Solidity compiler input description for the default values.

ℹ️ Note If you encounter compiler errors when using via_ir, explicitly enable the legacy optimizer and leave optimizer_steps as an empty string

Model checker

The Solidity model checker is a built-in opt-in module that is available in Solidity compilers for OSX and Linux. Learn more about the model checker in the Solidity compiler documentation

ℹ️ Note The model checker requires z3 version 4.8.8 or 4.8.14 on Linux.

The model checker settings are configured in the [model_checker] section of the configuration.

The model checker will run when forge build is invoked, and any findings will show up as warnings.

These are the recommended settings when using the model checker:

[profile.default.model_checker]
contracts = {'/path/to/project/src/Contract.sol' = ['Contract']}
engine = 'chc'
timeout = 10000
targets = ['assert']

Setting which contract should be verified is extremely important, otherwise all available contracts will be verified which may take a long time.

The recommended engine is chc, but bmc and all (which runs both) are also accepted.

It is also important to set a proper timeout (given in milliseconds), since the default time given to the underlying solver may not be enough.

If no verification targets are given, only assertions will be checked.

[model_checker]

The following keys are available in the model checker section.

model_checker.contracts
  • Type: table
  • Default: all
  • Environment: N/A

Specifies what contracts the model checker will analyze.

The key of the table is the path to a source file, and the value is an array of contract names to check.

For example:

[profile.default.model_checker]
contracts = { "src/MyContracts.sol" = ["ContractA", "ContractB"] }
model_checker.div_mod_with_slacks
  • Type: boolean
  • Default: false
  • Environment: N/A

Sets how division and modulo operations should be encoded.

Refer to the Solidity documentation for more information.

model_checker.engine
  • Type: string (see below)
  • Default: all
  • Environment: N/A

Specifies the model checker engine to run. Valid values are:

  • chc: The constrained horn clauses engine
  • bmc: The bounded model checker engine
  • all: Runs both engines

Refer to the Solidity documentation for more information on the engines.

model_checker.invariants
  • Type: array of strings
  • Default: N/A
  • Environment: N/A

Sets the model checker invariants. Valid values are:

  • contract: Contract Invariants
  • reentrancy: Reentrancy Properties

Refer to the Solidity documentation for more information on the invariants.

model_checker.show_unproved
  • Type: boolean
  • Default: false
  • Environment: N/A

Whether or not to output all unproved targets.

Refer to the Solidity documentation for more information.

model_checker.solvers
  • Type: array of strings
  • Default: N/A
  • Environment: N/A

Sets the model checker solvers. Valid values are:

  • cvc4
  • eld: introduced in v0.8.18
  • smtlib2
  • z3

Refer to the Solidity documentation for more information.

model_checker.timeout
  • Type: number (milliseconds)
  • Default: N/A
  • Environment: N/A

Sets the timeout for the underlying model checker engines (in milliseconds).

model_checker.targets
  • Type: array of strings
  • Default: assert
  • Environment: N/A

Sets the model checker targets. Valid values are:

  • assert: Assertions
  • underflow: Arithmetic underflow
  • overflow: Arithmetic overflow
  • divByZero: Division by zero
  • constantCondition: Trivial conditions and unreachable code
  • popEmptyArray: Popping an empty array
  • outOfBounds: Out of bounds array/fixed bytes index access
  • default: All of the above (note: not the default for Forge)

Testing

Configuration related to the behavior of forge test.

Sections

General

verbosity
  • Type: integer
  • Default: 0
  • Environment: FOUNDRY_VERBOSITY or DAPP_VERBOSITY

The verbosity level to use during tests.

  • Level 2 (-vv): Logs emitted during tests are also displayed.
  • 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.
ffi
  • Type: boolean
  • Default: false
  • Environment: FOUNDRY_FFI or DAPP_FFI

Whether or not to enable the ffi cheatcode.

Warning: Enabling this cheatcode has security implications for your project, as it allows tests to execute arbitrary programs on your computer.

sender
  • Type: string (address)
  • Default: 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38
  • Environment: FOUNDRY_SENDER or DAPP_SENDER

The value of msg.sender in tests.

tx_origin
  • Type: string (address)
  • Default: 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38
  • Environment: FOUNDRY_TX_ORIGIN or DAPP_TX_ORIGIN

The value of tx.origin in tests.

initial_balance
  • Type: string (hexadecimal)
  • Default: 0xffffffffffffffffffffffff
  • Environment: FOUNDRY_INITIAL_BALANCE or DAPP_INITIAL_BALANCE

The initial balance of the test contracts in wei, written in hexadecimal.

block_number
  • Type: integer
  • Default: 1
  • Environment: FOUNDRY_BLOCK_NUMBER or DAPP_BLOCK_NUMBER

The value of block.number in tests.

chain_id
  • Type: integer
  • Default: 31337
  • Environment: FOUNDRY_CHAIN_ID or DAPP_CHAIN_ID

The value of the chainid opcode in tests.

gas_limit
  • Type: integer or string
  • Default: 9223372036854775807
  • Environment: FOUNDRY_GAS_LIMIT or DAPP_GAS_LIMIT

The gas limit for each test case.

ℹ️ Note

Due to a limitation in a dependency of Forge, you cannot raise the gas limit beyond the default without changing the value to a string.

In order to use higher gas limits use a string:

gas_limit = "18446744073709551615" # u64::MAX
gas_price
  • Type: integer
  • Default: 0
  • Environment: FOUNDRY_GAS_PRICE or DAPP_GAS_PRICE

The price of gas (in wei) in tests.

block_base_fee_per_gas
  • Type: integer
  • Default: 0
  • Environment: FOUNDRY_BLOCK_BASE_FEE_PER_GAS or DAPP_BLOCK_BASE_FEE_PER_GAS

The base fee per gas (in wei) in tests.

block_coinbase
  • Type: string (address)
  • Default: 0x0000000000000000000000000000000000000000
  • Environment: FOUNDRY_BLOCK_COINBASE or DAPP_BLOCK_COINBASE

The value of block.coinbase in tests.

block_timestamp
  • Type: integer
  • Default: 1
  • Environment: FOUNDRY_BLOCK_TIMESTAMP or DAPP_BLOCK_TIMESTAMP

The value of block.timestamp in tests.

block_difficulty
  • Type: integer
  • Default: 0
  • Environment: FOUNDRY_BLOCK_DIFFICULTY or DAPP_BLOCK_DIFFICULTY

The value of block.difficulty in tests.

gas_reports
  • Type: array of strings (contract names)
  • Default: [“*”]
  • Environment: FOUNDRY_GAS_REPORTS or DAPP_GAS_REPORTS

The contracts to print gas reports for.

no_storage_caching
  • Type: boolean
  • Default: false
  • Environment: FOUNDRY_NO_STORAGE_CACHING or DAPP_NO_STORAGE_CACHING

If set to true, then block data from RPC endpoints in tests will not be cached. Otherwise, the data is cached to $HOME/.foundry/cache/<chain id>/<block number>.

[rpc_storage_caching]

The [rpc_storage_caching] block determines what RPC endpoints are cached.

rpc_storage_caching.chains
  • Type: string or array of strings (chain names)
  • Default: all
  • Environment: N/A

Determines what chains are cached. By default, all chains are cached.

Valid values are:

  • “all”
  • A list of chain names, e.g. ["optimism", "mainnet"]
rpc_storage_caching.endpoints
  • Type: string or array of regex patterns (to match URLs)
  • Default: remote
  • Environment: N/A

Determines what RPC endpoints are cached. By default, only remote endpoints are cached.

Valid values are:

  • all
  • remote (default)
  • A list of regex patterns, e.g. ["localhost"]
eth_rpc_url
  • Type: string
  • Default: none
  • Environment: FOUNDRY_ETH_RPC_URL or DAPP_ETH_RPC_URL

The url of the rpc server that should be used for any rpc calls.

etherscan_api_key
  • Type: string
  • Default: none
  • Environment: FOUNDRY_ETHERSCAN_API_KEY or DAPP_ETHERSCAN_API_KEY

The etherscan API key for RPC calls.

match-test
  • Type: regex
  • Default: none
  • Environment: FOUNDRY_MATCH_TEST or DAPP_MATCH_TEST

Only run test methods matching regex. Equivalent to forge test --match-test <TEST_PATTERN>

no-match-test
  • Type: regex
  • Default: none
  • Environment: FOUNDRY_NO_MATCH_TEST or DAPP_NO_MATCH_TEST

Only run test methods not matching regex. Equivalent to forge test --no-match-test <TEST_PATTERN_INVERSE>

match-contract
  • Type: regex
  • Default: none
  • Environment: FOUNDRY_MATCH_CONTRACT or DAPP_MATCH_CONTRACT

Only run test methods in contracts matching regex. Equivalent to forge test --match-contract <CONTRACT_PATTERN>

no-match-contract
  • Type: regex
  • Default: none
  • Environment: FOUNDRY_NO_MATCH_CONTRACT or DAPP_NO_MATCH_CONTRACT

Only run test methods in contracts not matching regex. Equivalent to forge test --no-match-contract <CONTRACT_PATTERN_INVERSE>

match-path
  • Type: regex
  • Default: none
  • Environment: FOUNDRY_MATCH_PATH or DAPP_MATCH_PATH

Only runs test methods on files matching the path. Equivalent to forge test --match-path <PATH_PATTERN>

no-match-path
  • Type: regex
  • Default: none
  • Environment: FOUNDRY_NO_MATCH_PATH or DAPP_NO_MATCH_PATH

Only runs test methods on files not matching the path. Equivalent to forge test --no-match-path <PATH_PATTERN_INVERSE>

threads
  • Type: integer
  • Default: none
  • Environment: FOUNDRY_THREADS

Number of threads to use. Not set or zero specifies the number of logical cores.

show_progress
  • Type: boolean
  • Default: false
  • Environment: FOUNDRY_SHOW_PROGRESS

Whether to show test execution progress.

block_gas_limit
  • Type: integer
  • Default: none
  • Environment: FOUNDRY_BLOCK_GAS_LIMIT or DAPP_BLOCK_GAS_LIMIT

The block.gaslimit value during EVM execution.

memory_limit
  • Type: integer
  • Default: 33554432
  • Environment: FOUNDRY_MEMORY_LIMIT or DAPP_MEMORY_LIMIT

The memory limit of the EVM in bytes.

names
  • Type: boolean
  • Default: false
  • Environment: FOUNDRY_NAMES or DAPP_NAMES

Print compiled contract names.

sizes
  • Type: boolean
  • Default: false
  • Environment: FOUNDRY_SIZES or DAPP_SIZES

Print compiled contract sizes.

rpc_endpoints
  • Type: table of RPC endpoints
  • Default: none
  • Environment: none

This section lives outside of profiles and defines a table of RPC endpoints, where the key specifies the RPC endpoints’s name and the value is the RPC endpoint itself.

The value can either be a valid RPC endpoint or a reference to an environment variable (wrapped with in ${}).

These RPC endpoints can be used in tests and Solidity scripts (see vm.rpc).

The following example defines an endpoint named optimism and an endpoint named mainnet that references an environment variable RPC_MAINNET:

[rpc_endpoints]
optimism = "https://optimism.alchemyapi.io/v2/..."
mainnet = "${RPC_MAINNET}"
prompt_timeout
  • Type: integer
  • Default: 120
  • Environment: FOUNDRY_PROMPT_TIMEOUT

The number of seconds to wait before vm.prompt reverts with a timeout.

Fuzz

Configuration values for [fuzz] section.

runs
  • Type: integer
  • Default: 256
  • Environment: FOUNDRY_FUZZ_RUNS or DAPP_FUZZ_RUNS

The amount of fuzz runs to perform for each fuzz test case. Higher values gives more confidence in results at the cost of testing speed.

max_test_rejects
  • Type: integer
  • Default: 65536
  • Environment: FOUNDRY_FUZZ_MAX_TEST_REJECTS

The maximum number of combined inputs that may be rejected before the test as a whole aborts. “Global” filters apply to the whole test case. If the test case is rejected, the whole thing is regenerated.

seed
  • Type: string (hexadecimal)
  • Default: none
  • Environment: FOUNDRY_FUZZ_SEED

Optional seed for the fuzzing RNG algorithm.

dictionary_weight
  • Type: integer (between 0 and 100)
  • Default: 40
  • Environment: FOUNDRY_FUZZ_DICTIONARY_WEIGHT

The weight of the dictionary. A higher dictionary weight will bias the fuzz inputs towards “interesting” values, e.g. boundary values like type(uint256).max or contract addresses from your environment.

include_storage
  • Type: boolean
  • Default: true
  • Environment: FOUNDRY_FUZZ_INCLUDE_STORAGE

The flag indicating whether to include values from storage.

include_push_bytes
  • Type: boolean
  • Default: true
  • Environment: FOUNDRY_FUZZ_INCLUDE_PUSH_BYTES

The flag indicating whether to include push bytes values.

show_logs
  • Type: boolean
  • Default: false
  • Environment: FOUNDRY_FUZZ_SHOW_LOGS

The flag indicates whether to display console logs in fuzz tests or not. Note that in order to enable displaying console logs, you’ll need to set show_logs = true and then use forge test -vv or set verbosity >= 2.

no_zksync_reserved_addresses
  • Type: boolean
  • Default: false

Avoid generating reserved ZKsync addresses within the fuzzer. This avoids having to use vm.assume(addr > 65535) during fuzzing that can ignore too many inputs causing max_test_rejects to trigger.

Invariant

Configuration values for [invariant] section.

ℹ️ Note

Configuration for [invariant] section has the fallback logic for common config entries (runs, seed, dictionary_weight etc).

  • If the entries are not set in either section, then the defaults will be used.
  • If the entries are set in the [fuzz] section, but are not set in the [invariant] section, these values will automatically be set to the values specified in the [fuzz] section.
  • For any profile other than default:
    • If at least one entry is set in the [invariant] (same as [profile.default.invariant]) section, then the values from [invariant] section will be used, including defaults.
    • If no entry is set in the [invariant] section, but there are entries in the [fuzz] (same as [profile.default.fuzz]) section, then the values from the [fuzz] section will be used.
    • If it’s none of the cases described above, then the defaults will be used.
runs
  • Type: integer
  • Default: 256
  • Environment: FOUNDRY_INVARIANT_RUNS

The number of runs that must execute for each invariant test group. See also fuzz.runs

depth
  • Type: integer
  • Default: 500
  • Environment: FOUNDRY_INVARIANT_DEPTH

The number of calls executed to attempt to break invariants in one run.

fail_on_revert
  • Type: boolean
  • Default: false
  • Environment: FOUNDRY_INVARIANT_FAIL_ON_REVERT

Fails the invariant fuzzing if a revert occurs.

call_override
  • Type: boolean
  • Default: false
  • Environment: FOUNDRY_INVARIANT_CALL_OVERRIDE

Overrides unsafe external calls when running invariant tests, useful for e.g. performing reentrancy checks.

dictionary_weight
  • Type: integer (between 0 and 100)
  • Default: 80
  • Environment: FOUNDRY_INVARIANT_DICTIONARY_WEIGHT

The weight of the dictionary. See also fuzz.dictionary_weight

include_storage
  • Type: boolean
  • Default: true
  • Environment: FOUNDRY_FUZZ_INCLUDE_STORAGE

The flag indicating whether to include values from storage. See also fuzz.include_storage

include_push_bytes
  • Type: boolean
  • Default: true
  • Environment: FOUNDRY_FUZZ_INCLUDE_PUSH_BYTES

The flag indicating whether to include push bytes values. See also fuzz.include_push_bytes

shrink_run_limit
  • Type: integer
  • Default: 5000
  • Environment: FOUNDRY_INVARIANT_SHRINK_RUN_LIMIT

The maximum number of attempts to shrink a failed the sequence. Shrink process is disabled if set to 0.

no_zksync_reserved_addresses
  • Type: boolean
  • Default: false

Avoid generating reserved ZKsync addresses within the invariant test. This avoids having to use vm.assume(addr > 65535) during invariant testing that can ignore too many inputs causing max_test_rejects to trigger.