12.07.2021|Georgios Konstantopoulos
You should use Foundry’s tools, forge and cast, if you want the fastest & most flexible Ethereum development environment which works out of the box without configuration or third party libraries.
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.
Most developers still test Solidity using Javascript or Typescript, which is not great.
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:
contract Foo { uint256 public x = 1; function set(uint256 _x) external { x = _x; } function double() external { x = 2 * x; } } contract FooTest { Foo foo; // The state of the contract gets reset before each // test is run, with the `setUp()` function being called // each time after deployment. Think of this like a JavaScript // `beforeEach` block function setUp() public { foo = new Foo(); } // A simple unit test function testDouble() public { require(foo.x() == 1); foo.double(); require(foo.x() == 2); } // A failing unit test (function name starts with `testFail`) function testFailDouble() public { require(foo.x() == 1); foo.double(); require(foo.x() == 4); } }
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:
function testDoubleWithFuzzing(uint256 x) public { foo.set(x); require(foo.x() == x); foo.double(); require(foo.x() == 2 * x); }
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:
function testDoubleWithFuzzingCounterExample(uint256 x) public { foo.set(x); require(foo.x() == x); foo.double(); require(foo.x() == 4 * x); }
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:
address constant CHEATCODE_ADDRESS = 0x7cFA93148B0B13d88c1DcE8880bd4e175fb0DeDF; interace Vm { // Sets the block.timestamp to `x`. function warp(uint256 x) external; } contract MyTest { Vm vm = Vm(CHEATCODE_ADDRESS); function testWarp() public { vm.warp(100); require(block.timestamp == 100); } }
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 supports runtime debug logging with ds-test’s emit log_
functions, as well as Hardhat’s console.log
.
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 brew
and 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 forge build
.
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?
In Summer 2020, we started with writing ethers-rs, a Rust port of ethers.js, with the goal of helping MEV traders build better bots.
Then, we built other infrastructure like MEV Inspect, Ethers Fireblocks, Ethers Flashbots, Ark Circom, Optics and more.
Now, we have built a flexible compilation pipeline (ethers-solc which may support new languages like Fe), abstractions over the EVM (evm-adapters) and fast test runners.
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.
There are still many features we want to add (both to get to dapptools feature parity and more new exciting features). You should check out Foundry on Github.
Finally, we’re hiring both internally at Paradigm and across the portfolio - check out all our open roles at jobs.paradigm.xyz, or reach out to me at georgios@paradigm.xyz.
Copyright © 2024 Paradigm Operations LP All rights reserved. “Paradigm” is a trademark, and the triangular mobius symbol is a registered trademark of Paradigm Operations LP