DEV Community

Cover image for Demystifying Foundry
Perelyn-sama
Perelyn-sama

Posted on

Demystifying Foundry

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
Enter fullscreen mode Exit fullscreen mode

This will download foundryup. Then install Foundry by running:

foundryup
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

foundry-project-structure

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, testand 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 
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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 {
Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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();
    }
Enter fullscreen mode Exit fullscreen mode

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);
    }

Enter fullscreen mode Exit fullscreen mode

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);
    }
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

After running forge test , you should see this in your terminal:

foundry-test

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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=
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

contract-deployed

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^-^.

Oldest comments (0)