Paradigm

Introducing the Foundry Ethereum development toolbox

Dec 07, 2021 | Georgios Konstantopoulos

Contents

I am excited to announce a project we have been developing for the last few months: Foundry.

Foundry is a portable, fast and modular toolkit for Ethereum application development.

Why Foundry?

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.

You should be writing your tests in Solidity

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);
  }
}

You should be fuzzing your functions

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).

You should be able to override VM state in your tests

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.

You should be able to run your tests against a live network’s state

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>].

You should be able to log debug information while running your tests

Forge supports runtime debug logging with ds-test’s emit log_ functions, as well as Hardhat’s console.log.

OK I’m sold, how do I start?

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.

How fast?

We have conducted benchmarks against some Dapptools repositories to compare the testing speed. Integration tests are also available here.

Project Forge Dapp Speedup
guni-lev 28.6s 2m36s 5.45x
solmate 6s 46s 7.66x
geb 11s 40s 3.63x
vaults 1.4s 5.5s 3.9x

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?

What’s the vision?

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.

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.

Written by:

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.