Hardhat fork 简明使用教程

Hardhat fork 可以使我们将主网的区块 fork 到本地,这样我们就可以在本地与真实的链上数据进行交互,同时也可以模拟任意账户,方便我们进行一些测试,速度快,并且不用花费 Gas。

基本命令

首先,我们先创建一个 Hardhat 项目,不熟悉的朋友可以看看这里。创建完毕之后,就可以在本地 node 中 fork 区块了。使用命令(替换自己的 key):

npx hardhat node --fork https://eth-mainnet.alchemyapi.io/v2/

这个命令会将当前最新的一个区块数据 fork 到本地,也就是说现在本地 node 链中存在的就是这个区块的数据。当然也可以指定区块号进行 fork:

npx hardhat node --fork https://eth-mainnet.alchemyapi.io/v2/ --fork-block-number 14913700

每次 fork 都使用命令行指定这一堆参数很麻烦,因此可以将其放在 Hardhat 的配置文件 hardhat.config.js 中:

module.exports = {
  solidity: "0.8.4",

  networks: {
    hardhat: {
      // 添加 forking 内容
      forking: {
        url: "https://eth-mainnet.alchemyapi.io/v2/<key>",
        // 如果不指定区块,则默认 fork 当前最新区块
        blockNumber: 14913700
      }
    }
  }
};

这样配置之后,就可以直接使用

npx hardhat node

此时本地 node 的数据就是 fork 的区块数据。

需要注意的一点是,在执行 npx hardhat test 运行单元测试时,测试内容运行的链不是本地 node,而是沙盒模式。因此如果想要在单元测试的沙盒模式中使用 fork 的数据,需要在配置文件中显式配置 forking 内容。如果希望单元测试在本地 node 中执行,需要指定网络:

npx hardhat test --network localhost

测试 fork 数据

接下来,我们利用单元测试来实际验证一下我们的 fork 是否成功。在 test 文件夹下创建 fork.test.js 文件,编写代码:

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

describe("Fork", function () {
  it("Testing fork data", async function () {
    console.log((await ethers.provider.getBlockNumber()).toString());
  });
});

执行测试,输出 14913700,说明 fork 成功。

现在我们来试试,实际调用主网的数据。我们以 USDT 合约为例,调用它的数据:

// 需要将 usdt 的 abi 保存在本地
const USDT_ABI = require("./usdt_abi.json");
// usdt 合约的主网地址
const USDT_ADDRESS = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
const { ethers } = require("hardhat");

describe("Fork", function () {
  it("Testing fork data", async function () {
    const provider = ethers.provider;
    // 构造 usdt 合约对象
    const USDT = new ethers.Contract(USDT_ADDRESS, USDT_ABI, provider);
    // 调用 usdt 的 totalSupply
    let totalSupply = await USDT.totalSupply();
    console.log(totalSupply.toString());
  });
});

结果为:

再去查看 USDT 的实际发行量:

验证数据正确。

fork 功能还有一个很有用的功能是,可以本地模拟任意账户,那么就意味着可以在本地拥有该地址的任意资产,这对我们做一些测试很有帮助。在测试文件中编写如下代码:

const mockAddress = "0x4c8CFE078a5B989CeA4B330197246ceD82764c63";

await network.provider.request({
  method: "hardhat_impersonateAccount",
  params: [mockAddress],
});

const signer = await ethers.provider.getSigner(mockAddress);

这样就可以模拟地址 0x4c8CFE078a5B989CeA4B330197246ceD82764c63,此时 signer 就是该地址的信息。先来查看该地址的账户信息:

let ETHBalance = await signer.getBalance();
console.log(`ETH balance is ${ETHBalance.toString() / 1e18}`);

let USDTBalance = await USDT.balanceOf(signer.getAddress()) / 1e6;
console.log(`USDT balance is ${USDTBalance.toString()}`);

执行结果为:

主网查询其资产为:

验证数据正确。我们再来看看如何模拟该账户发送交易,假设需要向其他地址发送一万 USDT,代码为:

// 打印转账前的账户余额
let USDTBalanceA = await USDT.balanceOf(signer.getAddress()) / 1e6;
console.log(`USDT balance before transfer is ${USDTBalanceA.toString()}`);

const recipient = "0x652361ED2a8FB7E9b15Fe073AAb9fE2cFacb0B52";

let USDTBalanceB = await USDT.balanceOf(recipient) / 1e6;
console.log(`USDT balance of recipient before transfer is ${USDTBalanceB.toString()}`);

console.log("========Transfering========");

// 转账操作
await USDT.connect(signer).transfer(
  "0x652361ED2a8FB7E9b15Fe073AAb9fE2cFacb0B52",
  ethers.utils.parseUnits("10000", 6)
);

// 打印转账后的账户余额
USDTBalanceA = await USDT.balanceOf(signer.getAddress()) / 1e6;
console.log(`USDT balance after transfer is ${USDTBalanceA.toString()}`);

USDTBalanceB = await USDT.balanceOf(recipient) / 1e6;
console.log(`USDT balance of recipient after transfer is ${USDTBalanceB.toString()}`);

结果为:

验证转账成功。

拓展

fork 下还有一些功能我们这里没有介绍,例如

// 设置账户余额
await network.provider.send("hardhat_setBalance", [
  "0x0d2026b3EE6eC71FC6746ADb6311F6d3Ba1C000B",
  "0x1000",
]);

// 设置账户 nonce
await network.provider.send("hardhat_setNonce", [
  "0x0d2026b3EE6eC71FC6746ADb6311F6d3Ba1C000B",
  "0x21",
]);

感兴趣的朋友可以查看文档自行了解。

总结

Hardhat fork 在平时的测试中非常方便,例如我们想要测试闪电贷,套利合约等,如果在主网测试,一是很慢,二是会花费 Gas。不过这个功能也不是 Hardhat 的专属,Ganache 和 Foundry 也有 fork 功能,感兴趣的朋友可以了解一下。

关于我

欢迎和我交流

参考

Subscribe to xyyme.eth
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.