12.7.2021 | Georgios Konstantopoulos
Foundry is a portable, fast and modular toolkit for Ethereum application development.
Acknowledgement: Foundry is a reimplementation of the testing framework dapptools, written in Rust to be blazing fast, easy to install, and friendly to a wider set of contributors. While our codebase is not a fork (and has many additional features like supporting multiple solc versions), none of this would have been possible without the DappHub team’s innovative work over the years. Thank you DappHub!
If you agree with the below Ethereum development tips, then Foundry is for you.
Testing in JS requires a lot of boilerplate, large dependencies (I’m looking at you node_modules/), and config files. As an example, feel free to look at Paul Berg’s solidity-template.
In addition to that, Ethereum numbers in JS require using a BigNumber library such as bignumber.js, BigNumber, bn, or JS’s new native BigInt, which frequently cause incompatibility issues & productivity loss.
Finally, testing in JS instead of Solidity means that you operate 1 level of abstraction away from what you actually want to test, requiring you to be familiar with Mocha and Ethers.js or Web3.js at a minimum. This increases the barrier to entry for Solidity developers.
Forge lets you write your tests in Solidity, so you can focus on what matters: writing good tests.
A simple Solidity test would look like this:
Even if you unit test every single function in your code and try to get 100% test coverage, there may be edge cases you did not test for. Fuzzing lets the Solidity test runner choose the arguments for you randomly, by simply giving arguments to your Solidity test function.
Here’s an example of a fuzzed test for the above smart contract:
The fuzzer will automatically try this function with random values of x. If it finds an input that makes the test fail, it will return it to you, so you can create a regression test after fixing the bug:
If you ran this test, you’d get the below response in the CLI:
[FAIL. Counterexample: calldata=0x44735ef10000000000000000000000000000000000000000000000000000000000000001, args=[Uint(1)]] testDoubleWithFuzzingCounterExample (gas: [fuzztest])
It also supports shrinking, so that you get a “minimal” counterexample that causes your code to fail (instead of, say, a very large number or byte string).
Have you tried testing a function that requires a certain block number? Sure, you can call the RPC method evm_mine, but what if you’re testing a Compound Governance contract, and you need to advance 40,000 blocks?
Have you tried simulating a mainnet transaction and wanting to give your account a certain token balance, or write access to a permissioned function?
To solve these problems (and many more), we provide VM cheatcodes, which allow modifying the VM’s state at test runtime. This is exposed to the test author via a contract that lives at a pre-configured address. The below simple example shows how to override a block’s timestamp:
More information on the other cheatcodes can be found in the README. Cheatcodes are quite powerful (e.g. store lets you override an arbitrary contract storage slot, and prank lets you make an arbitrary call from an arbitrary account). We recommend spending time using them to extend the code paths your tests explore, and encourage contributing with new ones.
Like most Ethereum development tools, Forge supports “forking” against a remote network’s state by specifying a node URL (and optionally a block number if you have an archive node, for pinning your tests against a block). Just run
forge test --fork-url <your node url> [--fork-block-number <the block number you want>].
Forge and Cast can be installed by running
cargo install --git https://github.com/gakonst/foundry --locked (you can install Rust here if you haven’t already). We also plan to distribute statically built binaries per-platform, and provide
apt packages. If you’ve done automatic release flows for projects before, reach out!
Once installed, you just need to
forge init to create a new project (by default at the current directory) and then
That’s it. You’re started in <2s.
We have conducted benchmarks against some Dapptools repositories to compare the testing speed. Integration tests are also available here.
We also compiled openzeppelin-contracts with Forge and Hardhat. Hardhat compilation took 15.244s, whereas Forge took 9.449s. Another benchmark also showed promising (and nuanced!) results. Maybe there should be a benchmarking test suite for compilation & testing frameworks?
Gradually but surely, we are creating modular, well-documented & high performance building blocks for the next 1 million Ethereum developers and entrepreneurs.
For more information on how to use Foundry’s CLI, look in the README.
Acknowledgements: Matthias Seitz, Rohit Narurkar, Nick Ward, t11s, Odysseas Lamtzidis, Brock Elmore, Matt Solomon, and the rest of the ethers-rs / foundry group chats participants.
Disclaimer: This post is for general information purposes only. It does not constitute investment advice or a recommendation or solicitation to buy or sell any investment and should not be used in the evaluation of the merits of making any investment decision. It should not be relied upon for accounting, legal or tax advice or investment recommendations. This post reflects the current opinions of the authors and is not made on behalf of Paradigm or its affiliates and does not necessarily reflect the opinions of Paradigm, its affiliates or individuals associated with Paradigm. The opinions reflected herein are subject to change without being updated.