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
Создайте проект:
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:
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:
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],
);
});
});Запустите тест:
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().
Что дальше
- Hardhat: юнит-тесты (ethers + chai) — закрепить базовый тестовый цикл
- ERC-20 transfer — понять, что происходит внутри
transfer() - Batch транзакции через Multicall — тестировать несколько вызовов как одну операцию