LLM Notice: This documentation site supports content negotiation for AI agents. Request any page with Accept: text/markdown or Accept: text/plain header to receive Markdown instead of HTML. Alternatively, append ?format=md to any URL. All markdown files are available at /md/ prefix paths. For all content in one file, visit /llms-full.txt
Skip to main content

Using Foundry with Flow

Foundry is a suite of development tools that simplifies the process to develop and deploy Solidity contracts to EVM networks. This guide will walk you through thow to deploy a Solidity contract to Flow EVM with the Foundry development toolchain. 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:

  • How to develop and teteststing a basic ERC-20 contract
  • Deploy the contract to Flow EVM with Foundry tools
  • How to query the Testnet state
  • How to mutate Testnet state by sending transactions

Overview

To use Flow across all Foundry tools you need to:

  1. Provide the Flow EVM RPC URL to the command you are using:


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

  2. Use the --legacy flag to turn off 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 with Foundry. You will see how the above flags are used in practice.

Example: Deploying an ERC-20 Token Contract to Flow EVM

ERC-20 tokens are the most common type of tokens on Ethereum. We'll use OpenZeppelin starter templates with Foundry on Flow Testnet to deploy our own token called MyToken.

Installation

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


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

Install the tools:


_10
foundryup

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

You may need to reload your shell after foundryup installation.

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

Wallet setup

We first need to generate a key pair for our EVM account. We 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 Testnet FLOW tokens.

You can verify the balance of the account after funding. Replace $YOUR_ADDRESS with the address of the account you funded:


_10
cast balance --ether --rpc-url https://testnet.evm.nodes.onflow.org $YOUR_ADDRESS

Project setup

First, create a new directory for your project:


_10
mkdir mytoken
_10
cd mytoken

We can use init to initialize a new project:


_10
forge init

This will create a 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 for Counter:


_10
forge test

The tests should pass.

Write the ERC-20 token contract

We'll use the OpenZeppelin ERC-20 contract template. To start, we'll add 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
}

The above is a basic ERC-20 token with the name MyToken and symbol MyT. It also mints the specified amount of tokens to the contract deployer. The amount is passed as a constructor argument during deployment.

Before we comnpile, we also need to update the test file.

Testing

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
}

To make sure everything is okay, compile the contracts:


_10
forge compile

Run the tests:


_10
forge test

They should all succeed.

Deploy to Flow Testnet

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


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

The above will print the deployed contract address. We'll use it in the next section to interact with the contract.

Verify Consumer DeFi

After you deploy the contract, you can verify it so that others can see the source code and interact with it from Flow's block explorer. You can use the forge verify-contract command:


_10
forge verify-contract --rpc-url https://testnet.evm.nodes.onflow.org/ \
_10
--verifier blockscout \
_10
--verifier-url https://evm-testnet.flowscan.io/api \
_10
$DEPLOYED_MYTOKEN_ADDRESS \
_10
src/MyToken.sol:MyToken

info

When you verify a Mainnet contract, be sure to use the Mainnet RPC and block explorer URLs.

Query Testnet state

Based on the given constructor arguments, the deployer should own 42,000,000 MyT. 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://testnet.evm.nodes.onflow.org \
_10
--erc20 $DEPLOYED_MYTOKEN_ADDRESS \
_10
$DEPLOYER_ADDRESS

This will return the amount specified during deployment. We can also call the associated function directly in the contract:


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

We can query other data like the token symbol:


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

Sending Transactions

Let's create a second account and move some tokens with a transaction. You can use cast wallet new to create a new test 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://testnet.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://testnet.evm.nodes.onflow.org \
_10
--erc20 $DEPLOYED_MYTOKEN_ADDRESS \
_10
$NEW_ADDRESS

The deployer will also own fewer tokens now:


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