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 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
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.
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
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
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
If you are expecting a failure, you can prefix the test name with
testFail rather than just
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.t.sol you could import this contract and write a test for it like this
When running tests, you can specifying verbosity by passing
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
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
forge test -vvvv and you should see
What’s going on here? Well, bar is requiring that
address(1) but current
msg.sender is just whatever address our test contract has. We can use
prank to call
forge test -vvvv
Now, we could also test for our expected revert using another cheat code,
expectRevert to the
and add a new test
forge test -vvvv
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
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
We can tell forge the correct place to look for this file by creating a
$ touch remappings.txt
and then in remappings.txt, put this line
This tells Forge, “Hey, anytime you hit
@openzepplin/, look in
Now if you run
forge build or
forge test, it should work fine.
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 the gas usage of your tests by running
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.