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:
-
Provide the Flow EVM RPC URL to the Foundry tools:
_10--rpc-url https://previewnet.evm.nodes.onflow.org -
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:
_10curl -L https://foundry.paradigm.xyz | bash
To install Foundry, you can then:
_10foundryup
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:
_10cast 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:
_10cast balance --ether --rpc-url https://previewnet.evm.nodes.onflow.org <0xAddress>
Project Setup​
First, create a new directory for your project:
_10mkdir my-erc20-token_10cd my-erc20-token
We can use init
to initialize a new project:
_10forge 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:
_10forge 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:
_10forge install OpenZeppelin/openzeppelin-contracts
Rename src/Counter.sol
to src/MyToken.sol
and replace the contents with the following:
_10pragma solidity ^0.8.20;_10_10import "@openzeppelin/contracts/token/ERC20/ERC20.sol";_10_10contract 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:
_65pragma solidity ^0.8.20;_65_65import {Test, console2, stdError} from "forge-std/Test.sol";_65import {MyToken} from "../src/MyToken.sol";_65_65contract 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:
_10forge compile
We can also run the tests. They should all succeed:
_10forge 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:
_10forge 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:
_10cast 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:
_10cast 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:
_10cast 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:
_10cast 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:
_10cast balance \_10 --rpc-url https://previewnet.evm.nodes.onflow.org \_10 --erc20 $DEPLOYED_MYTOKEN_ADDRESS \_10 $NEW_ADDRESS