在本地就能模拟 Uniswap 的 40 亿 TVL?主网分叉(Mainnet Fork)让你把真实合约状态复制到本地 HRE,用假 ETH 测试真实交互。这是 DeFi 开发者的必备工具。
13.2.1 本地网络的类型
| 网络 | 状态来源 | 与主网关系 | 用途 |
|---|---|---|---|
| Hardhat Network | 空(或用户预制) | 独立 | 基础单元测试 |
| Anvil / Ganache | 空(或 fork 后) | 独立或 fork | 快速验证 |
| Mainnet Fork | 主网状态快照 | 复制主网,独立运行 | 集成测试、漏洞复现 |
| Testnet(Sepolia) | 真实共识,真实 ETH | 公共测试环境 | 预生产验证 |
主网分叉的原理
sequenceDiagram
participant Dev as 开发者
participant Local as 本地 Hardhat/Anvil
participant Mainnet as 主网 RPC
participant Arch as 归档节点
Dev ->> Local: npx hardhat --fork mainnet
Local ->> Mainnet: 请求区块 19,000,000 状态
Mainnet -->> Local: 返回账户/合约/存储
Local ->> Arch: 请求历史状态(archive RPC)
Arch -->> Local: 返回历史 storage slots
Note over Dev,Local: 本地:相同的合约地址、<br/>相同的状态、相同的余额——<br/>但所有 ETH/操作都是本地的!
Dev ->> Local: 调用 Uniswap.swap()
Local -->> Dev: 返回值(来自本地副本)
本地节点懒加载(on-demand):只在被访问时从主网拉取状态,大幅节省带宽和启动时间。
13.2.2 主网分叉的 TypeScript 启动配置
typescript
/**
* Hardhat 主网 Fork 配置(TS 模拟,展示核心参数)
*/
interface ForkConfig {
url: string; // 主网 RPC(Infura / Alchemy / 自建)
blockNumber?: number; // 指定分叉区块,固定以获得可重复性
enabled: boolean;
}
const forkConfig: ForkConfig = {
url: "https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY", // 需要归档访问
blockNumber: 19000000, // 固定在某一天的区块
enabled: true,
};
// 启动后,你可以:
// 1. 与真实 Aave 的智能合约交互
// 2. 用 impersonate 模拟大户进行测试
// 3. 修改任何状态(不会上传)
interface ImpersonateAPI {
// 模拟某地址的签名
async impersonateAddress(address: string): Promise<void>;
// 给该地址置入 ETH(本地网络)
async setBalance(address: string, balance: bigint): Promise<void>;
}
// 示例:模拟「巨鲸」进行测试
async function testWhale(fork: ImpersonateAPI) {
const vitalikAddr = "0xd8dA6BF26964aF9D7aEd9e03E53415D3aD4803d1";
const aavePool = "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4e2";
await fork.impersonateAddress(vitalikAddr); // 模拟 Vitalik 的签名
await fork.setBalance(vitalikAddr, 1000n * 10n**18n); // 给 1000 ETH
// 现在可以使用 "Vitalik" 的身份调用 Aave 池
console.log("激活了 Vitalik 测试身份");
}13.2.3 时间操纵与状态控制
主网开发网络的核心超能力:
graph TB
subgraph Superpowers["本地网络超能力"]
T[时间旅行]
M[挖矿控制]
I[身份模拟]
S[状态重置]
G[Gas 任意设]
end
T --> |"evm_increaseTime"| EVM1[快进 7 天]
T --> |"evm_mine"| EVM2[强制出一个新块]
T --> |"evm_setNextBlockTimestamp"| EVM3[精确设置下一区块时间]
I --> |"impersonate"| EVM4[任何人都可以签名]
S --> |"evm_snapshot"| EVM5[快照+回滚]
style Superpowers fill:#e3f2fd
typescript
/**
* 时间旅行测试:测试 Uniswap 的 7 天 TWAP
*/
interface EVMTimeTravel {
async increaseTime(seconds: number): Promise<void>;
async mine(): Promise<void>;
async snapshot(): Promise<number>;
async revert(id: number): Promise<void>;
}
async function test7DayTWAP(evm: EVMTimeTravel) {
// 初始快照
const snap = await evm.snapshot();
// Day 0: 记录价格
const price0 = await getPrice();
// 快进 7 天(不需要等 7 天!)
await evm.increaseTime(7 * 24 * 60 * 60); // 604,800 秒
await evm.mine(); // 确认
// Day 7: 检查 TWAP
const twap = await get7DayTWAP();
console.assert(twap > 0, "TWAP should accumulate 7 days");
// 回滚,不影响其他测试
await evm.revert(snap);
}
function getPrice(): Promise<number> { return Promise.resolve(1000); }
function get7DayTWAP(): Promise<number> { return Promise.resolve(990); }
// 模拟
const mockEVM: EVMTimeTravel = {
async increaseTime(s) { console.log(`Jumped ${s}s`); },
async mine() { console.log("Mined block"); },
async snapshot() { return 1; },
async revert(id) { console.log("Reverted to snapshot", id); },
};
test7DayTWAP(mockEVM).then(() => console.log("Time travel test done"));13.2.4 Fork 测试的安全边界
| 注意 | 说明 |
|---|---|
| 需要归档 RPC | 普通节点只保存最近 128 个区块的 state;fork 过往状态需要 eth_getStorageAt 到任意旧区块 |
| 状态是只读的初始副本 | 本地修改不会影响主网 |
| 真实时间≠区块时间 | 本地可以 0 间隔出矿,与主网出块率不同 |
| 预言机价格不更新 | Chainlink 喂价不会自动更新,需手动调整 |
> ← 13.1 开发框架 | 前往 → 13.3 测试策略与 Gas 报告 |*
评论
0评论加载中…