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
To get started with Foundry-ZKsync, install Foundry-ZKsync and set up your first project.
This section will give you an overview of how to create and work with existing projects.
This section will give you an the necessary information to write and run tests for zkEVM.
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
andcast
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).
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" }
Add a Dependency Stored at a Specific Link
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
Just add a new project
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 projectmy-project
at version1.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 doimport '@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 withtest
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:
- All
persisted_accounts
storages are migrated to ZKsync storage. - Any EVM bytecode deployed under the migrated account is replaced by its
zksolc
variant. - Solidity globals such as
block.number
,address.balance
on the test level (which executes in EVM context) return ZKsync values. - 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 forzksolc
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 havingvm.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
andCALL
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 tovm
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
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:
-
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
-
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
-
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
orCALL
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 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.
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
Command | Status | Description |
---|---|---|
forge bind | ✅ Supported | Generates type-safe bindings for Solidity contracts, which can be used in other programming languages like Go. |
forge bind-json | ✅ Supported | Similar to forge bind, but generates bindings directly from JSON ABI files. |
forge build | ✅ Supported | Compiles Solidity contracts and generates build artifacts, such as ABI and bytecode files. |
forge clone | ✅ Supported | Clones an existing project from a Git repository, setting up a new Foundry project. |
forge completions | ✅ Supported | Generates shell completion scripts for forge, enhancing command-line usability. |
forge config | ✅ Supported | Displays or modifies the configuration settings for a Foundry project. |
forge create | ✅ Supported | Deploys a Solidity contract to a blockchain network, handling the transaction and deployment process. |
forge doc | ✅ Supported | Generates documentation for Solidity contracts, extracting comments and annotations into a readable format. |
forge flatten | ✅ Supported | Flattens a Solidity contract and its dependencies into a single file, useful for verification or analysis. |
forge coverage | ❌ Not Supported | Runs tests and generates a code coverage report, showing how much of the code is covered by tests. |
forge debug | ❌ Not Supported | Debugs a transaction on a local fork or a live network, allowing you to step through the execution. |
forge cache clean | ✅ Supported | Clears the local cache, removing stored build artifacts and other cached data. |
forge cache ls | ✅ Supported | Lists the contents of the local cache, including build artifacts and other data. |
forge clean | ❌ Not Supported | Removes build artifacts and resets the project’s build state. |
forge eip712 | ✅ Supported | Generates EIP-712 typed data structures for Solidity contracts, used for off-chain signing and verification. |
forge fmt | ✅ Supported | Formats Solidity source code according to a standard style guide, ensuring consistency. |
forge geiger | ✅ Supported | Analyzes a Solidity project for unsafe or potentially insecure code patterns, helping to improve security. |
forge generate | ✅ Supported | Automatically generates Solidity code or tests based on specified templates or patterns. |
forge generate test | ✅ Supported | Generates boilerplate test files for Solidity contracts, speeding up the testing process. |
forge generate-fig-spec | ✅ Supported | Generates a Fig spec for Forge, which can be used to create command-line autocomplete functionality. |
forge init | ✅ Supported | Initializes a new Foundry project, creating the necessary directories and configuration files. |
forge inspect | ❌ Not Supported | Inspects the details of a Solidity contract, such as ABI, bytecode, and other metadata. |
forge install | ✅ Supported | Installs dependencies from the Foundry package manager, adding them to the project. |
forge remappings | ✅ Supported | Manages remappings for Solidity imports, allowing for custom paths or package names. |
forge remove | ✅ Supported | Removes a dependency from the project, cleaning up any related files or configuration. |
forge script | ✅ Supported | Executes Solidity scripts, which can be used for tasks like deploying contracts or interacting with the blockchain. |
forge selectors | ✅ Supported | Extracts and manages function selectors from Solidity contracts, used for interacting with contracts. |
forge selectors collision | ✅ Supported | Detects and reports any selector collisions in Solidity contracts, preventing potential conflicts. |
forge selectors upload | ✅ Supported | Uploads function selectors to a specified registry, making them available for use in other projects. |
forge selectors list | ✅ Supported | Lists all function selectors in a Solidity contract, providing an overview of its interface. |
forge snapshot | ✅ Supported | Creates a snapshot of the current state of tests, which can be used to check for regressions. |
forge soldeer install | ✅ Supported | Installs a specific version of Soldeer, ensuring compatibility with the project. |
forge soldeer update | ✅ Supported | Updates the Soldeer installation to the latest version, applying any necessary patches or improvements. |
forge soldeer login | ✅ Supported | Logs into the Soldeer service, providing authentication for managing dependencies and projects. |
forge soldeer push | ✅ Supported | Pushes changes to a Soldeer project, syncing them with the remote repository or service. |
forge soldeer version-dry-run | ✅ Supported | Tests a version update of Soldeer without actually applying the changes, useful for checking compatibility. |
forge test | ✅ Supported | Runs unit tests for Solidity contracts, with options for gas reporting, fuzzing, and more. |
forge tree | ✅ Supported | Displays the dependency tree of the project, showing how contracts and libraries are interconnected. |
forge update | ✅ Supported | Updates the project’s dependencies to their latest versions, ensuring everything is up-to-date. |
forge verify-bytecode | ❌ Not Supported | Verifies that a deployed contract’s bytecode matches the expected source code, ensuring it hasn’t been tampered with. |
forge verify-check | ❌ Not Supported | Checks the verification status of a contract on a blockchain explorer like Etherscan, confirming that it has been successfully verified. |
forge verify-contract | ✅ Supported | Verifies a deployed contract on Etherscan, ensuring it matches the source code. |
cast 4byte | ✅ Supported | Fetches function signatures from the 4byte.directory by their selector. |
cast 4byte-decode | ✅ Supported | Decodes a given 4-byte selector into its associated function signature. |
cast 4byte-event | ✅ Supported | Fetches event signatures from the 4byte.directory by their selector. |
cast abi-decode | ✅ Supported | Decodes ABI-encoded data into a human-readable format. |
cast abi-encode | ✅ Supported | Encodes data into ABI format for function calls and transactions. |
cast access-list | ❌ Not Supported | Generates an access list for a transaction, which can be used to optimize gas usage. |
cast address-zero | ✅ Supported | Outputs the zero address (0x0000000000000000000000000000000000000000). |
cast admin | ✅ Supported | Returns the admin of a specified proxy contract. |
cast age | ✅ Supported | Calculates the age of a block in seconds. |
cast balance | ✅ Supported | Retrieves the balance of an address in wei or ether. |
cast base-fee | ✅ Supported | Fetches the base fee of the latest block, useful for estimating gas costs. |
cast bind (DEPRECATED) | ✅ Supported | Generates Go bindings for Solidity contracts, similar to forge bind. |
cast block | ✅ Supported | Retrieves detailed information about a specific block on the blockchain. |
cast block-number | ✅ Supported | Returns the current block number of the Ethereum blockchain. |
cast call | ✅ Supported | Executes a read-only (constant) call to a smart contract. |
cast call –create | ❌ Not Supported | Calls a contract and creates a new contract in the same transaction. |
cast calldata | ✅ Supported | Encodes function call data for a contract, which can be used in transactions. |
cast calldata-decode | ✅ Supported | Decodes encoded calldata back into its original arguments. |
cast chain | ❌ Not Supported | Displays information about the current Ethereum chain, including its name and ID. |
cast chain-id | ✅ Supported | Returns the chain ID of the Ethereum network, which is used for transaction signing. |
cast client | ✅ Supported | Fetches information about the connected Ethereum client, such as its version. |
cast code | ✅ Supported | Retrieves the bytecode of a contract deployed at a specific address. |
cast codesize | ✅ Supported | Returns the size of the bytecode at a specific address, in bytes. |
cast completions | ✅ Supported | Generates shell completions for cast, improving command-line usability. |
cast compute-address | ✅ Supported | Computes the Ethereum address for a contract deployed by a specific account. |
cast concat-hex | ✅ Supported | Concatenates multiple hexadecimal values into a single hex string. |
cast create2 | ✅ Supported | Computes the address of a contract deployed using the CREATE2 opcode. |
cast decode-eof | ✅ Supported | Decodes Ethereum Object Format (EOF) bytecode, used in Ethereum contracts. |
cast decode-transaction | ✅ Supported | Decodes the data and parameters of a raw transaction. |
cast disassemble | ❌ Not Supported | Disassembles contract bytecode into readable EVM assembly instructions. |
cast estimate | ❌ Not Supported | Estimates the gas cost of executing a transaction on the blockchain. |
cast estimate –create | ❌ Not Supported | Estimates the gas cost for deploying a contract with a creation transaction. |
cast etherscan-source | ✅ Supported | Fetches and displays the verified source code of a contract from Etherscan. |
cast find-block | ✅ Supported | Finds a block based on a given timestamp, returning the block number. |
cast format-bytes32-string | ✅ Supported | Converts a string into a bytes32 format for Solidity. |
cast from-bin | ✅ Supported | Decodes binary-encoded data into a human-readable format. |
cast from-fixed-point | ✅ Supported | Converts a fixed-point number into a human-readable string. |
cast from-rlp | ✅ Supported | Decodes RLP-encoded data, commonly used in Ethereum transactions. |
cast from-utf8 | ✅ Supported | Converts a UTF-8 string to a hex-encoded representation. |
cast from-wei | ✅ Supported | Converts a value from wei (the smallest unit of ether) to ether. |
cast gas-price | ✅ Supported | Fetches the current gas price on the Ethereum network. |
cast generate-fig-spec | ✅ Supported | Generates a Fig spec for Cast, which can be used for command-line autocomplete functionality. |
cast hash-message (DEPRECATED) | ✅ Supported | Hashes a message using Ethereum’s eth_sign method, preparing it for signing. |
cast hash-zero | ✅ Supported | Returns the hash of an empty string (0x000…000) using Keccak-256. |
cast implementation | ✅ Supported | Returns the implementation address of a specified proxy contract. |
cast index | ❌ Not Supported | Fetches the indexed logs of an event from the blockchain, useful for querying historical data. |
cast index-erc7201 | ✅ Supported | Fetches the logs of an ERC-7201 compliant event from the blockchain |
cast interface | ❌ Not Supported | Generates a Solidity interface from a deployed contract’s ABI. |
cast keccak | ✅ Supported | Computes the Keccak-256 hash of the provided input data. |
cast logs | ✅ Supported | Fetches logs and events from the blockchain, based on specified filters. |
cast lookup-address | ✅ Supported | Fetches the ENS name associated with a given Ethereum address, if any. |
cast max-int | ✅ Supported | Outputs the maximum value for a signed 256-bit integer. |
cast max-uint | ✅ Supported | Outputs the maximum value for an unsigned 256-bit integer. |
cast min-int | ✅ Supported | Outputs the minimum value for a signed 256-bit integer. |
cast mktx | ✅ Supported | Creates a transaction object without sending it, useful for offline signing. |
cast mktx –create | ❌ Not Supported | Creates a transaction that deploys a contract, without sending it. |
cast namehash | ✅ Supported | Computes the ENS namehash for a given domain name. |
cast nonce | ✅ Supported | Retrieves the nonce of an Ethereum address, useful for determining transaction order. |
cast parse-bytes32-address | ✅ Supported | Parses a bytes32 value into an Ethereum address. |
cast parse-bytes32-string | ✅ Supported | Parses a bytes32 value into a human-readable string. |
cast pretty-calldata | ✅ Supported | Formats calldata in a human-readable manner. |
cast proof | ❌ Not Supported | Retrieves and displays a Merkle proof for a specific storage slot or account. |
cast publish | ✅ Supported | Publishes a smart contract’s ABI to Etherscan. |
cast receipt | ✅ Supported | Fetches and displays the receipt of a transaction, including gas used and status. |
cast resolve-name | ✅ Supported | Resolves an ENS name to its associated Ethereum address. |
cast rpc | ✅ Supported | Sends a raw JSON-RPC request to an Ethereum node, allowing low-level interaction. |
cast run | ❌ Not Supported | Runs a script file, such as a .js or .ts file, with access to Cast functions. |
cast selectors | ❌ Not Supported | Fetches the function selectors for a given contract or ABI. |
cast send | ✅ Supported | Sends a transaction to the blockchain, including smart contract interactions. |
cast send –create | ❌ Not Supported | Sends a transaction that creates a new contract on the blockchain. |
cast shl | ✅ Supported | Performs a bitwise left shift on the provided input. |
cast shr | ✅ Supported | Performs a bitwise right shift on the provided input. |
cast sig | ✅ Supported | Outputs the Keccak-256 hash of a function signature. |
cast sig-event | ✅ Supported | Outputs the Keccak-256 hash of an event signature. |
cast storage | ✅ Supported | Fetches and displays the raw storage value of a contract at a specific slot. |
cast to-ascii | ✅ Supported | Converts a hexadecimal string to an ASCII string. |
cast to-base | ✅ Supported | Converts a number to a different base (e.g., from decimal to hexadecimal). |
cast to-bytes32 | ✅ Supported | Converts input data to a bytes32 format. |
cast to-check-sum-address | ✅ Supported | Converts an Ethereum address to a checksummed format, which includes capital letters for error detection. |
cast to-dec | ✅ Supported | Converts input data to a decimal number. |
cast to-fixed-point | ✅ Supported | Converts input data to a fixed-point number representation. |
cast to-hex | ✅ Supported | Converts input data to a hexadecimal format. |
cast to-hexdata | ✅ Supported | Converts input data to hex-encoded binary data. |
cast to-int256 | ✅ Supported | Converts input data to a signed 256-bit integer. |
cast to-rlp | ✅ Supported | Encodes input data in Recursive Length Prefix (RLP) format. |
cast to-uint256 | ✅ Supported | Converts input data to an unsigned 256-bit integer. |
cast to-unit | ✅ Supported | Converts ether or wei into different units, like gwei or finney. |
cast to-utf8 | ✅ Supported | Converts a hexadecimal string to a UTF-8 encoded string. |
cast to-wei | ✅ Supported | Converts ether or other units into wei, the smallest unit of ether. |
cast tx | ✅ Supported | Fetches and displays details of a specific Ethereum transaction. |
cast upload-signature | ✅ Supported | Uploads a function or event signature to the 4byte.directory. |
cast wallet | ✅ Supported | A suite of wallet-related commands, allowing you to manage Ethereum wallets, create new ones, sign transactions, and more. |
cast wallet new | ✅ Supported | Generates a new Ethereum wallet with a private key and address. |
cast wallet new-mnemonic | ✅ Supported | Creates a new wallet using a mnemonic phrase, which can be used to recover the wallet later. |
cast wallet vanity | ✅ Supported | Generates a new wallet with a custom, vanity address (e.g., one that starts with specific characters). |
cast wallet address | ✅ Supported | Outputs the Ethereum address associated with a given private key. |
cast wallet sign | ✅ Supported | Signs a message or transaction using the private key of a specified wallet. |
cast wallet sign-auth (DEPRECATED?) | ✅ Supported | Signs an authorization message with a private key, often used in authentication workflows. |
cast wallet verify | ✅ Supported | Verifies a signed message, confirming that it was signed by the holder of the private key associated with a specific address. |
cast wallet import | ✅ Supported | Imports an Ethereum wallet using a private key or mnemonic phrase. |
cast wallet list | ✅ Supported | Lists all wallets stored in a specific keystore. |
cast wallet private-key | ✅ Supported | Outputs the private key associated with a given wallet, provided proper authentication. |
cast wallet decrypt-keystore | ✅ Supported | Decrypts a keystore file to retrieve the private key, requiring the correct password. |
anvil | ✅ Supported | A local Ethereum node implementation, similar to Ganache, that can be used for testing and development. |
anvil completions | ✅ Supported | Generates shell completions for anvil, useful for auto-completing commands in the terminal. |
anvil generate-fig-spec | ✅ Supported | Generates a Fig autocomplete spec for anvil, providing interactive command suggestions. |
chisel | ✅ Supported | A tool used to interact with and modify smart contracts, providing operations like loading, listing, and clearing caches of tools. |
chisel list | ✅ Supported | Lists all available chisel tools or operations that can be applied to smart contracts. |
chisel load | ✅ Supported | Loads a specific chisel tool or operation, making it ready for use on a smart contract. |
chisel view | ✅ Supported | Displays the details or configuration of a loaded chisel tool or operation. |
chisel clear-cache | ✅ Supported | Clears 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 withtest
are run as a test case.
function test_NumberIs42() public {
assertEq(testNumber, 42);
}
testFail
: The inverse of thetest
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
orpublic
visibility. Functions declared asinternal
orprivate
won’t be picked up by Forge, even if they are prefixed withtest
.
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 appliedbytes[] 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 interfaceconsole.sol
andconsole2.sol
: Hardhat-style logging functionalityScript.sol
: Basic utilities for Solidity scriptingTest.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, thePUSH0
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 theINVALID
opcode instead, of value0xFE
. 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
orCALL
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:
- Forking Mode — use a single fork for all your tests via the
forge test --fork-url
flag - Forking Cheatcodes — create, select, and manage multiple forks directly in Solidity test code via forking cheatcodes
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:
block_number
chain_id
gas_limit
gas_price
block_base_fee_per_gas
block_coinbase
block_timestamp
block_difficulty
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 parameteramount
of typeuint32
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 namedowner
of typeaddress
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:
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 givenrun
. Invariants are asserted after each function call is made. If a function call reverts, thedepth
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 ifinvariant_A()
andinvariant_B()
functions are defined theninvariant_B()
won’t be asserted against EVM state ofinvariant_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:
Type | Explanation | Example |
---|---|---|
Direct assertions | Query a protocol smart contract and assert values are as expected. |
|
Ghost variable assertions | Query a protocol smart contract and compare it against a value that has been persisted in the test environment (ghost variable). |
|
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. |
|
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:
Function | Description |
---|---|
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 FuzzSelector s 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 FuzzArtifactSelector s 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 FuzzSelector s 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:
- Contracts that are manually added to the
targetContracts
array are added to the set of target contracts. - 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). - Contracts that are deployed in the
setUp
can be removed from the target contracts if they are added to theexcludeContracts
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.
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.
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
.
- Differential tests for Discrete GDAs
- Differential tests for Continuous GDAs
- Reference Python implementation
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
orCALL
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
orCALL
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
orCALL
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
- CLI Reference
- forge Commands
- cast Commands
- anvil Reference
- chisel Reference
- Config Reference
- Cheatcodes Reference
- Forge Standard Library Reference
- ds-test Reference
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
orDAPP_SRC
The path to the contract sources relative to the root of the project.
test
- Type: string
- Default: test
- Environment:
FOUNDRY_TEST
orDAPP_TEST
The path to the test contract sources relative to the root of the project.
script
- Type: string
- Default: script
- Environment:
FOUNDRY_SCRIPT
orDAPP_SCRIPT
The path to the script contract sources relative to the root of the project.
out
- Type: string
- Default: out
- Environment:
FOUNDRY_OUT
orDAPP_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
orDAPP_LIBS
An array of paths that contain libraries, relative to the root of the project.
cache
- Type: boolean
- Default: true
- Environment:
FOUNDRY_CACHE
orDAPP_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
orDAPP_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
orDAPP_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
orDAPP_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
orDAPP_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
orDAPP_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
orDAPP_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
orDAPP_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
orDAPP_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
orDAPP_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
orDAPP_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
ORDAPP_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
orDAPP_IGNORED_ERROR_CODES
An array of Solidity compiler error codes to ignore during build, such as warnings.
Valid values are:
license
: 1878code-size
: 5574func-mutability
: 2018unused-var
: 2072unused-param
: 5667unused-return
: 9302virtual-interfaces
: 5815missing-receive-ether
: 3628shadowing
: 2519same-varname
: 8760unnamed-return
: 6321unreachable
: 5740pragma-solidity
: 3420constructor-visibility
: 2462init-code-size
: 3860transient-storage
: 2394too-many-warnings
: 4591
deny_warnings
- Type: boolean
- Default: false
- Environment:
FOUNDRY_DENY_WARNINGS
orDAPP_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
orDAPP_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
orDAPP_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 ametadata.json
file in the artifacts directoryir
: Written as a.ir
file in the artifacts directoryirOptimized
: Written as a.iropt
file in the artifacts directoryewasm
: Written as a.ewasm
file in the artifacts directoryevm.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
orDAPP_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
orDAPP_SPARSE_MODE
Enables sparse mode for builds.
Optimizer
Configuration related to the Solidity optimizer.
optimizer
- Type: boolean
- Default: true
- Environment:
FOUNDRY_OPTIMIZER
orDAPP_OPTIMIZER
Whether or not to enable the Solidity optimizer.
optimizer_runs
- Type: integer
- Default: 200
- Environment:
FOUNDRY_OPTIMIZER_RUNS
orDAPP_OPTIMIZER_RUNS
The amount of optimizer runs to perform.
via_ir
- Type: boolean
- Default: false
- Environment:
FOUNDRY_VIA_IR
orDAPP_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 legacyoptimizer
and leaveoptimizer_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 enginebmc
: The bounded model checker engineall
: 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 Invariantsreentrancy
: 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.18smtlib2
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
: Assertionsunderflow
: Arithmetic underflowoverflow
: Arithmetic overflowdivByZero
: Division by zeroconstantCondition
: Trivial conditions and unreachable codepopEmptyArray
: Popping an empty arrayoutOfBounds
: Out of bounds array/fixed bytes index accessdefault
: 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
orDAPP_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
orDAPP_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
orDAPP_SENDER
The value of msg.sender
in tests.
tx_origin
- Type: string (address)
- Default: 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38
- Environment:
FOUNDRY_TX_ORIGIN
orDAPP_TX_ORIGIN
The value of tx.origin
in tests.
initial_balance
- Type: string (hexadecimal)
- Default: 0xffffffffffffffffffffffff
- Environment:
FOUNDRY_INITIAL_BALANCE
orDAPP_INITIAL_BALANCE
The initial balance of the test contracts in wei, written in hexadecimal.
block_number
- Type: integer
- Default: 1
- Environment:
FOUNDRY_BLOCK_NUMBER
orDAPP_BLOCK_NUMBER
The value of block.number
in tests.
chain_id
- Type: integer
- Default: 31337
- Environment:
FOUNDRY_CHAIN_ID
orDAPP_CHAIN_ID
The value of the chainid
opcode in tests.
gas_limit
- Type: integer or string
- Default: 9223372036854775807
- Environment:
FOUNDRY_GAS_LIMIT
orDAPP_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
orDAPP_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
orDAPP_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
orDAPP_BLOCK_COINBASE
The value of block.coinbase
in tests.
block_timestamp
- Type: integer
- Default: 1
- Environment:
FOUNDRY_BLOCK_TIMESTAMP
orDAPP_BLOCK_TIMESTAMP
The value of block.timestamp
in tests.
block_difficulty
- Type: integer
- Default: 0
- Environment:
FOUNDRY_BLOCK_DIFFICULTY
orDAPP_BLOCK_DIFFICULTY
The value of block.difficulty
in tests.
gas_reports
- Type: array of strings (contract names)
- Default: [“*”]
- Environment:
FOUNDRY_GAS_REPORTS
orDAPP_GAS_REPORTS
The contracts to print gas reports for.
no_storage_caching
- Type: boolean
- Default: false
- Environment:
FOUNDRY_NO_STORAGE_CACHING
orDAPP_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
orDAPP_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
orDAPP_ETHERSCAN_API_KEY
The etherscan API key for RPC calls.
match-test
- Type: regex
- Default: none
- Environment:
FOUNDRY_MATCH_TEST
orDAPP_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
orDAPP_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
orDAPP_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
orDAPP_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
orDAPP_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
orDAPP_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
orDAPP_BLOCK_GAS_LIMIT
The block.gaslimit value during EVM execution.
memory_limit
- Type: integer
- Default: 33554432
- Environment:
FOUNDRY_MEMORY_LIMIT
orDAPP_MEMORY_LIMIT
The memory limit of the EVM in bytes.
names
- Type: boolean
- Default: false
- Environment:
FOUNDRY_NAMES
orDAPP_NAMES
Print compiled contract names.
sizes
- Type: boolean
- Default: false
- Environment:
FOUNDRY_SIZES
orDAPP_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
orDAPP_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.