The Foundry EVM Development Environment

Jtriley.eth
7 min readMay 25, 2022

--

Foundry is the new EVM development environment on the block. With Solidity-native testing, powerful CLI’s, and high-performance Rust tools, Foundry is more than worth your time to learn.

Installation

The official installation guide can be found here. Personally, I had trouble getting anvil with foundryup, so if you want to build from source on a bash/zsh shell, use the following. Notice that git and cargo are required for building from source.

git clone https://github.com/foundry-rs/foundry && \
cd foundry && \
cargo install --path ./cli --bins --locked --force && \
cargo install --path ./anvil --locked --force

If you chose to build from source, it’s time to yeet your life savings into your computer because your compiler is going to send it to the moon.

The Components

Foundry consists of three different Command Line Interface (CLI) tools including forge, cast, and anvil. First we are going to dive into each of these, then we will build and test a smart contract.

Cast

Cast is a CLI tool for making RPC calls to Ethereum Virtual Machine (EVM) compatible blockchains. Using cast, we can make contract calls, query data, and handle encoding and decoding. There are far too many sub-commands to list here, so for a full reference, see the foundry book’s section on cast!

To perform a contract call without publishing a transaction, we can use the call sub-command:

cast call 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 \
"balanceOf(address)(uint256)" \
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 \
--rpc-url https://rpc.flashbots.net

This performs a balanceOf(address) query on the 0xC0..c2 (WETH) address, passing the 0xf3..66 address as the argument, and decoding the return data to a value of type uint256. In this and the following cast sub-commands, we are explicitly using the Flashbots RPC because the default is http://localhost:8545.

To query the Ether balance of an address, we can use the balance sub-command:

cast balance 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 \
--rpc-url https://rpc.flashbots.net

We can also query balances with ENS names:

cast balance yourmom.eth --rpc-url https://rpc.flashbots.net

To extract a contract state slot on a contract, we can use the storage sub-command:

cast storage 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 0 \
--rpc-url https://rpc.flashbots.net

This gets the storage slot 0 from the 0xC0..c2 (WETH) address.

Forge

Forge is a CLI tool for building, testing, fuzzing, deploying, and verifying Solidity contracts. Again, there are far too many sub-commands to list here, so here is the reference! By the way, if you’re here for the tutorial, no need to run these commands, everything is included in the “Creating a Foundry Repository” section.

A common sub-command is init, which initializes a new repository:

forge init my_gigabrain_protocol

The install sub-command allows you to install dependencies by indicating the owner and name of the repository to install.

forge install Rari-Capital/solmate@v6

This installs the solmate repository owned by Rari-Capital, specifically v6. Notice that the @v6 is optional.

To run tests, we can use the following:

forge test

Anvil

Anvil is a CLI tool for running a local EVM blockchain. This is comparable to ganache and the hardhat node, but like, faster.

To start anvil, simply use the anvil command:

anvil

You can also specify things like verbosity with -v, the URL to use if forking a public network with -fork-url <FORK_URL>, and more. To see a full list of anvil options, use the following:

anvil -h

Creating a Foundry Repository

Initialization

To get started, as mentioned above, we will use the following command:

forge init my_token && cd my_token

This will create a directory my_token, initialize a git repository, add a GitHub workflows directory, install the forge-std package, generate a foundry.toml file, a test directory, an src directory, and finally it changes directories into the my_token directory.

Let’s go ahead and remove the existing contract by running the following.

rm src/Contract.sol

Now first, let’s look at the foundry.toml file. The auto-generated file should look something like this.

[default]
src = 'src'
out = 'out'
libs = ['lib']

This sets the contract source directory to src, the compiler output directory to out and the library re-mappings to lib.

Dependencies

Now we will install a dependency. We can use the <owner>/<repository> pattern to install Rari-Capital's solmate.

forge install Rari-Capital/solmate

This installs solmate to the lib directory. Now let’s create an ERC20 contract in src/MyToken.sol.

We can import and use the solmate ERC20 implementation as follows.

// SPDX-License-Identifier: AGPLv3
pragma solidity ^0.8.13;
import {ERC20} from "solmate/tokens/ERC20.sol";error NotMinter();contract MyToken is ERC20 {
address public immutable minter;
// ERC20(name, symbol, decimals)
contructor() ERC20("MyToken", "MyT", 18) {
minter == msg.sender;
}
function mint(uint256 amount) external {
if (msg.sender != minter) revert NotMinter();
_mint(minter, amount);
}
}

So this is an ERC20 compliant token with a simple access control function for minting.

Notice the solmate/tokens/ERC20.sol import is remapped based on the libs/ directory specified in the foundry.toml file.

If you’re using VSCode and getting error messages, try creating a remappings.txt file in the project root and adding the following.

solmate/=lib/solmate/src/
forge-std/=lib/forge-std/src

Compilation

Let’s go ahead and compile the contract.

foundry build

Juicy.

Tests

Now we need to write some tests because a smart contract in charge of a non-negligible amount of value is effectively a non-consensual bug-bounty. Do not be a lesson. :)

To write tests, create a file in test. Let’s create a file called test/MyToken.t.sol. The .t.sol indicates this will be a test file.

// SPDX-License-Identifier: AGPLv3
pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol";
import {MyToken} from "../MyToken.sol";
contract MyTokenTest {
MyToken internal myToken;
address internal constant alice = address(1);
address internal constant bob = address(2);
function setUp() external {
vm.prank(alice);
myToken = new MyToken();
}
function testMint() external {
vm.prank(alice);
myToken.mint(1); assertEq(myToken.balanceOf(alice), 1);
}
function testFailMint() external {
vm.prank(bob);
myToken.mint(1);
}
}

There are a few things to unpack here.

The Test import from forge-std comes with the forge-generated repo by default. It includes an internal vm variable, which is a “cheat-code” runner.

The vm includes a slew of powerful cheat-codes, from timestamp manipulation to etching bytecode onto addresses to storage slot reading/writing. To see all of them, see the cheats reference here. In this example, we use vm.prank(address), where the address argument will be the msg.sender of the next external contract call.

To test a function, prefix test to the name of the function running the test. In this case, we are testing mint, so we can call it testMint. If it does not start with test, it will not be run on forge test. The assertEq function asserts the two values are equal. There are other assertions declared in the Test contract, which can be found in the assertion reference here.

To test for a revert, prefix testFail to the function name. In this case, we expect vm.prank(bob) to fail because in setUp, the minter of MyToken was set to alice by vm.prank(alice).

Now let’s run the tests.

forge test

Everything should pass. If you ever need to debug function calls, add -vvvv (verbosity 4) to the test command.

forge test -vvvv

If you ever need to log variables from inside the test, you can use the events declared in Test. You can log numbers with emit log_uint(uint) and you can label them with emit log_named_uint(string,uint). To show event logs, use -vv (verbosity 2) when running forge test.

Local Deployment

To deploy the contract locally, we need to first start an anvil instance.

anvil

This should print out some accounts when the local devnet starts running. Leave this running and open a new terminal window.

Now let’s take the private key of the first account from the anvil output and set it as the $PRIV_KEY environment variable. This is not required, it just keeps things clean.

export \
PRIV_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

NOTE these private keys are deterministic and public knowledge, so think twice before putting funds on it on a public network.

Now verify the private key is accessible.

echo $PRIV_KEY

If it prints the private key, you are good to go.

Now let’s deploy the contract to the local devnet.

forge create src/MyToken.sol:MyToken --private-key=$PRIV_KEY

This will create the contract MyToken in the src/MyToken.sol file with the private key that we loaded into the environment. Notice that the syntax used for the contract name and file name must always explicitly use the contract name.

forge create <filename>:<contractname> ...

In this case, we did not need constructor arguments, but if we did, we would need to pass the --constructor-args flag at the very end and write the constructor arguments, separated by whitespace.

forge create Filename.sol:Contractname \
--private-key $PRIV \
--constructor-args arg0 arg1 arg2

After this runs, you should have something like this printed to the terminal. Note that the contract address and the transaction hash may be different.

Deployer: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266Deployed to: 0x5fbdb2315678afecb367f032d93f642f64180aa3Transaction hash: 0x3f849ddc766b4851fed8798a26aa6fe527c74cd9d6f2639ec289f5e11ceaa3b5

Sweet. Now we can use cast to try out our newly minted token.

Again, to keep things clean, export your contract address to $CON_ADDRESS in your terminal environment. If yours is different, just replace it in the command below.

export CON_ADDRESS=0x5fbdb2315678afecb367f032d93f642f64180aa3

Now let’s query the .name() of our token.

cast call $CON_ADDRESS "name():(string)"

Notice we include :(string) in the function signature. This is to help cast decode the returned data, otherwise we get a gigantic hex string.

Now let’s call our mint function using the same private key that deployed it. If you use any other private key, remember it will fail because of the logic in our mint function.

cast send --private-key $PRIV_KEY $CON_ADDRESS "mint(uint256)" 1

This mints exactly 1 token to our account. You will also notice the transaction data has been printed to the screen. Awesome.

As a final verification on the local deployment, check the balance of your account. If you happen to be using a private key that is not provided by anvil, you can always use the following.

cast wallet address --private-key $PRIV_KEY

Once again, for convenience, just add the wallet address to the environment.

export WALLET=0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266

Now for the balance query.

cast call $CON_ADDRESS "balanceOf(address):uint256" $WALLET

This should return 1.

Public Deployment

Public network deployment works the same as above, but of course using your own private key, RPC endpoint, and chain ID.

Verification

To verify your publicly deployed contract with Etherscan, use the following command.

forge verify-contract \
--chain $CHAIN_ID \
--compiler-version $COMPILER_VERSION \
$CON_ADDRESS src/MyToken.sol:MyToken $ETHERSCAN_API_KEY

Conclusion

This one was a bit beefier than my usual articles. There was a lot to cover and we are still just scratching the surface of how deep the foundry rabbit-hole goes. I hope this was helpful to you on your programming and smart contract journey, and as always, good hacking 🤘

--

--