Using Foundry Start to End with Phron
Introduction
Foundry has become an increasingly popular development environment for smart contracts because it requires only one language: Solidity. Phron offers introductory documentation on using Foundry with Phron networks, which is recommended to read to get an introduction to using Foundry. In this tutorial, we will dip our toes deeper into the library to get a more cohesive look at properly developing, testing, and deploying with Foundry.
In this demonstration, we will deploy two smart contracts. One is a token, and the other will depend on that token. We will also write unit tests to ensure the contracts work as expected. To deploy them, we will write a script that Foundry will use to determine the deployment logic. Finally, we will verify the smart contracts on Phron's blockchain explorer.
Checking Prerequisites
To get started, you will need the following:
Have an account with funds. You can get DEV tokens for testing on Phron once every 24 hours from the Phron Faucet
To test out the examples in this guide on Phron, you will need to have your own endpoint and API key, which you can get from one of the supported Endpoint Providers
Have Foundry installed
Have a Phron API Key
Create a Foundry Project
The first step to start a Foundry project is, of course, to create it. If you have Foundry installed, you can run:
This will have the forge
utility initialize a new folder named foundry
with a Foundry project initialized within it. The script
, src
, and test
folders may have files in them already. Be sure to delete them, because we will be writing our own soon.
From here, there are a few things to do first before writing any code. First, we want to add a dependency to OpenZeppelin's smart contracts, because they include helpful contracts to use when writing token smart contracts. To do so, add them using their GitHub repository name:
This will add the OpenZeppelin git submodule to your lib
folder. To be sure that this dependency is mapped, you can override the mappings in a special file, remappings.txt
:
Every line in this file is one of the dependencies that can be referenced in the project's smart contracts. Dependencies can be edited and renamed so that it's easier to reference different folders and files when working on smart contracts. It should look similar to this with OpenZeppelin installed properly:
Finally, let's open up the foundry.toml
file. In preparation for Etherscan verification and deployment, add this to the file:
The first addition is a specification of the solc_version
, underneath profile.default
. The rpc_endpoints
tag allows you to define which RPC endpoints to use when deploying to a named network, in this case, Phron. The etherscan
tag allows you to add Etherscan API keys for smart contract verification, which we will review later.
Add Smart Contracts
Smart contracts in Foundry destined for deployment by default belong in the src
folder. In this tutorial, we'll write two smart contracts. Starting with the token:
Open the file and add the following to it:
As you can see, the OpenZeppelin ERC20
smart contract is imported by the mapping defined in remappings.txt
.
The second smart contract, which we'll name Container.sol
, will depend on this token contract. It is a simple contract that holds the ERC-20 token we'll deploy. You can create the file by executing:
Open the file and add the following to it:
The Container
smart contract can have its status updated based on how many tokens it holds and what its initial capacity value was set to. If the number of tokens it holds is above its capacity, its status can be updated to Overflowing
. If it holds tokens equal to capacity, its status can be updated to Full
. Otherwise, the contract will start and stay in the Unsatisfied
state.
Container
requires a MyToken
smart contract instance to function, so when we deploy it, we will need logic to ensure that it is deployed with a MyToken
smart contract.
Write Tests
Before we deploy anything to a TestNet or MainNet, however, it's good to test your smart contracts. There are many types of tests:
Unit tests — allow you to test specific parts of a smart contract's functionality. When writing your own smart contracts, it can be a good idea to break functionality into different sections so that it is easier to unit test
Fuzz tests — allow you to test a smart contract with a wide variety of inputs to check for edge cases
Integration tests — allow you to test a smart contract when it works in conjunction with other smart contracts, so that you know it works as expected in a deployed environment
Forking tests - integration tests that allows you to make a fork (a carbon copy of a network), so that you can simulate a series of transactions on a preexisting network
Unit Tests in Foundry
To get started with writing tests for this tutorial, make a new file in the test
folder:
By convention, all of your tests should end with .t.sol
and start with the name of the smart contract that it is testing. In practice, the test can be stored anywhere and is considered a test if it has a function that starts with the word "test".
Let's start by writing a test for the token smart contract. Open up MyToken.t.sol
and add:
Let's break down what's happening here. The first line is typical for a Solidity file: setting the Solidity version. The next two lines are imports. forge-std/Test.sol
is the standard library that Forge (and thus Foundry) includes to help with testing. This includes the Test
smart contract, certain assertions, and forge cheatcodes.
If you take a look at the MyTokenTest
smart contract, you'll see two functions. The first is setUp
, which is run before each test. So in this test contract, a new instance of MyToken
is deployed every time a test function is run. You know if a function is a test function if it starts with the word "test", so the second function, testConstructorMint
is a test function.
Great! Let's write some more tests, but for Container
.
And add the following:
This test smart contract has two tests, so when running the tests, there will be two deployments of both MyToken
and Container
, for four smart contracts. You can run the following command to see the result of the test:
When testing, you should see the following output:
Test Harnesses in Foundry
Sometimes you'll want to unit test an internal
function in a smart contract. To do so, you'll have to write a test harness smart contract, which inherits from the smart contract and exposes the internal function as a public one.
For example, in Container
, there is an internal function named _isOverflowing
, which checks to see if the smart contract has more tokens than its capacity. To test this, add the following test harness smart contract to the Container.t.sol
file:
Now, inside of the ContainerTest
smart contract, you can add a new test that tests the previously unreachable _isOverflowing
contract:
Now, when you run the test with forge test
, you should see that testIsOverflowingFalse
passes!
Fuzzing Tests in Foundry
When you write a unit test, you can only use so many inputs to test. You can test edge cases, a few select values, and perhaps one or two random ones. But when working with inputs, there are nearly an infinite amount of different ones to test! How can you be sure that they work for every value? Wouldn't you feel safer if you could test 10000 different inputs instead of less than 10?
One of the best ways that developers can test many inputs is through fuzzing, or fuzz tests. Foundry automatically fuzz tests when an input in a test function is included. To illustrate this, add the following test to the MyTokenTest
contract in MyToken.t.sol
.
This test includes uint256 amountToMint
as input, which tells Foundry to fuzz with uint256
inputs! By default, Foundry will input 256 different inputs, but this can be configured with the FOUNDRY_FUZZ_RUNS
environment variable.
Additionally, the first line in the function uses vm.assume
to only use inputs that are less than or equal to one ether since the mint
function reverts if someone tries to mint more than one ether at a time. This cheatcode helps you direct the fuzzing into the right range.
Let's look at another fuzzing test to put in the MyTokenTest
contract, but this time where we expect to fail:
In Foundry, when you want to test for a failure, instead of just starting your test function with the world "test", you start it with "testFail". In this test, we assume that the amountToMint
is above one ether, which should fail!
Now run the tests:
You should see something similar to the following in the console:
Forking Tests in Foundry
In Foundry, you can locally fork a network so that you can test out how the contracts would work in an environment with already deployed smart contracts. For example, if someone deployed smart contract A
on Phron that required a token smart contract, you could fork the Phron network and deploy your own token to test how smart contract A
would react to it.
NotePhron's custom precompile smart contracts currently do not work in Foundry forks because precompiles are Substrate-based whereas typical smart contracts are completely based on the EVM. Learn more about forking on Phron and the differences between Phron and Ethereum.
In this tutorial, you will test how your Container
smart contract interacts with an already deployed MyToken
contract on Phron
Let's add a new test function to the ContainerTest
smart contract in Container.t.sol
called testAlternateTokenOnPhronFork
:
The first step (and thus first line) in this function is to have the test function fork a network with vm.createFork
. Recall that vm
is a cheat code provided by the Forge standard library. All that's necessary to create a fork is an RPC URL, or an alias for an RPC URL that's stored in the foundry.toml
file. In this case, we added an RPC URL for "phron" in the setup step, so in the test function we will just pass the word "phronbase"
. This cheat code function returns an ID for the fork created, which is stored in an uint256
and is necessary for activating the fork.
On the second line, after the fork has been created, the environment will select and use the fork in the test environment with vm.selectFork
. The third line just demonstrates that the current fork, retrieved with vm.activeFork
, is the same as the Phron fork.
The fourth line of code retrieves an already deployed instance of MyToken
, which is what's so useful about forking: you can use contracts that are already deployed.
The rest of the code tests capacity like you would expect a local test to. If you run the tests (with the -vvvv
tag for extra logging), you'll see that it passes:
That's it for testing! You can find the complete Container.t.sol
and MyToken.t.sol
files below:
Deploy in Foundry with Solidity Scripts
Not only are tests in Foundry written in Solidity, the scripts are too! Like other developer environments, scripts can be written to help interact with deployed smart contracts or can help along a complex deployment process that would be difficult to do manually. Even though scripts are written in Solidity, they are never deployed to a chain. Instead, much of the logic is actually run off-chain, so don't worry about any additional gas costs for using Foundry instead of a JavaScript environment like Hardhat.
Deploy on Phron
In this tutorial, we will use Foundry's scripts to deploy the MyToken
and Container
smart contracts. To create the deployment scripts, create a new file in the script
folder:
By convention, scripts should end with s.sol
and have a name similar to the script they relate to. In this case, we are deploying the Container
smart contract, so we have named the script Container.s.sol
, though it's not the end of the world if you use a more suitable or descriptive name.
In this script, add:
Let's break this script down. The first line is standard: declaring the solidity version. The imports include the two smart contracts you previously added, which will be deployed. This includes additional functionality to use in a script, including the Script
contract.
Now let's look at the logic in the contract. There is a single function, run
, which is where the script logic is hosted. In this run
function, the vm
object is used often. This is where all of the Forge cheatcodes are stored, which determines the state of the virtual machine that the solidity is run in.
In the first line within the run
function, vm.envUint
is used to get a private key from the system's environment variables (we will do this soon). In the second line, vm.startBroadcast
starts a broadcast, which indicates that the following logic should take place on-chain. So when the MyToken
and the Container
contracts are instantiated with the new
keyword, they are instantiated on-chain. The final line, vm.stopBroadcast
ends the broadcast.
Before we run this script, let's set up some of our environment variables. Create a new .env
file:
And within this file, add the following:
NoteFoundry provides additional options for handling your private key. It is up to you to decide whether or not you would rather use it in the console, have it stored in your device's environment, using a hardware wallet, or using a keystore.
To add these environment variables, run the following command:
Now your script and project should be ready for deployment! Use the following command to do so:
This command runs the ContainerDeployScript
contract as a script. The --broadcast
option tells Forge to allow broadcasting of transactions, the --verify
option tells Forge to verify to Phronscan when deploying, -vvvv
makes the command output verbose, and --rpc-url phronbase
sets the network to what phronbase
was set to in foundry.toml
. The --legacy
flag instructs Foundry to bypass EIP-1559. While all Phron networks support EIP-1559, Foundry will refuse to submit the transaction to phronbase and revert to a local simulation if you omit the --legacy
flag.
You should see something like this as output:
You should be able to see that your contracts were deployed, and are verified on Phronscan! For example, this is where my Container.sol
contract was deployed.
The entire deployment script is available below:
Deploy on Phron MainNet
Let's say that you're comfortable with your smart contracts and you want to deploy on the Phron MainNet! The process isn't too different from what was just done, you just have to change the command's rpc-url from phronbase
to phron
, since you've already added Phron MainNet's information in the foundry.toml
file:
It's important to note that there are additional, albeit more complex, ways of handling private keys in Foundry. Some of these methods can be considered safer than storing a production private key in environment variables.
That's it! You've gone from nothing to a fully tested, deployed, and verified Foundry project. You can now adapt this so that you can use Foundry in your own projects!
This tutorial is for educational purposes only. As such, any contracts or code created in this tutorial should not be used in production.The information presented herein has been provided by third parties and is made available solely for general information purposes. Phron does not endorse any project listed and described on the Phron Doc Website (https://docs.Phron.ai/). Phron does not warrant the accuracy, completeness or usefulness of this information. Any reliance you place on such information is strictly at your own risk. Phron disclaims all liability and responsibility arising from any reliance placed on this information by you or by anyone who may be informed of any of its contents. All statements and/or opinions expressed in these materials are solely the responsibility of the person or entity providing those materials and do not necessarily represent the opinion of Phron. The information should not be construed as professional or financial advice of any kind. Advice from a suitably qualified professional should always be sought in relation to any particular matter or circumstance. The information herein may link to or integrate with other websites operated or content provided by third parties, and such other websites may link to this website. Phron has no control over any such other websites or their content and will have no liability arising out of or related to such websites or their content. The existence of any such link does not constitute an endorsement of such websites, the content of the websites, or the operators of the websites. These links are being provided to you only as a convenience and you release and hold Phron harmless from any and all liability arising from your use of this information or the information provided by any third-party website or service.
Last updated