Mainnet forking with Forge
How a solidity engineer feels while using Foundry
How a solidity engineer feels while using Foundry

Foundry is a fast, portable toolkit for Ethereum development. It’s an improvement on the amazing Dapptools framework, written in Rust (even God is rewriting the human genetic code in Rust and you have doubts anon?).

If you’re hearing about Foundry for the first time, then I suggest you first check out

What’s Mainnet forking?

Mainnet forking is when you fork the state of a live network and use it locally, usually to test your contracts against the live state. This can be extremely helpful while testing and can get you started without having to deploy a bunch of contracts to a local testnet to set up the environment.

Basically mainnet forking
Basically mainnet forking

Mainnet forking with Forge

In this article, we’re gonna locally simulate a swap on Uniswap. I chose Uniswap because playing around with the live state of something we all use sounded the most fun. I have intentionally chosen an approach that requires the least code to simulate the swap, so we can just focus on forge mainnet forking. We will be simulating the swap on the Rinkeby network since it’s easier for this example, but the steps should be identical while forking from Mainnet.

Making the swap using call data

In Ethereum, all you need to interact with a contract is the contract address, the function you’re calling, and the input parameters. The function name and the input parameters are encoded and set as the call data for the transaction.

The easiest way is to just attempt the same swap on the Uniswap interface and get the generated data from MetaMask. Let’s do that. In case you don’t have Rinkeby Eth, if not you can get some from Paradigm’s awesome MultiFaucet.

Go on to the Uniswap interface, make sure you’re on the Rinkeby network, and initiate a swap for 1 Eth to Dai. It should look like this.

Initiate the swap, and you should get the familiar MetaMask popup. Now instead of confirming the transaction, check out the Data and Hex tab.

Data information on MetaMask
Data information on MetaMask

We can see that we are calling the Multicall function with uint256 and bytes[] as input parameters. (Ignore the Fetch failed).

On the HEX tab, you’ll see parameter information and a lot of Hex data (cropped out here). This is the hex encoding of the function signature and the input parameters.

Call data generated by MetaMask
Call data generated by MetaMask

Now we have

  • The contract address to call. (Top right on MetaMask)
  • Hex encoded call data (you can copy the raw transaction data at the bottom of the popup)

We have everything to simulate the swap locally.

Small detour

Before we start, let’s quickly see what the call data encodes. For this, we can use this ETH Calldata Decoder by Apoorv Lathey. After decoding the calldata, we see that the uint256 parameter is deadline which is a UNIX timestamp. We can assume that’s it’s the deadline before the swap must take place, we will play with this later.

Setting up the project

Go to your terminal, and run the following commands to initialize a forge project and cd into the newly created folder.

forge init simulate-swap
cd simulate-swap

Navigate to src/test/Contract.t.sol , this is where we will be writing our simulation.

Writing the contract

Paste this snippet in Contract.t.sol.

// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.6;

import "ds-test/test.sol";

// for cheatcodes
interface Vm {
    function prank(address) external;

    function warp(uint256) external;
}

// to verfiy that we recevied Dai
interface Dai {
    function balanceOf(address) external view returns (uint256);
}

contract ContractTest is DSTest {
    Vm vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
Dai dai = Dai(0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735);

// your address
    address constant myAddress = "your ethereum address";

// contract address from MetaMask
    address constant swapDai = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45;

   
    

    function testSwap() public {

// to verify if we are actually forking
        emit log_named_uint("Current ether balance of myAddress", myAddress.balance);

        emit log_named_uint("Dai balance of myAddress before", dai.balanceOf(myAddress));
        
        
        
// type casting hex call data to bytes
        bytes
            memory data = hex"paste the hex call data here. Remove the leading '0x' ";

// setting the next call's msg.sender as myAddress
        vm.prank(myAddress);


        (bool sent, ) = payable(swapDai).call{value: 1 ether}(data);
        require(sent, "failed");

        

        emit log_named_uint("Dai balance of myAddress after", dai.balanceOf(myAddress));
        
    }
}

Explaining the code

What are those interfaces?

  • The Dai interface is to get the balance of our address before and after the swap.
  • The Vm interface is to use cheatcodes.
    • You might be familiar with this if you’re coming from Dapptools (or Grand Theft Auto). If you’re not, then think of it like this, Dapptools/Foundry uses a custom EVM to run tests in solidity, this enables us to do wacky stuff like changing the block.timestamp, changing the storage of contracts, setting the msg.sender, etc.
    • You can see how amazing this would be for testing. Add mainnet forking and you have a solid development framework.
    • Here vm.prank(myAddress) sets the msg.sender of the next call to myAddress
    • vm.warp(uint) allows you to set the block.timestamp.
    • For more details check out the repo

How are we making the swap?

  • We set the contract address we got from MetaMask.
  • We typecast the hex call data to bytes.
  • Make the contract call using call
  • Emitting the balance before and after to make sure the swap worked.

Environment set up

We just need to set one environment variable, ETH_RPC_URL to your RPC provider endpoint. Make sure it’s the endpoint for the network you want to fork.

Run this command in your terminal.

ETH_RPC_URL="paste your rpc url here"

Making the swap

Now that’s everything is set up, let’s run the simulation. Run this command in your terminal.

forge test --fork-url $ETH_RPC_URL -vv

You should see an output like this

Well done! You just successfully forked the state of a live network and played God with it.

Bonus

If you are experimenting with this swap for a considerable amount of time, you might notice that after a while the swap doesn’t work. This is because of the deadline parameter that we saw earlier. After the deadline the swap is invalid. What do can we do? You know where I’m going with this, cheatcodes!

This can be a fun exercise, using warp(uint) + the decoded deadline timestamp, play around and see when the swap is valid/invalid. Happy forging!

Subscribe to Sushi
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.