第4章 Fork 测试
Fork 测试 Fork 测试允许你基于真实的链上状态运行测试,而无需部署到实时网络。这对于测试与现有协议的集成至关重要。
基础 Fork 测试 对分叉网络运行测试: $ forge test --fork-url https://ethereum.reth.rs/rpc 固定到特定区块以实现可重现的测试: $ forge test --fork-url https://ethereum.reth.rs/rpc --fork-block-number 18000000
通过区块哈希读取状态的 RPC 如果你的 RPC 通过区块号提供 fork 状态,但拒绝通过哈希读取,请启用 --fork-state-by-number: forge test --fork-url "$RPC_URL" --fork-block-number 18000000 --fork-state-by-number forge script script/Deploy.s.sol:Deploy --rpc-url "$RPC_URL" --fork-state-by-number 基于哈希的读取仍然是默认行为。基于区块号的读取在防范重组方面的保护较弱,且不使用磁盘状态缓存。交易重放仍需要基于哈希的读取。
要为此配置文件启用此功能(包括由 cheatcodes 创建的 fork),请设置 fork_state_by_number = true。
在 foundry.toml 中配置 fork 为所有测试设置默认 fork: foundry.toml[profile.default] eth_rpc_url = "https://ethereum.reth.rs/rpc" fork_block_number = 18000000
模拟账户 使用 vm.prank() 以另一个地址执行单次调用: function test_ImpersonateWhale() public { address whale = 0xF977814e90dA44bFA03b6295A0616a897441aceC; // 币安热钱包 address dai = 0x6B175474E89094C44Da98b954EescdeCB5BE3830; vm.prank(whale); IERC20(dai).transfer(address(this), 1000 ether); assertEq(IERC20(dai).balanceOf(address(this)), 1000 ether); } 使用 vm.startPrank() 执行多次调用: function test_MultipleCallsAsWhale() public { address whale = 0xF977814e90dA44bFA03b6295A0616a897441aceC; vm.startPrank(whale); IERC20(dai).approve(address(vault), type(uint256).max); vault.deposit(1000 ether); vm.stopPrank(); }
时间敏感测试 许多 DeFi 协议包含基于时间的逻辑。使用 vm.warp() 来操作区块时间戳: function test_VestingUnlock() public { vesting.startVesting(alice, 1000 ether, 365 days); vm.warp(block.timestamp + 182 days); vm.prank(alice); uint256 claimed = vesting.claim(); assertApproxEqRel(claimed, 500 ether, 0.01e18); } 使用 vm.roll() 来更改区块号: function test_BlockBasedLogic() public { uint256 startBlock = block.number; // 前进 100 个区块 vm.roll(startBlock + 100); assertEq(block.number, startBlock + 100); }
使用真实协议状态进行测试
与 Uniswap 交互
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol";
interface IUniswapV2Router {
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external returns (uint256[] memory amounts);
}
interface IERC20 {
function approve(address spender, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
contract UniswapTest is Test {
IUniswapV2Router router = IUniswapV2Router(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
function setUp() public {
// 给此合约一些 WETH
deal(WETH, address(this), 10 ether);
}
function test_SwapWethForUsdc() public {
IERC20(WETH).approve(address(router), 1 ether);
address[] memory path = new address[](2);
path[0] = WETH;
path[1] = USDC;
uint256 usdcBefore = IERC20(USDC).balanceOf(address(this));
router.swapExactTokensForTokens(
1 ether,
0, // 接受任意数量
path,
address(this),
block.timestamp
);
uint256 usdcAfter = IERC20(USDC).balanceOf(address(this));
assertGt(usdcAfter, usdcBefore);
}
}
使用 Aave 进行测试
interface IPool {
function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external;
function borrow(address asset, uint256 amount, uint256 interestRateMode, uint16 referralCode, address onBehalfOf) external;
}
contract AaveTest is Test {
IPool pool = IPool(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2);
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
function test_SupplyAndBorrow() public {
deal(WETH, address(this), 10 ether);
IERC20(WETH).approve(address(pool), 10 ether);
// 提供 WETH 作为抵押品
pool.supply(WETH, 10 ether, address(this), 0);
// 借此借出 USDC
pool.borrow(USDC, 1000e6, 2, 0, address(this)); // 浮动利率
assertEq(IERC20(USDC).balanceOf(address(this)), 1000e6);
}
}
处理代币
使用 deal() 设置代币余额,而无需实际获取它们:
function setUp() public {
deal(address(dai), alice, 1000 ether);
deal(address(weth), bob, 10 ether);
vm.deal(address(this), 100 ether);
}
对于具有复杂余额存储结构的代币(例如带有黑名单机制的 USDC),请使用带 adjust 参数的 deal():
deal(address(usdc), alice, 1000e6, true); // 调整 totalSupply
多 Fork 测试
测试不同链之间的交互:
function test_CrossChainState() public {
uint256 mainnetFork = vm.createFork("https://ethereum.reth.rs/rpc");
uint256 optimismFork = vm.createFork("https://mainnet.optimism.io");
vm.selectFork(mainnetFork);
uint256 mainnetBalance = IERC20(mainnetUsdc).balanceOf(whale);
vm.selectFork(optimismFork);
uint256 optimismBalance = IERC20(optimismUsdc).balanceOf(whale);
console.log("Mainnet USDC:", mainnetBalance);
console.log("Optimism USDC:", optimismBalance);
}
Fork 缓存
Foundry 将 fork 数据缓存在 ~/.foundry/cache/rpc/
如果需要新数据,请清除缓存: $ rm -rf ~/.foundry/cache/rpc 最佳实践 实践:固定区块号。描述:始终使用 --fork-block-number 进行可重现测试。 实践:使用 deal()。描述:避免在测试中使用复杂的代币获取逻辑。 实践:缓存 RPC 调用。描述:让 Foundry 缓存 fork 数据以加速测试。 实践:测试边界情况。描述:使用 vm.warp() 测试时间敏感的边界情况。 实践:隔离 Fork 测试。描述:将 Fork 测试与单元测试分开,以加快 CI 流程。
故障排除 速率限制 如果你的 RPC 提供商对请求进行了速率限制: $ forge test --fork-url $RPC_URL --fork-retry-backoff 1000 该值是初始重试延迟(以毫秒为单位),因此 1000 意味着从一秒退避开始重试。如果省略,Foundry 将使用提供商默认值。
或者使用具有更高限制的专用 RPC 提供商。
缓存过时 如果协议升级后测试行为异常: $ forge clean $ forge test --fork-url $RPC_URL 这对您有帮助吗?在 GitHub 上建议修改