Ethereum Local Forking in Brownie Using Ganache
Forking and running local blockchain copy is essential if you create Web3 applications. Often, you need to test how your application behave in some historical block or you don't have access to some third party smart contracts.
If you are reading this article I bet you know that it is risky to keep your tokens on centralized exchanges. You should rather store them on own (soft or cold) wallet. Centralized exchanges usually offer spot trading (like stock brokers), so users can protect their funds from losses by creating limit orders. Unfortunately, wallets don't support this features since it isn't their role. It means many of us are still stuck with tokens on centralized exchanges. I needed to solve this problem, and I built my own order book for Ethereum called Warren. It is open source - you need to run it on your own but you control your tokens.
Feel free to check the source code on Github. Don't forget to give it a ⭐!
This article isn't about the order book per se. I want to focus on the lesson I needed to learn to complete it. I mean the blockchain forking. It is extremely important subject that will save you tons of time if you work with third party smart contracts like myself.
What are local forks?
Building order book isn't possible without decentralized exchange (DEX). To create and executed orders my application needs to be able to fetch token prices and submit transactions (exchange tokens). Speaking of the decentralized exchanges, I found plenty - Uniswap, Pancake Swap, Sushi Swap and you name it. What I found out is that the most of them used smart contract written by Uniswap. They open source all their code so I decide to integrate with order book with them. Besides that, Uniswap delivered an amazing documentation, so working with it is so easy. I will skip the implementation and going to focus on testing using local forks. But first, let me explain why.
Obviously, I could test the application using real tokens and performing real transactions. But I would be spending real money on paying gas fees and testing token swaps. I didn't want to do that, and I figured out that I have two options.
The first one – I could download smart contracts from Uniswap and deploy them to Goerli testnet. I could try to configure liquidity pools to reach similar to real world prices and perform transactions. Also, I would need to deploy some ERC-20 token such as DAI or Tether, so I could perform token swaps. It sounds complex, right?
The second option would be using local forks of Ethereum. Imagine, I can take a precisely defined snapshot of the Ethereum blockchain from the past, and test my application in a very deterministic environment. Sounds too good to be truth? It is actaully easier that you would think so. We just need to use one of the tools that allows to fork blockchain. There are two popular tools that do so - hardhat and ganache. I used both - hardhat at work and ganache for personal projects. It seems like ganache is more mature at this point of time, so I will use ganache in this example.
Fork Ethereum Mainnet using Ganache
Ganache is built in JavaScript. It means you need to have NodeJS installed. Have in mind that hardhat is also built in JavaScript so it doesn't save us time.
We will need to install ganache
using npm
or yarn
. .
npm install --global ganache
Important! We will be using ganache
not ganache-cli
! ganache-cli
is and old and deprecated software that doesn't support modern EVM versions. It means this tool will not be able to emulate any recent blocks.
Once ganache
is installed. You will need to find an Ethereum node that supports forking. I'm using Alchemy in this article but feel free to use any preferred one – I know that Infura and QuickNode support it as well.
I will be writing integration tests and I don't want them to fail due when token prices change. It means I will need to configure my local fork to start from one particular block.
The most recent block when I was building my order book was 16405896
so we will use it in this example. The only thing you need to know about this block is that 1.0 ETH
costs 1,517.02 DAI
on Uniswap (v3). I will write a test that swaps 1 Ether to DAI to confirm that.
Now, I need to create a new network in brownie by running this command:
brownie networks add Development test-mainnet-fork host=http://127.0.0.1 port=8545 cmd=ganache fork=$$YOUR_ETHEREUM_NODE_URL$$@16405896 accounts=10
Let's take a closer look into the fork
parameter. You will need to pass there your Ethereum API url and block number you want to fork (after the @
character). In my case, the URL will look like this: https://eth-mainnet.g.alchemy.com/v2/00000000000000000000000000000000@16405896
.
Once our network is configured, I will add the test:
import pytest
from brownie import accounts, Contract
from warren.utils.load_contract_abi import load_contract_abi
def test_uniswap_eth_to_dai_swap():
account = accounts[0]
weth9 = Contract.from_abi(
name="WETH9",
address="0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
# source: https://github.com/mateuszsokola/warren-the-cryptobot/blob/c355cd43e160e502cf5e0a08f939f51fdc4730cb/artifacts/tokens/WETH9.json
abi=load_contract_abi("WETH9.json", "artifacts/tokens")
)
dai = Contract.from_abi(
name="DAI",
address="0x6B175474E89094C44Da98b954EedeAC495271d0F",
# source: https://github.com/mateuszsokola/warren-the-cryptobot/blob/c355cd43e160e502cf5e0a08f939f51fdc4730cb/artifacts/tokens/IERC20.json
abi=load_contract_abi("IERC20.json", "artifacts/tokens")
)
uniswap_v3_router_address = "0xE592427A0AEce92De3Edee1F18E0157C05861564"
uniswap_v3_router = Contract.from_abi(
name="Router",
address=uniswap_v3_router_address,
# source: https://github.com/mateuszsokola/warren-the-cryptobot/blob/c355cd43e160e502cf5e0a08f939f51fdc4730cb/artifacts/uniswap/v3/ISwapRouter.json
abi=load_contract_abi("ISwapRouter.json", "artifacts/uniswap/v3")
)
amount_in = int(1 * 10**18)
weth9.deposit({"from": account, "value": amount_in})
weth9.approve(uniswap_v3_router_address, amount_in, {"from": account})
uniswap_v3_router.exactInputSingle(
(weth9.address, dai.address, 3000, account, 9999999999999999, amount_in, 0, 0), {"from": account, "value": 0}
)
# Actually, ETH doesn't cost 1517.02 DAI but 1517024094830368309726 DAI to be precise (remember about decimals!)
assert dai.balanceOf(account) == 1517024094830368309726
As you noticed, there are a few references to some functions and smart contracts from my project. You can find them on Github. Don't forget about the ⭐ please.
Now I will run this test against created network:
brownie test tests/warren/test_uniswap_eth_to_dai_swap.py --network testtest-fork -s

As you can see that the test passes.
I hope this article was useful and you learned something new from it. Subscribe to my newsletter for more articles and guides like that one. If you have any feedback, feel free to reach out to me via Twitter.
It would mean the world to me if you share this article on social media.