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 是否成功。在 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 功能,感兴趣的朋友可以了解一下。
欢迎和我交流