Foundry is a Solidity Framework for building, testing, fuzzing, debugging and deploying Solidity smart contracts. In this Foundry tutorial we will cover the following:
- Introduction To Foundry [Video]
- Getting Started With Foundry
- Using Foundry With Hardhat
- Testing With Solidity
- Foundry Cheat Codes
- Deploy & Use A Contract
Introduction To Foundry [Video]
Smart contracts are most often written in Solidity but tested and deployed using Javascript frameworks such as Truffle or Hardhat. Foundry provides a suite of tools built in Rust which allow a blockchain developer to write tests in Solidity and deploy and interact with contracts via the command line.
Why foundry?
- Write unit tests in solidity rather than Javascript
- Faster compilation & testing
- Built in fuzzing
- Gas optimisation tools
- Mainnet forking
- Etherscan verification
- Hardware wallet compatibility
- Solidity scripting
- Blockchain state manipulation via cheatcodes
Getting Started With Foundry
Installation instructions
Update October 2023 – I put together a starter boilerplate which sets up foundry and hardhat combined repository so you can use both frameworks from a single code base. Instructions on how to install in the Readme
Install Foundry Manually
To get started we need to install the foundry package which requires rust. Here are the commands for linux, mac, windows and docker.
Linux/Mac:
curl -L https://foundry.paradigm.xyz | bash;
foundryup
Windows: (Requires Rust, install from https://rustup.rs/)
cargo install --git https://github.com/foundry-rs/foundry --bins --locked
Docker:
docker pull ghcr.io/foundry-rs/foundry:latest
First steps with foundry
The foundry package comes with three main command line functions:-
- forge – build compile test local smart contracts
- cast – execute on-chain transactions with deployed smart contracts
- anvil – local EVM execution client used for testing
To clone a repo from Github we can use the forge command:
forge install jamesbachini/myVault -hh
Here we are using the Github username and repository name with the –hh modifier which is used to migrate hardhat repositories.
We can also initialise a new repo with the name “myrepo” with:
forge init myrepo
We can then go ahead and compile and test the smart contracts
forge build
forge test
How To Setup Foundry With Hardhat
Update October 2023 – I put together a starter boilerplate which sets up foundry and hardhat combined repository so you can use both frameworks from a single code base. Instructions on how to install in the Readme
Modifying An Existing Repo
Assuming we’ve got Foundry installed using the above instructions we can use it alongside Hardhat.
Note that we need a repo set up for the current working directory so use git init if there isn’t one in place already.
Then copy and paste the following into a new foundry.toml file in the root directory for the repository
[default]
src = 'contracts'
test = 'test'
out = 'artifacts/contracts'
libs = ['lib']
This will set the directory structure to replicate Hardhats. Foundry tests can go in the standard test folder using the normal MyContract.t.sol naming structure.
From there we can use Foundry alongside Hardhat for testing and deploying. For me personally, I like Hardhat’s scripting environment (especially for complex deployments) but recognise the benefits of testing and fuzzing using Foundry. Having both applications available in the same repository provides the best of both worlds.
Testing With Solidity
Before we start writing unit tests we need to install the standard library
forge install foundry-rs/forge-std
We can then import this into our test file which will be the same name as our contract with .t.sol suffix. i.e. MyContract.t.sol
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
contract MyContractTest is Test {
testWhatever(uint256 var1) public {
uint256 var2 = 1;
assertEq(var1,var2);
}
}
Note that in the example above we have given the function name a prefix of test -> testWhatever() this will fail on any revert. We could also prefix testFail -> testFailWhatever() and this would require a revert to pass.
Also note in the example above we are passing in a uint256 variable to var1. This value will be fuzzed which means it will loop over the function with various unusual values to try and create an edge case that causes a revert. In this case any value that isn’t 1 will cause the test to fail due to the assertEq() function.
It is possible to create custom assertions as well for example:-
function myAssertion(uint a, uint b) {
if (a != b) {
emit log_string("a != b");
fail();
}
}
We can then use this assertion throughout the contract or build a library of custom assertions and import them similarly to how we imported the standard library earlier.
If the repository contains many different smart contracts it is possible to isolate a single contract and it’s dependencies using –match-contract and even a specific test using –match-test command line options.
forge test --match-test optionalSpecificTest --match-contract optionalSpecificContract
Alternatively we can use forge run to execute a single solidity “script”
forge run src/Contract.sol // run a single script
forge run src/Contract.sol --debug // open script in debugger
forge run src/Contract.sol --sig "foo(string)" "hi" // execute a function
Once we have found a bug we can use the -v command to increase verbosity and get more details.
debug with logs -vv
debug with traces for failing tests -vvv
debug with traces for all tests -vvvv
Traces are a really powerful way to take a closer look at what goes on as the smart contract executes. It’s a bit like having Tenderly.co on the command line.
There is also an interactive debugger which takes me right back 1990’s linux debuggers. It’s not something I’ve used much but there are more instructions here: https://book.getfoundry.sh/forge/debugger.html
forge test --debug "testSomething"
It is possible to fork a blockchain locally and then test our contracts with external smart contracts. This is useful if you want to interact with other defi protocols like Uniswap for example or execute stress tests using real market data.
forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/abc123alchmeyApiKey
Gas Optimisation
Contract gas reports on compile can be set up via the configuration report foundry.toml
gas_reports = ["MyContract", "MyContractFactory"]
Then execute the command with forge test –gas-report option.
One way to optimise functions is to use test contracts and take snapshots before and after modifications:-
forge snapshot --snap gas1.txt
// make some changes
forge snapshot --diff gas1.txt
This will provide the difference between the previous gas report and the current snapshot.
Security Analysis With Slither
I generally use Foundry alongside other security tools when doing reviews of existing code bases. Slither is by no means a simple fix when it comes to smart contract security but it is useful and provides automated checks for things like reentrancy bugs. To use slither I run it from WSL (Windows subsystem for linux) and it can be installed with the following commands (note 0.8.13 is the current version of solc used in the foundry demo contract. Change this to whatever version you have set in the Solidity file):
apt install python3-pip
pip3 install slither-analyzer
pip3 install solc-select
solc-select install 0.8.13
solc-select use 0.8.13
Then copy this into a file called slither.config.json in the main directory we are working from
{
"filter_paths": "lib",
"solc_remaps": [
"ds-test/=lib/ds-test/src/",
"forge-std/=lib/forge-std/src/"
]
}
Then run a test using
slither src/Contract.sol
There is more information on slither in the official readme: https://github.com/crytic/slither
Foundry Cheat Codes
Foundry has a set of cheat codes which can be used when testing to make modifications to the state of the blockchain. These can be executed directly to contract: 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D but more often they are executed via the standard library and vm object.
Essential Cheat Codes – Full list here: https://github.com/foundry-rs/forge-std/blob/master/src/Vm.sol
- vm.warp(uint256) external; Set block.timestamp
- vm.roll(uint256) external; Set block.height
- vm.prank(address) external; Sets the next calls msg.sender to be the input address
- vm.startPrank(address) external; Sets all subsequent calls msg.sender to be the input address
- vm.stopPrank() external; Resets subsequent calls msg.sender to be `address(this)`
- vm.deal(address, uint256) external; Sets an addresses balance, (who, newBalance)
- vm.expectRevert(bytes calldata) external; Expects an error on next call
- vm.record() external; Record all storage reads and writes
- vm.expectEmit(true, false, false, false); emit Transfer(address(this)); transfer(); Check event topic 1 is equal on both events
- vm.load(address,bytes32) external returns (bytes32); Loads a storage slot from an address
- vm.store(address,bytes32,bytes32) external; Stores a value to an addresses storage slot, (who, slot, value)
These can be used to alter the course of tests as in this example which tells the test suite to expect a standard arithmetic error on the call.
vm.expectRevert(stdError.arithmeticError);
Deploy & Use A Contract
Foundry can also be used to deploy and interact with smart contracts.
To deploy a contract we can use the following command:-
forge create --rpc-url https://mainnet.infura.io --private-key abc123456789 src/MyContract.sol:MyContract --constructor-args "Hello Foundry" "Arg2"
Note we shouldn’t use a hard coded private key when deploying in production. One option would be to use –ledger or –trezor to execute via a hardware wallet. Alternatively we can use environmental variables to store private keys.
Alternatively environmental variables to store private key:-
Linux/Mac:
–privateKey $privateKey
export privateKey=abc123
Windows Powershell:
–privateKey $env:privateKey
$env:privateKey = "0x123abc"
Private key should not contain the 0x prefix or you’ll get an error “Invalid character ‘x’ at position 1”
We can also use the forge command to verify our contract on etherscan which enables us to interact with it using Etherscan’s UI and Metamask.
forge verify-contract --chain-id 1 --num-of-optimizations 200 --constructor-args (cast abi-encode "constructor(string)" "Hello Foundry" --compiler-version v0.8.10+commit.fc410830 0xContractAddressHere src/MyContract.sol:MyContract ABCetherscanApiKey123
It is also possible to use forge to flatten a contract which includes external contract dependencies into a single file.
forge flatten --output src/MyContract.flattened.sol src/MyContract.sol
To generate an ABI the following command can be used.
forge inspect src/MyContract.sol abi
Note any ABI can be converted to an interface and used directly within solidity using:- https://gnidan.github.io/abi-to-sol/
If the contract has been verified we can also use the following command to generate an interface:
cast interface 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984
Interact With Cast
We can call a contract without providing credentials to request on-chain data. We can also provide credentials to send a transaction much like we would sign a transaction in metamask.
cast call 0xabc123 "totalSupply()(uint256)" --rpc-url https://eth-mainnet.alchemyapi.io
cast send 0xabc123 "mint(uint256)" 3 --rpc-url https://eth-mainnet.alchemyapi.io --private-key=abc123
Once the block is confirmed cast can also provide information on the transaction itself.
cast tx 0xa1588a7c58a0ac632a9c7389b205f3999b7caee67ecb918d07b80f859aa605fd
It is also possible to execute transactions via flashbots protect using the cast send with the –flashbots modifier.
And finally you may want to estimate a gas cost which can also be done using cast:-
cast estimate 0xabc123 "mint(uint256)" 3 --rpc-url https://eth-mainnet.alchemyapi.io --private-key=abc123
Foundry provides a fast, efficient framework for testing and auditing smart contracts.