Introduction
If you're like me, you probably like trying out different technologies just for the fun of it and maybe you've been hearing everyone talk about Foundry and you don't know what it is or have the time to research about it or even go through the foundry book. Well, if you fit into this category, this article is for you because I'll be talking about what foundry is, why I think you should use it and how you can use it.
What is Foundry?
According to the foundry book:
Foundry is a smart contract development toolchain.
Foundry manages your dependencies, compiles your project, runs tests, deploys, and lets you interact with the chain from the command line.
You might be thinking isn't that what hardhat and truffle already do? Yeah, you're right but Foundry does it faster and personally, I think the best thing about Foundry is the tests. Usually, when working with hardhat or truffle, during smart contracts development you need to write tests for your smart contracts and they're usually in JavaScript. Some may enjoy this but not everyone enjoys switching between languages as they work and let's not forget the fact that not everyone knows JavaScript.
Foundry takes a different approach with writing tests for smart contracts, tests are written in solidity and developers are given the ability to manipulate their test EVM environment to an unbelievable length and are also introduced to various types of tests, not just plain old unit tests. Foundry introduces smart contract developers to other types of testing such as Fuzzing, differential testing and other goodies that the Foundry team are still working on behind the scenes.
At this point, you might be thinking “yeah, it has fancy tests so what?”, whether you thought of that or not I would like to stress the importance of testing smart contracts. At this early stage of blockchain technology in general, we are faced with various issues and one of the most annoying ones is security exploits on smart contracts which leads to the loss of millions of dollars in various protocols. These exploits push the reality of mass adoption of blockchain technologies further and further away and as blockchain developers, we do not want that because definitely when the ecosystem grows, so do we and that's why it is important for you to have a tool like Foundry in your arsenal to help you test your smart contracts beyond what other existing tools provide.
Installation
So now that you understand the usefulness of Foundry, it's time you add it to your development toolkit.
On Linux and macOS
If you use Linux or macOS, you can get Foundry by the following the steps:
Install the latest release by using foundry up
This is the easiest option for Linux and macOS users.
Open your terminal and type in the following command:
curl -L https://foundry.paradigm.xyz | bash
This will download foundryup
. Then install Foundry by running:
foundryup
If everything goes well, you will now have three binaries at your disposal: forge, cast and anvil.
On Windows, build from source
If you use Windows, you need to build from source to get Foundry.
Download and run rustup-init
from rustup.rs. It will start the installation in a console.
After this, run the following to build Foundry from source:
cargo install --git https://github.com/foundry-rs/foundry foundry-cli anvil --bins --locked
To update from source, run the same command again.
Getting Started
Now that we have Foundry installed on our machine, let’s do something with it and I think building, testing, deploying and verifying an ERC20 contract with Foundry would not hurt.
We will start by creating a Foundry project, first, go into the directory you want to create your project in then run the following commands:
# creates foundry project
forge init hey-foundry
# navigates into the project
cd hey-foundry
# shows you the content of the project
tree -L 2
These commands should create a new Foundry project called hey-foundry
, enter the directory of the project then show you the contents of your project.
In our newly generated Foundry project, we have three directories and one toml
file which are:
src
test
script
lib
foundry.toml
Now let’s talk briefly about what each of them is for, the src
directory is where our smart contracts are kept, the test
directory is where we write our tests, the script
directory is where we store how solidity scripts, the lib
directory is similar to a node_modules
directory and it contains dependencies we install and finally we have the foundry.toml
file, which we use to configure the behaviour of our foundry project.
💡 Note: the src
, test
and script
directories both have one file each in them by default called Contract.sol
, Contract.t.sol
and Contract.s.sol
respectively. The lib
directory has a folder in it too called forge-std
.
Next, you should open the hey-foundry
project in your preferred code editor.
Instead of writing our own ERC20 contract from scratch, let’s use the OpenZeppelin implementation and what’s a better way to use their contracts than installing their contracts into our project, so we can import them for our usage. Usually, with other toolchains, we would need to use the Node Package Manager(NPM) to install the OpenZeppelin contracts but with Foundry, we have the privilege to use something that is faster and also has less baggage. To install the OpenZeppelin contracts into our project we would need to run the following command:
forge install OpenZeppelin/openzeppelin-contracts
If you check the lib
directory you should you should see the openzeppelin-contracts
folder.
Now open the src
directory and delete the Contract.sol
file and create a new file called MyToken.sol
, the contents of this file should look like this:
Now open the src
directory and rename the Contract.sol
file as MyToken.sol
then copy and paste the code below into it.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
address public owner;
constructor() ERC20("My Token", "MTKN") {
owner = msg.sender;
_mint(msg.sender, 1000 * 10**decimals());
}
function mint(address account, uint256 amount) public {
require(msg.sender == owner, "Only Owner can mint");
_mint(account, amount);
}
function burn(address account, uint256 amount) public {
require(msg.sender == owner, "Only Owner can burn");
_burn(account, amount);
}
}
In your code editor you should be getting an error on line 4 import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
, this is because your code editor cannot find the path to the file we want to import. We can fix this by running:
forge remappings > remappings.txt
This should create a remappings.txt file which contains the path to all the dependencies that we have installed in our project and we can use this file to direct other files that want to import our dependencies on where they can get it. The contents of this file should look like this:
openzeppelin-contracts/=lib/openzeppelin-contracts/
forge-std/=lib/forge-std/src/
ds-test/=lib/forge-std/lib/ds-test/src/
And remember our MyToken.sol
file is trying to import the OpenZeppelin ERC20 contract from @openzeppelin
which would work if we had installed the dependency with NPM but since we installed it with forge
we have to do a little tweaking. Don’t worry it’s not that complex all we have to do is change the line that has openzeppelin-contracts/=lib/openzeppelin-contracts/
in the remappings.txt
file to this:
@openzeppelin/=lib/openzeppelin-contracts/
If you followed the steps correctly, you should notice that the error has been resolved. This is not the only way to solve this error but for simplicity's sake, we’ll just go with this for now.
Testing our contract
Now that we are done writing our smart contract, it is ideal we test it. So as we did before, rename the Contract.t.sol
file as MyToken.t.sol
. Once you’re done copy and paste the following code into the file.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/MyToken.sol";
contract MyTokenTest is Test {
// Target contract
MyToken myToken;
// Actors
address owner;
address ZERO_ADDRESS = address(0);
address spender = address(1);
address user = address(2);
// Test params
string public name = "My Token";
string public symbol = "MTKN";
uint256 public decimals = 18;
uint256 amount = 1000 * 1e18;
uint256 public initialSupply = 1000 * 1e18;
event Transfer(address indexed from, address indexed to, uint256 value);
// ===== Set up =====
function setUp() public {
owner = address(this);
myToken = new MyToken();
}
// ===== Initial state =====
/**
* @dev Tests the relevant initial state of the contract.
*
* - Token name is 'My Token'
* - Token symbol is 'MTKN'
* - Token initail supply is '1000000000000000000000'
* - Token decimals is '18'
*/
function testinitialState() public {
// assert if the corrent name was used
assertEq(myToken.name(), name);
// assert if the correct symbol was used
assertEq(myToken.symbol(), symbol);
// assert if the correct initial supply was set
assertEq(myToken.totalSupply(), initialSupply);
// assert if the correct decimal was set
assertEq(myToken.decimals(), decimals);
}
// ===== Functionality tests =====
/// @dev Test `mint`
// Only Owner should be able to mint
function testFailUnauthorizedMinter() public {
vm.prank(user);
myToken.mint(user, amount);
}
// Should not be able to mint to the zero address
function testFailMintToZeroAddress() public {
vm.prank(owner);
myToken.mint(ZERO_ADDRESS, amount);
}
// Should increase total supply
function testIncreseTotalSupply() public {
uint256 expectedSupply = initialSupply + amount;
vm.prank(owner);
myToken.mint(owner, amount);
assertEq(myToken.totalSupply(), expectedSupply);
}
// Should increase recipient balance
function testIncreaseRecipientBalance() public {
vm.prank(owner);
myToken.mint(user, amount);
assertEq(myToken.balanceOf(user), amount);
}
// Should emit Transfer event
function testEmitTransferEventForMint() public {
vm.expectEmit(true, true, false, true);
emit Transfer(ZERO_ADDRESS, user, amount);
vm.prank(owner);
myToken.mint(user, amount);
}
}
Don’t worry if some of it seems weird to you now, by the end of the tutorial you should have a good understanding of what is happening in the code above but for now just know we’re writing unit tests for our MyToken.sol
contract.
We’ll now go through the code together line by line.
import "forge-std/Test.sol";
import "../src/MyToken.sol";
Over here, we import two files, the contract we want to test - MyToken.sol
and a contract containing utilities we’ll need while testing our contract - Test.sol
from the forge standard library.
contract MyTokenTest is Test {
We create a contract named MyTokenTest
and inherit the Test
contract from "forge-std/Test.sol"
.
// Target contract
MyToken myToken;
// Actors
address owner;
address ZERO_ADDRESS = address(0);
address spender = address(1);
address user = address(2);
// Test params
string public name = "My Token";
string public symbol = "MTKN";
uint256 public decimals = 18;
uint256 amount = 1000 * 1e18;
uint256 public initialSupply = 1000 * 1e18;
event Transfer(address indexed from, address indexed to, uint256 value);
Think of these guys as the variables we need to make our tests work.
// ===== Set up =====
function setUp() public {
owner = address(this);
myToken = new MyToken();
}
This is an important function that is used when writing Foundry tests, as the name implies we are setting up our test. We usually do things like deploying our contracts in it and that’s what we’re doing here, we’re also assigning an address to the owner
state variable. The address of the MyTokenTest
is set as the owner because it is the address that deploys it.
// ===== Initial state =====
/**
* @dev Tests the relevant initial state of the contract.
*
* - Token name is 'My Token'
* - Token symbol is 'MTKN'
* - Token initail supply is '1000000000000000000000'
* - Token decimals is '18'
*/
function testinitialState() public {
// assert if the corrent name was used
assertEq(myToken.name(), name);
// assert if the correct symbol was used
assertEq(myToken.symbol(), symbol);
// assert if the correct initial supply was set
assertEq(myToken.totalSupply(), initialSupply);
// assert if the correct decimal was set
assertEq(myToken.decimals(), decimals);
}
Over here we are testing for the initial state of the MyToken
contract we just deployed in the setUp
function. Since we assigned the deployed contract to the identifier myToken
, we can invoke functions in it by writing myToken.<function_name>()
.
ERC20 tokens usually have a name, symbol, initial supply and decimals, so here we’re trying to check if the correct ones were set. We do this by using the assertEq
function, which checks if the two parameters passed into it are equal.
💡 Note: We get the assertEq
function from the Test.sol
file we imported earlier.
// ===== Functionality tests =====
/// @dev Test `mint`
// Only Owner should be able to mint
function testFailUnauthorizedMinter() public {
vm.prank(user);
myToken.mint(user, amount);
}
// Should not be able to mint to the zero address
function testFailMintToZeroAddress() public {
vm.prank(owner);
myToken.mint(ZERO_ADDRESS, amount);
}
// Should increase total supply
function testIncreseTotalSupply() public {
uint256 expectedSupply = initialSupply + amount;
vm.prank(owner);
myToken.mint(owner, amount);
assertEq(myToken.totalSupply(), expectedSupply);
}
// Should increase recipient balance
function testIncreaseRecipientBalance() public {
vm.prank(owner);
myToken.mint(user, amount);
assertEq(myToken.balanceOf(user), amount);
}
// Should emit Transfer event
function testEmitTransferEventForMint() public {
vm.expectEmit(true, true, false, true);
emit Transfer(ZERO_ADDRESS, user, amount);
vm.prank(owner);
myToken.mint(user, amount);
}
In this part of the code, we test the mint
function for different cases in a series of functions. The comments I added at the top of each function should provide enough information about what they do, although the lines where I used vm.prank()
and vm.expectEmit()
may seem alien to you.
These two functions are examples of what are known as cheat codes in Foundry, we use them to manipulate the test EVM environment to procure real-life situations, and there are cheat codes to manipulate block.timestamp
, block.number
, the nonce and so much more.
So what are vm.prank()
and vm.expectEmit()
used for? vm.prank()
is used to change the msg.sender
of the next call and you pass the address you want the next call to use as msg.sender
into vm.prank
as a parameter while vm.expectEmit
on the other hand, helps us check if the next event emitted in a function is what we expect.
💡 For more information on Foundry cheat codes, check out this link. For more information on vm.prank()
and vm.expectEmit()
check out the following links respectively, vm.prank() and vm.expectEmit().
Now that you understand the code, let’s run it. We run tests in Foundry with the following command:
forge test
After running forge test
, you should see this in your terminal:
And with that, we’re done testing our contract. Congrats for making it this far, here have some coffee ☕.
Deploying and Verifying
Prior to now, we would run extensive commands in our terminal to deploy our contracts but recently the Foundry team introduced something phenomenal and that’s solidity scripting. If you have experience using hardhat you should be familiar with using scripts to deploy smart contracts, that’s practically the same thing we’re doing here but instead of writing our scripts in JavaScript, we write them in solidity instead.
Now, that we have the theoretical part out of the way let’s get coding. First of all, we would need to rename the Contract.s.sol
file that’s in the script
directory to MyToken.s.sol
.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Script.sol";
import "../src/MyToken.sol";
contract MyTokenScript is Script {
function run() external {
vm.startBroadcast();
MyToken myToken = new MyToken();
}
}
Our script is pretty similar to a normal solidity smart contract so it easy to understand. Basically, it is a contract that has one function called run
and that’s where we deploy our contract, the only thing that is alien here is vm.startBroadcast
. All vm.startBroadcast
does is create transactions that can later be signed and sent onchain.
Now that we have written our script, it is time we run this code so we can deploy our contract to the Rinkeby testnet but before we do that we have to create a .env
file storing our Rinkeby RPC URL, private key and Etherscan key . The .env
file should have the following variables provided:
RINKEBY_RPC_URL=
PRIVATE_KEY=
ETHERSCAN_KEY=
Next, we run the following commands :
# To give our shell access to our environment variables
source .env
# To deploy and verify our contract
forge script script/MyToken.s.sol:MyScript --rpc-url $RINKEBY_RPC_URL --private-key $PRIVATE_KEY --broadcast --verify --etherscan-api-key $ETHERSCAN_KEY -vvvv
Well done! You’ve just deployed the MyToken
contract to the Rinkeby network and that’s not all you did, you also verified the contract on Etherscan all with one command.
Conclusion
Although Foundry is a new tool and some developers might be sceptical to use it because of how comfortable they have become with previously existing tools if you’re one of them I would ask you to give Foundry a shot because Foundry is a tool that is beyond its time and is essential to the general advancement of the Smart contract development community.
In this tutorial, we only covered the surface of Foundry. To learn more about Foundry you could try going through the Foundry book and use Foundry more. Thanks for reading till next time Fren^-^.
Top comments (0)