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.
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
.
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.
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
When running tests, you can specifying verbosity by passing -v
. More v
s, 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!
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 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!
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
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.
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.
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.