Foundry: Gen 2 of Ethereum Tooling
2022 has been and will continue to be a big year in Ethereum smart contract development tooling. At this year’s Eth DevConnect, there was even a dedicated formal verification tooling event, where some amazing brainstorming and development took place. Developers are finding that they can achieve higher software quality assurance with tools. We have seen the emergence of several open source lightweight verification tools, such as the Solidity SMTChecker and HEVM, which have simple toolchains developers can take advantage of. We are also seeing the next generation of development and testing tooling emerge. Heavily inspired by dapptools, Foundry is taking over the Ethereum development scene.
Where Dapptools could be considered gen 1.5 of Ethereum tooling, making all the right choices but too early for the market and without massive technical support, Foundry is Gen2 Ethereum tools. When developing in the Ethereum ecosystem, there are currently no tools available that can surpass the functionality of the Foundry toolchain.
Released in December 2021 by Georgios Konstantopoulos and the Paradigm team, the Foundry toolchain is currently one of the best platforms to develop smart contracts on, offering users unique features that make development and testing easier than ever before.
In this blog post, we aim to share how the verification engineers at Runtime Verification use the functionalities Foundry offers. We hope to get you excited about Foundry and what it can bring to your development journey and share some helpful tools and tips to get you started.
Smart Contract Development With the Forge Tool
Foundry is a smart contract development toolchain consisting of three tools: Cast, Anvil, and Forge. The focus of this post will be on Forge, as this is the tool we use most often in our audits.
With Forge, we can compile, test and deploy smart contracts, complete with fuzzing and mainnet interaction. Traditionally, it has been necessary to use other development frameworks such as Truffle or Hardhat. Such tools would require developers to use programming languages separate from Solidity to run the tests, which can be inconvenient. It’s disrupting to write programs in one language and tests in another. Every Python developer expects to be able to write their tests in Python, and every Java developer expects to write their tests in Java; why shouldn’t Solidity be the same?
A Simple Example
Let’s consider the simple example of writing a test for an ERC20 transfer()
function. The following incomplete ERC20 implementation provides just enough functionality:
contract ERC20 { address immutable owner; mapping(address => uint256) public balanceOf; constructor() { owner = msg.sender; } function mint(address user, uint256 amount) external { require(msg.sender == owner, "Only owner can mint"); balanceOf[user] += amount; } function transfer(address to, uint amount) external { balanceOf[msg.sender] -= amount; balanceOf[to] += amount; } // Other functions... }
Using the Forge test framework, we can write a test as follows:
contract ERC20Test is Test { ERC20 token; // Contract under test address Alice = makeAddr("Alice"); address Bob = makeAddr("Bob"); address Eve = makeAddr("Eve"); function setUp() public { token = new ERC20(); token.mint(Alice, 10 ether); token.mint(Bob, 20 ether); token.mint(Eve, 30 ether); } function testTransfer(address from, address to, uint256 amount) public { vm.assume(token.balanceOf(from) >= amount); uint256 preBalanceFrom = token.balanceOf(from); uint256 preBalanceTo = token.balanceOf(to); vm.prank(from); token.transfer(to, amount); if(from == to) { assertEq(token.balanceOf(from), preBalanceFrom); assertEq(token.balanceOf(to), preBalanceTo); } else { assertEq(token.balanceOf(from), preBalanceFrom - amount); assertEq(token.balanceOf(to), preBalanceTo + amount); } } }
Any contract that derives from Test
is automatically recognized by Forge as a testing contract. When we run forge test
on the command line, Forge goes through all testing contracts, calls the setUp()
function, and then any function whose name starts with test
from the state obtained after calling the setUp()
function.
In the above example, we want to test the transfer()
function of the ERC20 token contract. To this end, we create a new instance of the ERC20 contract in the setUp()
function and mint some tokens for the three users, Alice, Bob, and Eve. Their addresses are generated using the Forge-provided function makeAddr()
.
Next, the function testTransfer()
contains the actual testing logic. Note that it takes three arguments, from
, to
and amount
, making it a property test. Forge will call this test function with random values for these arguments (called random testing or fuzzing), which helps to detect corner cases the developer may have missed otherwise. However, in many cases, the code we want to test has certain expectations, and using completely random values is not always that useful. For example, transfer()
reverts if the balance of from
is less than amount
. Thus, to test whether transfer()
correctly transfers the right amount of tokens, we need to ensure that the sender has enough funds, which we do with the special function. Next, to make the actual transfer, we need to impersonate the sending account, which we can do with . Afterwards, we check whether the correct amount of tokens has been transferred.
If we have the above contract, we can run the test as follows:
Notice how in the output we see runs: 256
. This is Forge's indication that it ran this property test 256 times, each time guessing different random values to use for parameters from
, to
, and amount
.
If you want to run these tests, we have created a repository to get you executing the examples of this article in no time!
Running Tests Against Mainnet
Forge also allows users to fork Ethereum Mainnet in order to run tests in a realistic environment.
Oftentimes, smart contracts will communicate with external contracts in their operation. These interactions can be difficult to replicate locally without a lot of work. Also, using realistic data when testing is important to increase confidence in the correctness of the code. Hence, testing programs in isolation on a local machine or testnet is insufficient.
For example, suppose we’re developing a decentralized exchange. It is not enough to assume that our program will work correctly and securely with every ERC20 just because we tested the system against an ERC20 mock contract. We should test against every ERC20 implementation we want to support — no exceptions. With the Fork Mainnet functionality, it isn’t necessary to manually go to GitHub, fetch the source codes, re-compile them, and integrate the build artifact into our test suite. Instead, we can test our external contract calls with almost no configuration overhead. All we need to do is add the --fork-url <URL>
parameter to our forge-call. We can set it to any Ethereum JSON-RPC endpoint. Then we're ready to go to Etherscan to fetch the address of the third-party contract and simulate external calls to actual production-grade data, which saves us the hassle of writing mock contracts and increases our confidence in the test suite. We can also specify the --fork-block-number <BLOCK>
to execute tests against a past state of the network.
This technique can be used as a lightweight monitoring solution as well. Want to make a simple arbitrage bot between SushiSwap and UniSwap? Make a property test describing the arbitrage with parameters for the amount to trade, and use Foundry to run many simulations against mainnet when new blocks are produced! Want to watch for potentially bad states which could be reached by your contract in production? Make a Foundry test describing the bad state, and run that continuously against new blocks using Forge.
Why should you be using Foundry?
Forge, and Foundry as a whole, is a new tool in a rapidly changing space, and updates to Foundry itself constantly change how users interact with the tooling. But there is a reason developers are rushing to adopt it. Foundry offers its users a native testing framework in Solidity and fast processing speeds, which provides incredible ease of use, and has developer adoption through the roof.
Using Foundry allows for independent and detailed build artifact generation for every contract, making it easy to inspect the generated bytecode and integrate third-party tools. Foundry is also compatible with existing tools such as Hardhat, making a transition of existing projects more feasible. All of these together make building extensions and analysis tools much easier on top of Foundry.
Users gain access to a software-specification first approach to testing: Foundry exposes and improves dapptools property testing language directly for users. It encourages users to learn how to state properties they expect to hold for their tools and enables them to check that these properties hold with Foundry’s built-in fuzzing!
Foundry is a valuable tool at any stage of the development process. Getting started with the tooling early in software development can put your project in a better position to succeed. Early deployment of Foundry tools can make the development process more manageable and allow for a more streamlined and thorough audit in the future. Reaching out to experts at Runtime Verification for assistance setting up Foundry tools could be a great place to start.
Alchemix and Foundry Tooling
Alchemix is a future-yield-backed synthetic asset platform and community DAO. The platform gives you advances on your yield farming via a synthetic token that represents a fungible claim on any underlying collateral in the Alchemix protocol.
In short, users holding any accepted asset (say, 1000 DAI) can put their asset as collateral to take out a loan of the corresponding Alchemix synthetic asset (500 alDAI in this case, since users can borrow up to half of their collateral), represented as a CDP (collateralized debt position, or vault). The key ingredient here is that the user’s debt is being paid over time by the yield earned by the CDP’s collateral.
Runtime Verification began working with Alchemix in the summer of 2021, assisting the team with design and security concerns in the early days of v2 development. In the months since, both teams have worked to improve the contract design and code. Along the way, our teams have discussed audits and general security strategies.
Alchemix began their journey with Foundry in March of 2022, following the completion of their version 2 audit. Before using Foundry, Alchemix used Hardhat for their tooling. The Alchemix team found Foundry to be easier to use than Hardhat due to the ability to write code in Solidity, built-in fuzzing capabilities, and speed and built-in testing utilities that make building/testing/debugging a breeze.
Runtime Verification has assisted the Alchemix team with using Foundry by identifying and writing the code for invariants in the smart contract, enabling accurate property testing with Foundry tooling. Readers interested in a complete list of these properties can find them in the audit report.
A Word from the Alchemix Team
Q. Why did you decide to use Foundry?
A. It’s faster than hardhat, and we can write in only solidity (as opposed to having to write js/ts). It has built-in fuzzing capabilities and many other great testing utilities that make building/testing/debugging a breeze.
Q. What is your experience with the Foundry tool?
A. We love it. There have been quirks, but no show-stopping issues. We are eager to see the ecosystem develop and for more tooling to be built with/around it.
Q. What advice would you give a person new to Foundry? Can you offer any tips to avoid pitfalls you may have encountered?
A. There is no need to go all-in on Foundry from day one. If you’re migrating from hardhat, you can add foundry to your repo and workflow and slowly transition to using Foundry more and more over time while still getting the benefits of hardhat (scripting, deployments, etc).
Q. Has Foundry helped you to uncover any bugs or errors?
A. Yes, the fuzzing has helped us tease out some rounding edge-cases. The testing is very easy and has helped us to find plenty of errors in our code (though I can’t say for certain that we wouldn’t have been able to find them w/ just our hardhat env, it was certainly easier in Foundry).
Q. What are your plans for Foundry going forward?
A. We plan to use it for all solidity code going forward and hope to adopt any new tooling it supports as it releases.
Q. How’s the experience working with the RV team to set up/encode the invariants from v2 in Foundry?
A. The invariant tests have given us immense peace of mind in our test suite. I think going forward, for any new code, we should always start with invariant tests to ensure we’re testing the most critical aspects of the system.
Conclusion
With the release of Foundry tooling, it is clear that the Ethereum smart contract field is maturing. Foundry is a versatile tool with advanced capabilities and has the benefit of being highly adaptable. Many programmers, auditors, and researchers already use Foundry in one way or another for their daily work, such as the auditors at Runtime Verification.
Property testing is crucial to smart contract development and is a great way for any team to increase their software quality assurance. With the early implementation of Foundry and security partnerships with auditors like those at Runtime Verification, developers can make the most of the benefits such tools have to offer now and in the future.
Originally published at https://runtimeverification.com.