Skip to main content

Using Floudry with Flow

Foundry is a suite of development tools that simplifies the process of developing and deploying Solidity contracts to EVM networks. This guide will walk you through the process of deploying a Solidity contract to Flow EVM using Foundry tools. You can check out the official Foundry docs here.

In this guide, we'll deploy an ERC-20 token contract to Flow EVM using Foundry. We'll cover:

  • Developing and testing a basic ERC-20 contract
  • Deploying the contract to Flow EVM using Foundry tools
  • Querying state
  • Mutating state by sending transactions

Overview​

To use Flow across all Foundry tools you need to:

  1. Provide the Flow EVM RPC URL to the Foundry tools:


    _10
    --rpc-url https://previewnet.evm.nodes.onflow.org

  2. Use the --legacy flag to disable EIP-1559 style transactions. Flow will support EIP-1559 soon and this flag won't be needed.

As an example, we'll show you how to deploy a fungible token contract to Flow EVM using Foundry.

Example: Deploying an ERC-20 Fungible Token to Flow EVM​

ERC-20 tokens are the most common type of tokens on Ethereum. We'll use OpenZeppelin starter templates with Foundry. We'll then create a new account using the PreviewNet faucet and deploy the ERC-20 token contract to Flow EVM. We then demonstrate how to interact with the deployed contract.

Installation​

The best way to install Foundry, is to use the foundryup CLI tool. You can install it using the following command:


_10
curl -L https://foundry.paradigm.xyz | bash

To install Foundry, you can then:


_10
foundryup

This will install the Foundry tool set: forge, cast, anvil, and chisel.

Check out the official Installation guide for more information about different platforms or installing specific versions.

Wallet Setup​

We first need to create a key pair for our EVM account. You can do this using the cast tool:


_10
cast wallet new

cast will print the private key and address of the new account. We can then paste the account address into the Faucet to fund it with some PreviewNet FLOW tokens.

You can check the balance of the account after funding:


_10
cast balance --ether --rpc-url https://previewnet.evm.nodes.onflow.org <0xAddress>

Project Setup​

First, create a new directory for your project:


_10
mkdir my-erc20-token
_10
cd my-erc20-token

We can use init to initialize a new project:


_10
forge init

This will create a test contract called Counter in the contracts directory with associated tests and deployment scripts. We can replace this with our own ERC-20 contract. To verify the initial setup, you can run the tests:


_10
forge test

Writing and Testing the ERC-20 Token Contract​

We'll use the OpenZeppelin ERC-20 contract template. We can start by adding OpenZeppelin to our project:


_10
forge install OpenZeppelin/openzeppelin-contracts

Rename src/Counter.sol to src/MyToken.sol and replace the contents with the following:


_10
pragma solidity ^0.8.20;
_10
_10
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
_10
_10
contract MyToken is ERC20 {
_10
constructor(uint256 initialMint_) ERC20("MyToken", "MyT") {
_10
_mint(msg.sender, initialMint_);
_10
}
_10
}

We also need to update the test file. Rename test/Counter.t.sol to test/MyToken.t.sol and replace the contents with the following:


_65
pragma solidity ^0.8.20;
_65
_65
import {Test, console2, stdError} from "forge-std/Test.sol";
_65
import {MyToken} from "../src/MyToken.sol";
_65
_65
contract MyTokenTest is Test {
_65
uint256 initialSupply = 420000;
_65
_65
MyToken public token;
_65
address ownerAddress = makeAddr("owner");
_65
address randomUserAddress = makeAddr("user");
_65
_65
function setUp() public {
_65
vm.prank(ownerAddress);
_65
token = new MyToken(initialSupply);
_65
}
_65
_65
/*
_65
Test general ERC-20 token properties
_65
*/
_65
function test_tokenProps() public view {
_65
assertEq(token.name(), "MyToken");
_65
assertEq(token.symbol(), "MyT");
_65
assertEq(token.decimals(), 18);
_65
assertEq(token.totalSupply(), initialSupply);
_65
assertEq(token.balanceOf(address(0)), 0);
_65
assertEq(token.balanceOf(ownerAddress), initialSupply);
_65
}
_65
_65
/*
_65
Test Revert transfer to sender with insufficient balance
_65
*/
_65
function test_transferRevertInsufficientBalance() public {
_65
vm.prank(randomUserAddress);
_65
vm.expectRevert(abi.encodeWithSignature("ERC20InsufficientBalance(address,uint256,uint256)", randomUserAddress, 0, 42));
_65
token.transfer(ownerAddress, 42);
_65
}
_65
_65
/*
_65
Test transfer
_65
*/
_65
function test_transfer() public {
_65
vm.prank(ownerAddress);
_65
assertEq(token.transfer(randomUserAddress, 42), true);
_65
assertEq(token.balanceOf(randomUserAddress), 42);
_65
assertEq(token.balanceOf(ownerAddress), initialSupply - 42);
_65
}
_65
_65
/*
_65
Test transferFrom with approval
_65
*/
_65
function test_transferFrom() public {
_65
vm.prank(ownerAddress);
_65
token.approve(randomUserAddress, 69);
_65
_65
uint256 initialRandomUserBalance = token.balanceOf(randomUserAddress);
_65
uint256 initialOwnerBalance = token.balanceOf(ownerAddress);
_65
_65
vm.prank(randomUserAddress);
_65
assertEq(token.transferFrom(ownerAddress, randomUserAddress, 42), true);
_65
assertEq(token.balanceOf(randomUserAddress), initialRandomUserBalance + 42);
_65
assertEq(token.balanceOf(ownerAddress), initialOwnerBalance - 42);
_65
assertEq(token.allowance(ownerAddress, randomUserAddress), 69 - 42);
_65
}
_65
}

You can now make sure everything is alright:


_10
forge compile

We can also run the tests. They should all succeed:


_10
forge test

Deploying to Flow PreviewNet​

We can now deploy MyToken using the forge create command. We need to provide the RPC URL, private key from a funded account using the faucet, and constructor arguments which is the inital supply in this case. We can use the --legacy flag to disable EIP-1559 style transactions. Replace $DEPLOYER_PRIVATE_KEY with the private key of the account you created earlier:


_10
forge create --rpc-url https://previewnet.evm.nodes.onflow.org \
_10
--private-key $DEPLOYER_PRIVATE_KEY \
_10
--constructor-args 42000000 \
_10
--legacy \
_10
src/MyToken.sol:MyToken

The above will return the newly deployed contract address.

Interacting with Deployed Contracts​

Based on the given constructor arguments, the initial supply of the token is 42,000,000. We can check the MyToken balance of the contract owner. Replace $DEPLOYED_MYTOKEN_ADDRESS with the address of the deployed contract and $DEPLOYER_ADDRESS with the address of the account you funded earlier:


_10
cast balance \
_10
--rpc-url https://previewnet.evm.nodes.onflow.org \
_10
--erc20 $DEPLOYED_MYTOKEN_ADDRESS \
_10
$DEPLOYER_ADDRESS

This should return the initial supply of the token passed during deployment. You can also call functions directly in the contract:


_10
cast call $DEPLOYED_MYTOKEN_ADDRESS \
_10
--rpc-url https://previewnet.evm.nodes.onflow.org \
_10
"balanceOf(address)(uint256)" \
_10
$DEPLOYER_ADDRESS

Or query other data like the token symbol:


_10
cast call $DEPLOYED_MYTOKEN_ADDRESS \
_10
--rpc-url https://previewnet.evm.nodes.onflow.org \
_10
"symbol()(string)"

Let's create a second account and move some tokens using a transaction. You can use cast wallet new to create a new account. You don't need to fund it to receive tokens. Replace $NEW_ADDRESS with the address of the new account:


_10
cast send $DEPLOYED_MYTOKEN_ADDRESS \
_10
--rpc-url https://previewnet.evm.nodes.onflow.org \
_10
--private-key $DEPLOYER_PRIVATE_KEY \
_10
--legacy \
_10
"transfer(address,uint256)(bool)" \
_10
$NEW_ADDRESS 42

We can check the balance of the new account:


_10
cast balance \
_10
--rpc-url https://previewnet.evm.nodes.onflow.org \
_10
--erc20 $DEPLOYED_MYTOKEN_ADDRESS \
_10
$NEW_ADDRESS