A Starting Guide to Foundry - Ethereum Development Toolchain
Get up and running with the blazing-fast Ethereum development toolchain Foundry
Foundry is the new kid on the block. It's a new addition to Ethereum development tools like Truffle and Hardhat.
Foundry allows Ethereum developers to test, deploy and verify their smart contract projects. Scripting, running a local testnet, and sending transactions from CLI are also some of the features.
There are two features that put Foundry ahead of its competitors. Everything is written in Solidity and it's blazing fast thanks to the Rust code base.
In this guide, I'll walk you through the basics. Starting from installation to deployment, I'll cover every step and more for a successful project.
Enough into. Let's dive into it ๐คฟ
Installation
The easiest way to install Foundry on your system is using their installation script. Use the line below in your terminal to download and run the script. Afterward, follow the on-screen prompts, and you'll be set.
curl -L https://foundry.paradigm.xyz | bash
Creating a Project
Foundry provides a few CLI tools for different purposes as part of its toolset. forge
is the first one we'll interact with. Using forge
, we can create, test, build, and deploy our Solidity projects.
Run the below line in your terminal to create a project from scratch.
forge init hello-foundry
Running this line creates a directory called hello-foundry
, and the project template with example files.
.
โโโ foundry.toml
โโโ lib
โ โโโ forge-std
โโโ script
โ โโโ Counter.s.sol
โโโ src
โ โโโ Counter.sol
โโโ test
โโโ Counter.t.sol
foundry.toml
is the project configuration file. External dependencies are installed inside the lib
folder with git submodules. Our smart contracts live inside the src
folder. Scripts and tests are in the corresponding folders.
Configuring VS Code
A coding editor or an IDE is a must for any coding. I use VS Code for everything development related, and there are a few steps to have a good developer experience with Foundry projects.
The first step is installing the Solidity extension. It adds great features like syntax highlighting and code completion.
Next, we need to tell VS Code about our project settings. Instead of changing these settings globally, we'll include them inside the project. This way whoever gets the project uses the same settings as everybody. Create a .vscode/settings.json
file under the project root and include the following settings.
{
"solidity.compileUsingRemoteVersion": "v0.8.17",
"solidity.formatter": "forge",
"solidity.packageDefaultDependenciesContractsDirectory": "src",
"solidity.packageDefaultDependenciesDirectory": "lib",
"[solidity]": {
"editor.defaultFormatter": "JuanBlanco.solidity"
}
}
After that, we need to align the Solidity compiler version between VS Code and the project. Edit the foundry.toml
file to set the compiler version. Your foundry.toml
file should look like this.
[profile.default]
src = 'src'
out = 'out'
libs = ['lib']
solc = "0.8.17"
Lastly, we need to create a remappings.txt
file. It's a file containing aliases to modules. Instead of importing from absolute paths, we can use these aliases in our code. Running the following line in a terminal creates a remappings.txt
file under the project root that includes some remappings to the lib
folder.
forge remappings > remappings.txt
That's it. Now we are almost ready to code ๐งโ๐ป
Installing Dependencies
We'll create an NFT project for this guide. As a bonus, you'll be able to mint this NFT on the Polygon PoS chain for free! The NFT has an AI-generated cool art that represents your achievement in learning Foundry ๐
Back to our topic, the first step is installing the base ERC-721 contract from OpenZeppelin. As mentioned before, Foundry uses git submodules for dependency management as default. This means we can install any GitHub repository using the foundry install
command.
Run the below line in your terminal to install OpenZeppelin/openzeppelin-contracts package.
forge install OpenZeppelin/openzeppelin-contracts
If you see an error message saying this command needs clean working areas, just commit your changes before running it again.
After successfully running the command, forge
installs the package under the lib
folder and commit a message to record the changes.
Next, we need to create a remapping to import smart contracts from the installed package.
Open your remappings.txt
file and add the following line.
openzeppelin/=lib/openzeppelin-contracts/contracts
By doing this, we'll be able to import from the package using openzeppelin
remapping instead of writing the full path. Here is an example.
import "openzeppelin/token/ERC721/ERC721.sol"
Now we're ready to create our ERC-721 contract!
Writing Smart Contracts
There should be Counter*.sol
files under the (script,src,test)
folders. These are just example files created by the forge init
command. Feel free to delete these files as we don't need them.
For the writing part, create a src/ForgeMaster.sol
file and put the below ERC-721 code into it.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
import "openzeppelin/token/ERC721/ERC721.sol";
import "openzeppelin/access/Ownable.sol";
import "openzeppelin/utils/Counters.sol";
contract ForgeMaster is ERC721, Ownable {
using Counters for Counters.Counter;
string private __baseURI;
Counters.Counter private _tokenIdCounter;
constructor() ERC721("ForgeMaster", "FM") {}
function setBaseURI(string memory uri) public onlyOwner {
__baseURI = uri;
}
function _baseURI() internal view override returns (string memory) {
return __baseURI;
}
function safeMint(address to) public {
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(to, tokenId);
}
}
Don't worry if you don't understand the code much. It's a pretty standard NFT smart contract allowing anybody to mint an NFT. Since we already configured VS Code, we'll benefit from everything like syntax highlighting and code completion.
Testing
Smart contracts are immutable. Once deployed there is no going back. We need to make sure our written smart contract works as intended.
Tests are written in Solidity with Foundry. This is great because there is no need to depend on a client library. Import your smart contract into a test contract and it's ready!
The forge init
command installs a library called forge-std
(Forge Standard Library) by default. This is a utility library consisting of helpful contracts like Test.sol
.
Test.sol
brings a lot of helpers like assertions, conventions, cheatcodes, etc. We'll use it to test the basic functionality of our smart contract.
Create a test/ForgeMaster.t.sol
file and put the below code into it.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import "forge-std/Test.sol";
import "src/ForgeMaster.sol";
contract ForgeMasterTest is Test {
ForgeMaster forgeMaster;
address owner = makeAddr("owner");
address minter = makeAddr("minter");
function setUp() public {
vm.prank(owner);
forgeMaster = new ForgeMaster();
}
function testSafeMint() public {
vm.prank(minter);
forgeMaster.safeMint();
assertEq(forgeMaster.ownerOf(0), minter);
}
function testTokenURI() public {
vm.prank(minter);
forgeMaster.safeMint();
vm.prank(owner);
forgeMaster.setTokenURI("uri");
assertEq(forgeMaster.tokenURI(0), "uri");
}
function testContractURI() public {
vm.prank(owner);
forgeMaster.setContractURI("uri");
assertEq(forgeMaster.contractURI(), "uri");
}
}
In the code, we create a test contract called ForgeMasterTest
that inherits the Test.sol
from the forge-std
library. Next, we create an instance of our smart contract under the setUp
method. This method runs before every test case.
We create two addresses to represent the contract owner and a minter. vm.prank
changes the msg.sender
address for the next call. We make the owner
address the owner of the tested contract by using vm.prank(owner)
before creating a new instance inside the setUp
method.
Then we write our three test
cases with the test prefix. We test the minting functionality and URI methods of our contract.
Now, let's run our tests to see if we are ready to deploy. Run the below line in your terminal to start.
forge test
This will compile the project if not already and then run the tests. We'll see two passing tests. For me, the compilation took around 2.5 seconds, and the tests were 0.6 seconds. That's pretty fast!
After all this work it's finally time to deploy!
Deployment
We'll deploy our smart contract to Polygon Mumbai testnet. Foundry is EVM compatible. We are not limited to Ethereum. Also, it's much easier to find Mumbai MATIC than Goerli ETH ๐ You can try the official Polygon Faucet or Alchemy Faucet. If your MetaMask is not configured for it, go to Polygonscan Mumbai and click the Add Mumbai Network button in the footer.
Test and deployment commands compile the project automatically. For demonstration purposes, we'll clean the project and then compile it again.
Run the below lines one by one in your terminal to clean and then compile the project.
forge clean
forge build
forge clean
clears the cache and the build artifacts.
forge build
compiles the project. It creates a folder called out
in the project root and outputs all the compiled code. It also creates a cache
folder for incremental compilation. This way when we change our code it will compile only what's necessary instead of everything all over again.
Next, we'll create a .env
file to make everything easier. Create a .env
file in the project root and fill it with the below variables.
CHAIN=polygon-mumbai
ETH_RPC_URL=https://matic-mumbai.chainstacklabs.com
Next, we'll use the forge create
command to deploy our smart contract to Polygon Mumbai. Before deployment, you'll need your wallet address private key with some Mumbai MATIC in it. Follow this MetaMask guide to learn how to get your private key.
PS: Keep your private key safe. Don't share it with anybody. Don't use your main account for testing. Create a new account for testing and get Mumbai MATIC in it.
Run the below line in your terminal to deploy our smart contract to Polygon Mumbai.
forge create --private-key YOUR_PRIVATE_KEY src/ForgeMaster.sol:ForgeMaster
If you get a Page not found error, try another RPC URL from Chainlist.
The forge create
command can deploy a contract at a time. That's why we use src/ForgeMaster.sol:ForgeMaster
instead of just the file name. src/ForgeMaster.sol
is the file path, ForgeMaster
is the contract name in that file.
After a successful deployment, we'll see these three addresses in our terminal.
Deployer: 0xc11773b9162CF11071A2052Ed82e39C24c2d8...
Deployed to: 0x5D01c3c1E493C950DB8aD1Cc751900FceA6fd...
Transaction hash: 0xc02deb3d3682f6901619a293350d1bff47b360094a386ccf785bdcec3f503...
Deployer is the public address of our deployment wallet account. The one we used its private key.
Deployed to is the contract public address. Our contract lives inside that address in Polygon Mumbai.
Transaction hash is the deployment transaction identifier.
Feel free to search for these addresses from your terminal on Polygonscan.
Now go to Polygonscan, find your contract with the Deployed to address, and open the contract tab. You'll see a ton of hex numbers with a Verify and Publish message. In general, you'll see the contract source code here for other projects. You can even interact with the contracts directly there.
This requires a source code verification process. In short, we want to verify the compiled byte code and the Solidity code are the same. This way, people can read our code and trust what it does is what they see. You can read about the topic here in detail.
We could add a --verify
flag to the foundry create
command to do it with the deployment but we didn't. It was intentional so we can learn how to verify smart contracts after deployment ๐คก
Verifying
Verification commands can get quite scary. It's advised to do verification with deployment.
You'll need your deployed contract address from your terminal and a Polygonscan AP I key. After gathering the requirements, add the API key to the .env
file.
ETHERSCAN_API_KEY=
Now, run the below line in your terminal to start the verification process.
forge verify-contract --num-of-optimizations 200 --watch --compiler-version v0.8.17+commit.8df45f5f DEPLOYED_CONTRACT_ADDRESS src/ForgeMaster.sol:ForgeMaster
Now, hold on a bit. That command line is quite long and a lot going on there. Let's breakdown the flags and parameters to understand.
--num-of-optimization
is related to the Solidity optimizer and configured on the build step. It's 200 by default.
--watch
let the command wait until the verification is complete in the terminal and shows the status.
--compiler-version v0.8.17+commit.8df45f5f
is the solc version used in the build step. We can see the exact version with the ~/.svm/0.8.17/solc-0.8.17 --version
command and find the full name on Etherscan using commit hash.
The rest is related to the deployed contract as we're already familiar with it.
When you see the below message in your terminal head over to Polygonscan, open the Contract tab, and voila. Now you should see the Solidity code and also interact with it there.
Response: `OK`
Details: `Pass - Verified`
Contract successfully verified
Scripting
So far we've learned all the basics and are ready to build projects with Foundry. However, when you start building big projects there can be a lot of smart contracts. Imagine managing all those deploy commands with different parameters.
This is where Solidity scripting comes in handy. We can write complicated deployment scripts for once and then run them with a simple command.
Writing scripts in Solidity is one of the many advantages of Foundry. There is no need to learn another language or client libraries.
Before writing a script we need to add another .env
variable to use inside our scripts. Once again we need a wallet address private key with some MATIC in it as the deployer address. You can use the one from the previous step. Fill it in the .env
file like the below example.
PRIVATE_KEY=YOUR_PRIVATE_KEY
Next, let's create a script to deploy our smart contract to Polygon Mumbai testnet.
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.17;
import "forge-std/Script.sol";
import "src/ForgeMaster.sol";
contract ForgeMasterScript is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
ForgeMaster forgeMaster = new ForgeMaster();
vm.stopBroadcast();
}
}
forge-std
library has a helper contract called Script.sol
which is similar to Test.sol
. It's there to make writing tests easier.
In the code, we create a test script called ForgeMasterScript
and inherit from Script.sol
. We define a function called run
as the script code. Inside, we read the PRIVATE_KEY
variable from the .env
file.
Between vm.startBroadcast()
and vm.stopBoradcast()
command, we write the code that needs to be broadcasted to the network. Creating an instance of a contract simply deploying it.
Run the below line to run the script.
forge script script/ForgeMaster.s.sol:ForgeMasterScript --rpc-url https://matic-mumbai.chainstacklabs.com --broadcast --verify
It will run the script and then broadcast changes to testnet because of the --broadcast
flag. In this case, contract deployment. Then it will submit a verification request for the deployed smart contract because of the --verify
flag.
Don't think scripts are useful only for deployments. You can write repeating tasks in scripts to make your life easier.
Local Testnet
We already deployed our contract to the public Polygon Mumbai testnet twice. But, it wouldn't be the best if we had to deploy it there every time to test it. It's not as efficient to test small changes. Finding testnet currencies is a limiting factor as well. That's why local testnets exist and Foundry provides one.
We learned about the core functionality for the forge
CLI tool. Now it's time to meet with anvil
.
anvil
is a local testnet CLI tool. It's like having Ethereum or any other EVM chain on your computer. Having full control over it makes everything much easier. We can even get rich on it for testing ๐ค
Run the following line in your terminal to start a local testnet with 3 accounts and a million ETH in each.
anvil --accounts 3 --balance 1000000
After running the command, there will be a lot of info in the terminal. There will be three account public addresses as well as private keys. A wallet mnemonic we can import to a wallet app. Some data like gas price and an RPC endpoint.
Let's deploy the same smart contract into this local instance. Run the below line in your terminal first.
forge create --rpc-url http://127.0.0.1:8545 --private-key A_PRIVATE_KEY_FROM_TERMINAL src/ForgeMaster.sol:ForgeMaster
The result should be the same as the previous deployment step. This time, it's deployed to our local instance instead of Polygon Mumbai. You won't be able to find these addresses on Polygonscan.
Performing RPC Calls from CLI
The last topic I want to cover is cast
. It is possible to call methods or send transactions from this CLI tool.
Here is an example of getting the first token's owner address from the Bored Ape Yacht Club contract. If you already have a different chain config in your .env
file override those with CLI flags like below.
cast call 0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D "ownerOf(uint256)(address)" 0 --rpc-url https://eth.llamarpc.comConclusion
This is just a simple example. It's especially useful when working with a local testnet.
Conclusion
Foundry is great. Writing scripts and tests in Solidity is wonderful. The speed is amazing. We've just covered the surface. There are a ton of other features for advanced use cases. However, it's not perfect.
The documentation website is lacking. Finding what you want can be challenging at times. There is no GUI. Everything is in a terminal.
I have to say it's not suitable for newbies. If you're just starting coding or smart contract development I don't recommend it. Otherwise, it's a great tool for people with experience.
Bonus
Suppose you followed till here, congrats. You should be comfortable working with Foundry and that's an achievement. No achievement should be left without a reward!
As a reward, I deployed the ForgeMaster NFT in the guide to Polygon mainnet. It's an AI-generated art representing this achievement; being a forge master ๐ท
It's a free mint that requires only the gas fee.
Here is the catch. Since we're all developers, there is no shiny mint page ๐คก You need to use the safeMint
method to do it. You can use Polygonscan UI or cast
CLI tool as we covered above.
See the collection on OpenSea.