Foundry Primer
Foundry
is a portable, fast and modular toolkit for Ethereum application development.
It is a re-implementation of Dapptools in Rust (we like the crab ๐ฆ) with no external dependencies and allows writing tests in Solidity directly.
Bye bye JavaScript/TypeScript ๐ !
๐ก Recommended read: The Foundry Book
Installation
First we will need to download and install Foundry
to our machine.
Follow these steps:
- (OPTIONAL) if
foundry|forge
was previously installed, make sure to delete the previous binaries or you might not get the latest update and it breaks stuff. e.g:rm -rf ~/.cargo/bin/cast && rm -rf ~/.cargo/bin/forge
on macOS. curl -L https://foundry.paradigm.xyz | bash
source ~/.(bash|zsh|...)rc
or start a new terminal instance- Run
foundryup
, that should install two binaries:forge
&cast
- Sanity check:
forge --version
, the output should contain a timestamp like this:forge 0.1.0 (98f0771 2022-02-25T00:42:54.200449+00:00)
Essential commands
forge
is the main CLI binary and takes in a bunch of subcommands. As with everything in Rust, documentation is well written and should be rather straightforward to follow. We will just go over the main commands that we will be using throughout the series.
forge init
: this will create a new directory with the default template structureforge build
: compiles all the smart contracts in the/src
directory.forge test
: runs all tests (by default) in the/src/test
directory.forge test -m ContractName -vvv
: only run tests which name matchesContractName
and print verbose information.
- (OPTIONAL)
forge remappings > remappings.txt
: generate the mappings for our editor (VSCode).
Default Project Layout
Most Foundry projects should have a similar structure, here is how the default tempalte looks like with annotations:
musanking hello_foundry (master)
โ tree
.
โโโ foundry.toml // Project configuration file.
โโโ lib // Contains libraries & dependencies installed with `forge install`.
โ โโโ ds-test // `ds-test` is the default test suite shipped with every Foundry project.
โ โโโ LICENSE
โ โโโ Makefile
โ โโโ default.nix
โ โโโ demo
โ โ โโโ demo.sol
โ โโโ src
โ โโโ test.sol
โโโ out // Generated after running `forge build`. Contains compiled contracts and Application Binary Interface (ABI).
โ โโโ Contract.sol
โ โ โโโ Contract.json
โ โโโ Contract.t.sol
โ โ โโโ ContractTest.json
โ โโโ test.sol
โ โโโ DSTest.json
โโโ remmapings.txt // Helps editors like VSCode to locate library imports.
โโโ src // Your smart contracts should go in this directory. `forge build` will compile this by default.
โโโ Contract.sol
โโโ test // Your tests should go in this directory. `forge test` will compile and run this by default.
โโโ Contract.t.sol // Naming convention for tests in Foundry.
6 directories, 9 files
- More information on the available settings for
foundry.toml
can be found here: onbjerg.github.io/foundry-book/reference/co...
This should be enough to get you started with Foundry. Now let's setup wargame!
Ethernaut
Ethernaut is a Web3/Solidity based wargame inspired in overthewire.org, to be played in the Ethereum Virtual Machine. Each level is a smart contract that needs to be 'hacked'.
The official game can be played over at ethernaut.openzeppelin.com. It runs on Etherereum testnet Rinkeby so instead, we will be deploying our own local instance.
Our setup will be based on the following two repository, credits to their respective authors:
- Official repository: github.com/OpenZeppelin/ethernaut
- Template repository: github.com/ciaranmcveigh5/ethernaut-x-foundry
Setting up the Project
At this point we could just fork Ciaran's repository and start hacking things up, which is what I initially did, but then I didn't really understand how it was designed.
So Instead we will be re-creating a similar structure of our own.
Installing libraries
Foundry provides an easy way to install libraries from GitHub repositories with the forge install <githubName/repository>
subcommand. For this project we will only use two:
openzeppelin-contracts
: a library for secure smart contract development. Build on a solid foundation of community-vetted code.forge-std
: leverages forge's cheatcodes to make writing tests easier and faster while improving UX.
To simplify imports, we will also edit the foundry.toml
file and generate mappings for VSCode
[default]
src = 'src'
out = 'out'
libs = ['lib']
remappings = [
'ds-test/=lib/ds-test/src/',
'forge-std/=lib/forge-std/src/',
'openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/',
]
# See more config options https://github.com/gakonst/foundry/tree/master/config
Then run forge remapping > remappings.txt
(make sure the file is in the base directory alongside /src
Game Directory Layout
musanking ethernaut-foundry/src (main)[โ!?โก]
โ tree
.
โโโ core
โ โโโ BaseLevel.sol
โ โโโ Ethernaut.sol
โโโ levels
โ โโโ 01-Fallback
โ โโโ Fallback.sol
โ โโโ FallbackFactory.sol
โโโ test
โโโ 01-Fallback.t.sol
4 directories, 5 files
The
/core
directory contains the game's main logic inEthernaut.sol
taken from OpenZeppelin's Github and updated to Solidity v0.8.10.Each level is composed of two contracts:
<LevelName>.sol
which contains the main logic.<LevelName>Factory.sol
which extends the game'sBaseLevel
and helps us create level instances as well as validate win conditions.
The solution of each level is represented as a test file in the
src/test
directory and follow Foundry's name convention<LevelName>.t.sol
How to Write a Test
All of our tests will follow the same template:
- Initiate the main Ethernaut contract by calling the
setUp()
function. - Create a level instance using that calls the appropriate Factory function.
- Program the attack logic to solve the challenge.
- Submit the solution and check the result.
๐ก More information about the ds-test
library can be found in the Foundry Book
pragma solidity ^0.8.10;
import "ds-test/test.sol";
import "forge-std/Vm.sol";
import "../levels/01-Fallback/FallbackFactory.sol";
import "../core/Ethernaut.sol";
contract FallbackTest is DSTest {
Vm vm = Vm(address(HEVM_ADDRESS));
Ethernaut ethernaut;
address eoaAddress = address(1337);
function setUp() public {
ethernaut = new Ethernaut();
vm.deal(eoaAddress, 1 ether);
}
function testFallbackHack() public {
/////////////////
// LEVEL SETUP //
/////////////////
FallbackFactory fallbackFactory = new FallbackFactory();
ethernaut.registerLevel(fallbackFactory);
vm.startPrank(eoaAddress);
address levelAddress = ethernaut.createLevelInstance(fallbackFactory);
Fallback ethernautFallback = Fallback(payable(levelAddress));
//////////////////
// LEVEL ATTACK //
//////////////////
// (...)
//////////////////////
// LEVEL SUBMISSION //
//////////////////////
bool levelSuccessfullyPassed = ethernaut.submitLevelInstance(
payable(levelAddress)
);
vm.stopPrank();
assert(levelSuccessfullyPassed);
}
}
This should be enough to get us started, make sure to follow each step and everything should work out fine. If you have any problem, drop a comment below and I'll try to help.