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 can be downloaded from the GitHub releases page. For easier management, we recommend using Foundryup. Currently due to active development it’s recommended to use the latest nightly from the releases page.
Using Foundryup-zksync
Foundryup-zksync is the official installer for the Foundry-ZKsync toolchain. You can learn more about it here.
To install Foundryup-zksync, open your terminal and run the following command:
curl -L https://raw.githubusercontent.com/matter-labs/foundry-zksync/main/install-foundry-zksync | bash
This will install Foundryup-zksync. Simply follow the on-screen instructions, and the foundryup-zksync
command will become available in your CLI.
Running foundryup-zksync
automatically installs the latest nightly versions of the precompiled binaries, including forge
and cast
. Additionally, it fetches the most recent version of the precompiled binary anvil-zksync
from the anvil-zksync releases.
For additional options, such as installing a specific version or commit, run foundryup-zksync --help
.
ℹ️ Note
Currently only
forge
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’ll need the Rust compiler and Cargo, Rust’s package manager. The easiest way to install both is by using
rustup.rs
.
Foundry-ZKsync generally supports building only with the configured nightly Rust version.
The presence of rust-toolchain
file automatically downloads the correct nightly rust version when commands are run from the Foundry-ZKsync directory.
For Windows users, you’ll also need a recent version of Visual Studio, with the “Desktop Development With C++” workload installed.
Building
You can either use the different Foundryup-ZKsync flags:
foundryup-zksync --branch main
foundryup-zksync --path path/to/foundry-zksync
Alternatively, you can install via Cargo with the following command:
cargo install --git https://github.com/matter-labs/foundry-zksync --profile release --locked forge cast
Or, by manually building from a local copy of the Foundry-ZKsync repository:
# clone the repository
git clone https://github.com/matter-labs/foundry-zksync.git
cd foundry
# install Forge
cargo install --path ./crates/forge --profile release --force --locked
# install Cast
cargo install --path ./crates/cast --profile release --force --locked
CI Installation with GitHub Actions
The latest binaries for the appropriate architecture can be installed directly using the following GitHub Action:
steps:
- name: Install Foundry-ZKsync
uses: dutterbutter/foundry-zksync-toolchain@v1
For further details, visit the foundry-zksync-toolchain repository.
Using Foundry with Docker
ℹ️ Note
No prebuilt images are available for docker yet.
First Steps with Foundry-ZKsync
This section introduces the forge
command-line tool. We will walk through creating a new project, compiling it, and running tests.
To start a new project with Foundry-ZKsync, use the forge init
command:
$ forge init hello_foundry
Now, let’s explore the structure that forge
has generated for us:
$ cd hello_foundry
$ tree . -d -L 1
.
├── lib
├── script
├── src
└── test
4 directories
You can compile the project using forge build --zksync
:
$ forge build --zksync
Compiling 27 files with zksolc and solc 0.8.26
zksolc and solc 0.8.26 finished in 3.97s
Compiler run successful!
To run the tests, use the forge test --zksync
command:
$ forge test --zksync
Compiling 25 files with Solc 0.8.27
Solc 0.8.27 finished in 902.81ms
Compiler run successful!
No files changed, compilation skipped
Ran 2 tests for test/Counter.t.sol:CounterTest
[PASS] testFuzz_SetNumber(uint256) (runs: 256, μ: 31210, ~: 31288)
[PASS] test_Increment() (gas: 31303)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 5.75ms (5.36ms CPU time)
Ran 1 test suite in 10.34ms (5.75ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)
💡 Tip
You can always view detailed help for any command or subcommand by appending
--help
to it.
For visual learners, be sure to check out these beginner tutorials.
Creating a New Project
To start a new project with Foundry-ZKsync, use forge init
:
$ forge init hello_foundry
This creates a new directory hello_foundry
from the default template. This also initializes a new git
repository.
If you want to create a new project using a different template, you would pass the --template
flag, like so:
$ forge init --template https://github.com/foundry-rs/forge-template hello_template
For now, let’s check what the default template looks like:
$ cd hello_foundry
$ tree . -d -L 1
.
├── lib
├── script
├── src
└── test
4 directories
The default template comes with one dependency installed: Forge Standard Library. This is the preferred testing library used for Foundry projects. Additionally, the template also comes with an empty starter contract and a simple test.
Let’s build the project:
$ forge build --zksync
Compiling 27 files with zksolc and solc 0.8.26
zksolc and solc 0.8.26 finished in 3.97s
Compiler run successful!
And run the tests:
$ forge test --zksync
Compiling 25 files with Solc 0.8.27
Solc 0.8.27 finished in 902.81ms
Compiler run successful!
No files changed, compilation skipped
Ran 2 tests for test/Counter.t.sol:CounterTest
[PASS] testFuzz_SetNumber(uint256) (runs: 256, μ: 31210, ~: 31288)
[PASS] test_Increment() (gas: 31303)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 5.75ms (5.36ms CPU time)
Ran 1 test suite in 10.34ms (5.75ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)
You’ll notice that two new directories have popped up: out
, zkout
and cache
.
The out
directory contains your EVM contract artifact, such as the ABI, the zkout
directory contains the zkEVM contract artifacts, while the cache
is used by forge
to only recompile what is necessary.
Working on an Existing Project
Foundry makes developing with existing projects have no overhead.
For this example, we will use PaulRBerg’s
foundry-template
.
First, clone the project and run forge install
inside the project directory.
$ git clone https://github.com/PaulRBerg/foundry-template
$ cd foundry-template
$ forge install
$ bun install # install Solhint, Prettier, and other Node.js deps
We run forge install
to install the submodule dependencies that are in the project.
To build, use forge build
:
$ forge build --zksync
Compiling 28 files with zksolc and solc 0.8.25
zksolc and solc 0.8.25 finished in 6.15s
Compiler run successful!
And to test, use forge test
:
$ forge test --zksync
Compiling 25 files with Solc 0.8.25
Solc 0.8.25 finished in 925.41ms
Compiler run successful!
No files changed, compilation skipped
Ran 3 tests for test/Foo.t.sol:FooTest
[PASS] testFork_Example() (gas: 3779)
[PASS] testFuzz_Example(uint256) (runs: 1000, μ: 213166, ~: 213166)
[PASS] test_Example() (gas: 215916)
Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 6.51s (6.51s CPU time)
Ran 1 test suite in 6.52s (6.51s CPU time): 3 tests passed, 0 failed, 0 skipped (3 total tests)
Clone a Verified Contract on Chain
To clone an on-chain verified contract as a Forge project, use forge clone
, say WETH9 on Ethereum mainnet:
$ forge clone 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 WETH9
This creates a new directory WETH9
, configures it as a foundry project and clones all the source code of the contract into it. This also initializes a new git
repository.
Downloading the source code of 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 from Etherscan...
Initializing /home/zhan4987/WETH9...
Installing forge-std in /home/zhan4987/WETH9/lib/forge-std (url: Some("https://github.com/foundry-rs/forge-std"), tag: None)
Cloning into '/home/zhan4987/WETH9/lib/forge-std'...
remote: Enumerating objects: 2243, done.
remote: Counting objects: 100% (2238/2238), done.
remote: Compressing objects: 100% (778/778), done.
remote: Total 2243 (delta 1489), reused 2097 (delta 1391), pack-reused 5
Receiving objects: 100% (2243/2243), 649.07 KiB | 8.89 MiB/s, done.
Resolving deltas: 100% (1489/1489), done.
Installed forge-std v1.8.1
Initialized forge project
Collecting the creation information of 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 from Etherscan...
Waiting for 5 seconds to avoid rate limit...
[⠊] Compiling...
[⠒] Compiling 1 files with 0.4.19
[⠢] Solc 0.4.19 finished in 9.50ms
Compiler run successful!
The cloned Forge project comes with an additional .clone.meta
metadata file besides those ordinary files that a normal Forge project has.
Let’s see what the .clone.meta
file looks like:
{
"path": "src/Contract.sol",
"targetContract": "WETH9",
"address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"chainId": 1,
"creationTransaction": "0xb95343413e459a0f97461812111254163ae53467855c0d73e0f1e7c5b8442fa3",
"deployer": "0x4f26ffbe5f04ed43630fdc30a87638d53d0b0876",
"constructorArguments": "0x",
"storageLayout": {
"storage": [],
"types": {}
}
}
clone.meta
is a compact JSON data file that contains the information of the on-chain contract instance, e.g., contract address, constructor arguments, etc. More details of the metadata can be found in the reference.
Dependencies
Forge manages dependencies using git submodules by default, which means that it works with any GitHub repository that contains smart contracts.
Adding a dependency
To add a dependency, run forge install
:
$ forge install transmissions11/solmate
Installing solmate in /tmp/tmp.ECnnuozGYk/deps/lib/solmate (url: Some("https://github.com/transmissions11/solmate"), tag: None)
Installed solmate
This pulls the solmate
library, stages the .gitmodules
file in git and makes a commit with the message “Installed solmate”.
If we now check the lib
folder:
$ tree lib -L 1
lib
├── forge-std
├── solmate
└── weird-erc20
3 directories, 0 files
We can see that Forge installed solmate
!
By default, forge install
installs the latest master branch version. If you want to install a specific tag or commit, you can do it like so:
$ forge install transmissions11/solmate@v7
Remapping dependencies
Forge can remap dependencies to make them easier to import. Forge will automatically try to deduce some remappings for you:
$ forge remappings
ds-test/=lib/solmate/lib/ds-test/src/
forge-std/=lib/forge-std/src/
solmate/=lib/solmate/src/
weird-erc20/=lib/weird-erc20/src/
These remappings mean:
- To import from
forge-std
we would write:import "forge-std/Contract.sol";
- To import from
ds-test
we would write:import "ds-test/Contract.sol";
- To import from
solmate
we would write:import "solmate/Contract.sol";
- To import from
weird-erc20
we would write:import "weird-erc20/Contract.sol";
You can customize these remappings by creating a remappings.txt
file in the root of your project.
Let’s create a remapping called solmate-utils
that points to the utils
folder in the solmate repository!
@solmate-utils/=lib/solmate/src/utils/
You can also set remappings in foundry.toml
.
remappings = [
"@solmate-utils/=lib/solmate/src/utils/",
]
Now we can import any of the contracts in src/utils
of the solmate repository like so:
import {LibString} from "@solmate-utils/LibString.sol";
Updating dependencies
You can update a specific dependency to the latest commit on the version you have specified using forge update <dep>
. For example, if we wanted to pull the latest commit from our previously installed master-version of solmate
, we would run:
$ forge update lib/solmate
Alternatively, you can do this for all dependencies at once by just running forge update
.
Removing dependencies
You can remove dependencies using forge remove <deps>...
, where <deps>
is either the full path to the dependency or just the name. For example, to remove solmate
both of these commands are equivalent:
$ forge remove solmate
# ... is equivalent to ...
$ forge remove lib/solmate
Hardhat compatibility
Forge also supports Hardhat-style projects where dependencies are npm packages (stored in node_modules
) and contracts are stored in contracts
as opposed to src
.
To enable Hardhat compatibility mode pass the --hh
flag.
Soldeer as a Package Manager
As explained here, Foundry has been using git submodules to handle dependencies up until now.
The need for a native package manager started to emerge as projects became more complex.
A new approach has been in the making, soldeer.xyz, which is a Solidity native dependency manager built in Rust and open sourced (check the repository https://github.com/mario-eth/soldeer).
Initialize a new project
If you’re using Soldeer for the first time in a new Foundry project, you can use the init
command to install a fresh instance of Soldeer, complete with the necessary configurations and the latest version of forge-std
.
forge soldeer init
Adding a Dependency
Add a Dependency Stored in the Central Repository
To add a dependency, you can visit soldeer.xyz and search for the dependency you want to add (e.g., openzeppelin 5.0.2).
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" }
Add a Dependency Stored in GIT
If you choose to use Git as a source for your dependencies — though we generally discourage this, since Git isn’t designed to be a dependency manager — you can provide the Git repository link as an additional argument. Soldeer will then automatically handle the installation using a Git subprocess. For example:
forge soldeer install forge-std~1.9.2 https://github.com/foundry-rs/forge-std.git
If you want to use a specific revision, branch, or tag, you can do so by appending the following arguments to the command: --rev/--tag/--branch
e.g.
forge soldeer install forge-std~1.9.2 https://github.com/foundry-rs/forge-std.git --rev 4695fac44b2934aaa6d7150e2eaf0256fdc566a7
Updating Dependencies
Because Soldeer specifies the dependencies in a config file (foundry or soldeer toml), sharing a dependency configuration within the team is much easier.
For example, having this Foundry config file in a git repository, one can pull the repository and then run forge soldeer update
. This command will automatically install all the dependencies specified under the [dependencies]
tag.
# Full reference https://github.com/foundry-rs/foundry/tree/master/crates/config
[profile.default]
auto_detect_solc = false
bytecode_hash = "none"
fuzz = { runs = 1_000 }
libs = ["dependencies"] # <= This is important to be added
gas_reports = ["*"]
[dependencies] # <= Dependencies will be added under this config
"@openzeppelin-contracts" = { version = "5.0.2" }
"@uniswap-universal-router" = { version = "1.6.0" }
"@prb-math" = { version = "4.0.2" }
forge-std = { version = "1.8.1" }
Removing Dependencies
You can use forge soldeer uninstall DEPENDENCY
.
Example: forge soldeer uninstall @openzeppelin-contracts
. This will action will remove:
- the config entry
- the
dependencies
artifacts - the
soldeer.lock
entry - the
remappings
entry (txt or config remapping)
Additionally you can manually remove a dependency by just removing the artifacts: dependency files, config entry, remappings entry.
Remappings
The remappings are now fully configurable, the config TOML file (foundry.toml) accepts a
[soldeer]
field with the following options
[soldeer]
# whether soldeer manages remappings
remappings_generate = true
# whether soldeer re-generates all remappings when installing, updating or uninstalling deps
remappings_regenerate = false
# whether to suffix the remapping with the version: `name-a.b.c`
remappings_version = true
# a prefix to add to the remappings ("@" would give `@name`)
remappings_prefix = ""
# where to store the remappings ("txt" for `remappings.txt` or "config" for `foundry.toml`)
# ignored when `soldeer.toml` is used as config (uses `remappings.txt`)
remappings_location = "txt"
Installing dependencies of dependencies aka sub-dependencies
Whenever you install a dependency, that dependency might have other dependencies that need to be installed as well. Currently, you can handle this by either specifying the recursive_deps
field as a configuration entry in the config file or by passing the --recursive-deps
argument when running the install or update command. This will ensure that all necessary sub-dependencies are automatically pulled in.
e.g.
[soldeer]
recursive_deps = true
Pushing a New Version to the Central Repository
Soldeer acts like npmjs/crates.io, encouraging all developers to publish their projects to the central repository.
To do that, you have to go to soldeer.xyz, create an account, verify it, then
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
Dry-run
In case you want to simulate what would happen if you push a version, you can use the --dry-run
flag. This will create a zip file that you can inspect before pushing it to the central repository.
forge soldeer push my-project~1.0.0 --dry-run
Login Data
By default, Soldeer saves the login token in the ~/.soldeer/.soldeer_login
file, which is used to push files to the central repository. If you prefer to save the token in a different location, you can set the environment variable SOLDEER_LOGIN_FILE
.
Warning ⚠️
- Once a project is created, it cannot be deleted.
- Once a version is pushed, it cannot be deleted.
- You cannot push the same version twice.
- The project name in the command that you run in the terminal must match the project name that you created on the Soldeer website.
- We encourage everyone to use version pinning when importing them into the contracts, this will help with securing your code by knowing exactly what version of a dependency you are using. Furthermore, it will help security researchers in their work.
- Make sure you delete this zip file before pushing the version if you run dry-run. e.g. instead of using
import '@openzeppelin-contracts/token/ERC20.sol'
you should 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
8 directories, 11 files
- You can configure Foundry’s behavior using
foundry.toml
. - Remappings are specified in
remappings.txt
. - The default directory for contracts is
src/
. - The default directory for tests is
test/
, where any contract with a function that starts 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
- Compilation Overview
- Limitations
- Additional Cheatcodes
- Gas Overview
- Paymaster Overview
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 }
Non inlinable libraries
Compiling contracts without linking non-inlinable libraries is currently not supported. Libraries need to be deployed before building contracts using them.
When building the contracts, the addresses need to be passed using the libraries
config which contains a list of CONTRACT_PATH
:ADDRESS
mappings.
on foundry.toml
:
libraries = [
"src/MyLibrary.sol:MyLibrary:0xfD88CeE74f7D78697775aBDAE53f9Da1559728E4"
]
as a cli
flag:
forge build --zksync --libraries src/MyLibrary.sol:MyLibrary:0xfD88CeE74f7D78697775aBDAE53f9Da1559728E4
For more information please refer to official docs.
Listing missing libraries
To scan missing non-inlinable libraries, you can build the project using the --zk-detect-missing-libraries
flag. This will give a list of the libraries that need to be deployed and their addresses provided via the libraries
option for the contracts to compile. Metadata about the libraries will be saved in .zksolc-libraries-cache/missing_library_dependencies.json
.
Broadcast Limitations
These limitations apply when using cast
to broadcast transactions.
No Batch Support
Batching is currently not supported on ZKsync networks, as such, any batched transactions may not be executed in order. This can often lead to failures as in the following case:
contract Calculator {
function add(uint8 a, uint8 b) return (uint8) {
return a+b;
}
}
contract FooScript is Script {
function run() public {
vm.startBroadcast();
Calculator calc = new Calculator(); // tx1
uint8 sum = calc.add(1, 2); // tx2
vm.assertEqual(3, sum);
vm.stopBroadcast();
}
}
forge script script/FooTest.s.sol:FooScript ... --zksync --rpc-url https://sepolia.era.zksync.dev --broadcast
Here the recorded transactions tx1
and tx2
would be batched as a single transaction with appropriate nonces. However, upon broadcasting to a ZKsync network, tx2
may be executed before tx1
which would cause a revert.
To circumvent this, the --slow
flag may be used to sequentially send the transactions to the rpc endpoint, which keeps them in-order.
forge script script/FooTest.s.sol:FooScript ... --zksync --rpc-url https://sepolia.era.zksync.dev --broadcast --slow
Emitted Events
zkEVM in addition to user events, emits its own system events, like Transfer
, Withdrawl
, ContractCreated
, etc. These events are not printed as part of traces, as currently it’s not trivial to match emitted events with zkEVM traces.
These system events can be observed via setting the RUST_LOG
env variable:
RUST_LOG=foundry_zksync_core::vm::inspect=info,era_test_node::formatter=info forge test --zksync
==== 2 events
EthToken System Contract
Topics:
0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
0x0000000000000000000000001804c8ab1f12e6bbf3894d4083f33e07309d1f38
0x0000000000000000000000000000000000000000000000000000000000008001
Data (Hex): 0x00000000000000000000000000000000000000000000000003dfd24000000000
...
Issues with expectEmit
This can often come as a surprise to users, who have the following test structure in place:
contract Number {
uint256 accesses;
function one() public returns (uint8) {
accesses++;
return 1;
}
function two() public returns (uint8) {
accesses++;
return 2;
}
function three() public returns (uint8) {
accesses++;
return 3;
}
}
contract Calculator {
event Added(uint8 indexed sum);
function add(uint8 a, uint8 b) public returns (uint8) {
uint8 sum = a + b;
emit Added(sum);
return sum;
}
}
contract FooTest is Test {
event Added(uint8 indexed sum);
function testFoo() public {
Number num = new Number();
// We emit the event we expect to see.
vm.expectEmit();
emit Added(num.three()); // num.three() will emit zkEVM events
Calculator calc = new Calculator();
calc.add(num.one(), num.two());
}
}
This test would currently fail as the non-static call to num.three()
when setting up vm.expectEmit()
.
If run with RUST_LOG
enabled as specified above, the following output will be observed:
┌──────────────────────────┐
│ VM EXECUTION RESULTS │
└──────────────────────────┘
Cycles Used: 6703
Computation Gas Used: 106816
Contracts Used: 26
════════════════════════════
=== Console Logs:
=== Calls:
Call(Normal) Account Code Storage 4de2e468 4227857424
Call(Normal) System context 02fa5779 4227853014
...
Call(Normal) Bootloader utilities ebe4a3d7 4227834933
...
Call(Normal) 0x1804c8ab1f12e6bbf3894d4083f33e07309d1f38 202bcce7 78705333
Call(Normal) Nonce Holder e1239cd8 77474754
...
Call(Normal) EthToken System Contract 9cc7f708 77468328
Call(Normal) Keccak 00000000 76257342
Call(Normal) Nonce Holder 6ee1dc20 78694182
Call(Normal) Keccak 00000000 77464044
Call(Normal) EthToken System Contract 9cc7f708 78692796
Call(Normal) Keccak 00000000 77462658
Call(Normal) 0x1804c8ab1f12e6bbf3894d4083f33e07309d1f38 e2f318e3 78691095
Call(Normal) Msg Value System Contract 0x 77460741
Call(Normal) EthToken System Contract 579952fc 76249719
...
Call(Normal) Event writer 00000000 75052593
Call(Mimic) bootloader 0x 76243293
Call(Normal) EthToken System Contract 9cc7f708 78682086
Call(Normal) Keccak 00000000 77452137
Call(Normal) EthToken System Contract 579952fc 78680889
...
Call(Normal) Event writer 00000000 77449239
Call(Normal) Known code storage e516761e 78656571
Call(Normal) Account Code Storage 4de2e468 78654114
Call(Normal) System context a851ae78 78653421
Call(Normal) 0x1804c8ab1f12e6bbf3894d4083f33e07309d1f38 df9c1589 78652161
Call(Normal) 0xf9e9ba9ed9b96ab918c74b21dd0f1d5f2ac38a30 45caa117 77422023
Call(Normal) System context a851ae78 4227757947
...
==== 3 events
EthToken System Contract
Topics:
0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
0x0000000000000000000000001804c8ab1f12e6bbf3894d4083f33e07309d1f38
0x0000000000000000000000000000000000000000000000000000000000008001
Data (Hex): 0x00000000000000000000000000000000000000000000000003dfd24000000000
EthToken System Contract
Topics:
0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
0x0000000000000000000000000000000000000000000000000000000000008001
0x0000000000000000000000001804c8ab1f12e6bbf3894d4083f33e07309d1f38
Data (Hex): 0x00000000000000000000000000000000000000000000000003dfd24000000000
EthToken System Contract
Topics:
0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
0x0000000000000000000000000000000000000000000000000000000000008001
0x0000000000000000000000001804c8ab1f12e6bbf3894d4083f33e07309d1f38
Data (String):
zk vm decoded result 0000000000000000000000000000000000000000000000000000000000000003
Here we observe that 3 events were emitted when we called num.three()
in zkEVM. These correspond to the Transfer(address indexed from, address indexed to, uint256 value)
event, which denotes change of L2 ETH. As a result the vm.expectEmit
will register the first event emitted, and try to match the next two events, which will fail and so will the test with:
[FAIL. Reason: log != expected log] testFoo() (gas: 35515)
Traces:
[35515] 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496::testFoo()
├─ [0] → new <unknown>@0xF9E9ba9Ed9B96AB918c74B21dD0f1D5f2ac38a30
│ └─ ← [Return] 32 bytes of code
├─ [0] VM::expectEmit()
│ └─ ← [Return]
├─ [0] 0xF9E9ba9Ed9B96AB918c74B21dD0f1D5f2ac38a30::three()
│ └─ ← [Return] 3
└─ ← [Revert] log != expected log
To avoid such a scenario, it’s recommended to explicitly emit the events for expectEmit
, with hard-coded values.
Trace Limitations
zkEVM traces are attached to the EVM traces that are printed with -vvvv
.
- The events emitted from within the zkEVM will not show on traces. See events in zkEVM.
- The system call traces from within the zkEVM’s bootloader are currently ignored in order to simplify the trace output.
- Executing each
CREATE
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;
/// Enables the use of a paymaster for the next transaction.
function zkUsePaymaster(address paymaster, bytes calldata paymaster_input) external pure;
}
Usage
Refer to the forge-zksync-std section on how to access these cheatcodes in your tests.
zkRegisterContract
Signature
function zkRegisterContract(
string calldata name,
bytes32 evmBytecodeHash,
bytes calldata evmDeployedBytecode,
bytes calldata evmBytecode,
bytes32 zkBytecodeHash,
bytes calldata zkDeployedBytecode
) external pure;
Description
Registers bytecodes for ZK-VM for transact/call and create instructions.
This is especially useful if there are certain contracts already deployed on-chain (EVM or ZKsync). Since we compile with both solc
and zksolc
as defined in the Dual Compilation section, if there’s an already existing EVM bytecode that must be translated into its zkEVM counterpart, we need to define it with this cheatcode.
Such an operation must be carried out separately where the source of the pre-deployed contract must be obtained and compiled with zksolc. The artifact json will contain the zkBytecodeHash
and zkDeployedBytecode
parameters. The process is similar for obtaining EVM parameters with solc
- evmBytecodeHash
, evmDeployedBytecode
, evmBytecode
.
The name
parameter must be unique if possible and not clash with locally existing contracts.
Examples
// LeetContract is pre-deployed on EVM on address(65536)
/// interface ILeetContract {
/// function leet() public {
/// // do something
/// }
/// }
vmExt.zkVm(true);
ILeetContract(address(65536)).leet(); // fails, as the contract was not found locally so not migrated to zkEVM
vmExt.zkRegisterContract("LeetContract", 0x111.., 0x222.., 0x333..., 0x444..., 0x555...); // register LeetContract for migration
vmExt.zkVm(true);
ILeetContract(address(65536)).leet(); // succeeds, as the contract was registered via cheatcode, so migrated to zkEVM
zkVm
Signature
function zkVm(bool enable) external pure;
Description
Enables/Disables ZKsync context for transact/call and create instructions within a test or script execution.
Switching VMs is a intensive process that translates the entire storage back-and-forth between EVM and zkEVM, and as such must be used sparingly in a test to switch between contexts.
See Execution Overview for further details.
See zkVmSkip for a one-off and much simpler operation.
Examples
/// contract LeetContract {
/// constructor(uint8 value) public {
/// // do something
/// }
/// }
vmExt.zkVm(true);
new LeetContract(1); // deployed in zkEVM
new LeetContract(2); // deployed in zkEVM
vmExt.zkVm(false);
new LeetContract(3); // deployed in EVM
new LeetContract(4); // deployed in EVM
zkVmSkip
Signature
function zkVmSkip() external pure;
Description
When running in zkEVM context, skips the next CREATE or CALL, executing it on the EVM instead.
All 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
}
}
zkUsePaymaster
Signature
function zkUsePaymaster(address paymaster, bytes calldata input) external pure;
Description
This cheatcode enables the use of a paymaster for the next transaction in the contract. The parameters specify the address of the paymaster and the pre-encoded data to be passed to the paymaster. The paymaster should be deployed before using this cheatcode.
Examples
import {TestExt} from "forge-zksync-std/TestExt.sol";
contract Test is TestExt {
function test_zkUsePaymaster() public {
MyPaymaster paymaster = new MyPaymaster();
// Encode paymaster input
bytes memory paymaster_encoded_input = abi.encodeWithSelector(
bytes4(keccak256("general(bytes)")),
bytes("0x")
);
vmExt.zkUsePaymaster(address(paymaster), paymaster_encoded_input);
}
}
The paymaster flow depends on the type of paymaster used. Here’s an example of the simplest usage of a ‘general’ paymaster in Foundry:
- Write a custom paymaster:
contract MyPaymaster is IPaymaster {
modifier onlyBootloader() {
require(msg.sender == BOOTLOADER_FORMAL_ADDRESS, "Only bootloader can call this method");
_;
}
constructor() payable {}
function validateAndPayForPaymasterTransaction(bytes32, bytes32, Transaction calldata _transaction)
external
payable
onlyBootloader
returns (bytes4 magic, bytes memory context)
{
// Always accept the transaction
magic = PAYMASTER_VALIDATION_SUCCESS_MAGIC;
// Pay for the transaction fee
uint256 requiredETH = _transaction.gasLimit * _transaction.maxFeePerGas;
(bool success,) = payable(BOOTLOADER_FORMAL_ADDRESS).call{value: requiredETH}("");
require(success, "Failed to transfer tx fee to the bootloader");
}
function postTransaction(
bytes calldata _context,
Transaction calldata _transaction,
bytes32,
bytes32,
ExecutionResult _txResult,
uint256 _maxRefundedGas
) external payable override onlyBootloader {}
receive() external payable {}
}
- Deploy the paymaster:
You can deploy the paymaster either in a test or script:
MyPaymaster paymaster = new MyPaymaster();
Or using the forge create
command:
forge create ./src/MyPaymaster.sol:MyPaymaster --rpc-url {RPC_URL} --private-key {PRIVATE_KEY} --zksync
- Use the cheatcode to set the paymaster for the next transaction:
vmExt.zkUsePaymaster(address(paymaster), abi.encodeWithSelector(
bytes4(keccak256("general(bytes)")),
bytes("0x")
));
For more examples, see the Foundry ZkSync Paymaster Tests.
Also, see the ZKsync Paymaster Documentation for more information.
zkUseFactoryDep
Signature
function zkUseFactoryDep(string calldata name) external pure;
Description
Marks a given contract as a factory dependency only for the next CREATE or CALL, unmarking it afterwards, similar to prank
.
This cheatcode is useful when deploying contracts through factories that do not directly depend on a given contract, as it allows explicitly marking this type of contract as a factory dependency, enabling the factory to deploy the contract. More information on factory dependencies can be found in the official ZKsync docs.
Examples
contract Deployer {
// Factory does not directly depend on TwoUserMultisig, so we need to mark it explicitly
// as a factory dependency to allow deployment through the factory
// Deploy the factory
Factory factory = new Factory(multisigBytecodeHash);
// Mark the bytecode as a factory dependency
vmExt.zkUseFactoryDep("TwoUserMultisig");
// Deploy the account using the factory
factory.deployAccount(multisigBytecodeHash);
}
Gas Overview
Gas reported back to the EVM
Foundry has an isolate
mode for the EVM where all CALL
/CREATE
operations at the root level of a test (i.e: with depth 1) will be intercepted and treated as independent transactions. This allows for accounting of the actual transaction gas and include for example the fixed 21000 gas cost charged to the user.
Running in zkEVM mode is anologous to running in isolate
mode but using the zkEVM instead. Every CALL
/CREATE
will be intercepted, a transaction representing the operation built, and finally a VM with that transaction in the bootloader’s heap will be spawned and run in order to simulate the execution of that transaction. The gas used reported back to the EVM, and hence the one seen on traces and gas-reports, is what would be charged to the user for submitting that transaction. That value differs from the computational cost of running the called contract code and includes:
- Intrinsic costs: Overhead charged on each transaction.
- Validation costs: Gas spent on transaction validation. May vary depending on the account making the transaction. See Account Abstraction docs.
- Execution costs: Gas spent on marking factory deps and executing the transaction.
- Pubdata costs: Gas spent on publishing pubdata, is influenced by the
gasPerPubdata
network value.
More info about ZKSync Era’s fee model can be found here.
Transaction/Network values that impact gas cost
The gas cost mentioned above is influenced by transaction and network values. The values are set when running the VM in the following way:
- Transaction Params:
max_fee_per_gas
: will be gas price of the root evm transaction (e.g: when running tests, the value of--gas-price
option is used) with a minimum value of0.26GWei
, which is the base fee used in some test environments/networks.gas_limit
: The sender remaining balance capped to a max of2^31 - 1
. Note that, no matter the gas limit, the vm caps how much gas a single transaction can use toMAX_GAS_PER_TRANSACTION
, currently set to80_000_000
.
- Network Params:
fair_l2_gas_price
: set to the minumum ofmax_fee_per_gas
and the base fee of the root evm transaction (e.g: when running tests, the value of the--base-fee
option).l1_gas_price
: set to the same asfair_l2_gas_price
, with a minimum value of1000
.
Deriving relevant transaction gas values
From the params above we can get all gas related values used in the transaction:
fair_pubdata_price
:l1_gas_price
*L1_GAS_PER_PUBDATA_BYTE
.baseFee
: Maximum value betweenfair_l2_gas_price
and(fair_pubdata_price / MAX_L2_GAS_PER_PUBDATA)
.gasPerPubdata
:fairPubdataPrice / baseFee
.
L1_GAS_PER_PUBDATA_BYTE
and MAX_L2_GAS_PER_PUBDATA
are system constants currently set to 17
and 50_000
respectively.
Paymaster Overview
Paymasters in the ZKsync ecosystem represent a groundbreaking approach to handling transaction fees. They are special accounts designed to subsidize transaction costs for other accounts, potentially making certain transactions free for end-users. This feature is particularly useful for dApp developers looking to improve their platform’s accessibility and user experience by covering transaction fees on behalf of their users.
How Paymasters Work
Paymasters are smart contracts that implement the IPaymaster
interface. They are designed to be used in conjunction with the ZKsync network’s transaction processing mechanism. When a transaction is sent from an account, the paymaster is specified in the transaction’s metadata. The paymaster is then responsible for paying the transaction fee and any other costs associated with the transaction.
How to interact with a Paymaster using Foundry
As of right now, the ways to interact with a paymaster contract using Foundry are using cast send
, forge create
or with the use of the zkUsePaymaster
cheatcode.
Using cast send
cast send
signs and publishes a transaction. The documentation can be found here.
To pair this with a paymaster contract, you need to specify the paymaster address and the encoded paymaster input in the command.
The flags for this are:
--zk-paymaster-address
The address where the paymaster contract is deployed.
--zk-paymaster-input
The encoded input for the paymaster contract. This depends on the paymaster contract implementation.
To encode the paymaster input, you can use the cast calldata
command which can be found here.
cast send 0xdb8bA5F5DfB1636361d2fE851d7D3ed93acfc487 "increment()" --rpc-url https://sepolia.era.zksync.dev --private-key <your-private-key> --zk-paymaster-address 0x3cB2b87D10Ac01736A65688F3e0Fb1b070B3eeA3 --zk-paymaster-input $(cast calldata "approvalBased(address,uint256,bytes)" 0x31c43ac5e6A0fe62954B9056441b0A214722516e 1000000000000000000 "0x")
Using forge create
forge create
is a command-line tool for deploying smart contracts using the Foundry framework. The documentation can be found here.
To deploy a paymaster contract using forge create
, you need to specify the paymaster contract address in the command.
The flags for this are:
--zk-paymaster-address
The address where the paymaster contract is deployed.
--zk-paymaster-input
The encoded input for the paymaster contract. This depends on the paymaster contract implementation.
To encode the paymaster input, you can use the cast calldata
command also which can be found here.
forge create Greeter.sol:Greeter --rpc-url "https://sepolia.era.zksync.dev" --private-key <your-private-key> --zksync --zk-paymaster-address 0x3cB2b87D10Ac01736A65688F3e0Fb1b070B3eeA3 --zk-paymaster-input $(cast calldata "approvalBased(address,uint256,bytes)" 0x31c43ac5e6A0fe62954B9056441b0A214722516e 1 "0x")
Also, see the ZKsync Paymaster Documentation for more information.
ZKsync specific examples
Using the zkUsePaymaster Cheatcode in General Flow Paymaster Contracts
This example covers the use of a general flow paymaster contract.
For this example we will use the GaslessPaymaster
contract from the paymaster example repository here.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@matterlabs/era-contracts/interfaces/IPaymaster.sol";
import "@matterlabs/era-contracts/interfaces/IPaymasterFlow.sol";
import "@matterlabs/era-contracts/Constants.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/// @author Matter Labs
/// @notice This contract does not include any validations other than using the paymaster general flow.
contract GaslessPaymaster is IPaymaster, Ownable {
constructor() Ownable(msg.sender) {}
modifier onlyBootloader() {
require(
msg.sender == BOOTLOADER_FORMAL_ADDRESS,
"Only bootloader can call this method"
);
// Continue execution if called from the bootloader.
_;
}
function validateAndPayForPaymasterTransaction(
bytes32,
bytes32,
Transaction calldata _transaction
)
external
payable
onlyBootloader
returns (bytes4 magic, bytes memory context)
{
// By default we consider the transaction as accepted.
magic = PAYMASTER_VALIDATION_SUCCESS_MAGIC;
require(
_transaction.paymasterInput.length >= 4,
"The standard paymaster input must be at least 4 bytes long"
);
bytes4 paymasterInputSelector = bytes4(
_transaction.paymasterInput[0:4]
);
if (paymasterInputSelector == IPaymasterFlow.general.selector) {
// Note, that while the minimal amount of ETH needed is tx.gasPrice * tx.gasLimit,
// neither paymaster nor account are allowed to access this context variable.
uint256 requiredETH = _transaction.gasLimit *
_transaction.maxFeePerGas;
// The bootloader never returns any data, so it can safely be ignored here.
(bool success, ) = payable(BOOTLOADER_FORMAL_ADDRESS).call{
value: requiredETH
}("");
require(
success,
"Failed to transfer tx fee to the Bootloader. Paymaster balance might not be enough."
);
} else {
revert("Unsupported paymaster flow in paymasterParams.");
}
}
function postTransaction(
bytes calldata _context,
Transaction calldata _transaction,
bytes32,
bytes32,
ExecutionResult _txResult,
uint256 _maxRefundedGas
) external payable override onlyBootloader {
// Refunds are not supported yet.
}
function withdraw(address payable _to) external onlyOwner {
// send paymaster funds to the owner
uint256 balance = address(this).balance;
(bool success, ) = _to.call{value: balance}("");
require(success, "Failed to withdraw funds from paymaster.");
}
receive() external payable {}
}
This contract is a general flow paymaster, which means that it can pay for any account. To be able to use this paymaster we need to first deploy it in the intended network.
For this example we will deploy it in the era-test-node and then use the zkUsePaymaster
cheatcode to pay for a transaction using a script.
pragma solidity ^0.8.0;
import {Script} from "forge-std/Script.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "../src/GeneralPaymaster.sol";
// We need to import the TestExt to use the zkUsePaymaster cheatcode
// as this is a ZKsync specific cheatcode
import "../src/Counter.sol";
import {TestExt} from "forge-zksync-std/TestExt.sol";
contract PaymasterUsageScript is Script, TestExt {
Counter public counter;
function run() public {
vm.startBroadcast();
GaslessPaymaster paymaster = new GaslessPaymaster();
// fund the paymaster
address(paymaster).call{value: 0.05 ether}("");
bytes memory paymaster_encoded_input = abi.encodeWithSelector(
bytes4(keccak256("general(bytes)")),
bytes("0x")
);
vmExt.zkUsePaymaster(
address(paymaster),
paymaster_encoded_input
);
counter = new Counter();
vm.stopBroadcast();
}
}
The key part of this script is encoding the paymaster call with the general(bytes)
selector and then using the zkUsePaymaster
cheatcode to pay for the transaction. This will vary depending on the paymaster contract that you are using.
// This is the encoding for the GaslessPaymaster
bytes memory paymaster_encoded_input = abi.encodeWithSelector(
bytes4(keccak256("general(bytes)")),
bytes("0x")
);
// Using the encoded parameters to call the zkUsePaymaster cheatcode
vmExt.zkUsePaymaster(
address(paymaster),
paymaster_encoded_input
);
After calling the zkUsePaymaster
cheatcode, the paymaster will pay for the following transaction.
Using the zkUsePaymaster Cheatcode in Approval-Based Paymaster Contracts
This example covers the use of an approval-based paymaster contract. The paymaster contract used is the testnet paymaster of zkSync documented here.
Steps Overview
- Setup and Initialization
- Create a custom ERC20 token contract.
- Deploy the ERC20 contract.
- Mint tokens to the address using the paymaster.
- Approval and Paymaster Preparation
- Create a paymaster contract.
- Encode the paymaster call with required parameters.
- Use the zkUsePaymaster cheatcode.
Step-by-Step
Let’s start by deploying the ERC20 contract and minting some tokens to the account that is using the paymaster. The idea behind the approval-based paymaster is that users transfer ERC20 tokens to the paymaster, which in turn pays for the transaction.
This is the code for the ERC20 contract:
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyERC20 is ERC20 {
constructor() ERC20("SPITTE", "SPT") {}
function mint(address to, uint256 amount) public {
_mint(to, amount);
}
}
Now, in the script we are going to run, we deploy the contract and mint some tokens to the account that is using the paymaster:
import {Script} from "forge-std/Script.sol";
import {console2} from "../lib/forge-std/src/console2.sol";
// We need to import the TestExt to use the zkUsePaymaster cheatcode
// as this is a ZKsync specific cheatcode
import {TestExt} from "forge-zksync-std/TestExt.sol";
contract PaymasterApprovalScript is Script, TestExt {
function run() external {
vm.startBroadcast();
MyERC20 erc20 = new MyERC20();
erc20.mint(address(tx.origin), 10);
vm.stopBroadcast();
}
}
Next, we prepare the encoded input for the paymaster:
// Encode the paymaster input
bytes memory paymaster_encoded_input = abi.encodeWithSelector(
bytes4(keccak256("approvalBased(address,uint256,bytes)")),
address(erc20), // ERC20 token address
uint256(1 ether), // Approval amount
bytes("0x") // Additional data (empty in this case)
);
Here, we are encoding the paymaster input with the approvalBased method signature and the required parameters. The second parameter is the address of the recently deployed ERC20 contract, the third parameter is the amount of tokens that the paymaster consumes from the user to pay for the transaction, and the last one is empty bytes in this case.
With the encoded input prepared, we can now use the zkUsePaymaster cheatcode to prepare the next call to be executed using the paymaster:
// Using zkUsePaymaster with the encoded input
vmExt.zkUsePaymaster(address(0x3cB2b87D10Ac01736A65688F3e0Fb1b070B3eeA3), paymaster_encoded_input);
Counter counter = new Counter();
counter.increment();
The counter.increment()
call will be executed using the paymaster that we set up in the encoded input.
Complete code
Below is the full code for the PaymasterTestScript demonstrating all steps:
pragma solidity ^0.8.0;
import {Script} from "forge-std/Script.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
// We need to import the TestExt to use the zkUsePaymaster cheatcode
// as this is a ZKsync specific cheatcode
import {TestExt} from "forge-zksync-std/TestExt.sol";
contract PaymasterTestScript is Script, TestExt {
function run() external {
vm.startBroadcast();
// Deploy the ERC20 contract
MyERC20 erc20 = new MyERC20();
// Mint some tokens
erc20.mint(address(tx.origin), 10);
// Encode the paymaster input
bytes memory paymaster_encoded_input = abi.encodeWithSelector(
bytes4(keccak256("approvalBased(address,uint256,bytes)")), // Function selector
address(erc20), // ERC20 address
uint256(1 ether), // The uint256 value
bytes("0x") // Empty bytes "0x"
);
// Create a new Counter contract
Counter counter = new Counter();
// Use the zkUsePaymaster cheatcode to prepare the next call to be executed using the paymaster
vm.zkUsePaymaster(address(0x3cB2b87D10Ac01736A65688F3e0Fb1b070B3eeA3), paymaster_encoded_input);
// Increment the counter
counter.increment();
vm.stopBroadcast();
}
}
contract Counter {
uint256 public count = 0;
function increment() public {
count++;
}
function getCount() public view returns (uint256) {
return count;
}
}
contract MyERC20 is ERC20 {
constructor() ERC20("SPITTE", "SPT") {}
function mint(address to, uint256 amount) public {
_mint(to, amount);
}
}
Using a Ledger device
This example covers the use of a Ledger device to interact with ZKsync network.
Steps Overview
- Enable blind signing in the Ethereum App.
- Deploy a custom Paymaster contract using
forge create --ledger
. - Fund the paymaster using
cast send --ledger
. - Deploy a
Counter
contract using the paymaster withforge script --ledger
Note that the steps are showcasing different usages of
--ledger
, and not necessarily what are best the best practices for executing a similar flow.
Step-by-Step
Enable blind signing in the Ethereum App
To proceed with the usage of the device, one must first enable “Blind Signing” in the Ethereum App. To do so, open the app on the device, navigate to “Settings”, and then to “Blind signing”. Toggle the option so that it is “Enabled”.
Custom paymaster deployment with forge create
We will be deploying the MyPaymaster
contract introduced in zkUsePaymaster
cheatcode:
forge create ./src/MyPaymaster.sol:MyPaymaster --rpc-url {RPC_URL} --ledger --zksync
Proceed on your device to sign the transaction.
Take note of the resulting deployment address displayed on your terminal, henceforth referred to as $PAYMASTER_ADDRESS
.
Paymaster funding with cast send
To ensure paymaster has sufficient funds to pay for our transactions, we’ll be funding it with some base token:
cast send $PAYMASTER_ADDRESS --value 0.1ether --rpc-url $RPC_URL --ledger
Again, proceed to sign the transaction on your device.
Contract deployment with forge script
We will be deploying a Counter
contract using the paymaster, from within a script using:
vmExt.zkUsePaymaster(vm.envAddress("PAYMASTER_ADDRESS"),
abi.encodeWithSelector(
bytes4(keccak256("general(bytes)")),
bytes("0x")
));
Counter cnt = new Counter();
We can now execute script using forge script --ledger
:
forge script ./scripts/Counter.s.sol --zksync --rpc-url $RPC_URL --broadcast --slow --ledger
In this case, proceed to sign on your device the single broadcastable transaction generated for the deployment. When broadcasting multiple transactions, a proportionate number of signatures is necessary.
Complete code
The following is the full script and source code referenced in this document:
import {Script} from "forge-std/Script.sol";
import {TestExt} from "forge-zksync-std/TestExt.sol";
contract Counter {
uint256 public count = 0;
function increment() public {
count++;
}
function getCount() public view returns (uint256) {
return count;
}
}
contract CounterScript is Script, TestExt {
function setUp() public {}
function run() public {
vm.startBroadcast();
vmExt.zkUsePaymaster(vm.envAddress("PAYMASTER_ADDRESS"),
abi.encodeWithSelector(
bytes4(keccak256("general(bytes)")),
bytes("0x")
));
Counter cnt = new Counter();
vm.stopBroadcast();
}
}
Deploying a multisig smart account
This example covers the configuration and deployment of a multisig smart account.
Steps Overview
- Specify the owners of the multisig account
- Execute the deployment script
Contracts
For this example, we will use 3 contracts:
AAFactory
- A factory contract that will be used to deploy the multisig account.TwoUserMultisig
- A multisig account with 2 owners.DeployMultisig
- A script to deploy the multisig account through the factory.
AAFactory
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@era-contracts/Constants.sol";
import "@era-contracts/libraries/SystemContractsCaller.sol";
contract AAFactory {
bytes32 public aaBytecodeHash;
constructor(bytes32 _aaBytecodeHash) {
aaBytecodeHash = _aaBytecodeHash;
}
function deployAccount(
bytes32 salt,
address owner1,
address owner2
) external returns (address accountAddress) {
(bool success, bytes memory returnData) = SystemContractsCaller
.systemCallWithReturndata(
uint32(gasleft()),
address(DEPLOYER_SYSTEM_CONTRACT),
uint128(0),
abi.encodeCall(
DEPLOYER_SYSTEM_CONTRACT.create2Account,
(
salt,
aaBytecodeHash,
abi.encode(owner1, owner2),
IContractDeployer.AccountAbstractionVersion.Version1
)
)
);
require(success, "Deployment failed");
(accountAddress) = abi.decode(returnData, (address));
}
}
TwoUserMultisig
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@era-contracts/interfaces/IAccount.sol";
import "@era-contracts/libraries/TransactionHelper.sol";
import "@era-contracts/Constants.sol";
import "@era-contracts/libraries/SystemContractsCaller.sol";
import "@openzeppelin/contracts/interfaces/IERC1271.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract TwoUserMultisig is IAccount, IERC1271 {
// to get transaction hash
using TransactionHelper for Transaction;
// state variables for account owners
address public owner1;
address public owner2;
bytes4 constant EIP1271_SUCCESS_RETURN_VALUE = 0x1626ba7e;
modifier onlyBootloader() {
require(
msg.sender == BOOTLOADER_FORMAL_ADDRESS,
"Only bootloader can call this function"
);
// Continue execution if called from the bootloader.
_;
}
constructor(address _owner1, address _owner2) {
owner1 = _owner1;
owner2 = _owner2;
}
function validateTransaction(
bytes32,
bytes32 _suggestedSignedHash,
Transaction calldata _transaction
) external payable override onlyBootloader returns (bytes4 magic) {
return _validateTransaction(_suggestedSignedHash, _transaction);
}
function _validateTransaction(
bytes32 _suggestedSignedHash,
Transaction calldata _transaction
) internal returns (bytes4 magic) {
// Incrementing the nonce of the account.
// Note, that reserved[0] by convention is currently equal to the nonce passed in the transaction
SystemContractsCaller.systemCallWithPropagatedRevert(
uint32(gasleft()),
address(NONCE_HOLDER_SYSTEM_CONTRACT),
0,
abi.encodeCall(
INonceHolder.incrementMinNonceIfEquals,
(_transaction.nonce)
)
);
bytes32 txHash;
// While the suggested signed hash is usually provided, it is generally
// not recommended to rely on it to be present, since in the future
// there may be tx types with no suggested signed hash.
if (_suggestedSignedHash == bytes32(0)) {
txHash = _transaction.encodeHash();
} else {
txHash = _suggestedSignedHash;
}
// The fact there is enough balance for the account
// should be checked explicitly to prevent user paying for fee for a
// transaction that wouldn't be included on Ethereum.
uint256 totalRequiredBalance = _transaction.totalRequiredBalance();
require(
totalRequiredBalance <= address(this).balance,
"Not enough balance for fee + value"
);
if (
isValidSignature(txHash, _transaction.signature) ==
EIP1271_SUCCESS_RETURN_VALUE
) {
magic = ACCOUNT_VALIDATION_SUCCESS_MAGIC;
} else {
magic = bytes4(0);
}
}
function executeTransaction(
bytes32,
bytes32,
Transaction calldata _transaction
) external payable override onlyBootloader {
_executeTransaction(_transaction);
}
function _executeTransaction(Transaction calldata _transaction) internal {
address to = address(uint160(_transaction.to));
uint128 value = Utils.safeCastToU128(_transaction.value);
bytes memory data = _transaction.data;
if (to == address(DEPLOYER_SYSTEM_CONTRACT)) {
uint32 gas = Utils.safeCastToU32(gasleft());
// Note, that the deployer contract can only be called
// with a "systemCall" flag.
SystemContractsCaller.systemCallWithPropagatedRevert(
gas,
to,
value,
data
);
} else {
bool success;
assembly {
success := call(
gas(),
to,
value,
add(data, 0x20),
mload(data),
0,
0
)
}
require(success);
}
}
function executeTransactionFromOutside(
Transaction calldata _transaction
) external payable {
bytes4 magic = _validateTransaction(bytes32(0), _transaction);
require(magic == ACCOUNT_VALIDATION_SUCCESS_MAGIC, "NOT VALIDATED");
_executeTransaction(_transaction);
}
function isValidSignature(
bytes32 _hash,
bytes memory _signature
) public view override returns (bytes4 magic) {
magic = EIP1271_SUCCESS_RETURN_VALUE;
if (_signature.length != 130) {
// Signature is invalid anyway, but we need to proceed with the signature verification as usual
// in order for the fee estimation to work correctly
_signature = new bytes(130);
// Making sure that the signatures look like a valid ECDSA signature and are not rejected rightaway
// while skipping the main verification process.
_signature[64] = bytes1(uint8(27));
_signature[129] = bytes1(uint8(27));
}
(
bytes memory signature1,
bytes memory signature2
) = extractECDSASignature(_signature);
if (
!checkValidECDSASignatureFormat(signature1) ||
!checkValidECDSASignatureFormat(signature2)
) {
magic = bytes4(0);
}
address recoveredAddr1 = ECDSA.recover(_hash, signature1);
address recoveredAddr2 = ECDSA.recover(_hash, signature2);
// Note, that we should abstain from using the require here in order to allow for fee estimation to work
if (recoveredAddr1 != owner1 || recoveredAddr2 != owner2) {
magic = bytes4(0);
}
}
// This function verifies that the ECDSA signature is both in correct format and non-malleable
function checkValidECDSASignatureFormat(
bytes memory _signature
) internal pure returns (bool) {
if (_signature.length != 65) {
return false;
}
uint8 v;
bytes32 r;
bytes32 s;
// Signature loading code
// we jump 32 (0x20) as the first slot of bytes contains the length
// we jump 65 (0x41) per signature
// for v we load 32 bytes ending with v (the first 31 come from s) then apply a mask
assembly {
r := mload(add(_signature, 0x20))
s := mload(add(_signature, 0x40))
v := and(mload(add(_signature, 0x41)), 0xff)
}
if (v != 27 && v != 28) {
return false;
}
// EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
// unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
// the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most
// signatures from current libraries generate a unique signature with an s-value in the lower half order.
//
// If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
// with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
// vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
// these malleable signatures as well.
if (
uint256(s) >
0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0
) {
return false;
}
return true;
}
function extractECDSASignature(
bytes memory _fullSignature
) internal pure returns (bytes memory signature1, bytes memory signature2) {
require(_fullSignature.length == 130, "Invalid length");
signature1 = new bytes(65);
signature2 = new bytes(65);
// Copying the first signature. Note, that we need an offset of 0x20
// since it is where the length of the `_fullSignature` is stored
assembly {
let r := mload(add(_fullSignature, 0x20))
let s := mload(add(_fullSignature, 0x40))
let v := and(mload(add(_fullSignature, 0x41)), 0xff)
mstore(add(signature1, 0x20), r)
mstore(add(signature1, 0x40), s)
mstore8(add(signature1, 0x60), v)
}
// Copying the second signature.
assembly {
let r := mload(add(_fullSignature, 0x61))
let s := mload(add(_fullSignature, 0x81))
let v := and(mload(add(_fullSignature, 0x82)), 0xff)
mstore(add(signature2, 0x20), r)
mstore(add(signature2, 0x40), s)
mstore8(add(signature2, 0x60), v)
}
}
function payForTransaction(
bytes32,
bytes32,
Transaction calldata _transaction
) external payable override onlyBootloader {
bool success = _transaction.payToTheBootloader();
require(success, "Failed to pay the fee to the operator");
}
function prepareForPaymaster(
bytes32, // _txHash
bytes32, // _suggestedSignedHash
Transaction calldata _transaction
) external payable override onlyBootloader {
_transaction.processPaymasterInput();
}
fallback() external {
// fallback of default account shouldn't be called by bootloader under no circumstances
assert(msg.sender != BOOTLOADER_FORMAL_ADDRESS);
// If the contract is called directly, behave like an EOA
}
receive() external payable {
// If the contract is called directly, behave like an EOA.
// Note, that is okay if the bootloader sends funds with no calldata as it may be used for refunds/operator payments
}
}
DeployMultisig
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "forge-std/Script.sol";
import "@era-contracts/libraries/SystemContractsCaller.sol";
import {Create2Factory} from "@era-contracts/Create2Factory.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "../src/AAFactory.sol";
import "../src/TwoUserMultisig.sol";
contract DeployMultisig is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
// Owners for the multisig account
// Can be random
address owner1 = vm.envAddress("OWNER_1");
address owner2 = vm.envAddress("OWNER_2");
// Read artifact file and get the bytecode hash
string memory artifact = vm.readFile(
"zkout/TwoUserMultisig.sol/TwoUserMultisig.json"
);
bytes32 multisigBytecodeHash = vm.parseJsonBytes32(artifact, ".hash");
console.log("Bytecode hash: ");
console.logBytes32(multisigBytecodeHash);
bytes32 salt = "1234";
vm.startBroadcast(deployerPrivateKey);
AAFactory factory = new AAFactory(multisigBytecodeHash);
console.log("Factory deployed at: ", address(factory));
// Mark the bytecode as a factory dependency
vmExt.zkUseFactoryDep("TwoUserMultisig");
factory.deployAccount(salt, owner1, owner2);
vm.stopBroadcast();
}
}
Running the script
forge script ./script/DeployMultisig.s.sol:DeployMultisig --rpc-url <RPC_URL> --private-key <PRIVATE_KEY> --broadcast --via-ir --zk-enable-eravm-extensions --zksync
For the complete source code, visit the minimal account abstraction multisig repository.
Foundry-ZKSync Supported Commands
This is a comprehensive review of all the Foundry commands actually supported in the actual stage of development.
🔄 Last update: September 12, 2024
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 | ✅ 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 | ✅ Supported | Checks the contract’s verification status on either the ZKsync block explorer (using --verifier ) or Etherscan, confirming successful verification. |
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.27
Solc 0.8.27 finished in 902.81ms
Compiler run successful!
No files changed, compilation skipped
Ran 2 tests for test/Counter.t.sol:CounterTest
[PASS] testFuzz_SetNumber(uint256) (runs: 256, μ: 31210, ~: 31288)
[PASS] test_Increment() (gas: 31303)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 5.75ms (5.36ms CPU time)
Ran 1 test suite in 10.34ms (5.75ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)
You can also run specific tests by passing a filter:
$ forge test --zksync --match-contract ComplicatedContractTest --match-test test_Deposit
Compiling 24 files with Solc 0.8.10
Solc 0.8.10 finished in 1.14s
Compiler run successful!
Compiling 24 files with zksolc and solc 0.8.10
zksolc and solc 0.8.10 finished in 5.54s
Compiler run successful!
Ran 2 tests for test/ComplicatedContract.t.sol:ComplicatedContractTest
[PASS] test_DepositERC20() (gas: 102193)
[PASS] test_DepositETH() (gas: 61414)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.28ms (1.39ms CPU time)
Ran 1 test suite in 5.27ms (1.28ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)
This will run the tests in the ComplicatedContractTest
test contract with testDeposit
in the name.
Inverse versions of these flags also exist (--no-match-contract
and --no-match-test
).
You can run tests in filenames that match a glob pattern with --match-path
.
$ forge test --zksync --match-path test/ContractB.t.sol
Compiling 1 files with Solc 0.8.10
Solc 0.8.10 finished in 1.08s
Compiler run successful!
Compiling 1 files with zksolc and solc 0.8.10
zksolc and solc 0.8.10 finished in 5.17s
Compiler run successful!
Ran 1 test for test/ContractB.t.sol:ContractBTest
[PASS] testExample() (gas: 257)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 397.78µs (61.04µs CPU time)
Ran 1 test suite in 4.86ms (397.78µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
The inverse of the --match-path
flag is --no-match-path
.
Logs and traces
The default behavior for forge test
is to only display a summary of passing and failing tests. You can control this behavior by increasing the verbosity (using the -v
flag). Each level of verbosity adds more information:
- Level 2 (
-vv
): Logs emitted during tests are also displayed. That includes assertion errors from tests, showing information such as expected vs actual. - Level 3 (
-vvv
): Stack traces for failing tests are also displayed. - Level 4 (
-vvvv
): Stack traces for all tests are displayed, and setup traces for failing tests are displayed. - Level 5 (
-vvvvv
): Stack traces and setup traces are always displayed.
Watch mode
Forge can re-run your tests when you make changes to your files using forge test --watch
.
By default, only changed test files are re-run. If you want to re-run all tests on a change, you can use forge test --zksync --watch --run-all
.
Writing Tests
Tests are written in Solidity. If the test function reverts, the test fails, otherwise it passes.
Let’s go over the most common way of writing tests, using the Forge Standard Library’s Test
contract, which is the preferred way of writing tests with Forge.
In this section, we’ll go over the basics using the functions from the Forge Std’s Test
contract, which is itself a superset of DSTest. You will learn how to use more advanced stuff from the Forge Standard Library soon.
DSTest provides basic logging and assertion functionality. To get access to the functions, import forge-std/Test.sol
and inherit from Test
in your test contract:
import {Test} from "forge-std/Test.sol";
Let’s examine a basic test:
pragma solidity 0.8.10;
import {Test} from "forge-std/Test.sol";
contract ContractBTest is Test {
uint256 testNumber;
function setUp() public {
testNumber = 42;
}
function test_NumberIs42() public {
assertEq(testNumber, 42);
}
function testFail_Subtract43() public {
testNumber -= 43;
}
}
Forge uses the following keywords in tests:
setUp
: An optional function invoked before each test case is run.
function setUp() public {
testNumber = 42;
}
test
: Functions prefixed 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.
Note: To use
stdError
constants (likearithmeticError
in the example below), make sure to importStdError.sol
:import {stdError} from "forge-std/StdError.sol";
Now, instead of using testFail
, you know exactly what reverted and with which error:
function test_CannotSubtract43() public {
vm.expectRevert(stdError.arithmeticError);
testNumber -= 43;
}
Tests are deployed to 0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84
. If you deploy a contract within your test, then
0xb4c...7e84
will be its deployer. If the contract deployed within a test gives special permissions to its deployer,
such as Ownable.sol
’s onlyOwner
modifier, then the test contract 0xb4c...7e84
will have those permissions.
⚠️ Note
Test functions must have either
external
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 {Test} from "forge-std/Test.sol";
error Unauthorized();
contract OwnerUpOnly {
address public immutable owner;
uint256 public count;
constructor() {
owner = msg.sender;
}
function increment() external {
if (msg.sender != owner) {
revert Unauthorized();
}
count++;
}
}
contract OwnerUpOnlyTest is Test {
OwnerUpOnly upOnly;
function setUp() public {
upOnly = new OwnerUpOnly();
}
function test_IncrementAsOwner() public {
assertEq(upOnly.count(), 0);
upOnly.increment();
assertEq(upOnly.count(), 1);
}
}
If we run forge test
now, we will see that the test passes, since OwnerUpOnlyTest
is the owner of OwnerUpOnly
.
$ forge test
Compiling 24 files with Solc 0.8.10
Solc 0.8.10 finished in 801.67ms
Compiler run successful!
Compiling 24 files with zksolc and solc 0.8.10
zksolc and solc 0.8.10 finished in 5.98s
Compiler run successful!
Ran 1 test for test/OwnerUpOnly.t.sol:OwnerUpOnlyTest
[PASS] test_IncrementAsOwner() (gas: 734680)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 34.55ms (19.27ms CPU time)
Ran 1 test suite in 35.22ms (34.55ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Let’s make sure that someone who is definitely not the owner can’t increment the count:
contract OwnerUpOnlyTest is Test {
OwnerUpOnly upOnly;
// ...
function testFail_IncrementAsNotOwner() public {
vm.prank(address(0));
upOnly.increment();
}
}
If we run forge test
now, we will see that all the test pass.
$ forge test
No files changed, compilation skipped
No files changed, compilation skipped
Ran 2 tests for test/OwnerUpOnly.t.sol:OwnerUpOnlyTest
[PASS] testFail_IncrementAsNotOwner() (gas: 216319)
[PASS] test_IncrementAsOwner() (gas: 734680)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 35.53ms (27.31ms CPU time)
Ran 1 test suite in 36.34ms (35.53ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)
The test passed because the prank
cheatcode changed our identity to the zero address for the next call (upOnly.increment()
). The test case passed since we used the testFail
prefix, however, using testFail
is considered an anti-pattern since it does not tell us anything about why upOnly.increment()
reverted.
If we run the tests again with traces turned on, we can see that we reverted with the correct error message.
$ forge test -vvvv --match-test testFail_IncrementAsNotOwner
No files changed, compilation skipped
No files changed, compilation skipped
Ran 1 test for test/OwnerUpOnly.t.sol:OwnerUpOnlyTest
[PASS] testFail_IncrementAsNotOwner() (gas: 216319)
Traces:
[216319] OwnerUpOnlyTest::testFail_IncrementAsNotOwner()
├─ [0] VM::prank(0x0000000000000000000000000000000000000000)
│ └─ ← [Return]
├─ [208252] OwnerUpOnly::increment()
│ └─ ← [Revert] Unauthorized()
└─ ← [Revert] Unauthorized()
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 21.84ms (6.47ms CPU time)
Ran 1 test suite in 22.73ms (21.84ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
To be sure in the future, let’s make sure that we reverted because we are not the owner using the expectRevert
cheatcode:
contract OwnerUpOnlyTest is Test {
OwnerUpOnly upOnly;
// ...
// Notice that we replaced `testFail` with `test`
function test_RevertWhen_CallerIsNotOwner() public {
vm.expectRevert(Unauthorized.selector);
vm.prank(address(0));
upOnly.increment();
}
}
If we run forge test
one last time, we see that the test still passes, but this time we are sure that it will always fail if we revert for any other reason.
$ forge test
No files changed, compilation skipped
No files changed, compilation skipped
Ran 1 test for test/OwnerUpOnly.t.sol:OwnerUpOnlyTest
[PASS] test_IncrementAsOwner() (gas: 734680)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 34.49ms (19.06ms CPU time)
Ran 1 test suite in 35.33ms (34.49ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Another cheatcode that is perhaps not so intuitive is the expectEmit
function. Before looking at expectEmit
, we need to understand what an event is.
Events are inheritable members of contracts. When you emit an event, the arguments are stored on the blockchain. The indexed
attribute can be added to a maximum of three parameters of an event to form a data structure known as a “topic.” Topics allow users to search for events on the blockchain.
pragma solidity 0.8.10;
import {Test} from "forge-std/Test.sol";
contract EmitContractTest is Test {
event Transfer(address indexed from, address indexed to, uint256 amount);
function test_ExpectEmit() public {
ExpectEmit emitter = new ExpectEmit();
// Check that topic 1, topic 2, and data are the same as the following emitted event.
// Checking topic 3 here doesn't matter, because `Transfer` only has 2 indexed topics.
vm.expectEmit(true, true, false, true);
// The event we expect
emit Transfer(address(this), address(1337), 1337);
// The event we get
emitter.t();
}
function test_ExpectEmit_DoNotCheckData() public {
ExpectEmit emitter = new ExpectEmit();
// Check topic 1 and topic 2, but do not check data
vm.expectEmit(true, true, false, false);
// The event we expect
emit Transfer(address(this), address(1337), 1338);
// The event we get
emitter.t();
}
}
contract ExpectEmit {
event Transfer(address indexed from, address indexed to, uint256 amount);
function t() public {
emit Transfer(msg.sender, address(1337), 1337);
}
}
When we call vm.expectEmit(true, true, false, true);
, we want to check the 1st and 2nd indexed
topic for the next event.
The expected Transfer
event in test_ExpectEmit()
means we are expecting that from
is address(this)
, and to
is address(1337)
. This is compared against the event emitted from emitter.t()
.
In other words, we are checking that the first topic from emitter.t()
is equal to address(this)
. The 3rd argument in expectEmit
is set to false
because there is no need to check the third topic in the Transfer
event, since there are only two. It does not matter even if we set to true
.
The 4th argument in expectEmit
is set to true
, which means that we want to check “non-indexed topics”, also known as data.
For example, we want the data from the expected event in test_ExpectEmit
- which is amount
- to equal to the data in the actual emitted event. In other words, we are asserting that amount
emitted by emitter.t()
is equal to 1337
. If the fourth argument in expectEmit
was set to false
, we would not check amount
.
In other words, test_ExpectEmit_DoNotCheckData
is a valid test case, even though the amounts differ, since we do not check the data.
📚 Reference
See the Cheatcodes Reference for a complete overview of all the available cheatcodes.
Forge Standard Library Overview
Forge Standard Library (Forge Std for short) is a collection of helpful contracts that make writing tests easier, faster, and more user-friendly.
Using Forge Std is the preferred way of writing tests with Foundry.
It provides all the essential functionality you need to get started writing tests:
Vm.sol
: Up-to-date cheatcodes 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 {Test} from "forge-std/Test.sol";
contract ContractTest is Test { ...
Now, you can:
// Access Hevm via the `vm` instance
vm.startPrank(alice);
// Assert and log using Dappsys Test
assertEq(dai.balanceOf(alice), 10000e18);
// Log with the Hardhat `console` (`console2`)
console.log(alice.balance);
// Use anything from the Forge Std std-libraries
deal(address(dai), alice, 10000e18);
To import the Vm
interface or the console
library individually:
import {Vm} from "forge-std/Vm.sol";
import {console} from "forge-std/console.sol";
Note: console2.sol
contains patches to console.sol
that allows Forge to decode traces for calls to the console, but it is not compatible with Hardhat.
import {console2} from "forge-std/console2.sol";
Standard libraries
Forge Std currently consists of six standard libraries.
Std Logs
Std Logs expand upon the logging events from the DSTest
library.
Std Assertions
Std Assertions expand upon the assertion functions from the DSTest
library.
Std Cheats
Std Cheats are wrappers around Forge cheatcodes that make them safer to use and improve the DX.
You can access Std Cheats by simply calling them inside your test contract, as you would any other internal function:
// set up a prank as Alice with 100 ETH balance
hoax(alice, 100 ether);
Std Errors
Std Errors provide wrappers around common internal Solidity errors and reverts.
Std Errors are most useful in combination with the expectRevert
cheatcode, as you do not need to remember the internal Solidity panic codes yourself. Note that you have to access them through stdError
, as this is a library.
// expect an arithmetic error on the next call (e.g. underflow)
vm.expectRevert(stdError.arithmeticError);
Std Storage
Std Storage makes manipulating contract storage easy. It can find and write to the storage slot(s) associated with a particular variable.
The Test
contract already provides a StdStorage
instance stdstore
through which you can access any std-storage functionality. Note that you must add using stdStorage for StdStorage
in your test contract first.
// find the variable `score` in the contract `game`
// and change its value to 10
stdstore
.target(address(game))
.sig(game.score.selector)
.checked_write(10);
Std Math
Std Math is a library with useful mathematical functions that are not provided in Solidity.
Note that you have to access them through stdMath
, as this is a library.
// get the absolute value of -10
uint256 ten = stdMath.abs(-10)
📚 Reference
See the Forge Standard Library Reference for a complete overview of Forge Standard Library.
Forge ZKsync Standard Library Overview
Forge ZKsync Standard Library is an addition to the Forge Standard Library that adds further collection of helpful contracts that make writing tests easier, faster, and more user-friendly for the ZKsync ecosystem.
Refer to this section for more details.
Understanding Traces
Forge can produce traces either for failing tests (-vvv
) or all tests (-vvvv
).
Traces follow the same general format:
[<Gas Usage>] <Contract>::<Function>(<Parameters>)
├─ [<Gas Usage>] <Contract>::<Function>(<Parameters>)
│ └─ ← <Return Value>
└─ ← <Return Value>
Each trace can have many more subtraces, each denoting a call to a contract and a return value.
If your terminal supports color, the traces will also come with a variety of colors:
- Green: For calls that do not revert
- Red: For reverting calls
- Blue: For calls to cheat codes
- Cyan: For emitted logs
- Yellow: For contract deployments
The gas usage (marked in square brackets) is for the entirety of the function call. You may notice, however, that sometimes the gas usage of one trace does not exactly match the gas usage of all its subtraces:
[24661] OwnerUpOnlyTest::testIncrementAsOwner()
├─ [2262] OwnerUpOnly::count()
│ └─ ← 0
├─ [20398] OwnerUpOnly::increment()
│ └─ ← ()
├─ [262] OwnerUpOnly::count()
│ └─ ← 1
└─ ← ()
The gas unaccounted for is due to some extra operations happening between calls, such as arithmetic and store reads/writes.
Forge will try to decode as many signatures and values as possible, but sometimes this is not possible. In these cases, the traces will appear like so:
[<Gas Usage>] <Address>::<Calldata>
└─ ← <Return Data>
Some traces might be harder to grasp at first glance. These include:
- The
OOG
shorthand stands for “Out Of Gas”. - The acronym
EOF
stands for “Ethereum Object Format”, which introduces an extensible and versioned container format for EVM bytecode. For more information, read here. NotActivated
means the feature or opcode is not activated. Some versions of the EVM only support certain opcodes. You may need to use a more recent version usign the--evm_version
flag. For example, 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 {Test} from "forge-std/Test.sol";
contract Safe {
receive() external payable {}
function withdraw() external {
payable(msg.sender).call{value: address(this).balance}("");
}
}
contract SafeTest is Test {
Safe safe;
// Needed so the test contract itself can receive ether
// when withdrawing
receive() external payable {}
function setUp() public {
safe = new Safe();
}
function test_Withdraw() public {
payable(address(safe)).call{value: 1 ether}("");
uint256 preBalance = address(this).balance;
safe.withdraw();
uint256 postBalance = address(this).balance;
assertEq(preBalance + 1 ether, postBalance);
}
}
Running the test, we see it passes:
$ forge test --zksync
Compiling 24 files with Solc 0.8.10
Solc 0.8.10 finished in 1.11s
Compiler run successful with warnings:
Warning (9302): Return value of low-level calls not used.
--> test/Safe.t.sol:11:9:
|
11 | payable(msg.sender).call{value: address(this).balance}("");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Warning (9302): Return value of low-level calls not used.
--> test/Safe.t.sol:27:9:
|
27 | payable(address(safe)).call{value: 1 ether}("");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Compiling 24 files with zksolc and solc 0.8.10
zksolc and solc 0.8.10 finished in 5.81s
Compiler run successful with warnings:
Warning (9302)
Warning: Return value of low-level calls not used.
--> test/Safe.t.sol:11:9:
|
11 | payable(msg.sender).call{value: address(this).balance}("");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Warning (9302)
Warning: Return value of low-level calls not used.
--> test/Safe.t.sol:27:9:
|
27 | payable(address(safe)).call{value: 1 ether}("");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2024-10-06T00:06:07.481112Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
Ran 1 test for test/Safe.t.sol:SafeTest
[PASS] test_Withdraw() (gas: 1199829)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 28.85ms (13.44ms CPU time)
Ran 1 test suite in 29.66ms (28.85ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
This unit test does test that we can withdraw ether from our safe. However, who is to say that it works for all amounts, not just 1 ether?
The general property here is: given a safe balance, when we withdraw, we should get whatever is in the safe.
Forge will run any test that takes at least one parameter as a property-based test, so let’s rewrite:
contract SafeTest is Test {
// ...
function testFuzz_Withdraw(uint256 amount) public {
payable(address(safe)).call{value: amount}("");
uint256 preBalance = address(this).balance;
safe.withdraw();
uint256 postBalance = address(this).balance;
assertEq(preBalance + amount, postBalance);
}
}
If we run the test now, we can see that Forge runs the property-based test, but it fails for high values of amount
:
$ forge test
Compiling 1 files with Solc 0.8.10
Solc 0.8.10 finished in 819.99ms
Compiler run successful with warnings:
Warning (9302): Return value of low-level calls not used.
--> test/Safe.t.sol:11:9:
|
11 | payable(msg.sender).call{value: address(this).balance}("");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Warning (9302): Return value of low-level calls not used.
--> test/Safe.t.sol:30:9:
|
30 | payable(address(safe)).call{value: amount}("");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Compiling 1 files with zksolc and solc 0.8.10
zksolc and solc 0.8.10 finished in 5.79s
Compiler run successful with warnings:
Warning (9302)
Warning: Return value of low-level calls not used.
--> test/Safe.t.sol:11:9:
|
11 | payable(msg.sender).call{value: address(this).balance}("");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Warning (9302)
Warning: Return value of low-level calls not used.
--> test/Safe.t.sol:30:9:
|
30 | payable(address(safe)).call{value: amount}("");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2024-10-06T00:06:15.308856Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
Ran 1 test for test/Safe.t.sol:SafeTest
[FAIL. Reason: assertion failed; counterexample: calldata=0x29facca700000000000000000000000000000000000001e35e0786a154ba56aa29228e12 args=[38296303183566758584479465508370 [3.829e31]]] testFuzz_Withdraw(uint256) (runs: 0, μ: 0, ~: 0)
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 29.00ms (13.46ms CPU time)
Ran 1 test suite in 29.74ms (29.00ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)
The default amount of ether that the test contract is given is 2**96 wei
(as in DappTools), so we have to restrict the type of amount to uint96
to make sure we don’t try to send more than we have:
function testFuzz_Withdraw(uint96 amount) public {
And now it passes:
$ forge test --zksync
Compiling 1 files with Solc 0.8.10
Solc 0.8.10 finished in 847.95ms
Compiler run successful with warnings:
Warning (9302): Return value of low-level calls not used.
--> test/Safe.t.sol:11:9:
|
11 | payable(msg.sender).call{value: address(this).balance}("");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Warning (9302): Return value of low-level calls not used.
--> test/Safe.t.sol:29:9:
|
29 | payable(address(safe)).call{value: amount}("");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Compiling 1 files with zksolc and solc 0.8.10
zksolc and solc 0.8.10 finished in 5.99s
Compiler run successful with warnings:
Warning (9302)
Warning: Return value of low-level calls not used.
--> test/Safe.t.sol:11:9:
|
11 | payable(msg.sender).call{value: address(this).balance}("");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Warning (9302)
Warning: Return value of low-level calls not used.
--> test/Safe.t.sol:29:9:
|
29 | payable(address(safe)).call{value: amount}("");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2024-10-06T00:06:23.331938Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.345903Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.359428Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.372780Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.386122Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.399445Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.412821Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.426576Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.439887Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.453148Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.466502Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.479790Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.493327Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.508274Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.521597Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.534853Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.548315Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.561608Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.575000Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.588318Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.601598Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.614948Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.628305Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.641584Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.654867Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.668118Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.681380Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.694643Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.708046Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.721287Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.734602Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.747917Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.761136Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.774378Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.787596Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.800848Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.814072Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.827425Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.840706Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.854035Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.867305Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.880672Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.893991Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.907290Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.920613Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.933875Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.947108Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.960362Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.973694Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:23.986924Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.000161Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.013385Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.026762Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.040011Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.053185Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.066446Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.079639Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.093074Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.106308Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.119607Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.132847Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.146042Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.159229Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.172625Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.185998Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.199339Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.212739Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.226044Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.239326Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.252540Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.265805Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.279235Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.292659Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.306850Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.320262Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.333621Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.346983Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.360354Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.373691Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.387072Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.400302Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.413762Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.427217Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.440618Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.453915Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.467234Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.480611Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.494411Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.507939Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.521187Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.534513Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.547832Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.561113Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.574438Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.587761Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.601179Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.614612Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.627890Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.641183Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.654465Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.667758Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.681066Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.694356Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.707827Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.721143Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.734479Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.747757Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.761018Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.774276Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.787530Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.800799Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.814083Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.827371Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.840678Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.853984Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.867329Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.882303Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.896239Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.909740Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.923287Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.936861Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.950356Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.968098Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:24.988084Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.002207Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.015815Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.029436Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.043060Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.056630Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.070191Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.084092Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.097437Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.110758Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.124160Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.137051Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.150173Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.163572Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.176865Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.190256Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.203621Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.217100Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.230452Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.243789Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.257124Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.270453Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.283888Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.297215Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.310586Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.325137Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.338738Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.352459Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.365767Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.379083Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.392345Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.405695Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.419153Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.434180Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.447513Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.460802Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.474155Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.487444Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.500676Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.514007Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.527266Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.540562Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.553775Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.567087Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.580410Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.593703Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.607090Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.620294Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.633672Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.647031Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.660402Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.674061Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.687594Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.700859Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.715210Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.728717Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.742385Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.755831Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.769295Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.782724Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.796195Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.809492Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.822718Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.836027Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.849216Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.862508Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.875737Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.888952Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.902123Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.915368Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.928775Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.942060Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.955427Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.968683Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.981962Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:25.995160Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.008416Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.021826Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.035135Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.048334Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.061572Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.074915Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.088196Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.101535Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.114856Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.127698Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.140786Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.154128Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.167431Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.180673Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.193932Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.207174Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.220574Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.233890Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.247199Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.260473Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.273766Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.287069Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.300298Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.313551Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.326910Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.340148Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.353474Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.366865Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.380371Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.393685Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.406930Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.420321Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.433653Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.446937Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.460151Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.473379Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.486611Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.499907Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.513228Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.526535Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.539798Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.553094Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.566456Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.579731Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.592566Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.605634Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.619056Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.632333Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.645624Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.658878Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.672160Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.685420Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.698786Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.712119Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.725413Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.738752Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.751953Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
2024-10-06T00:06:26.765158Z ERROR foundry_zksync_core::vm::tracers::cheatcode: call may fail or behave unexpectedly due to empty code target=0x7fa9385be102ac3eac297483dd6233d62b3e1496 calldata=""
Ran 1 test for test/Safe.t.sol:SafeTest
[PASS] testFuzz_Withdraw(uint96) (runs: 257, μ: 1072652, ~: 995989)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.46s (3.45s CPU time)
Ran 1 test suite in 3.46s (3.46s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
You may want to exclude certain cases using the assume
cheatcode. In those cases, fuzzer will discard the inputs and start a new fuzz run:
function testFuzz_Withdraw(uint96 amount) public {
vm.assume(amount > 0.1 ether);
// snip
}
There are different ways to run property-based tests, notably parametric testing and fuzzing. Forge only supports fuzzing.
Interpreting results
You might have noticed that fuzz tests are summarized a bit differently compared to unit tests:
- “runs” refers to the amount of scenarios the fuzzer tested. By default, the fuzzer will generate 256 scenarios, but this and other test execution parameters can be setup by the user. Fuzzer configuration details are provided
here
. - “μ” (Greek letter mu) is the mean gas used across all fuzz runs
- “~” (tilde) is the median gas used across all fuzz runs
Configuring fuzz test execution
Fuzz tests execution is governed by parameters that can be controlled by users via Forge configuration primitives. Configs can be applied globally or on a per-test basis. For details on this topic please refer to
📚 Global config
and 📚 In-line config
.
Fuzz test fixtures
Fuzz test fixtures can be defined when you want to make sure a certain set of values is used as inputs for fuzzed parameters. These fixtures can be declared in tests as:
- storage arrays prefixed with
fixture
and followed by param name to be fuzzed. For example, fixtures to be used when fuzzing 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 {Test} from "forge-std/Test.sol";
contract TestContract is Test {
function testMyFFI () public {
string[] memory cmds = new string[](2);
cmds[0] = "cat";
cmds[1] = "address.txt"; // assume contains abi-encoded address.
bytes memory result = vm.ffi(cmds);
address loadedAddress = abi.decode(result, (address));
// Do something with the address
// ...
}
}
An address has previously been written to address.txt
, and we read it in using the FFI cheatcode. This data can now be used throughout your test contract.
Example: Differential Testing Merkle Tree Implementations
Merkle Trees are a cryptographic commitment scheme frequently used in blockchain applications. Their popularity has led to a number of different implementations of Merkle Tree generators, provers, and verifiers. Merkle roots and proofs are often generated using a language like JavaScript or Python, while proof verification usually occurs on-chain in Solidity.
Murky is a complete implementation of Merkle roots, proofs, and verification in Solidity. Its test suite includes differential tests against OpenZeppelin’s Merkle proof library, as well as root generation tests against a reference JavaScript implementation. These tests are powered by Foundry’s fuzzing and ffi
capabilities.
Differential fuzzing against a reference TypeScript implementation
Using the ffi
cheatcode, Murky tests its own Merkle root implementation against a TypeScript implementation using data provided by Forge’s fuzzer:
function testMerkleRootMatchesJSImplementationFuzzed(bytes32[] memory leaves) public {
vm.assume(leaves.length > 1);
bytes memory packed = abi.encodePacked(leaves);
string[] memory runJsInputs = new string[](8);
// Build ffi command string
runJsInputs[0] = 'npm';
runJsInputs[1] = '--prefix';
runJsInputs[2] = 'differential_testing/scripts/';
runJsInputs[3] = '--silent';
runJsInputs[4] = 'run';
runJsInputs[5] = 'generate-root-cli';
runJsInputs[6] = leaves.length.toString();
runJsInputs[7] = packed.toHexString();
// Run command and capture output
bytes memory jsResult = vm.ffi(runJsInputs);
bytes32 jsGeneratedRoot = abi.decode(jsResult, (bytes32));
// Calculate root using Murky
bytes32 murkyGeneratedRoot = m.getRoot(leaves);
assertEq(murkyGeneratedRoot, jsGeneratedRoot);
}
Note: see
Strings2.sol
in the Murky Repo for the library that enables(bytes memory).toHexString()
Forge runs npm --prefix differential_testing/scripts/ --silent run generate-root-cli {numLeaves} {hexEncodedLeaves}
. This calculates the Merkle root for the input data using the reference JavaScript implementation. The script prints the root to stdout, and that printout is captured as bytes
in the return value of vm.ffi()
.
The test then calculates the root using the Solidity implementation.
Finally, the test asserts that the both roots are exactly equal. If they are not equal, the test fails.
Differential fuzzing against OpenZeppelin’s Merkle Proof Library
You may want to use differential testing against another Solidity implementation. In that case, ffi
is not needed. Instead, the reference implementation is imported directly into the test.
import {MerkleProof} from "openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol";
//...
function testCompatibilityOpenZeppelinProver(bytes32[] memory _data, uint256 node) public {
vm.assume(_data.length > 1);
vm.assume(node < _data.length);
bytes32 root = m.getRoot(_data);
bytes32[] memory proof = m.getProof(_data, node);
bytes32 valueToProve = _data[node];
bool murkyVerified = m.verifyProof(root, proof, valueToProve);
bool ozVerified = MerkleProof.verify(proof, root, valueToProve);
assertTrue(murkyVerified == ozVerified);
}
Differential testing against a known edge case
Differential tests are not always fuzzed – they are also useful for testing known edge cases. In the case of the Murky codebase, the initial implementation of the log2ceil
function did not work for certain arrays whose lengths were close to a power of 2 (like 129). As a safety check, a test is always run against an array of this length and compared to the TypeScript implementation. You can see the full test here.
Standardized Testing against reference data
FFI is also useful for injecting reproducible, standardized data into the testing environment. In the Murky library, this is used as a benchmark for gas snapshotting (see forge snapshot).
bytes32[100] data;
uint256[8] leaves = [4, 8, 15, 16, 23, 42, 69, 88];
function setUp() public {
string[] memory inputs = new string[](2);
inputs[0] = "cat";
inputs[1] = "src/test/standard_data/StandardInput.txt";
bytes memory result = vm.ffi(inputs);
data = abi.decode(result, (bytes32[100]));
m = new Merkle();
}
function testMerkleGenerateProofStandard() public view {
bytes32[] memory _data = _getData();
for (uint i = 0; i < leaves.length; ++i) {
m.getProof(_data, leaves[i]);
}
}
src/test/standard_data/StandardInput.txt
is a text file that contains an encoded bytes32[100]
array. It’s generated outside of the test and can be used in any language’s Web3 SDK. It looks something like:
0xf910ccaa307836354233316666386231414464306335333243453944383735313..423532
The standardized testing contract reads in the file using ffi
. It decodes the data into an array and then, in this example, generates proofs for 8 different leaves. Because the data is constant and standard, we can meaningfully measure gas and performance improvements using this test.
Of course, one could just hardcode the array into the test! But that makes it much harder to do consistent testing across contracts, implementations, etc.
Example: Differential Testing Gradual Dutch Auctions
The reference implementation for Paradigm’s Gradual Dutch Auction mechanism contains a number of differential, fuzzed tests. It is an excellent repository to further explore differential testing using ffi
.
- 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 ZKsync Block Explorer, Etherscan, Sourcify or Blockscout, if the network is supported, by passing --verify
.
It is recommended to make use of ZKsync Block Explorer by specifying --verifier zksync
and using the verification URL (e.g. ZKsync Sepolia https://explorer.sepolia.era.zksync.dev/contract_verification
).
$ forge create --zksync \
--rpc-url <your_rpc_url> \
--constructor-args "ForgeUSD" "FUSD" 18 1000000000000000000000 \
--private-key <your_private_key> \
--verifier zksync \
--verifier-url https://explorer.sepolia.era.zksync.dev/contract_verification \
--verify \
src/MyToken.sol:MyToken
Verifying a pre-existing contract
It is recommended to use the --verify
flag with forge create
to automatically verify the contract on explorer after a deployment using --verifier zksync
to target ZKsync Block Explorer instance.
If you are verifying an already deployed contract, read on.
You can verify a contract on ZKsync Block Explorer, Etherscan, Sourcify, oklink or Blockscout with the forge verify-contract
command.
You must provide:
- the contract address
- the contract name or the path to the contract
<path>:<contractname>
- your Etherscan API key (env:
ETHERSCAN_API_KEY
) (if verifying on Etherscan).
Moreover, you may need to provide:
- the constructor arguments in the ABI-encoded format, if there are any
- compiler version used for build, with 8 hex digits from the commit version prefix (the commit will usually not be a nightly build). It is auto-detected if not specified.
- the number of optimizations, if the Solidity optimizer was activated. It is auto-detected if not specified.
- the chain ID, if the contract is not on Ethereum Mainnet
Let’s say you want to verify MyToken
(see above). You set the number of optimizations to 1 million, compiled it with v0.8.10, and deployed it, as shown above, to the Sepolia testnet (chain ID: 11155111). Note that --num-of-optimizations
will default to 0 if not set on verification, while it defaults to 200 if not set on deployment, so make sure you pass --num-of-optimizations 200
if you left the default compilation settings.
Here’s how to verify it:
forge verify-contract \
--zksync \
--chain zksync-testnet \
--num-of-optimizations 1000000 \
--watch \
--verifier zksync \
--verifier-url https://explorer.sepolia.era.zksync.dev/contract_verification \
--constructor-args $(cast abi-encode "constructor(string,string,uint256,uint256)" "ForgeUSD" "FUSD" 18 1000000000000000000000) \
<the_contract_address> \
src/MyToken.sol:MyToken
Submitting verification for [src/MyToken.sol:MyToken] at address 0x21d6dffe4B406c59E80CD62b4cB1763363c8a040.
Verification submitted successfully. Verification ID: 27574
Checking verification status for ID: 27574 using verifier: ZKsync at URL: https://explorer.sepolia.era.zksync.dev/contract_verification
Verification was successful.
It is recommended to use the --watch
flag along
with verify-contract
command in order to poll for the verification result.
If the --watch
flag was not supplied, you can check
the verification status with the forge verify-check
command:
$ forge verify-check --zksync --chain zksync-testnet <verificationId> --verifier zksync
Contract successfully verified.
💡 Tip
Use Cast’s
abi-encode
to ABI-encode arguments.In this example, we ran
cast abi-encode "constructor(string,string,uint8,uint256)" "ForgeUSD" "FUSD" 18 1000000000000000000000
to ABI-encode the arguments.
Troubleshooting
missing hex prefix ("0x") for hex string
Make sure the private key string begins with 0x
.
EIP-1559 not activated
EIP-1559 is not supported or not activated on the RPC server. Pass the --legacy
flag to use legacy transactions instead of the EIP-1559 ones. If you do development in a local environment, you can use Hardhat instead of Ganache.
Failed to parse tokens
Make sure the passed arguments are of correct type.
Signature error
Make sure the private key is correct.
Compiler version commit for verify
If you want to check the exact commit you are running locally, try: ~/.svm/0.x.y/solc-0.x.y --version
where x
and
y
are major and minor version numbers respectively. The output of this will be something like:
solc, the solidity compiler commandline interface
Version: 0.8.12+commit.f00d7308.Darwin.appleclang
Note: You cannot just paste the entire string “0.8.12+commit.f00d7308.Darwin.appleclang” as the argument for the compiler-version. But you can use the 8 hex digits of the commit to look up exactly what you should copy and paste from compiler version.
Known Issues
Verifying Contracts With Ambiguous Import Paths
Forge passes source directories (src
, lib
, test
etc) as --include-path
arguments to the compiler.
This means that given the following project tree
|- src
|-- folder
|--- Contract.sol
|--- IContract.sol
it is possible to import IContract
inside the Contract.sol
using folder/IContract.sol
import path.
Etherscan is not able to recompile such sources. Consider changing the imports to use relative import path.
Verifying Contracts With No Bytecode Hash
Currently, it’s not possible to verify contracts on Etherscan with bytecode_hash
set to none
.
Click here to learn more about
how metadata hash is used for source code verification.
Gas Tracking
🚨 Important
Gas tracking may not be entirely accurate in the ZKsync context. This is mostly due to the additional overhead to executing each
CREATE
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 three different tools for this job:
- Gas reports: Gas reports give you an overview of how much Forge thinks the individual functions in your contracts will consume in gas.
- Gas function snapshots: Gas function snapshots give you an overview of how much each test function consumes in gas.
- Gas section snapshots: Gas section snapshots give you the ability to capture gas usage over arbitrary sections inside of test functions.
This also tracks internal gas usage. You can access this by using the
snapshotGas*
cheatcodes inside your tests.
Gas reports, gas function snapshots and gas section snapshots differ in some ways:
- Gas reports use tracing to figure out gas costs for individual contract calls.
This gives more granular insight, at the cost of speed. - Gas function snapshots have more built-in tools, such as diffs and exporting the results to a file. Snapshots are not as granular as gas reports, but they are faster to generate.
- Gas section snapshots provides the most granular way to capture gas usage. Every captured gas snapshot is written to a file in a
snapshots
directory. By default these snapshots are grouped by the contract name of the test.
Gas Reports
🚨 Important
Gas reports may not be entirely accurate in the ZKsync context. This is mostly due to the additional overhead to executing each
CREATE
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 Function Snapshots
🚨 Important
Gas snapshots may not be entirely accurate in the ZKsync context. This is mostly due to the > additional overhead to executing each
CREATE
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
Gas Section Snapshots
🚨 Important
Gas snapshots may not be entirely accurate in the ZKsync context. This is mostly due to the > additional overhead to executing each
CREATE
orCALL
in its own zkEVM which has additional bootloader gas costs.
Forge can capture gas snapshots over arbitrary sections inside of your test functions. This can be useful to get a granular measurement of how much gas your logic is consuming as both external calls and internal gas usage are measured.
Instead of running a command like forge snapshot
or forge test --gas-report
, you use the snapshotGas
cheatcodes in your tests to capture gas usage as follows:
snapshotGas
cheatcodes
Signature
/// Start a snapshot capture of the current gas usage by name.
/// The group name is derived from the contract name.
function startSnapshotGas(string calldata name) external;
/// Start a snapshot capture of the current gas usage by name in a group.
function startSnapshotGas(string calldata group, string calldata name) external;
/// Stop the snapshot capture of the current gas by latest snapshot name, capturing the gas used since the start.
function stopSnapshotGas() external returns (uint256 gasUsed);
/// Stop the snapshot capture of the current gas usage by name, capturing the gas used since the start.
/// The group name is derived from the contract name.
function stopSnapshotGas(string calldata name) external returns (uint256 gasUsed);
/// Stop the snapshot capture of the current gas usage by name in a group, capturing the gas used since the start.
function stopSnapshotGas(string calldata group, string calldata name) external returns (uint256 gasUsed);
/// Snapshot capture an arbitrary numerical value by name.
/// The group name is derived from the contract name.
function snapshotValue(string calldata name, uint256 value) external;
/// Snapshot capture an arbitrary numerical value by name in a group.
function snapshotValue(string calldata group, string calldata name, uint256 value) external;
/// Snapshot capture the gas usage of the last call by name from the callee perspective.
function snapshotGasLastCall(string calldata name) external returns (uint256 gasUsed);
/// Snapshot capture the gas usage of the last call by name in a group from the callee perspective.
function snapshotGasLastCall(string calldata group, string calldata name) external returns (uint256 gasUsed);
Description
snapshotGas*
cheatcodes allow you to capture gas usage in your tests. This can be useful to track how much gas your logic is consuming. You can capture the gas usage of the last call by name, capture an arbitrary numerical value by name, or start and stop a snapshot capture of the current gas usage by name.
In order to strictly compare gas usage across test runs, set the FORGE_SNAPSHOT_CHECK
environment variable to true
before running your tests. This will compare the gas usage of your tests against the last snapshot and fail if the gas usage has changed. By default the snapshots directory will be newly created and its contents removed before each test run to ensure no stale data is present.
It is intended that the snapshots
directory created when using the snapshotGas*
cheatcodes is checked into version control. This allows you to track changes in gas usage over time and compare gas usage during code reviews.
When running forge clean
the snapshots
directory will be deleted.
Examples
Capturing the gas usage of a section of code that calls an external contract:
contract SnapshotGasTest is Test {
uint256 public slot0;
Flare public flare;
function setUp() public {
flare = new Flare();
}
function testSnapshotGas() public {
vm.startSnapshotGas("externalA");
flare.run(256);
uint256 gasUsed = vm.stopSnapshotGas();
}
}
Capturing the gas usage of multiple sections of code that modify the internal state:
contract SnapshotGasTest is Test {
uint256 public slot0;
/// Writes to `snapshots/SnapshotGasTest.json` group with name `internalA`, `internalB`, and `internalC`.
function testSnapshotGas() public {
vm.startSnapshotGas("internalA");
slot0 = 1;
vm.stopSnapshotGas();
vm.startSnapshotGas("internalB");
slot0 = 2;
vm.stopSnapshotGas();
vm.startSnapshotGas("internalC");
slot0 = 0;
vm.stopSnapshotGas();
}
}
Capturing the gas usage of a section of code that modifies both the internal state and calls an external contract:
contract SnapshotGasTest is Test {
uint256 public slot0;
Flare public flare;
function setUp() public {
flare = new Flare();
}
/// Writes to `snapshots/SnapshotGasTest.json` group with name `combinedA`.
function testSnapshotGas() public {
vm.startSnapshotGas("combinedA");
flare.run(256);
slot0 = 1;
vm.stopSnapshotGas();
}
}
Capturing an arbitrary numerical value (such as the bytecode size of a contract):
```solidity
contract SnapshotGasTest is Test {
uint256 public slot0;
/// Writes to `snapshots/SnapshotGasTest.json` group with name `valueA`, `valueB`, and `valueC`.
function testSnapshotValue() public {
uint256 a = 123;
uint256 b = 456;
uint256 c = 789;
vm.snapshotValue("valueA", a);
vm.snapshotValue("valueB", b);
vm.snapshotValue("valueC", c);
}
}
Capturing the gas usage of the last call from the callee perspective:
contract SnapshotGasTest is Test {
Flare public flare;
function setUp() public {
flare = new Flare();
}
/// Writes to `snapshots/SnapshotGasTest.json` group with name `lastCallA`.
function testSnapshotGasLastCall() public {
flare.run(1);
vm.snapshotGasLastCall("lastCallA");
}
}
For each of the above examples you can also use the group
variant of the cheatcodes to group the snapshots together in a custom group.
contract SnapshotGasTest is Test {
uint256 public slot0;
/// Writes to `snapshots/CustomGroup.json` group with name `internalA`, `internalB`, and `internalC`.
function testSnapshotGas() public {
vm.startSnapshotGas("CustomGroup", "internalA");
slot0 = 1;
vm.stopSnapshotGas();
vm.startSnapshotGas("CustomGroup", "internalB");
slot0 = 2;
vm.stopSnapshotGas();
vm.startSnapshotGas("CustomGroup", "internalC");
slot0 = 0;
vm.stopSnapshotGas();
}
}
Overview of Cast
Cast is Foundry’s command-line tool for performing Ethereum RPC calls. You can make smart contract calls, send transactions, or retrieve any type of chain data - all from your command-line!
How to use Cast
To use Cast, run the cast
command followed by a subcommand:
$ cast <subcommand>
Examples
Let’s use cast
to retrieve the total supply of the DAI token:
$ cast call 0x6b175474e89094c44da98b954eedeac495271d0f "totalSupply()(uint256)" --rpc-url https://eth-mainnet.alchemyapi.io/v2/Lc7oIGYeL_QvInzI0Wiu_pOZZDEKBrdf
3110058793252544024496112199 [3.11e27]
cast
also provides many convenient subcommands, such as for decoding calldata:
$ cast 4byte-decode 0x1F1F897F676d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e7
1) "fulfillRandomness(bytes32,uint256)"
0x676d000000000000000000000000000000000000000000000000000000000000
999
You can also use cast
to send arbitrary messages. Here’s an example of sending a message between two Anvil accounts.
$ cast send --private-key <Your Private Key> 0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc $(cast from-utf8 "hello world") --rpc-url http://127.0.0.1:8545/
📚 Reference
See the
cast
Reference for a complete overview of all the available subcommands.
Overview of Anvil-ZKsync
Anvil-ZKsync is a local testnet node shipped with Foundry-ZKsync. You can use it for testing your contracts from frontends or for interacting over RPC.
Anvil-ZKsync is part of the Foundry-ZKsync suite and is installed alongside forge
, and cast
. If you haven’t installed Foundry-ZKsync yet, see Foundry-ZKsync installation.
Note: If you have an older version of Foundry-ZKsync installed, you’ll need to re-install
foundryup-zksync
in order for Anvil-ZKsync to be downloaded.
How to use Anvil-ZKsync
To use Anvil-ZKsync, simply type anvil-zksync
. You should see a list of accounts and private keys available for use, as well as the address and port that the node is listening on.
Anvil-ZKsync is highly configurable. You can run anvil-zksync -h
to see all the configuration options.
Some basic options are:
# Number of dev accounts to generate and configure. [default: 10]
anvil-zksync -a, --accounts <ACCOUNTS>
# Port number to listen on. [default: 8545]
anvil-zksync -p, --port <PORT>
📚 Reference
See the
anvil-zksync
Reference for in depth information on Anvil-ZKsync and its capabilities.
Overview of Chisel
Chisel is an advanced Solidity REPL shipped with Foundry. It can be used to quickly test the behavior of Solidity snippets on a local or forked network.
Officially we do not support chisel for ZKsync related operations.
Configuring with foundry.toml
Forge can be configured using a configuration file called foundry.toml
, which is placed in the root of your project.
Configuration can be namespaced by profiles. The default profile is named default
, from which all other profiles inherit. You are free to customize the default
profile, and add as many new profiles as you need.
Additionally, you can create a global foundry.toml
in your home directory.
Let’s take a look at a configuration file that contains two profiles: the default profile, which always enables the optimizer, as well as a CI profile, that always displays traces:
[profile.default]
optimizer = true
optimizer_runs = 20_000
[profile.ci]
verbosity = 4
When running forge
, you can specify the profile to use using the FOUNDRY_PROFILE
environment variable.
Standalone sections
Besides the profile sections, the configuration file can also contain standalone sections ([fmt]
, [fuzz]
, [invariant]
etc). By default, each standalone section belongs to the default
profile.
i.e. [fmt]
is equivalent to [profile.default.fmt]
.
To configure the standalone section for different profiles other than default
, use syntax [profile.<profile name>.<standalone>]
.
i.e. [profile.ci.fuzz]
.
📚 Reference
See the
foundry.toml
Reference for a complete overview of what you can configure.
Creating an NFT with Solmate
This tutorial will walk you through creating an OpenSea compatible NFT with Foundry ZKsync and Solmate. A full implementation of this tutorial can be found here.
This tutorial is for illustrative purposes only and provided on an as-is basis. The tutorial is not audited nor fully tested. No code in this tutorial should be used in a production environment.
Create project and install dependencies
Start by setting up a Foundry project following the steps outlined in the Getting started section. We will also install Solmate for their ERC721 implementation, as well as some OpenZeppelin utility libraries. Install the dependencies by running the following commands from the root of your project:
forge install transmissions11/solmate Openzeppelin/openzeppelin-contracts
These dependencies will be added as git submodules to your project.
Implement a basic NFT
Next, we will remove the boilerplate contracts found in src/Counter.sol
, test/Counter.t.sol
, and script/Counter.s.sol
. After that, create a new file in the src/
directory named NFT.sol
and replace its content with the following code:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;
import {ERC721} from "solmate/tokens/ERC721.sol";
import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol";
contract NFT is ERC721 {
uint256 public currentTokenId;
constructor(
string memory _name,
string memory _symbol
) ERC721(_name, _symbol) {}
function mintTo(address recipient) public payable returns (uint256) {
uint256 newItemId = ++currentTokenId;
_safeMint(recipient, newItemId);
return newItemId;
}
function tokenURI(uint256 id) public view virtual override returns (string memory) {
return Strings.toString(id);
}
}
Let’s take a look at this very basic implementation of an NFT. We start by importing two contracts from our git submodules. We import solmate’s gas optimized implementation of the ERC721 standard which our NFT contract will inherit from. Our constructor takes the _name
and _symbol
arguments for our NFT and passes them on to the constructor of the parent ERC721 implementation. Lastly we implement the mintTo
function which allows anyone to mint an NFT. This function increments the currentTokenId
and makes use of the _safeMint
function of our parent contract.
Compile & deploy with forge
To compile the NFT contract run forge build --zksync
. You might experience a build failure due to wrong mapping:
Error:
Compiler run failed
error[6275]: ParserError: Source "lib/openzeppelin-contracts/contracts/contracts/utils/Strings.sol" not found: File not found. Searched the following locations: "/PATH/TO/REPO".
--> src/NFT.sol:5:1:
|
5 | import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol";
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
this can be fixed by setting up the correct remapping. Create a file remappings.txt
in your project and add the line
openzeppelin-contracts/=lib/openzeppelin-contracts/
(You can find out more on remappings in the dependencies documentation.
By default the compiler output will be in the zkout
directory. To deploy our compiled contract with Forge we have to set environment variables for the RPC endpoint and the private key we want to use to deploy.
Set your environment variables by running:
export RPC_URL=<Your RPC endpoint>
export PRIVATE_KEY=<Your wallets private key>
Once set, you can deploy your NFT with Forge by running the below command while adding the relevant constructor arguments to the NFT contract:
forge create NFT --rpc-url=$RPC_URL --private-key=$PRIVATE_KEY --constructor-args <name> <symbol> --zksync
If successfully deployed, you will see the deploying wallet’s address, the contract’s address as well as the transaction hash printed to your terminal.
Minting NFTs from your contract
Calling functions on your NFT contract is made simple with Cast, Foundry’s command-line tool for interacting with smart contracts, sending transactions, and getting chain data. Let’s have a look at how we can use it to mint NFTs from our NFT contract.
Given that you already set your RPC and private key env variables during deployment, mint an NFT from your contract by running:
cast send --rpc-url=$RPC_URL <contractAddress> "mintTo(address)" <arg> --private-key=$PRIVATE_KEY
Well done! You just minted your first NFT from your contract. You can sanity check the owner of the NFT with currentTokenId
equal to 1 by running the below cast call
command. The address you provided above should be returned as the owner.
cast call --rpc-url=$RPC_URL --private-key=$PRIVATE_KEY <contractAddress> "ownerOf(uint256)" 1
Extending our NFT contract functionality and testing
Let’s extend our NFT by adding metadata to represent the content of our NFTs, as well as set a minting price, a maximum supply and the possibility to withdraw the collected proceeds from minting. To follow along you can replace your current NFT contract with the code snippet below:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;
import {ERC721} from "solmate/tokens/ERC721.sol";
import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol";
import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol";
error MintPriceNotPaid();
error MaxSupply();
error NonExistentTokenURI();
error WithdrawTransfer();
contract NFT is ERC721, Ownable {
using Strings for uint256;
string public baseURI;
uint256 public currentTokenId;
uint256 public constant TOTAL_SUPPLY = 10_000;
uint256 public constant MINT_PRICE = 0.08 ether;
constructor(
string memory _name,
string memory _symbol,
string memory _baseURI
) ERC721(_name, _symbol) Ownable(msg.sender) {
baseURI = _baseURI;
}
function mintTo(address recipient) public payable returns (uint256) {
if (msg.value != MINT_PRICE) {
revert MintPriceNotPaid();
}
uint256 newTokenId = currentTokenId + 1;
if (newTokenId > TOTAL_SUPPLY) {
revert MaxSupply();
}
currentTokenId = newTokenId;
_safeMint(recipient, newTokenId);
return newTokenId;
}
function tokenURI(uint256 tokenId)
public
view
virtual
override
returns (string memory)
{
if (ownerOf(tokenId) == address(0)) {
revert NonExistentTokenURI();
}
return
bytes(baseURI).length > 0
? string(abi.encodePacked(baseURI, tokenId.toString()))
: "";
}
function withdrawPayments(address payable payee) external onlyOwner {
if (address(this).balance == 0) {
revert WithdrawTransfer();
}
(bool success, ) = payable(payee).call{value: address(this).balance}("");
require(success, "Transfer failed");
}
function _checkOwner() internal view override {
require(msg.sender == owner(), "Ownable: caller is not the owner");
}
}
Among other things, we have added metadata that can be queried from any front-end application like OpenSea, by calling the tokenURI
method on our NFT contract.
Note: If you want to provide a real URL to the constructor at deployment, and host the metadata of this NFT contract please follow the steps outlined here.
Let’s test some of this added functionality to make sure it works as intended. Foundry offers an extremely fast EVM native testing framework through Forge.
Within your test folder create the test file NFT.t.sol
. This file will contain all tests regarding the NFT’s mintTo
method. Next, replace the existing boilerplate code with the below:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;
import {Test} from "forge-std/Test.sol";
import {NFT} from "../src/NFT.sol";
contract NFTTest is Test {
using stdStorage for StdStorage;
NFT private nft;
function setUp() public {
// Deploy NFT contract
nft = new NFT("NFT_tutorial", "TUT", "baseUri");
}
function test_RevertMintWithoutValue() public {
vm.expectRevert(MintPriceNotPaid.selector);
// Make use of an address outside of the reserved address range
nft.mintTo(address(65536));
}
function test_MintPricePaid() public {
// Make use of an address outside of the reserved address range
nft.mintTo{value: 0.08 ether}(address(68536));
}
function test_RevertMintMaxSupplyReached() public {
uint256 slot = stdstore
.target(address(nft))
.sig("currentTokenId()")
.find();
bytes32 loc = bytes32(slot);
bytes32 mockedCurrentTokenId = bytes32(abi.encode(10000));
vm.store(address(nft), loc, mockedCurrentTokenId);
vm.expectRevert(MaxSupply.selector);
// Make use of an address outside of the reserved address range
nft.mintTo{value: 0.08 ether}(address(65536));
}
function test_RevertMintToZeroAddress() public {
vm.expectRevert("INVALID_RECIPIENT");
nft.mintTo{value: 0.08 ether}(address(0));
}
function test_NewMintOwnerRegistered() public {
// Make use of an address outside of the reserved address range
nft.mintTo{value: 0.08 ether}(address(68536));
uint256 slotOfNewOwner = stdstore
.target(address(nft))
.sig(nft.ownerOf.selector)
.with_key(address(1))
.find();
uint160 ownerOfTokenIdOne = uint160(
uint256(
(vm.load(address(nft), bytes32(abi.encode(slotOfNewOwner))))
)
);
assertEq(address(ownerOfTokenIdOne), address(68536));
}
function test_BalanceIncremented() public {
// Make use of an address outside of the reserved address range
nft.mintTo{value: 0.08 ether}(address(68536));
uint256 slotBalance = stdstore
.target(address(nft))
.sig(nft.balanceOf.selector)
.with_key(address(68536))
.find();
uint256 balanceFirstMint = uint256(
vm.load(address(nft), bytes32(slotBalance))
);
assertEq(balanceFirstMint, 1);
nft.mintTo{value: 0.08 ether}(address(68536));
uint256 balanceSecondMint = uint256(
vm.load(address(nft), bytes32(slotBalance))
);
assertEq(balanceSecondMint, 2);
}
function test_SafeContractReceiver() public {
Receiver receiver = new Receiver();
nft.mintTo{value: 0.08 ether}(address(receiver));
uint256 slotBalance = stdstore
.target(address(nft))
.sig(nft.balanceOf.selector)
.with_key(address(receiver))
.find();
uint256 balance = uint256(vm.load(address(nft), bytes32(slotBalance)));
assertEq(balance, 1);
}
function test_RevertUnSafeContractReceiver() public {
// Make use of an address outside of the reserved address range
// Ensure bytecode is divisible by 32
vm.etch(address(65538), bytes.concat(bytes("mock code"), new bytes(23)));
vm.expectRevert(bytes(""));
nft.mintTo{value: 0.08 ether}(address(65538));
}
function test_WithdrawalWorksAsOwner() public {
// Mint an NFT, sending eth to the contract
Receiver receiver = new Receiver();
address payable payee = payable(address(65539));
uint256 priorPayeeBalance = payee.balance;
nft.mintTo{value: nft.MINT_PRICE()}(address(receiver));
// Check that the balance of the contract is correct
assertEq(address(nft).balance, nft.MINT_PRICE());
uint256 nftBalance = address(nft).balance;
// Withdraw the balance and assert it was transferred
nft.withdrawPayments(payee);
assertEq(payee.balance, priorPayeeBalance + nftBalance);
}
function test_WithdrawalFailsAsNotOwner() public {
// Mint an NFT, sending eth to the contract
Receiver receiver = new Receiver();
nft.mintTo{value: nft.MINT_PRICE()}(address(receiver));
// Check that the balance of the contract is correct
assertEq(address(nft).balance, nft.MINT_PRICE());
// Confirm that a non-owner cannot withdraw
vm.expectRevert("Ownable: caller is not the owner");
vm.startPrank(address(65540));
nft.withdrawPayments(payable(address(65540)));
vm.stopPrank();
}
}
contract Receiver is ERC721TokenReceiver {
function onERC721Received(
address operator,
address from,
uint256 id,
bytes calldata data
) external override returns (bytes4) {
return this.onERC721Received.selector;
}
}
The test suite is set up as a contract with a setUp
method which runs before every individual test.
As you can see, Forge offers a number of cheatcodes to manipulate state to accommodate your testing scenario.
For example, our testFailMaxSupplyReached
test checks that an attempt to mint fails when the max supply of NFT is reached. Thus, the currentTokenId
of the NFT contract needs to be set to the max supply by using the store cheatcode which allows you to write data to your contracts storage slots. The storage slots you wish to write to can easily be found using the
forge-std
helper library. You can run the test with the following command:
forge test --zksync
If you want to put your Forge skills to practice, write tests for the remaining methods of our NFT contract. Feel free to PR them to nft-tutorial, where you will find the full implementation of this tutorial.
That’s it, I hope this will give you a good practical basis of how to get started with foundry. We think there is no better way to deeply understand solidity than writing your tests in solidity. You will also experience less context switching between javascript and solidity. Happy coding!
Deterministic deployment using CREATE2 on ZKsync
Introduction
Enshrined into the EVM as part of the Constantinople fork of 2019, CREATE2
is an opcode that started its journey as EIP-1014.
CREATE2
allows you to deploy smart contracts to deterministic addresses, based on parameters controlled by the deployer.
As a result, it’s often mentioned as enabling “counterfactual” deployments, where you can interact with an addresses that haven’t been created yet because CREATE2
guarantees known code can be placed at that address.
This is in contrast to the CREATE
opcode, where the address of the deployed contract is a function of the deployer’s nonce.
With CREATE2
, you can use the same deployer account to deploy contracts to the same address across multiple networks, even if the address has varying nonces.
ℹ️ Note This guide is intended to help understand
CREATE2
. In most use cases, you won’t need to write and use your own deployer, and can use an existing deterministic deployer (new MyContract{salt: salt}()
).
In this tutorial, we will:
- Look at a
CREATE2
factory implementation. - Deploy the factory using the traditional deployment methods.
- Use this deployed factory to in turn deploy a simple counter contract at a deterministic address.
- Simulate this set of events by writing a simple test using Foundry ZKsync.
Prerequisites
-
Some familiarity with Solidity and Foundry is required, and some familiarity with inline assembly is recommended. Refer to the official Solidity docs for a primer on inline assembly.
-
Make sure you have Foundry ZKsync installed on your system.
-
Initialize a new Foundry project.
-
Install the ZKsync contracts by running the following command in your project directory:
forge install matter-labs/era-contracts
CREATE2 Factory
Create a file named Create2ZK.sol
Inside the src
directory.
Initialize a contract named Create2ZK
like this:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Create2ZK {
error Create2FailedDeployment();
}
The error is meant to enforce some sanity checks on the factory deployment, and revert the whole transaction when triggered.
The Create2FailedDeployment()
error triggers if the deployment fails for any reason.
ℹ️ Note
Please note that a
CREATE2
deployment may fail due to a number of reasons. For example, if the bytecodeHash is invalid, or if a contract is already deployed at the computed address. Your deployment may also fail if your constructor reverts for any reason.
Next, create a function named deploy
:
function deploy(bytes32 salt, bytes32 bytecodeHash, bytes calldata inputData) external payable returns (address addr) {
}
This function takes 3 inputs:
- The
salt
used to calculate the final address. This can basically be any random value we want it to be. - The
bytecodeHash
of the contract that we want to deploy. - The
inputData
which are the constructor parameters of the contract.
The address of the newly deployed contract is the returned after a successful deploy.
ℹ️ Note
You can send ETH to a contract that is being deployed using
CREATE2
, but only if it has a payable constructor. If you try to send ETH to it without a payable constructor, the transaction will revert.
Next, we will call the create2
function from the ContractDeployer
system contract on ZKsync. This can be done by calling SystemContractsCaller.systemCallWithReturndata
to interact with system contracts:
To call the create2
function, we need to pass in 3 parameters:
(bool success, bytes memory returnData) = SystemContractsCaller
.systemCallWithReturndata(
uint32(gasleft()),
address(DEPLOYER_SYSTEM_CONTRACT),
uint128(0),
abi.encodeCall(
DEPLOYER_SYSTEM_CONTRACT.create2,
(
salt,
bytecodeHash,
inputData
)
)
);
- The salt: This is used to differentiate contract deployments and ensure unique contract addresses. It is a key part of the deterministic address generation in
CREATE2
. - The bytecodeHash: In ZKsync, contracts are deployed using the hash of the bytecode, not the bytecode itself.
- The inputData: This contains the constructor arguments for the contract being deployed. Similar to traditional contract deployment, this field passes the initialization data to the contract being deployed.
Alternatively, instead of writing your own deployment logic, you can leverage the CREATE2Factory.sol
system contract, which simplifies calling the create2
method. In many cases, you won’t need to manually write a deployer function since you can use existing deterministic deployers, such as the CREATE2Factory.sol
system contract, or deploy contracts directly using the new MyContract{salt: salt}()
syntax.
Here’s an example of how you can use the CREATE2Factory.sol
:
import {Create2Factory} from "era-contracts/system-contracts/contracts/Create2Factory.sol";
Create2Factory create2Factory = new Create2Factory();
address deployedAddress = create2Factory.create2(
salt,
bytecodeHash,
abi.encode()
);
This method allows you to deploy a contract deterministically without having to write the deployment logic from scratch. It handles the create2
call and returns the address of the newly deployed contract.
This approach simplifies the deployment process by using a pre-built deployer contract, making it easier to manage and reuse your deployment logic across different projects.
Finally, if the deployment fails for any reason, you can handle it by reverting the transaction, similar to how you would handle failure in the EVM:
if (!success) {
revert Create2FailedDeployment();
}
Computing the Contract Address on zkSync
Lastly, we will create a view function named computeAddress
. This function should take in the salt
, bytecodeHash
, and constructorInput
as parameters and return the address of the contract that would be deployed using the deploy
function on ZKsync:
function computeAddress(
address sender,
bytes32 salt,
bytes32 bytecodeHash,
bytes32 constructorInputHash
) external view returns (address addr) {
}
Inside the function, we’ll use the L2ContractHelper.computeCreate2Address
method, which follows the address calculation logic specific to ZKsync:
import {L2ContractHelper} from "era-contracts/l2-contracts/contracts/L2ContractHelper.sol";
function computeAddress(
address sender,
bytes32 salt,
bytes32 bytecodeHash,
bytes32 constructorInputHash
) external view returns (address addr) {
address computedAddress = L2ContractHelper.computeCreate2Address(
sender,
salt,
bytecodeHash,
constructorInputHash
);
}
Here’s the breakdown of the parameters and logic used in ZKsync’s CREATE2
address calculation:
- Sender: This refers to the address of the contract (typically the factory contract) calling the
create2
function. - Salt: The
salt
is used to differentiate deployments and ensure unique contract addresses, just like in traditionalCREATE2
usage. - Bytecode Hash: In ZKsync, you must pass the hash of the contract bytecode. This hash must be known to the operator, as the actual bytecode is provided in the
factory_deps
field of the transaction. For more info on this refer to the docs here. - Constructor Input Hash: ZKsync requires the constructor input (or initialization) data to be hashed using
keccak256
. This hash is then included in the address derivation formula.
The ZKsync-specific address derivation formula differs slightly from Ethereum’s traditional CREATE2
:
bytes32 hash = keccak256(
bytes.concat(
CREATE2_PREFIX, // zkSync-specific prefix
bytes32(uint256(uint160(_sender))), // Address of the contract deployer
_salt, // Salt for the deployment
_bytecodeHash, // Hash of the bytecode
constructorInputHash // Hash of the constructor input data
)
);
ℹ️ Note
The prefix (
CREATE2_PREFIX
) is specific to ZKsync, helping avoid collisions with Ethereum’sCREATE2
opcode. Thekeccak256
function is used to compute the hash from these components, and the address is derived from this hash.
Finally, we will return the calculated address, ensuring it conforms to the ZKsync address derivation rules:
return address(uint160(uint256(hash)));
Formula Recap
The formula that ZKsync uses to calculate the contract address is:
keccak256(zksyncCreate2 ++ address ++ salt ++ keccak256(bytecode) ++ keccak256(constructorInput))[12:]
zksyncCreate2
is a ZKsync-specific prefix to avoid collisions.address
is the contract deployer’s address.salt
is the deployment salt.keccak256(bytecode)
is the hash of the contract bytecode.keccak256(constructorInput)
is the hash of the constructor data.
These values are concatenated and passed through keccak256
to produce a 32-byte hash, and the last 20 bytes are used as the deployed contract’s address.
ℹ️ Note
You can check out the complete code for this implementation here.
Testing our factory
Create a file named Create2ZK.t.sol
inside the test
directory. Initialize a contract named Create2ZKTest
like this:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {Counter} from "../src/Counter.sol";
import {ZKCreate2} from "../src/Create2zk.sol";
import {ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT} from "era-contracts/system-contracts/contracts/Constants.sol";
contract Create2ZKTest is Test {
}
Initialize the following state variables and the setUp()
function:
Create2ZK internal create2ZK;
Counter internal counter;
function setUp() public {
create2ZK = new Create2ZK();
counter = new Counter();
}
Deterministic Deployment Test
We’ll now create a function named testDeterministicDeployment()
to do the following:
- Deploy a new instance of the
ZKCreate2
contract. - Allocate 100 ETH to the deployer address, using the
vm.deal
cheat code, and impersonate this address with theprank
cheat code. - Set up the
salt
andbytecodeHash
parameters. - Use the
zkCreate2
contract to deploy theCounter
contract at a deterministic address using thecreate2
system contract. - Assert that the computed address is equal to the deployed address.
function testDeterministicDeployment() public {
address deployerAddress = address(create2ZK);
// Deal 100 ETH to the deployer address
vm.deal(deployerAddress, 100 ether);
vm.startPrank(deployerAddress);
// Set up salt and retrieve bytecode hash
bytes32 salt = "12345";
bytes32 bytecodeHash = ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT.getRawCodeHash(address(counter));
// Compute the expected address using ZKsync's specific `CREATE2` logic
address expectedAddress = zkCreate2.computeCreate2Address(
deployerAddress,
salt,
bytecodeHash,
keccak256(abi.encode()) // constructor input data hash
);
// Deploy the contract using the `ZKCreate2` contract
address deployedAddress = create2ZK.deploy(
salt,
bytecodeHash,
abi.encode() // constructor input data
);
vm.stopPrank();
// Log the computed and deployed addresses for debugging
console.log("Computed address:", expectedAddress);
console.log("Deployed address:", deployedAddress);
// Assert that the computed address matches the deployed address
assertEq(deployedAddress, expectedAddress);
}
Explanation
vm.deal
: This cheat code allocates 100 ETH to the deployer address, allowing it to fund contract deployments.vm.startPrank
: This makes the deployer address impersonate the caller for all subsequent calls, so we simulate real-world deployment scenarios.bytes32 salt
: The salt is used to ensure the deployed contract has a deterministic address.bytes32 bytecodeHash
: We retrieve the bytecode hash of theCounter
contract from theACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT
to pass it to the ZKsyncCREATE2
function.abi.encode()
: We use this to pass constructor input data, hashed usingkeccak256
.computeCreate2Address
: This function computes the expected address based on ZKsync’s deterministic address calculation forCREATE2
.deploy
: This deploys the contract using ZKsync’sContractDeployer
system contract.
Finally, we assert that the expected address matches the deployed address, ensuring that the contract was deployed deterministically.
Save all your files, and run the test using forge test --match-path test/Create2ZK.t.sol --zksync --enable-eravm-extensions -vvvv
.
Your test should pass without any errors.
Solidity Scripting
Introduction
Solidity scripting is a way to declaratively deploy contracts using Solidity, instead of using the more limiting and less user friendly forge create
.
Solidity scripts are like the scripts you write when working with tools like Hardhat; what makes Solidity scripting different is that they are written in Solidity instead of JavaScript, and they are run on the fast Foundry backend, which provides dry-run capabilities.
High Level Overview
forge script
does not work in a sync manner. First, it collects all transactions from the script, and only then does it broadcast them all. It can essentially be split into 4 phases:
- Local Simulation - The contract script is run in a local evm. If a rpc/fork url has been provided, it will execute the script in that context. Any external call (not static, not internal) from a
vm.broadcast
and/orvm.startBroadcast
will be appended to a list. - Onchain Simulation - Optional. If a rpc/fork url has been provided, then it will sequentially execute all the collected transactions from the previous phase here.
- Broadcasting - Optional. If the
--broadcast
flag is provided and the previous phases have succeeded, it will broadcast the transactions collected at step1
. and simulated at step2
.
Given this flow, it’s important to be aware that transactions whose behaviour can be influenced by external state/actors might have a different result than what was simulated on step 2
. Eg. frontrunning.
Set Up
Let’s try to deploy the NFT contract made in the solmate tutorial with solidity scripting. First of all, we would need to create a new Foundry project via:
forge init solidity-scripting
Since the NFT contract from the solmate tutorial inherits both solmate
and OpenZeppelin
contracts, we’ll have to install them as dependencies by running:
# Enter the project
cd solidity-scripting
# Install Solmate and OpenZeppelin contracts as dependencies
forge install transmissions11/solmate Openzeppelin/openzeppelin-contracts@v5.0.1
Next, we have to delete the Counter.sol
file in the src
folder and create another file called NFT.sol
. You can do this by running:
rm src/Counter.sol test/Counter.t.sol script/Counter.s.sol && touch src/NFT.sol && ls src
Once that’s done, you should open up your preferred code editor and copy the code below into the NFT.sol
file.
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.10;
import {ERC721} from "solmate/tokens/ERC721.sol";
import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol";
import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol";
error MintPriceNotPaid();
error MaxSupply();
error NonExistentTokenURI();
error WithdrawTransfer();
contract NFT is ERC721, Ownable {
using Strings for uint256;
string public baseURI;
uint256 public currentTokenId;
uint256 public constant TOTAL_SUPPLY = 10_000;
uint256 public constant MINT_PRICE = 0.08 ether;
constructor(
string memory _name,
string memory _symbol,
string memory _baseURI
) ERC721(_name, _symbol) Ownable(msg.sender) {
baseURI = _baseURI;
}
function mintTo(address recipient) public payable returns (uint256) {
if (msg.value != MINT_PRICE) {
revert MintPriceNotPaid();
}
uint256 newTokenId = ++currentTokenId;
if (newTokenId > TOTAL_SUPPLY) {
revert MaxSupply();
}
_safeMint(recipient, newTokenId);
return newTokenId;
}
function tokenURI(uint256 tokenId)
public
view
virtual
override
returns (string memory)
{
if (ownerOf(tokenId) == address(0)) {
revert NonExistentTokenURI();
}
return
bytes(baseURI).length > 0
? string(abi.encodePacked(baseURI, tokenId.toString()))
: "";
}
function withdrawPayments(address payable payee) external onlyOwner {
uint256 balance = address(this).balance;
(bool transferTx, ) = payee.call{value: balance}("");
if (!transferTx) {
revert WithdrawTransfer();
}
}
}
Now, let’s try compiling our contract to make sure everything is in order.
forge build --zksync
If your output looks like this, the contracts successfully compiled.
Deploying our contract
We’re going to deploy the NFT
contract to the ZKsync Sepolia testnet, but to do this we’ll need to configure Foundry ZKsync a bit, by setting things like a ZKsync Sepolia RPC URL, and the private key of an account that’s funded with ZKsync Sepolia Eth.
💡 Note: You can get some ZKsync Sepolia testnet ETH here .
Environment Configuration
Once you have all that create a .env
file and add the variables. Foundry automatically loads in a .env
file present in your project directory.
The .env file should follow this format:
ZKSYNC_SEPOLIA_RPC_URL=
PRIVATE_KEY=
We now need to edit the foundry.toml
file. There should already be one in the root of the project.
Add the following lines to the end of the file:
[rpc_endpoints]
zksync-sepolia = "${ZKSYNC_SEPOLIA_RPC_URL}"
This creates a RPC alias for ZKsync Sepolia.
Writing the Script
Next, we have to create a folder and name it script
and create a file in it called NFT.s.sol
. This is where we will create the deployment script itself.
The contents of NFT.s.sol
should look like this:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Script} from "forge-std/Script.sol";
import {NFT} from "../src/NFT.sol";
contract MyScript is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
NFT nft = new NFT("NFT_tutorial", "TUT", "baseUri");
vm.stopBroadcast();
}
}
Now let’s read through the code and figure out what it actually means and does.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
Remember even if it’s a script it still works like a smart contract, but is never deployed, so just like any other smart contract written in Solidity the pragma version
has to be specified.
import {Script} from "forge-std/Script.sol";
import {NFT} from "../src/NFT.sol";
Just like we may import Forge Std to get testing utilities when writing tests, Forge Std also provides some scripting utilities that we import here.
The next line just imports the NFT
contract.
contract MyScript is Script {
We have created a contract called MyScript
and it inherits Script
from Forge Std.
function run() external {
By default, scripts are executed by calling the function named run
, our entrypoint.
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
This loads in the private key from our .env
file. Note: you must be careful when exposing private keys in a .env
file and loading them into programs. This is only recommended for use with non-privileged deployers or for local / test setups. For production setups please review the various wallet options that Foundry supports.
vm.startBroadcast(deployerPrivateKey);
This is a special cheatcode that records calls and contract creations made by our main script contract. We pass the deployerPrivateKey
in order to instruct it to use that key for signing the transactions. Later, we will broadcast these transactions to deploy our NFT contract.
NFT nft = new NFT("NFT_tutorial", "TUT", "baseUri");
Here we have just created our NFT contract. Because we called vm.startBroadcast()
before this line, the contract creation will be recorded by Forge, and as mentioned previously, we can broadcast the transaction to deploy the contract on-chain. The broadcast transaction logs will be stored in the broadcast
directory by default. You can change the logs location by setting broadcast
in your foundry.toml
file.
The broadcasting sender is determined by checking the following in order:
- If
--sender
argument was provided, that address is used. - If exactly one signer (e.g. private key, hardware wallet, keystore) is set, that signer is used.
- Otherwise, the default Foundry sender (
0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38
) is attempted to be used.
Now that you’re up to speed about what the script smart contract does, let’s run it.
You should have added the variables we mentioned earlier to the .env
for this next part to work.
At the root of the project run:
# To load the variables in the .env file
source .env
# To deploy our contract
forge script --chain zksync-testnet script/NFT.s.sol:MyScript --rpc-url $ZKSYNC_SEPOLIA_RPC_URL --broadcast --zksync -vvvv
Forge is going to run our script and broadcast the transactions for us - this can take a little while, since Forge will also wait for the transaction receipts.
This confirms that you have successfully deployed the NFT
contract to the ZKsync Sepolia testnet.
💡 Note: A full implementation of this tutorial can be found here and for further reading about solidity scripting, you can check out the
forge script
reference. This confirms that you have successfully deployed theNFT
contract to the Sepolia testnet and have also verified it on Etherscan, all with one command.
Scripting with Arguments
Let’s enhance our script to accept arguments, making it more flexible and reusable. This approach allows us to deploy different NFT contracts with varying names, symbols, and base URIs without modifying the script each time. We’ll start by modifying the NFT.s.sol
script:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Script} from "forge-std/Script.sol";
import {NFT} from "../src/NFT.sol";
contract MyScript is Script {
function run(
string calldata _name,
string calldata _symbol,
string calldata _baseUri
) external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
NFT nft = new NFT(_name, _symbol, _baseUri);
vm.stopBroadcast();
}
}
At the root of the project run:
# To load the variables in the .env file
source .env
# To deploy and verify our contract
forge script --chain sepolia script/NFT.s.sol:MyScript "NFT tutorial" TUT baseUri --sig 'run(string,string,string)' --rpc-url $SEPOLIA_RPC_URL --broadcast --verify -vvvv
Let’s break down the additions to our command:
"NFT tutorial" TUT baseUri --sig 'run(string,string,string)'
"NFT tutorial"
- is the first argument of the new run command - the name of the collectionTUT
- is the second argument - the symbol of the collectionbaseUri
- is the third argument - the baseURI of the collection--sig 'run(string,string,string)'
- changes the signature of the function we want to call in the contract
Forge is going to run our script and broadcast the transactions using the parameters we specified on the command line. You should see an output similar to the previous section.
Deploying locally
You can deploy to Anvil, the local testnet, by configuring the port as the fork-url
.
Here, we have two options in terms of accounts. We can either start anvil without any flags and use one of the private keys provided. Or, we can pass a mnemonic to anvil to use.
Using Anvil’s Default Accounts
First, start Anvil:
anvil
Update your .env
file with a private key given to you by Anvil.
Then run the following script:
forge script script/NFT.s.sol:MyScript --fork-url http://localhost:8545 --broadcast
Using a Custom Mnemonic
Add the following line to your .env
file and complete it with your mnemonic:
MNEMONIC=
It is expected that the PRIVATE_KEY
environment variable we set earlier is one of the first 10 accounts in this mnemonic.
Start Anvil with the custom mnemonic:
source .env
anvil -m $MNEMONIC
Then run the following script:
forge script script/NFT.s.sol:MyScript --fork-url http://localhost:8545 --broadcast
💡 Note: A full implementation of this tutorial can be found here and for further reading about solidity scripting, you can check out the
forge script
reference.
References
anvil-zksync
NAME
anvil-zksync - Create a local testnet node for deploying and testing smart contracts. It can also be used to fork other ZK chains.
SYNOPSIS
anvil_zksync
[options]
DESCRIPTION
Create a local testnet node for deploying and testing smart contracts. It can also be used to fork other ZK chains.
This section covers an extensive list of information about Supported Transport Layers, Supported RPC Methods, Anvil-ZKsync flags and their usages. You can run multiple flags at the same time.
Mining Modes
Mining modes refer to how frequent blocks are mined using anvil-zksync. By default, it automatically generates a new block as soon as a transaction is submitted.
You can change this setting to interval mining if you will, which means that a new block will be generated in a given period of time selected by the user. If you want to go for this type of mining, you can do it by adding the --block-time <block-time-in-seconds>
flag, like in the following example.
# Produces a new block every 10 seconds
anvil-zksync --block-time 10
There’s also a third mining mode called never. In this case, it disables auto and interval mining, and mine on demand instead. You can do this by typing:
# Enables never mining mode
anvil-zksync --no-mining
Supported Transport Layers
HTTP and Websocket connections are supported. The server listens on port 8011 by default, but it can be changed by running the following command:
anvil-zksync --port <PORT>
Default CREATE2 Deployer
Anvil-ZKsync, when used without forking, includes the default CREATE2 deployer proxy at the address 0x0000000000000000000000000000000000010000
.
This allows you to test CREATE2 deployments locally without forking.
Supported RPC Methods
ANVIL Namespace
The anvil_*
namespace provides custom methods for advanced node manipulation and testing.
anvil_setMinGasPrice
Status: NOT IMPLEMENTED
Description: Set the minimum gas price for the node. Unsupported for ZKsync as it is only relevant for pre-EIP1559 chains.
anvil_setLoggingEnabled
Status: SUPPORTED
Description: Enables or disables logging.
anvil_snapshot
Status: SUPPORTED
Description: Snapshot the state of the blockchain at the current block.
anvil_revert
Status: SUPPORTED
Description: Revert the state of the blockchain to a previous snapshot.
anvil_setTime
Status: SUPPORTED
Description: Sets the internal clock time to the given timestamp.
anvil_increaseTime
Status: SUPPORTED
Description: Jump forward in time by the given amount of time, in seconds.
anvil_setNextBlockTimestamp
Status: SUPPORTED
Description: Sets the timestamp of the next block.
anvil_setBlockTimestampInterval
Status: SUPPORTED
Description: Sets a recurring interval for block timestamps in seconds.
anvil_removeBlockTimestampInterval
Status: SUPPORTED
Description: Removes the recurring block timestamp interval, if set.
anvil_getAutomine
Status: SUPPORTED
Description: Retrieves the current status of the automine feature.
anvil_setAutomine
Status: SUPPORTED
Description: Enables or disables the automine feature.
anvil_setIntervalMining
Status: SUPPORTED
Description: Configures the node to mine blocks at regular intervals specified in seconds.
anvil_autoImpersonateAccount
Status: SUPPORTED
Description: Sets auto impersonation status.
anvil_setNonce
Status: SUPPORTED
Description: Sets the nonce of an address.
anvil_impersonateAccount
Status: SUPPORTED
Description: Impersonate an account.
anvil_stopImpersonatingAccount
Status: SUPPORTED
Description: Stop impersonating an account after previously impersonating it.
anvil_reset
Status: SUPPORTED
Description: Resets the state of the network; cannot revert to past block numbers, unless they’re in a fork.
anvil_mine
Status: SUPPORTED
Description: Mines any number of blocks at once, in constant time.
anvil_setBalance
Status: SUPPORTED
Description: Modifies the balance of an account.
anvil_setCode
Status: SUPPORTED
Description: Sets the bytecode of a given account.
anvil_setStorageAt
Status: SUPPORTED
Description: Sets the storage value at a given key for a given account.
CONFIG Namespace
Configuration methods to adjust node settings dynamically.
config_getShowCalls
Status: SUPPORTED
Description: Gets the current value of show_calls
that’s originally set with the --show-calls
option.
config_getShowOutputs
Status: SUPPORTED
Description: Gets the current value of show_outputs
that’s originally set with the --show-outputs
option.
config_getCurrentTimestamp
Status: SUPPORTED
Description: Gets the value of current_timestamp
for the node.
config_setResolveHashes
Status: SUPPORTED
Description: Updates resolve-hashes
to call OpenChain for human-readable ABI names in call traces.
config_setShowCalls
Status: SUPPORTED
Description: Updates show_calls
to print more detailed call traces.
config_setShowOutputs
Status: SUPPORTED
Description: Updates show_outputs
to print calls outputs.
config_setShowStorageLogs
Status: SUPPORTED
Description: Updates show_storage_logs
to print storage log reads/writes.
config_setShowVmDetails
Status: SUPPORTED
Description: Updates show_vm_details
to print more detailed results from VM execution.
config_setShowGasDetails
Status: SUPPORTED
Description: Updates show_gas_details
to print more details about gas estimation and usage.
config_setLogLevel
Status: SUPPORTED
Description: Sets the logging level for the node and only displays the node logs.
config_setLogging
Status: SUPPORTED
Description: Sets the fine-tuned logging levels for the node and any of its dependencies.
DEBUG Namespace
Debugging tools to trace and inspect transactions and blocks.
debug_traceCall
Status: SUPPORTED
Description: Performs a call and returns structured traces of the execution.
debug_traceTransaction
Status: SUPPORTED
Description: Returns a structured trace of the execution of the specified transaction.
debug_traceBlockByHash
Status: SUPPORTED
Description: Returns structured traces for operations within the block of the specified block hash.
debug_traceBlockByNumber
Status: SUPPORTED
Description: Returns structured traces for operations within the block of the specified block number.
ETH Namespace
Standard Ethereum JSON-RPC methods.
eth_accounts
Status: SUPPORTED
Description: Returns a list of addresses owned by the client.
eth_chainId
Status: SUPPORTED
Description: Returns the currently configured chain ID (default is 260
).
eth_coinbase
Status: NOT IMPLEMENTED
Description: Returns the client coinbase address.
eth_estimateGas
Status: SUPPORTED
Description: Generates and returns an estimate of how much gas is necessary for the transaction to complete.
eth_feeHistory
Status: SUPPORTED
Description: Returns a collection of historical block gas data (hardcoded with gas price of 50_000_000
).
eth_gasPrice
Status: SUPPORTED
Description: Returns the current price per gas in wei (hardcoded to 50_000_000
).
eth_getBalance
Status: SUPPORTED
Description: Returns the balance of the account of given address.
eth_getBlockByHash
Status: SUPPORTED
Description: Returns information about a block by block hash.
eth_getBlockByNumber
Status: SUPPORTED
Description: Returns information about a block by block number.
eth_getBlockTransactionCountByHash
Status: SUPPORTED
Description: Number of transactions in a block matching the given block hash.
eth_getBlockTransactionCountByNumber
Status: SUPPORTED
Description: Number of transactions in a block matching the given block number.
eth_getCompilers
Status: NOT IMPLEMENTED
Description: Returns a list of available compilers.
eth_getProof
Status: NOT IMPLEMENTED
Description: Returns the account’s Merkle proof and storage values for specified storage keys.
eth_getStorageAt
Status: SUPPORTED
Description: Returns the value from a storage position at a given address.
eth_getTransactionByHash
Status: SUPPORTED
Description: Returns information about a transaction requested by transaction hash.
eth_getTransactionCount
Status: SUPPORTED
Description: Returns the number of transactions sent from an address.
eth_getTransactionByBlockHashAndIndex
Status: SUPPORTED
Description: Returns information about a transaction by block hash and transaction index.
eth_getTransactionByBlockNumberAndIndex
Status: SUPPORTED
Description: Returns information about a transaction by block number and transaction index.
eth_getTransactionReceipt
Status: SUPPORTED
Description: Returns the receipt of a transaction by transaction hash.
eth_getUncleByBlockHashAndIndex
Status: NOT IMPLEMENTED
Description: Returns information about an uncle of a block by hash and uncle index.
eth_getUncleByBlockNumberAndIndex
Status: NOT IMPLEMENTED
Description: Returns information about an uncle of a block by number and uncle index.
eth_getUncleCountByBlockHash
Status: NOT IMPLEMENTED
Description: Returns the number of uncles in a block matching the given block hash.
eth_getUncleCountByBlockNumber
Status: NOT IMPLEMENTED
Description: Returns the number of uncles in a block matching the given block number.
eth_getWork
Status: NOT IMPLEMENTED
Description: Returns mining-related data such as current block header pow-hash, seed hash, and target.
eth_hashrate
Status: NOT IMPLEMENTED
Description: Returns the number of hashes per second that the node is mining with.
eth_maxPriorityFeePerGas
Status: NOT IMPLEMENTED
Description: Returns a maxPriorityFeePerGas
value suitable for quick transaction inclusion.
eth_mining
Status: NOT IMPLEMENTED
Description: Returns true
if the client is actively mining new blocks.
eth_newBlockFilter
Status: SUPPORTED
Description: Creates a filter in the node to notify when a new block arrives.
eth_newFilter
Status: SUPPORTED
Description: Creates a filter object to notify when state changes (logs) occur.
eth_newPendingTransactionFilter
Status: SUPPORTED
Description: Creates a filter to notify when new pending transactions arrive.
eth_protocolVersion
Status: SUPPORTED
Description: Returns the current Ethereum protocol version.
eth_sendTransaction
Status: SUPPORTED
Description: Creates a new message call transaction or a contract creation.
eth_sendRawTransaction
Status: SUPPORTED
Description: Creates a new message call transaction or a contract creation for signed transactions.
eth_sign
Status: NOT IMPLEMENTED
Description: Calculates an Ethereum-specific signature.
eth_signTransaction
Status: NOT IMPLEMENTED
Description: Signs a transaction for later submission using eth_sendRawTransaction
.
eth_signTypedData
Status: NOT IMPLEMENTED
Description: Identical to eth_signTypedData_v4
.
eth_signTypedData_v4
Status: NOT IMPLEMENTED
Description: Returns a hex-encoded signature.
eth_submitHashrate
Status: NOT IMPLEMENTED
Description: Used for submitting mining hashrate.
eth_submitWork
Status: NOT IMPLEMENTED
Description: Used for submitting a proof-of-work solution.
eth_subscribe
Status: NOT IMPLEMENTED
Description: Starts a subscription to a particular event.
eth_syncing
Status: SUPPORTED
Description: Returns an object containing data about the sync status or false
when not syncing.
eth_uninstallFilter
Status: SUPPORTED
Description: Uninstalls a filter with the given ID.
eth_unsubscribe
Status: NOT IMPLEMENTED
Description: Cancels a subscription to a particular event.
HARDHAT Namespace
Custom methods provided by Hardhat for advanced testing and node manipulation.
hardhat_addCompilationResult
Status: NOT IMPLEMENTED
Description: Add information about compiled contracts.
hardhat_dropTransaction
Status: NOT IMPLEMENTED
Description: Remove a transaction from the mempool.
hardhat_impersonateAccount
Status: SUPPORTED
Description: Impersonate an account.
hardhat_getAutomine
Status: PARTIALLY SUPPORTED
Description: Currently always returns true
as anvil_zksync
by default mines new blocks with each transaction.
hardhat_metadata
Status: NOT IMPLEMENTED
Description: Returns the metadata of the current network.
hardhat_mine
Status: SUPPORTED
Description: Mine any number of blocks at once, in constant time.
hardhat_reset
Status: PARTIALLY SUPPORTED
Description: Resets the state of the network; cannot revert to past block numbers, unless they’re in a fork.
hardhat_setBalance
Status: SUPPORTED
Description: Modifies the balance of an account.
hardhat_setCode
Status: SUPPORTED
Description: Sets the bytecode of a given account.
hardhat_setCoinbase
Status: NOT IMPLEMENTED
Description: Sets the coinbase address.
hardhat_setLoggingEnabled
Status: NOT IMPLEMENTED
Description: Enables or disables logging in Hardhat Network.
hardhat_setMinGasPrice
Status: NOT IMPLEMENTED
Description: Sets the minimum gas price.
hardhat_setNextBlockBaseFeePerGas
Status: NOT IMPLEMENTED
Description: Sets the base fee per gas for the next block.
hardhat_setPrevRandao
Status: NOT IMPLEMENTED
Description: Sets the PREVRANDAO value of the next block.
hardhat_setNonce
Status: SUPPORTED
Description: Sets the nonce of a given account.
hardhat_setStorageAt
Status: SUPPORTED
Description: Sets the storage value at a given key for a given account.
hardhat_stopImpersonatingAccount
Status: SUPPORTED
Description: Stop impersonating an account after previously impersonating it.
EVM Namespace
Ethereum Virtual Machine manipulation methods for testing and development.
evm_addAccount
Status: NOT IMPLEMENTED
Description: Adds any arbitrary account.
evm_increaseTime
Status: SUPPORTED
Description: Jump forward in time by the given amount of time, in seconds.
evm_mine
Status: SUPPORTED
Description: Force a single block to be mined.
evm_removeAccount
Status: NOT IMPLEMENTED
Description: Removes an account.
evm_revert
Status: SUPPORTED
Description: Revert the state of the blockchain to a previous snapshot.
evm_setAccountBalance
Status: NOT IMPLEMENTED
Description: Sets the given account’s balance to the specified WEI value.
evm_setAccountCode
Status: NOT IMPLEMENTED
Description: Sets the given account’s code to the specified data.
evm_setAccountNonce
Status: SUPPORTED
Description: Sets the given account’s nonce to the specified value.
evm_setAccountStorageAt
Status: NOT IMPLEMENTED
Description: Sets the given account’s storage slot to the specified data.
evm_setAutomine
Status: NOT IMPLEMENTED
Description: Enables or disables the automatic mining of new blocks with each transaction.
evm_setBlockGasLimit
Status: NOT IMPLEMENTED
Description: Sets the Block Gas Limit of the network.
evm_setIntervalMining
Status: NOT IMPLEMENTED
Description: Enables or disables automatic mining of blocks at regular intervals.
evm_setNextBlockTimestamp
Status: SUPPORTED
Description: Sets the timestamp of the next block.
evm_setTime
Status: SUPPORTED
Description: Sets the current timestamp for the node.
evm_snapshot
Status: SUPPORTED
Description: Snapshot the state of the blockchain at the current block.
evm_revert
Status: SUPPORTED
Description: Revert the state of the blockchain to a previous snapshot.
WEB3 Namespace
Standard Web3 methods.
web3_clientVersion
Status: SUPPORTED
Description: Returns the client version (e.g., zkSync/v2.0
).
ZKS Namespace
ZkSync-specific methods for enhanced functionality on the zkRollup.
zks_estimateFee
Status: SUPPORTED
Description: Gets the fee estimation data for a given request.
zks_estimateGasL1ToL2
Status: NOT IMPLEMENTED
Description: Estimate of the gas required for an L1 to L2 transaction.
zks_getAllAccountBalances
Status: SUPPORTED
Description: Returns all balances for confirmed tokens given by an account address.
zks_getBridgeContracts
Status: SUPPORTED
Description: Returns L1/L2 addresses of default bridges.
zks_getBlockDetails
Status: SUPPORTED
Description: Returns additional zkSync-specific information about the L2 block.
zks_getBytecodeByHash
Status: NOT IMPLEMENTED
Description: Returns bytecode of a transaction given by its hash.
zks_getConfirmedTokens
Status: SUPPORTED
Description: Returns [address, symbol, name, and decimal] information of all tokens within a range of IDs.
zks_getBaseTokenL1Address
Status: SUPPORTED
Description: Returns the L1 base token address (hard-coded to 0x0000000000000000000000000000000000000001
).
zks_getL1BatchBlockRange
Status: NOT IMPLEMENTED
Description: Returns the range of blocks contained within a batch given by batch number.
zks_getL1BatchDetails
Status: NOT IMPLEMENTED
Description: Returns data pertaining to a given batch.
zks_getL2ToL1LogProof
Status: NOT IMPLEMENTED
Description: Returns the proof for the corresponding L2 to L1 log.
zks_getL2ToL1MsgProof
Status: NOT IMPLEMENTED
Description: Returns the proof for the message sent via the L1Messenger system contract.
zks_getMainContract
Status: NOT IMPLEMENTED
Description: Returns the address of the zkSync Era contract.
zks_getRawBlockTransactions
Status: SUPPORTED
Description: Returns data of transactions in a block.
zks_getTestnetPaymaster
Status: NOT IMPLEMENTED
Description: Returns the address of the testnet paymaster.
zks_getTokenPrice
Status: SUPPORTED
Description: Gets the USD price of a token (ETH
is hard-coded to 1_500
, while some others are 1
).
zks_getTransactionDetails
Status: SUPPORTED
Description: Returns data from a specific transaction given by the transaction hash.
zks_L1BatchNumber
Status: NOT IMPLEMENTED
Description: Returns the latest L1 batch number.
zks_L1ChainId
Status: IMPLEMENTED
Description: Returns the chain ID of the underlying L1.
OPTIONS
General Options
-h, --help
Print help information.
-V, --version
Print version information.
--offline
Run in offline mode (disables all network requests).
--health-check-endpoint
Enable health check endpoint. It will be available for GET requests at /health
. The endpoint will return 200 OK
if the node is healthy.
--config-out <OUT_FILE>
Writes output of era-test-node
as JSON to the user-specified file.
Account Configuration
-a, --accounts <NUM>
Number of dev accounts to generate and configure. [default: 10]
--balance <NUM>
The balance of every dev account in Ether. [default: 10000]
-m, --mnemonic <MNEMONIC>
BIP39 mnemonic phrase used for generating accounts. Cannot be used if --mnemonic-random
or --mnemonic-seed-unsafe
are used.
--mnemonic-random [<MNEMONIC_RANDOM>]
Automatically generates a BIP39 mnemonic phrase and derives accounts from it. Cannot be used with other --mnemonic
options. You can specify the number of words you want in the mnemonic. [default: 12]
--mnemonic-seed-unsafe <MNEMONIC_SEED>
Generates a BIP39 mnemonic phrase from a given seed. Cannot be used with other --mnemonic
options. CAREFUL: This is NOT SAFE and should only be used for testing. Never use the private keys generated in production.
--derivation-path <DERIVATION_PATH>
Sets the derivation path of the child key to be derived. [default: m/44'/60'/0'/0/
]
--auto-impersonate
Enables automatic impersonation on startup. This allows any transaction sender to be simulated as different accounts, which is useful for testing contract behavior.
[aliases: auto-unlock
]
Network Options
--port <PORT>
Port to listen on. [default: 8011]
--host <IP_ADDR>
The IP address the server will listen on.
[env: ANVIL_ZKSYNC_IP_ADDR
]
[default: 127.0.0.1
]
--chain-id <CHAIN_ID>
Specify chain ID. [default: 260
]
Debugging Options
-d, --debug-mode
Enable default settings for debugging contracts.
--show-calls <SHOW_CALLS>
Show call debug information.
[possible values: none
, user
, system
, all
]
--show-outputs <SHOW_OUTPUTS>
Show call output information.
[possible values: true
, false
]
--show-storage-logs <SHOW_STORAGE_LOGS>
Show storage log information.
[possible values: none
, read
, write
, paid
, all
]
--show-vm-details <SHOW_VM_DETAILS>
Show VM details information.
[possible values: none
, all
]
--show-gas-details <SHOW_GAS_DETAILS>
Show gas details information.
[possible values: none
, all
]
--resolve-hashes <RESOLVE_HASHES>
If true
, the tool will try to resolve ABI and topic names for better readability. May decrease performance.
[possible values: true
, false
]
Gas Configuration
--l1-gas-price <L1_GAS_PRICE>
Custom L1 gas price (in wei).
--l2-gas-price <L2_GAS_PRICE>
Custom L2 gas price (in wei).
--l1-pubdata-price <L1_PUBDATA_PRICE>
Custom L1 pubdata price (in wei).
--price-scale-factor <PRICE_SCALE_FACTOR>
Gas price estimation scale factor.
--limit-scale-factor <LIMIT_SCALE_FACTOR>
Gas limit estimation scale factor.
System Configuration
--override-bytecodes-dir <OVERRIDE_BYTECODES_DIR>
Directory to override bytecodes.
--dev-system-contracts <DEV_SYSTEM_CONTRACTS>
Option for system contracts (default: built-in
).
[possible values: built-in
, local
, built-in-without-security
]
--emulate-evm
Enables EVM emulation. Requires local system contracts.
Logging Configuration
--log <LOG>
Log level (default: info
).
[possible values: trace
, debug
, info
, warn
, error
]
--log-file-path <LOG_FILE_PATH>
Log file path. [default: anvil_zksync.log
]
Cache Options
--cache <CACHE>
Cache type (none
, memory
, or disk
). [default: disk
]
[possible values: none
, memory
, disk
]
--reset-cache <RESET_CACHE>
Reset the local disk cache.
[possible values: true
, false
]
--cache-dir <CACHE_DIR>
Cache directory location for disk cache. [default: .cache
]
Commands
run
Starts a new empty local network.
fork
Starts a local network that is a fork of another network.
replay_tx
Starts a local network that is a fork of another network and replays a given transaction on it.
help
Print this message or the help of the given subcommand(s).
Command-Specific Options
fork
Command
--fork-url <FORK_URL>
Network to fork from (e.g., http://XXX:YY
, mainnet
, sepolia-testnet
).
--fork-block-number <BLOCK>
Fetch state from a specific block number over a remote endpoint.
--fork-transaction-hash <TRANSACTION>
Fetch state from a specific transaction hash over a remote endpoint.
See --fork-url
.
replay_tx
Command
<TX>
Transaction hash to replay.
Additional Options
--timestamp <NUM>
The timestamp of the genesis block.
--init <PATH>
Initialize the genesis block with the given genesis.json
file.
Usage Examples:
-
Start a new empty local network:
anvil-zksync run [OPTIONS]
-
Start a forked network:
anvil-zksync fork [OPTIONS] --fork-url <FORK_URL>
-
Replay a transaction on a forked network:
anvil-zksync replay_tx --fork-url <FORK_URL> <TX>
-
Display help information:
anvil-zksync --help
Shell Completions
anvil_zksync completions
shell
Generates a shell completions script for the given shell.
Supported shells are:
- bash
- elvish
- fish
- powershell
- zsh
EXAMPLES
- Generate shell completions script for zsh:
anvil-zksync completions zsh > $HOME/.oh-my-zsh/completions/_anvil_zksync
Using genesis.json
The genesis.json
file in Anvil serves a similar purpose as in Geth, defining the network’s initial state, consensus rules, and preallocated accounts to ensure all nodes start consistently and maintain network integrity. All values, including balance, gas limit and such, are to be defined as hexadecimals.
GENESIS BLOCK FIELDS
hash
: The hash of the block.parent_hash
: The hash of the parent block. All zeros for the genesis block since there is no parent.block_number
: The block number, with the genesis block being0
.timestamp
: The creation time of the genesis block in Unix time.l1_batch_env
: The environment configuration for the Layer 1 batch, containing:-
previous_batch_hash
:The hash of the previous batch.null
for the first batch. -
number
: The batch number. -
timestamp
: The timestamp of the batch in Unix time. -
fee_input
: Details of the fee inputs:PubdataIndependent
: Contains independent fee parameters:l1_gas_price
: The gas price for Layer 1 transactions.fair_l2_gas_price
: The fair gas price for Layer 2 transactions.fair_pubdata_price
: The fair pubdata price.
-
fee_account
: The address designated for fee collection. Defaults to0x0000000000000000000000000000000000000000
. -
enforced_base_fee
: The enforced base fee for transactions.null
if not enforced. -
first_l2_block
: Details of the first Layer 2 block:number
: The block number.timestamp
: The timestamp of the block in Unix time.prev_block_hash
: The hash of the previous block. All zeros for the genesis block.max_virtual_blocks_to_create
: The maximum number of virtual blocks that can be created.
-
transactions
: An array of transactions included in the block. Empty array for the genesis block.gas_used
: The total amount of gas used in the block.logs_bloom
: The bloom filter for logs in the block.
Example:
{
"hash": null,
"parent_hash": null,
"block_number": 0,
"timestamp": 1638316800,
"l1_batch_env": {
"previous_batch_hash": null,
"number": 0,
"timestamp": 1732653220,
"fee_input": {
"PubdataIndependent": {
"l1_gas_price": 1,
"fair_l2_gas_price": 2,
"fair_pubdata_price": 1000000000000000
}
},
"fee_account": "0x0000000000000000000000000000000000000000",
"enforced_base_fee": null,
"first_l2_block": {
"number": 0,
"timestamp": 1638316800,
"prev_block_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"max_virtual_blocks_to_create": 0
}
},
"transactions": [],
"gas_used": null,
"logs_bloom": null
}