Guide to Foundry (and how to write Solidity tests with it)
A guide to using Foundry and how to write tests for your Solidity contracts with it
Table of Contents for Guide to Foundry (and how to write Solidity tests with it)
How to install and set up Forge
It is best to check out the official guide on the foundry docs. Once you install it return here for a guide on setting the rest of foundry up.
How to set up a new project in Forge
You can start a new empty project with this command:
forge init your_project_name
Quick start guide/Common commands
- Set up a new project with
forge init
- Build Solidity files with
forge build
- Test your files with
forge test
Most of this guide will focus on the tests, as that is the main feature of Forge.
The tests for your Solidity code are also written asSolidity contracts. Forge will call each of the functions in those test files, and if they revert the test fails. Otherwise, it classes the test as passed.
There are other helper functions built in. Here are some of the most common/important helpers:
- adding support for
console.log(...)
(you have to runforge test --vv
to see the output). Very useful to debug things.
Writing tests in Forge
Your test files use the Forge’s standard library which can be found on https://github.com/foundry-rs/forge-std. You can import it by adding import "forge-std/Test.sol";
at the top of your test files.
Here is a simple example of a test file:
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/Counter.sol";
contract CounterTest is Test {
Counter public counter;
function setUp() public {
counter = new Counter();
counter.setNumber(0);
}
function testIncrement() public {
counter.increment();
assertEq(counter.number(), 1);
}
function testSetNumber(uint256 x) public {
counter.setNumber(x);
assertEq(counter.number(), x);
}
}
setUp
function
This is a function you can optionally add and if it is in the contract then it will be run before each test case.
(It is similar to Jest’s beforeEach()
)
Functions prefixed with test
(e.g. testSetNumber()
)
Functions with a test
prefix are the tests that are run automatically. If the function reverts when called, then it is seen as a failed test. Otherwise the test passes.
They must be either public
or external
. Functions prefixed with test
that are internal
or private
will not be run.
For example:
function testNumberIs42() public {
assertEq(testNumber, 42);
}
There is also testFail
prefix - where it will pass if the function call reverts. for example:
function testFailSubtract43() public {
testNumber -= 43;
}
It is also common in Forge tests to see things prefixed with testCannot
. There is no specific support for the ‘cannot’ word - but inside these tests you will see some set up to say it expects a revert.
function testCannotSubtract43() public {
vm.expectRevert(stdError.arithmeticError);
testNumber -= 43;
}
Note: the vm.expectRevert
is what is known as a cheat code. These will be explained later.
Get traces
Run your tests with -vvv
(or -vvvv
) to see trace output.
The output from the traces can be read like this:
[<Gas Usage>] <Contract>::<Function>(<Parameters>)
├─ [<Gas Usage>] <Contract>::<Function>(<Parameters>)
│ └─ ← <Return Value>
└─ ← <Return Value>
They are color coded:
- Blue: For calls to cheat codes
- Cyan: For emitted logs
- Green: For calls that do not revert
- Red: For reverting calls
- Yellow: For contract deployments
Forking blockchain with Foundry
Forking allows you to run tests in the context of the mainnet blockchain.
This means you can run scripts locally and they can interact with smart contracts on the mainnet, as if it was really connecting to mainnet. But of course, it is all simulated and just for your test environment.
This can be very useful when testing things out or running audits.
You can pick a specific block to fork to, and change what address msg.sender
is set to.
The cheatcode vm.warp(uint256 blockNum)
is used to change to that block. And you can use vm.prank(address someAddress)
to set msg.sender
to that address.
To use the warp feature you need to set up a RPC URL.
Then you can run this:
forge test --fork-url <your-rpc-url> -vv
When you warp it will also set:
- block_number
- chain_id
- gas_limit
- gas_price
- block_base_fee_per_gas
- block_coinbase
- block_timestamp
- block_difficulty
For more details see here.
Fuzz testing in Foundry
Fuzz testing is a way to automate some tests, by letting the test runner provide different values for some inputs to a functions.
You often use it to check that the state of the code being tested does not become invalid.
For example, if you were testing a function that takes in two parameters and adds them (function (a) { return a + a; }
), you might want to automatically run tests on various values for a
, and check they make sense (e.g. not result in a odd number, that the function runs and does not throw an error etc).
With fuzz testing you don’t really give an input and look for a specific output. It is more about checking that something “bad” doesn’t happen.
In Solidity you will often fuzz test and check balances are the same as they were at the start, or increased. It depends on the function you’re testing.
With Foundry it is very simple to fuzz test. Just write a test function, and add a param (e.g. function testSomething(uint256 someValue) ...
), and Foundry will run the test function multiple times with different values.
If you write a test like this:
contract SafeTest is Test {
// ...
function testWithdraw(uint256 amount) public {
payable(address(safe)).transfer(amount);
uint256 preBalance = address(this).balance;
safe.withdraw();
uint256 postBalance = address(this).balance;
assertEq(preBalance + amount, postBalance);
}
}
Foundry will see that this test function accepts a uint256
param, so it will try various values for this (such as 0
, 2^256
, and various numbers in between).
By default it will generate 256 ‘runs’ (each run has a different value) but this is configurable.
Differential fuzz testing
Differential testing is where you check that the outputs of two functions are the same.
For example, in JS if you had const f1 = x => x * 2
and const f2 = x => x + x
, you may want to run differential testing to ensure they always have the same output.
Diff fuzzing is like that, but it automatically generates different inputs to try and find differences in the two functions under test.
This is useful if you are upgrading/updating code, or have written a more optimized version but you want to verify it works the same way as the old or slower function.
Diff fuzz testing is easy to do in Foundry. See here for full docs.
Deploying with Forge
You can use the command line forge to deploy your contracts.
Example command to deploy it:
$ forge create --rpc-url <your_rpc_url> --private-key <your_private_key> src/MyContract.sol:MyContract
compiling...
success.
Deployer: 0xa735b3c25f...
Deployed to: 0x4054415432...
Transaction hash: 0x6b4e0ff93a...
You can also use the --verify
to automatically verify the contract once it is on the blockchain (this gets the smart contract source code to show up on Etherscan).
Get info about gas usage in your contracts
Tracking the amount of gas your smart contracts consume when run is very important.
You can do this in Foundry. Set the following in your foundry.toml
:
gas_reports = ["*"]
(You can also specify an array of specific contracts to generate gas reports for, with gas_reports = ["YourContract", "AnotherContract"]
)
Then run forge test --gas-report
and you will get the full gas report.
Using the Forge debugger
You can use the interactive debugger that comes with the forge CLI tool.
How to use it:
Run a command such as forge test --debug "testSomething()"
When the debugger is running, you can step through your code. here is a guide to using the debugger.
Cheatcodes
Cheatcodes are a concept in Forge that are testing specific helpers.
You’ve seen one in the example above - vm.expectRevert(stdError.arithemeticError)
. These are helpers you can run in Foundry tests, but of course they would fail to work if deployed to a real blockchain. Its just within the forge tests that it will work.
Cheatcodes are run by calling certain functions on the 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D
address.
You can also access this contract with the vm
variable (if you import the standard Forge Test contract with import "forge-std/Test.sol";
).
Examples:
Set the next call’s msg.sender
address with vm.prank(someAddress)
Set the current caller as address 0x000....
with vm.prank(address(0))
.
After calling that, msg.sender
will appear to come from 0x0000...
.
If you want to set msg.sender
for all subsequent calls (and not just the next one) you can use vm.startPrank(someAddr)
and stop it with vm.stopPrank()
.
(vm.stopPrank()
resets it back to address(this)
.)
You can also set a 2nd param when calling prank
or startPrank
and it will be used as the tx.origin
address, like this:
vm.prank(msgSenderAddr, txOriginAddr)
- or
vm.startPrank(msgSenderAddr, txOriginAddr)
Examples:
/// function withdraw() public {
/// require(msg.sender == owner);
vm.prank(owner);
myContract.withdraw(); // [PASS]
Set an account balance (ETH)
Use vm.deal(someAddress, 1000000)
to set the address of someAddress
to have balance of 1,000,000.
Assert that events were emitted with vm.expectEmit()
Use expect emit
to assert that events were emitted.
This works in an unusual way, with 3 main steps:
- You first call
vm.expectEmit()
(with some boolean arguments), - then you emit an event (of the same type that you are expecting)
- then you run the actual function you want to test
Foundry will then check that the emitted event (in step 2) matches an event that was emitted in step 3.
The boolean values passed to vm.expectEmit(true, true, true, true)
are either true or false, and if they are true then their topic must match the event you emitted (step 2). First topic is the event name btw.
Set block.timestamp with vm.warp(uint256)
If you need to change the block timestamp, call this in your Foundry tests:
vm.warp(1641080800);
emit log_uint(block.timestamp); // 1641080800
Load / Set storage data in a slot, at an address
You can load or set storage data at a specific slot at an address with vm.load()
and vm.store()
// Loads a storage slot from an address
function load(address account, bytes32 slot) external returns (bytes32);
// Stores a value to an address' storage slot
function store(address account, bytes32 slot, bytes32 value) external;
Convert normal Solidity types to strings
Sometimes when running foundry tests you want to see some data represented as a string. This can be useful when debugging. Here are some helper functions in Foundry:
function toString(address) external returns(string memory);
function toString(bytes calldata) external returns(string memory);
function toString(bytes32) external returns(string memory);
function toString(bool) external returns(string memory);
function toString(uint256) external returns(string memory);
function toString(int256) external returns(string memory);
Making assertions in Foundry
Assert booleans in Foundry
You can use assertTrue(val)
or assertFalse(val)
to check that a value is true or false.
bool someReturnValue = yourFunctionCall();
assertTrue(someReturnValue);
Assert two values are equal
If you want to assert two values (bools, bytes, int256 and uint256 types) are equal, use assertEq(valA, valB)
.
vm.expectEmit() (cheatcodes)
Explained in more detail above, but you can use vm.expectEmit(...)
to check that events were emitted.
You first call expectEmit
, passing in some booleans. Each boolean corresponds to the topics of an event that you want to check match an example event that you manually emit straight after calling expectEmit. Then you run your function you are testing, and it will check that the function emits a similar event as your sample one. Its a bit unusual the first few times using it, but it makes sense quite quickly.
Example from docs:
event Transfer(address indexed from, address indexed to, uint256 amount);
function testERC20EmitsTransfer() public {
// Only `from` and `to` are indexed in ERC20's `Transfer` event,
// so we specifically check topics 1 and 2 (topic 0 is always checked by default),
// as well as the data (`amount`).
vm.expectEmit(true, true, false, true);
// We emit the event we expect to see.
emit MyToken.Transfer(address(this), address(1), 10);
// We perform the call.
myToken.transfer(address(1), 10);
}
vm.expectCall() (cheatcodes)
Use this cheatcode to check that a specific function (or calldata) was called on a specific address. Note : this only works for external calls, not internal calls.
Example from Foundry docs:
address alice = address(10);
vm.expectCall( address(token), abi.encodeCall(token.transfer, (alice, 10)));
token.transfer(alice, 10);
vm.expectRevert() (cheatcodes)
After calling vm.expectRevert()
, the next call should revert. If it does not, the test fails.
You can pass in a msg
param to vm.expectRevert(bytes("some error msg"))
to assert that a specific revert message was thrown.
Example
function testACoupleOfReverts() public {
// assertion 1:
vm.expectRevert(abi.encodePacked("NO_AMOUNT"));
vault.send(user, 0);
// assertion 2:
vm.expectRevert(abi.encodePacked("NO_ADDRESS"));
vault.send(address(0), 200);
}
Using Cast
You can use the cast
cli tool (included with Foundry) to make RPC calls to the Ethereum blockchain.
It can be used for things like:
- send transactions (on mainnet & testnet)
- make contract calls
- retrieve information from the blockchain
- and lots of helpers, such as ways to decode calldata
Here is an example command to get the total supply from the DAI smart contract:
cast call 0x6b175474e89094c44da98b954eedeac495271d0f "totalSupply()(uint256)" --rpc-url https://eth-mainnet.alchemyapi.io/v2/<your-token-here>
- 0x6b175474e89094c44da98b954eedeac495271d0f is the address of the DAI contract
"totalSupply()(uint256)"
is the contract function you want to call- and lastly the
--rpc-url
is the RPC provider, in this case Alchemy. You can also set these via config so you don’t have to put the url every time
Example of using cast to decode calldata. This will decode it via ABI-encoded calldata from https://sig.eth.samczsun.com. In the next example it shows the 1 match that 1F1F897F676d
could be - a function called fulfillRandomness
.
$ cast 4byte-decode 0x1F1F897F676d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e7
1) "fulfillRandomness(bytes32,uint256)"
0x676d000000000000000000000000000000000000000000000000000000000000
999
Here are some more commands you can use with Cast.
cast-publish
will publish a raw transaction to the network (link)cast-send
will sign and publish a transaction (link)cast gas-price
will get the current gas price. (link)cast balance
gets the balance of an account in wei (link)cast keccak
hashes data with kekkak256 (link)- and many more not listed here
It is a very useful command to have in your toolkit when working with EVM blockchains.
Full documentation for cast can be found on [https://book.getfoundry.sh/reference/cast/cast](the official site)
Using Anvil
You can run test files in Foundry without spinning up (or connecting to) an actual Ethereum blockchain. But sometimes you will want to test things in a frontend, or have another client interact with your smart contracts over RPC.
For that you can use Anvil to spin up a testnet.
Anvil is included when you install Foundry, and is easy to get set up.
# Number of dev accounts to generate and configure. [default: 10]
anvil -a, --accounts <ACCOUNTS>
# The EVM hardfork to use. [default: latest]
anvil --hardfork <HARDFORK>
# Port number to listen on. [default: 8545]
anvil -p, --port <PORT>
# Produces a new block every 10 seconds
anvil --block-time 10
# Enables never mining mode
anvil --no-mining
# Set the number of accounts to 15 and their balance to 300 ETH
anvil --accounts 15 --balance 300
# Choose the address which will execute the tests
anvil --sender 0xC8479C45EE87E0B437c09d3b8FE8ED14ccDa825E
# Change how transactions are sorted in the mempool to FIFO
anvil --order fifo
For full documentation please see here
Configuring Foundry
The most common way to set config for Foundry is with foundry.toml
Forge can be configured using a configuration file called foundry.toml, which is placed in the root of your project.
Configuration can be namespaced by profiles. The default profile is named default, from which all other profiles inherit. You are free to customize the default profile, and add as many new profiles as you need.
Additionally, you can create a global foundry.toml in your home directory.
Example:
[profile.default]
optimizer = true
optimizer_runs = 20_000
[profile.ci]
verbosity = 4
Using Foundry in CI tests (like Github actions)
You can easily set up Foundry to run tests on your Solidity code as part of CI/CD.
Here is an example Github actions workflow to run Foundry tests on every push to a github repo.
on: [push]
name: test
jobs:
check:
name: CryptoGuide.dev Foundry Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Installation
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Run foundry tests
run: forge test -vvv
Static analysis with Slither and Mythril
It is possible to use Slither to run static analysis in a Foundry project.
For more see slither.
You can also do it for mythril. Instructions can be found on the official Foundry docs site.
Using Hardhat with Foundry
You can easily use Hardhat with Foundry.
Hardhat uses Javascript to write tests for your Solidity contracts, but Foundry uses Solidity contracts to test your Solidity contracts.
While I am a big fan of foundry, there are times when you will be wishing you could use HH too. Its quite easy to get it set up.
this tends to be the best guide I’ve found.
Other notes
- Use the
.t.sol
suffix for your test files. For example, to testMyToken.sol
, the common naming convention for test files for foundry isMyToken.t.sol
. - If your test file is huge, its common to see test files split up, so you might have
MyToken.transfers.t.sol
,MyToken.owner.t.sol
etc. - Don’t put assertions in your
setUp
function. If they fail, it will not fail the test. - Its a common pattern in Foundry tests to organize your test files in the same order as your contract (so the functions at the start (top) of your contract are also the first tested). This can make auditing and reading the tests much easier.
- You can test internal functions with what is called a test harness. The harness contract is one that extends the contract you’re testing (“Contract under test” or “CuT”), and exposes the internal function that you want to test. The common naming pattern for these is
exposed_yourInternalFuntion()
.- Private functions cannot be easily tested in this way. You can either copy/paste them, or make them
internal
.
- Private functions cannot be easily tested in this way. You can either copy/paste them, or make them
Common issues with Forge
How to see console.log output in Forge?
- Make sure you import
import "forge-std/console.sol";
at the top of your Solidity files - Then add
console.log("something")
, or things such asconsole.logInt(1234);
- But if you run
forge test
you won’t see any output. To see logs when running forge test you have to:- run the test command in verbose mode (
forge test --vv
)
- run the test command in verbose mode (
You can also check out this guide on debugging console log output in forge
Running tests fail, and how to debug it
Sometimes it can be hard to figure out why a Forge test is failing. A solution I often resort to is to run it with more verbosity. forge test --vvv
or even forge test --vvvv
.
Difference between Foundry and Forge
- Foundry is the main test runner/development environment.
- Forge is the CLI tool (included in foundry)
This post is incomplete and a work-in-progress
I'll update it soon and flesh it out with
more
info!
Spotted a typo or have a suggestion to make this crypto dev article better? Please let me know!
Next post
Previous post
📙 Solidity Auditing online quiz
Learn how to audit smart contracts by looking at some example code and trying to find the bugs
⛽ Solidity Gas Optimizations Guide
How to optimize and reduce gas usage in your smart contracts in Solidity
🧪 Guide to testing with Foundry
Guide to adding testing for your Solidity contracts, using the Foundry and Forge tools
📌 Guide to UTXO
UTXO and the UTXO set (used by blockchains such as Bitcoin) explained
📐 Solidity Assembly Guide
Introduction guide to using assembly in your Solidity smart contracts
📦 Ethereum EOF format explained
Information explaining what the upcoming Ethereum EOF format is all about