A Starting Guide to Foundry - Ethereum Development Toolchain

A Starting Guide to Foundry - Ethereum Development Toolchain

Get up and running with the blazing-fast Ethereum development toolchain Foundry

ยท

13 min read

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.

ย