Ethereum Mainnet Testing with Python and Brownie
or: how I learned to stop mocking and embrace the --fork
An Indulgent Introduction…
Looking at Ethereum smart contract development over the years all I can think is —man, the times, they are a-changin’. When I wrote my first smart contract, my approach to security was to view that contract as an island. I sought to build an impenetrable fortress, capable of protecting the wealth within and preventing unsolicited interactions from what I viewed as an unknown horde of potential attackers.
As I kept learning and tinkering I grew to understand the unique challenges that immutability presented me. I started using libraries and building interconnected systems. I became comfortable with the idea that my own contracts were but a drop in a vast sea. Looking beyond the borders of my small empire of code, I stopped seeing threats and started seeing a world of opportunity. And the complexity continued to grow.
I wasn’t the only one having my smart contract coming-of-age. Collectively we have seriously leveled up at this stuff. Look around today and you’ll be hard pressed to find someone building a project that doesn’t interact with at least one existing protocol. DeFi has spurred an explosion of development as we find new and creative ways to put together our money legos. Increasingly complex things are popping up everywhere — it can be hard to keep up.
I can’t imagine how daunting this must be for a new developer. The attack surface is exponentially more than what it was just two years ago, and people can and do get rekt far too often. The digital wild west can be an unforgiving world.
But do not lose hope! As the complexity of protocols has grown, so too have the tools available for building and securing them. As the ecosystem matures we’re not only innovating in what we build, we’re also coming up with better ways to make sure it doesn’t break.
And so we --fork…
Ganache’s — fork
option (hereafter called “fork mode”) is an absolute game changer. Through a bit of magic it provides a local development chain that’s been forked from the mainnet. Are you tired of struggling with mocks or figuring out how to locally deploy all those complex protocols you need to test against? Fork mode’s got you covered!
Of course it’s not without drawbacks — it’s a lot slower than working in a purely local environment. And while it works with Infura, it’s a much more responsive experience when using your own node.
But on the other hand, it does work with Infura. Stop for a moment to appreciate how amazing it is that such a powerful resource is publicly available with such a low barrier to entry.
Now, let’s dive in.
Using Ganache’s Fork Mode with Brownie
Initial Setup
To get started using Brownie on a forked mainnet, all you need to do is install Brownie and set up an Infura API key. There’s no additional configuration required — it’s that easy!
Once installed, we can test it by opening the console:
brownie console --network mainnet-fork
Interacting with Mainnet Projects
Brownie is integrated with the Etherscan API to simplify interactions with other projects. All you need is an address in order to interact with any contract that has verified source code.
As an example, let’s query the current price of Ether from Chainlink’s ETH/USD oracle:
address = "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419"
oracle = Contract.from_explorer(address)
oracle.latestAnswer()
In only three lines of code and without providing an ABI or source file, we’ve created an object to interact with a mainnet contract and fetched information from it. Impressive!
We can also access NatSpec documentation for any contract function that implements it — perfect for exploring an unfamiliar protocol. Chainlink makes good use of NatSpec, so let’s try it in our current session:
oracle.updateRequestDetails.info()
Unit Testing on Mainnet
Now that we’ve confirmed everything works and gotten a taste for Brownie in fork mode, let’s write some tests! If you’re following along in a terminal, now is the time to create a new Brownie project.
To effectively write unit tests for the mainnet we’re going to build some fixtures. If you’re unfamiliar — a fixture is a test component that handles the setup and teardown phases of one or more unit tests. Fixtures reduce duplicated code and ensure that tests are run against the same initial conditions, producing consistent and repeatable results. They are a core component in any pytest test suite and an extremely powerful tool.
Let’s start by building a very simple fixture to access the DAI stablecoin:
How it works:
- The
@pytest.fixture
decorator on line 4 marks thedai
function as a fixture. Within the fixture we are querying Etherscan to generate aContract
object for the DAI smart contract. - The
test_dai_fixture
function on line 10 is our test case. By includingdai
in the function inputs, we tell pytest that this function requires thedai
fixture. When the test executes, the logic withindai
is executed as the setup phase of our test, and the return value is made available to the actual test.
Save this test as tests/test_dai_example.py
within your Brownie project, and then run it with the following command:
brownie test --network mainnet-fork
It worked! So now you might be thinking “Ok great, I can interact with the DAI contract. But how do I actually get some DAI?”
Let’s build another fixture!
- First, we add another fixture named
uniswap_dai_exchange
to allow access to the Uniswap DAI exchange. - Next we create a new test named
test_buy_dai
. It uses both thedai
anduniswap_dai_exchange
fixtures, as well as the builtin Brownieaccounts
fixture. Within this test we’re calling to Uniswap to exchange 10 ether for DAI, and then asserting that the exchange was successful.
That’s pretty cool… but can we improve on this further? Let’s make some tweaks.
In this example:
- First we moved the call to purchase DAI out of the test and into another fixture named
buy_dai
. - We then add a flag
autouse=True
to the fixture. This tells pytest to automatically run the fixture, even if a test doesn’t specifically require it. - We also add
scope
arguments to each of our fixtures. Scoping lets us alter how frequently the logic within a fixture executes — in this case, only once for the entire test session.
The end result? In under 25 lines of code we’ve constructed a base testing environment in which we always have DAI available for use in our actual tests!
What’s next…
We’ve only scratched the surface of what’s possible with Brownie when combined with Ganache’s fork mode. I hope this leaves you feeling inspired to go out and r̶e̶w̶r̶i̶t̶e̶ ̶y̶o̶u̶r̶ ̶t̶e̶s̶t̶ ̶s̶u̶i̶t̶e buidl something new and incredible with these tools!
To learn more about Brownie and keep up to date with all the new features in development, please follow our Twitter account and read our other Medium articles. You can also say hi on Gitter if you have any questions about using pytest, working in fork mode, or anything else related to Ethereum or Python development.