китаев.tech

Hardhat: fork mainnet для тестов

Как запустить тест Hardhat на fork mainnet, прочитать реальный ERC-20 баланс и выполнить вызов от имени whale-адреса.

Что такое mainnet fork

Mainnet fork — это локальная Hardhat-сеть, которая берёт состояние Ethereum mainnet из RPC: контракты, storage, балансы и код по адресам.

Транзакции выполняются локально. Вы не тратите ETH, но тест видит реальные контракты Uniswap, WETH, USDC и другие mainnet-адреса.


Тестируем WETH на fork mainnet

Создайте проект:

bash
mkdir hardhat-mainnet-fork-demo
cd hardhat-mainnet-fork-demo

npm init -y
npm pkg set type="module"
npm install --save-dev hardhat typescript tsx @types/node @nomicfoundation/hardhat-toolbox-mocha-ethers

mkdir test

Создайте файл hardhat.config.ts:

typescript
import { defineConfig } from "hardhat/config";
import hardhatToolboxMochaEthers from "@nomicfoundation/hardhat-toolbox-mocha-ethers";

export default defineConfig({
  plugins: [hardhatToolboxMochaEthers],
  solidity: {
    version: "0.8.28",
  },
  networks: {
    mainnetFork: {
      type: "edr-simulated",
      forking: {
        url: "https://ethereum-rpc.publicnode.com",
        blockNumber: 19_000_000,
      },
    },
  },
});

Создайте файл test/WethFork.ts:

typescript
import { expect } from "chai";
import { network } from "hardhat";

const { ethers, networkHelpers } = await network.create();

const WETH_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
const WETH_WHALE = "0xF04a5cC80B1E94C69B48f5ee68B9b633A3cE0804";
const RECEIVER = "0x000000000000000000000000000000000000dEaD";

const wethAbi = [
  "function balanceOf(address owner) view returns (uint256)",
  "function transfer(address to, uint256 amount) returns (bool)",
];

describe("WETH mainnet fork", function () {
  it("читает реальный баланс WETH", async function () {
    const weth = await ethers.getContractAt(wethAbi, WETH_ADDRESS);

    const balance = await weth.balanceOf(WETH_WHALE);

    expect(balance).to.be.greaterThan(ethers.parseEther("1000"));
  });

  it("отправляет WETH от имени whale-адреса локально", async function () {
    const weth = await ethers.getContractAt(wethAbi, WETH_ADDRESS);

    await networkHelpers.impersonateAccount(WETH_WHALE);
    await networkHelpers.setBalance(WETH_WHALE, ethers.parseEther("1"));

    const whaleSigner = await ethers.getSigner(WETH_WHALE);
    const amount = ethers.parseEther("10");

    await expect(weth.connect(whaleSigner).transfer(RECEIVER, amount)).to.changeTokenBalances(
      weth,
      [WETH_WHALE, RECEIVER],
      [-amount, amount],
    );
  });
});

Запустите тест:

bash
npx hardhat test --network mainnetFork

Как это работает

type: "edr-simulated" — включает локальную симулированную сеть Hardhat.

forking.url — RPC, из которого Hardhat читает mainnet-состояние.

blockNumber — фиксирует высоту блока. Без неё тест может сломаться завтра, если баланс whale изменится.

network.create({ network: "mainnetFork" }) — подключает тест к сети из hardhat.config.ts.

impersonateAccount() — разрешает подписывать транзакции от имени существующего mainnet-адреса внутри локального fork.

setBalance() — выдаёт impersonated-адресу ETH для оплаты локального gas.

Частые ошибки

Не фиксируете blockNumber → тест зависит от текущего mainnet-состояния и становится нестабильным.

Путаете fork с mainnet → fork выполняет транзакции локально. Он не меняет состояние настоящего Ethereum.

Используете whale без ETH → ERC-20 баланс есть, но gas всё равно платится ETH. Перед транзакцией вызовите setBalance().


Что дальше

Материалы китаev.tech публикуются в образовательных целях и не являются инвестиционной рекомендацией. Примеры кода и описания протоколов — для обучения; использование в продакшне на ваш собственный риск. Дисклеймер и политика конфиденциальности.