DEV Community

Cover image for Fork mainnet using hardhat to test and build on DeFi protocols and more
Vishak Bhaskar
Vishak Bhaskar

Posted on

Fork mainnet using hardhat to test and build on DeFi protocols and more

So what does forking mainnet actually mean? According to hardhat docs :

Forking the mainnet will simulate having the same state as mainnet, but it will work as a local development network. That way you can interact with deployed protocols and test complex interactions locally.

But why is this awesome? I personally love it because it is faster than testing on a testnet and you never run out of test ether. If you've never tried forking mainnet on hardhat, after this tutorial you'll be equipped with the knowledge to do so and you will not have to use the testnets much and never worry about running out of test ether (at least not as much as you used to)

Forking mainnet is basically copying the current or a previous state of the mainnet and pasting it into your local development network. This way you will be able to access the deployed smart contracts in the mainnnet for testing purposes and the rest of the transactions or blocks will be mined and added to your local development network.

Testing your project on a forked mainnet allows you to avoid a lot of issues you might encounter when you actually deploy it the mainnet. In this tutorial we will also learn to impersonate an account on mainnet and send transactions from it to test your contract (only if you could do it on mainnet ๐Ÿ˜‰). I will keep everything as beginner friendly as possible so everyone can follow along. You can refer to this Github repository if you get stuck or reach out to me here

In this tutorial we're gonna interact with the USDC contract on ethereum mainnet, unlock a user's account and impersonate that account and drain some USDC. So let's get to it ๐Ÿš€

Before, head to Alchemy and get your API key. You can refer to this tutorial to learn how to get your key.

Let's open the terminal, create a folder to start with, move into it and install hardhat, initialise the project and open it in our code editor and also install dotenv package.

mkdir mainnet-fork-tutorial
cd mainnet-fork-tutorial
npm i dotenv
code .
Enter fullscreen mode Exit fullscreen mode
npm install --save-dev hardhat
npx hardhat
Enter fullscreen mode Exit fullscreen mode

We'll be creating a javascript project for this tutorial

Initialising Hardhat

Install the dependencies needed

Install hardhat project dependencies

Now create a file named .env in your root folder and paste the key that you grabbed from alchemy. Your .env file should look something like this now :

ALCHEMY_API_KEY = `NtZu1RjBg--your API KEY---_B678`
Enter fullscreen mode Exit fullscreen mode

Now that we're done setting up the project, head to hardhat.config.js file and make the following changes so the file looks something like this now :

require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();

module.exports = {
  solidity: "0.8.17",
  networks: {
    hardhat: {
      forking: {
        enabled: true,
        url: `https://eth-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}`,
      },
    },
  },
};

Enter fullscreen mode Exit fullscreen mode

Now create a folder named IERC20.sol inside your contracts folder and paste the following code into it. This is the ERC20 interface we will need to interact with the USDC contract. You can also find this code here

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol)

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev Returns the amount of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the amount of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves `amount` tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 amount) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 amount) external returns (bool);

    /**
     * @dev Moves `amount` tokens from `from` to `to` using the
     * allowance mechanism. `amount` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
Enter fullscreen mode Exit fullscreen mode

Since we will be working with USDC, the address of USDC on Ethereum mainnet is 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48

Go to the tracker here and click the "holders" tab and find an account that holds a good amount of USDC. Make sure the address you choose is not a smart contract. At the time of writing this this address holds some USDC :
0x203520F4ec42Ea39b03F62B20e20Cf17DB5fdfA7

Unlock an account to be impersonated in hardhat :

{
    await network.provider.request({
      method: "hardhat_impersonateAccount",
      params: [USDC_WHALE],
    });
  }
Enter fullscreen mode Exit fullscreen mode

Where USDC_WHALE is the address that holds a large amount of USDC tokens. Once the account has been unlocked, some ether should be sent to it so it can pay for the gas fees. After this transactions can be carried out.

Now, go to the scripts folder and create a javascript file. I named it transfer.js

Paste this code into the file :

const { ethers, network } = require("hardhat");

const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";

const USDC_WHALE = "0x203520F4ec42Ea39b03F62B20e20Cf17DB5fdfA7";

async function main() {
  {
    await network.provider.request({
      method: "hardhat_impersonateAccount",
      params: [USDC_WHALE],
    });
  }

  const whale = await ethers.getSigner(USDC_WHALE);
  const usdc = await ethers.getContractAt("IERC20", USDC);

  const accounts = await ethers.getSigners();
  const attacker = accounts[0];

  const HUNDRED_THOUSAND = ethers.utils.parseUnits("100000", 6);

  let whaleBal = await usdc.balanceOf(USDC_WHALE);
  let attackerBal = await usdc.balanceOf(attacker.address);

  console.log(
    "Initial USDC balance of whale : ",
    ethers.utils.formatUnits(whaleBal, 6)
  );

  console.log(
    "Initial USDC balance of attacker : ",
    ethers.utils.formatUnits(attackerBal, 6)
  );

  await accounts[0].sendTransaction({
    to: whale.address,
    value: ethers.utils.parseEther("50.0"), // Sends exactly 50.0 ether
  });

  await usdc.connect(whale).transfer(accounts[0].address, HUNDRED_THOUSAND);

  let newWhaleBal = await usdc.balanceOf(USDC_WHALE);
  let newAttackerBal = await usdc.balanceOf(attacker.address);

  console.log(
    "Final USDC balance of whale : ",
    ethers.utils.formatUnits(newWhaleBal, 6)
  );

  console.log(
    "Final USDC balance of attacker : ",
    ethers.utils.formatUnits(newAttackerBal, 6)
  );
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Enter fullscreen mode Exit fullscreen mode

This script is responsible for unlocking the USDC_WHALE account and sending USDC to another account. The balances will be logged into your terminal when you run this.

Next, open up your terminal and enter the following :

npx hardhat node --fork https://eth-mainnet.g.alchemy.com/v2/<YOUR_ALCHEMY_API_KEY>
Enter fullscreen mode Exit fullscreen mode

You'll find something like this appear on the terminal :

Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/

Accounts
========

WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.

Account #0: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

Account #1: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 (10000 ETH)
Private Key: 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d

Account #2: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC (10000 ETH)
Private Key: 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a

Account #3: 0x90F79bf6EB2c4f870365E785982E1f101E93b906 (10000 ETH)
Private Key: 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6

Account #4: 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 (10000 ETH)
Private Key: 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a

Account #5: 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc (10000 ETH)
Private Key: 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba

Account #6: 0x976EA74026E726554dB657fA54763abd0C3a0aa9 (10000 ETH)
Private Key: 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e

Account #7: 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955 (10000 ETH)
Private Key: 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356

Account #8: 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f (10000 ETH)
Private Key: 0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97

Account #9: 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720 (10000 ETH)
Private Key: 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6

Account #10: 0xBcd4042DE499D14e55001CcbB24a551F3b954096 (10000 ETH)
Private Key: 0xf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897

Account #11: 0x71bE63f3384f5fb98995898A86B02Fb2426c5788 (10000 ETH)
Private Key: 0x701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82

Account #12: 0xFABB0ac9d68B0B445fB7357272Ff202C5651694a (10000 ETH)
Private Key: 0xa267530f49f8280200edf313ee7af6b827f2a8bce2897751d06a843f644967b1

Account #13: 0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec (10000 ETH)
Private Key: 0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd

Account #14: 0xdF3e18d64BC6A983f673Ab319CCaE4f1a57C7097 (10000 ETH)
Private Key: 0xc526ee95bf44d8fc405a158bb884d9d1238d99f0612e9f33d006bb0789009aaa

Account #15: 0xcd3B766CCDd6AE721141F452C550Ca635964ce71 (10000 ETH)
Private Key: 0x8166f546bab6da521a8369cab06c5d2b9e46670292d85c875ee9ec20e84ffb61

Account #16: 0x2546BcD3c84621e976D8185a91A922aE77ECEc30 (10000 ETH)
Private Key: 0xea6c44ac03bff858b476bba40716402b03e41b8e97e276d1baec7c37d42484a0

Account #17: 0xbDA5747bFD65F08deb54cb465eB87D40e51B197E (10000 ETH)
Private Key: 0x689af8efa8c651a91ad287602527f3af2fe9f6501a7ac4b061667b5a93e037fd

Account #18: 0xdD2FD4581271e230360230F9337D5c0430Bf44C0 (10000 ETH)
Private Key: 0xde9be858da4a475276426320d5e9262ecfc3ba460bfac56360bfa6c4c28b4ee0

Account #19: 0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199 (10000 ETH)
Private Key: 0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e

WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.

Enter fullscreen mode Exit fullscreen mode

Now open a new terminal and run the following :

hh run scripts/transfer.js
Enter fullscreen mode Exit fullscreen mode

You'll find that the attacker's balance has been increased by 100,000 USDC.

Initial USDC balance of whale :  45274535.206439
Initial USDC balance of attacker :  0.137975
Final USDC balance of whale :  45174535.206439
Final USDC balance of attacker :  100000.137975
Enter fullscreen mode Exit fullscreen mode

I hope you learnt to fork the mainnet for testing your applications in this tutorial. If you have any doubts or errors feel free to reach out to me on Twitter.

Cheers

Top comments (0)