Getting Started With Forge

Forge is an Ethereum development framework. You can use it to create Solidity projects, manage dependencies, run tests, and more. It is inspired by Dapp and has the important similarity that tests are written in Solidity. This is unlike other Ethereum development frameworks to date. It is written in Rust and is very fast.

This is a beginners guide. I will go over how to create a project, manage dependencies, and write tests. The intended audience is someone familiar with Solidity who wants to learn more about developing with Forge.

Getting Started

First, you need to install Foundry, which is a broader Ethereum toolkit that Forge lives within. I recommend checking the repo for the latest install instructions, but this is the current install command.

$ cargo install --git https://github.com/gakonst/foundry --bin forge --locked

Note that if you do not have Rust/Cargo installed, you will need to install that, first. See instructions here.

(Forgeup is a useful tool for pulling the latest Forge version or point to a specific branch.)

Next, create a folder to work in and init a project

$ mkdir forge-tutorial
$ cd forge-tutorial
$ forge init

Great! Now you should have two directories inside forge-tutorial: lib and src.

Lib is where all your installed dependencies will live. These are managed as git submodules. You’ll see in lib there is already ds-test which is a dependency installed by default. ds-test, from the creators of Dapp, has a contract with bunch of useful functions and events for testing. You can see the code on Github here.

Src is where your code will live. At the top level you’ll see Contract.sol and a test directory. In test is Contract.t.sol.

Testing

Since this document is geared towards those already familiar with Solidity, I am going focus on testing, as that is mainly what is unique about using Forge.

Getting started

Run forge test and you should see something like this.

The test is passing, and it tells you the amount of gas that test function used.

Let’s open up Contract.t.sol to see what is happening.

First thing you should notice is that we’re writing tests in Solidity! Many of us have gotten used to writing tests for Solidity in other languages, which is kind of odd when you think about it. Can you think of any other programming language that requires you to test your code in a different language? This is a big point for the creator of Forge: how can we make great Solidity developers if every Solidity developer also has to know these other languages?

Let’s get into what’s going on. First, we notice that the ds-test import at the top, and that the contract is inheriting from DSTest with is DSTest. This gives ContractTest access to all the handy testing functions/events in ds-test/test.sol, which I mentioned above. For example, the assertTrue function being used is defined in DSTest. I recommend taking a look at the test.sol in the ds-test folder to see all the different kinds of asserts available.

setUp a special function that will be called before any of the tests run. Modify the code slightly to see this at work.

If you run forge test this test should pass.

Change the 10 to 9

and run forge test and you should see.

Nice! You’re doing great. It failed, as expected. Note that test functions must have “test” in the name. If the function was just called example, it would not automatically run with forge test.

If you are expecting a failure, you can prefix the test name with testFail rather than just test.

If you run forge test, this should pass. Note this will work for reverts as well.

To be honest, I find the testFail pattern to be kind of odd (you know something failed, but not exactly what), will discuss a preferred option, expectRevert, in the Cheat Codes below.

To actually test your contract, first let’s add some code to Contract.sol

Then in Contract.t.sol you could import this contract and write a test for it like this

Verbosity, Logging, and Traces

When running tests, you can specifying verbosity by passing -v. More vs, the higher the verbosity, with 5 (-vvvvv) being the highest. Here’s what each level gets you

1: Default (what you’ve seen so far when running tests)2: print logs3: print test trace for failing tests4: always print test trace, print setup for failing tests5: always print test trace and setup

Let’s add a log line and run our tests with -vv to see it. I’m going to add an emit log_string to my code.

If you’re less familiar with Solidity, contracts can emit events. But where is the log_string event defined? In test.sol in the ds-test repo.

Run forge test -vv

Check out the other log events in test.sol and try some others!

Next, let’s pass -vvvv so we can see the traces from our tests. Run forge test -vvvv

Woah! Super cool, right? This is showing you that our test function testAddone calls to addOne, and that the addOne call used 717 gas and returned 3!

Fuzzing

A very cool feature of Forge is test fuzzing. Rather than specifying static inputs to a function, fuzzed tests give you random values of a particular type. For example, we could make testAddOne a fuzzed test like this by changing the function to take an argument, like this

If you run forge test, you should see

This is telling you it ran 256 times (each time with a random uint256 value for x), and that the mean gas across these runs was 2789 and the median was 2791.

(Something to note, as of writing this, there is an issue where if you have already run your tests/compiled your code and change a non-test contract and not the test contract, e.g. change just Contract.sol and run forge test, the tests will run as if you hadn’t made any changes to Contract.sol. To manage this, you can force a recompile with forge test --force.)

Cheat Codes

Cheat codes are the bread and butter of testing with forge. Cheat codes exist in Dapp and have been expanded in Forge. Cheat codes are being updated frequently, so check the README for the latest. Basically these a contract calls to a “VM” contract that cause the vm to modify its ordinary execution behavior. I’ll give a couple examples here.

First, let’s talk about the prank cheat code, which can be used to set msg.sender for the next call. If that doesn’t make sense, just keep reading and you’ll see what I meant.

First, I am going to add a dummy contract to the top of `Contract.t.sol`.

Next I’ll update my test contract to use Foo

Note, I could simplify this to

but I am trying to model the use of setUp , which is needed for more complex setup.

Now, we need to add our VM contract that will receive the cheat code calls.

The VM is always at this address. Where does it come from? address(bytes20(uint160(uint256(keccak256('hevm cheat code'))))) = 0x7109709ecfa91a80626ff3989d68f67f5b1dd12d. We also need to define a Vm interface so the compiler knows what methods we expect to be able to call. You can add as many of the cheat codes as you want, for now I’ll just add prank.

The Forge README has all of the cheat codes defined in a format that you can copy and paste to your own interface. Finally let’s update my test to be named testBar and to call `foo.bar()`My test file now looks like this

Run forge test -vvvv and you should see

What’s going on here? Well, bar is requiring that msg.sender be address(1) but current msg.sender is just whatever address our test contract has. We can use prank to call bar from address(1)

Run forge test -vvvv

Now, we could also test for our expected revert using another cheat code, expectRevert. Add expectRevert to the Vm interface

and add a new test

Run forge test -vvvv

Awesome!

Adding Dependencies

Let’s add say we want to use someone else’s contracts. Maybe Solmate from Rari. We can install by running forge install rari-capital/solmate. After running this command, you will see solmate has been added to lib.

I can import a specific contract like this

Remappings

Some of your contracts or the contracts you import may have imports in the NPM format, e.g. import "@openzeppelin/contracts/access/Ownable.sol; Let’s look at how to handle this.

First install the OpenZeppelin contracts forge install OpenZeppelin/openzeppelin-contracts .

Next, let’s just add that import line to our test file

Run forge build

We can tell forge the correct place to look for this file by creating a remappings.txt.

$ touch remappings.txt

and then in remappings.txt, put this line

@openzeppelin/=lib/openzeppelin-contracts/

This tells Forge, “Hey, anytime you hit @openzepplin/, look in lib/openzeppelin-contracts/ instead.”

Now if you run forge build or forge test, it should work fine.

Advanced testing

Matching flag

If you want to only run some tests, there is a handy flag -m, which will match to the test name. E.g. run forge test -vvvv -m Revert and any test with “Revert” in the function name will run!

Snapshot

Snapshot the gas usage of your tests by running forge snapshot.

Forking mode

Rather than starting from a blank state, you can run your tests with state seeded from a live Ethereum network. To do this, you need to pass an Ethereum node uri to your tests using the -f flag, i.e. forge test -f <uri>. (You can get such a URI from Alchemy or Infura.) This is super cool: In your tests you could call to an address, say the Mainnet USDC address, and get responses with live network state. This is especially useful for testing against contracts or states that might be particularly hard to mock.

Goodbye!

There is more to say, but it strikes me that this is plenty for now. Feel free to reach out to me directly with any questions.

Subscribe to Wilson
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.